Copy everything
5
.dockerignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
target
|
||||||
|
package-lock.json
|
||||||
|
.*
|
||||||
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
og-image.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
49
.github/workflows/main.yaml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
name: Deploy everything
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
env:
|
||||||
|
CONTAINER_REGISTRY: schmelczera
|
||||||
|
DOMAIN: "161.35.71.163"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout current branch with lfs
|
||||||
|
uses: actions/checkout@main
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Setup auth tokens
|
||||||
|
run: |
|
||||||
|
docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push job
|
||||||
|
run: |
|
||||||
|
docker build . -t $CONTAINER_REGISTRY/photos
|
||||||
|
docker push $CONTAINER_REGISTRY/photos
|
||||||
|
|
||||||
|
push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
steps:
|
||||||
|
- name: Checkout current branch with lfs
|
||||||
|
uses: actions/checkout@main
|
||||||
|
with:
|
||||||
|
lfs: true
|
||||||
|
|
||||||
|
- name: Setup auth tokens
|
||||||
|
run: |
|
||||||
|
# SSH key
|
||||||
|
mkdir ~/.ssh
|
||||||
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 400 ~/.ssh/id_ed25519
|
||||||
|
ssh -o StrictHostKeyChecking=no root@$DOMAIN uptime
|
||||||
|
|
||||||
|
- name: Stack deploy
|
||||||
|
run: |
|
||||||
|
DOCKER_HOST=ssh://root@$DOMAIN docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
DOCKER_HOST=ssh://root@$DOMAIN docker stack deploy photos -c docker-compose.yml --with-registry-auth
|
||||||
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
target
|
||||||
|
package-lock.json
|
||||||
9
.idea/dictionaries/andras.xml
generated
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<component name="ProjectDictionaryState">
|
||||||
|
<dictionary name="andras">
|
||||||
|
<words>
|
||||||
|
<w>andrás</w>
|
||||||
|
<w>imgs</w>
|
||||||
|
<w>schmelczer</w>
|
||||||
|
</words>
|
||||||
|
</dictionary>
|
||||||
|
</component>
|
||||||
6
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="JavaScriptSettings">
|
||||||
|
<option name="languageLevel" value="ES6" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/photos.iml" filepath="$PROJECT_DIR$/.idea/photos.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/photos.iml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
65
.idea/watcherTasks.xml
generated
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectTasksOptions">
|
||||||
|
<TaskOptions isEnabled="true">
|
||||||
|
<option name="arguments" value="--write $FilePathRelativeToProjectRoot$" />
|
||||||
|
<option name="checkSyntaxErrors" value="true" />
|
||||||
|
<option name="description" />
|
||||||
|
<option name="exitCodeBehavior" value="ERROR" />
|
||||||
|
<option name="fileExtension" value="ts" />
|
||||||
|
<option name="immediateSync" value="false" />
|
||||||
|
<option name="name" value="Prettier" />
|
||||||
|
<option name="output" value="$FilePathRelativeToProjectRoot$" />
|
||||||
|
<option name="outputFilters">
|
||||||
|
<array />
|
||||||
|
</option>
|
||||||
|
<option name="outputFromStdout" value="false" />
|
||||||
|
<option name="program" value="$ProjectFileDir$/node_modules/.bin/prettier" />
|
||||||
|
<option name="runOnExternalChanges" value="true" />
|
||||||
|
<option name="scopeName" value="Project Files" />
|
||||||
|
<option name="trackOnlyRoot" value="false" />
|
||||||
|
<option name="workingDir" value="$ProjectFileDir$" />
|
||||||
|
<envs />
|
||||||
|
</TaskOptions>
|
||||||
|
<TaskOptions isEnabled="true">
|
||||||
|
<option name="arguments" value="--write $FilePathRelativeToProjectRoot$" />
|
||||||
|
<option name="checkSyntaxErrors" value="true" />
|
||||||
|
<option name="description" />
|
||||||
|
<option name="exitCodeBehavior" value="ERROR" />
|
||||||
|
<option name="fileExtension" value="scss" />
|
||||||
|
<option name="immediateSync" value="false" />
|
||||||
|
<option name="name" value="Prettier" />
|
||||||
|
<option name="output" value="$FilePathRelativeToProjectRoot$" />
|
||||||
|
<option name="outputFilters">
|
||||||
|
<array />
|
||||||
|
</option>
|
||||||
|
<option name="outputFromStdout" value="false" />
|
||||||
|
<option name="program" value="$ProjectFileDir$/node_modules/.bin/prettier" />
|
||||||
|
<option name="runOnExternalChanges" value="true" />
|
||||||
|
<option name="scopeName" value="Project Files" />
|
||||||
|
<option name="trackOnlyRoot" value="false" />
|
||||||
|
<option name="workingDir" value="$ProjectFileDir$" />
|
||||||
|
<envs />
|
||||||
|
</TaskOptions>
|
||||||
|
<TaskOptions isEnabled="true">
|
||||||
|
<option name="arguments" value="--write $FilePathRelativeToProjectRoot$" />
|
||||||
|
<option name="checkSyntaxErrors" value="true" />
|
||||||
|
<option name="description" />
|
||||||
|
<option name="exitCodeBehavior" value="ERROR" />
|
||||||
|
<option name="fileExtension" value="xhtml" />
|
||||||
|
<option name="immediateSync" value="false" />
|
||||||
|
<option name="name" value="Prettier" />
|
||||||
|
<option name="output" value="$FilePathRelativeToProjectRoot$" />
|
||||||
|
<option name="outputFilters">
|
||||||
|
<array />
|
||||||
|
</option>
|
||||||
|
<option name="outputFromStdout" value="false" />
|
||||||
|
<option name="program" value="$ProjectFileDir$/node_modules/.bin/prettier" />
|
||||||
|
<option name="runOnExternalChanges" value="true" />
|
||||||
|
<option name="scopeName" value="Project Files" />
|
||||||
|
<option name="trackOnlyRoot" value="false" />
|
||||||
|
<option name="workingDir" value="$ProjectFileDir$" />
|
||||||
|
<envs />
|
||||||
|
</TaskOptions>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
87
.idea/workspace.xml
generated
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="67a50e5c-f550-4763-aa56-c67942ec6666" name="Default Changelist" comment="" />
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="PackageJsonUpdateNotifier">
|
||||||
|
<dismissed value="$PROJECT_DIR$/package.json" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectId" id="1WHpfQesortnFO5XuuiNHiPc6f1" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showExcludedFiles" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">
|
||||||
|
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
|
||||||
|
<property name="WebServerToolWindowFactoryState" value="false" />
|
||||||
|
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||||
|
<property name="nodejs_package_manager_path" value="npm" />
|
||||||
|
<property name="prettierjs.PrettierConfiguration.Package" value="/usr/local/lib/node_modules/prettier" />
|
||||||
|
<property name="settings.editor.selected.configurable" value="watcher.settings" />
|
||||||
|
<property name="ts.external.directory.path" value="$PROJECT_DIR$/node_modules/typescript/lib" />
|
||||||
|
</component>
|
||||||
|
<component name="RecentsManager">
|
||||||
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="$PROJECT_DIR$/src" />
|
||||||
|
</key>
|
||||||
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="$PROJECT_DIR$" />
|
||||||
|
</key>
|
||||||
|
</component>
|
||||||
|
<component name="ServiceViewManager">
|
||||||
|
<option name="viewStates">
|
||||||
|
<list>
|
||||||
|
<serviceView>
|
||||||
|
<treeState>
|
||||||
|
<expand />
|
||||||
|
<select />
|
||||||
|
</treeState>
|
||||||
|
</serviceView>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="SvnConfiguration">
|
||||||
|
<configuration />
|
||||||
|
</component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="67a50e5c-f550-4763-aa56-c67942ec6666" name="Default Changelist" comment="" />
|
||||||
|
<created>1578816476846</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1578816476846</updated>
|
||||||
|
<workItem from="1578816478170" duration="27472000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="1" />
|
||||||
|
</component>
|
||||||
|
<component name="WindowStateProjectService">
|
||||||
|
<state x="1571" y="336" width="679" height="462" key="EditFileWatcherDialog" timestamp="1578818832892">
|
||||||
|
<screen x="0" y="27" width="2560" height="1053" />
|
||||||
|
</state>
|
||||||
|
<state x="1571" y="336" width="679" height="462" key="EditFileWatcherDialog/0.27.2560.1053@0.27.2560.1053" timestamp="1578818832892" />
|
||||||
|
<state x="1699" y="311" width="424" height="491" key="FileChooserDialogImpl" timestamp="1578818955042">
|
||||||
|
<screen x="0" y="27" width="2560" height="1053" />
|
||||||
|
</state>
|
||||||
|
<state x="1699" y="311" width="424" height="491" key="FileChooserDialogImpl/0.27.2560.1053@0.27.2560.1053" timestamp="1578818955042" />
|
||||||
|
<state x="1420" y="188" width="992" height="737" key="SettingsEditor" timestamp="1578818833773">
|
||||||
|
<screen x="0" y="27" width="2560" height="1053" />
|
||||||
|
</state>
|
||||||
|
<state x="1420" y="188" width="992" height="737" key="SettingsEditor/0.27.2560.1053@0.27.2560.1053" timestamp="1578818833773" />
|
||||||
|
<state x="1533" y="257" width="774" height="592" key="find.popup" timestamp="1578852407526">
|
||||||
|
<screen x="0" y="27" width="2560" height="1053" />
|
||||||
|
</state>
|
||||||
|
<state x="1533" y="257" width="774" height="592" key="find.popup/0.27.2560.1053@0.27.2560.1053" timestamp="1578852407526" />
|
||||||
|
<state x="1583" y="259" width="672" height="678" key="search.everywhere.popup" timestamp="1578831923180">
|
||||||
|
<screen x="0" y="27" width="2560" height="1053" />
|
||||||
|
</state>
|
||||||
|
<state x="1583" y="259" width="672" height="678" key="search.everywhere.popup/0.27.2560.1053@0.27.2560.1053" timestamp="1578831923180" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
23
Dockerfile
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
FROM schmelczera/error-pages as build-error-pages
|
||||||
|
RUN python build.py 403 404 50x
|
||||||
|
|
||||||
|
FROM node:latest as build-webpage
|
||||||
|
WORKDIR /home/node
|
||||||
|
|
||||||
|
COPY src src
|
||||||
|
COPY package.json custom.d.ts tsconfig.json webpack.config.js ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
|
RUN rm -rf *
|
||||||
|
COPY --from=build-webpage /home/node/dist .
|
||||||
|
COPY --from=build-error-pages /home/python/built errors
|
||||||
|
RUN find . -type f | xargs gzip -k9 &&\
|
||||||
|
chmod -R 555 .
|
||||||
|
|
||||||
|
COPY nginx-config /etc/nginx/
|
||||||
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Photos
|
||||||
|
|
||||||
|
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/hu/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/2.5/hu/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/2.5/hu/">Creative Commons Attribution-NonCommercial-ShareAlike 2.5 Hungary License</a>.
|
||||||
16
custom.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
declare module '*.svg' {
|
||||||
|
const content: string;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpg' {
|
||||||
|
import { ResponsiveImage } from 'src/model/responsive-image';
|
||||||
|
const content: ResponsiveImage;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
import { ResponsiveImage } from 'src/model/responsive-image';
|
||||||
|
const content: ResponsiveImage;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
6
desktop.ini
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[.ShellClassInfo]
|
||||||
|
IconResource=D:\Projects\icons\camera.ico,0
|
||||||
|
[ViewState]
|
||||||
|
Mode=
|
||||||
|
Vid=
|
||||||
|
FolderType=Generic
|
||||||
34
docker-compose.yml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
photos:
|
||||||
|
init: true
|
||||||
|
image: schmelczera/photos
|
||||||
|
networks:
|
||||||
|
- network
|
||||||
|
ports:
|
||||||
|
- target: 80
|
||||||
|
protocol: tcp
|
||||||
|
mode: ingress
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 32M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.2'
|
||||||
|
memory: 32M
|
||||||
|
placement:
|
||||||
|
max_replicas_per_node: 1
|
||||||
|
update_config:
|
||||||
|
parallelism: 1
|
||||||
|
failure_action: rollback
|
||||||
|
delay: 10s
|
||||||
|
monitor: 10s
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
window: 30s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
network:
|
||||||
55
nginx-config/nginx.conf
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server_tokens off;
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
sendfile_max_chunk 1m;
|
||||||
|
tcp_nopush on;
|
||||||
|
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_static on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_min_length 10240;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location ~* \.(jpg|jpeg|png|ico)$ {
|
||||||
|
expires 30d;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 403 /403.html;
|
||||||
|
error_page 404 /404.html;
|
||||||
|
error_page 500 501 502 503 504 /50x.html;
|
||||||
|
|
||||||
|
location ~ ^/(403|404|50x).html$ {
|
||||||
|
root /usr/share/nginx/html/errors;
|
||||||
|
internal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
og-image.jpg
(Stored with Git LFS)
Normal file
61
package.json
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"name": "portfolio",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "An easily configurable portfolio.",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "webpack-dev-server --mode development",
|
||||||
|
"build": "webpack && find dist -type f -regex \".*\\(js\\|css\\|txt\\)\" | xargs rm"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/schmelczerandras/timeline.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"CV",
|
||||||
|
"portfolio",
|
||||||
|
"resume",
|
||||||
|
"resumé"
|
||||||
|
],
|
||||||
|
"author": "András Schmelczer",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/schmelczerandras/timeline/issues"
|
||||||
|
},
|
||||||
|
"postcss": {
|
||||||
|
"plugins": {
|
||||||
|
"autoprefixer": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"defaults"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/schmelczerandras/timeline#readme",
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^9.8.6",
|
||||||
|
"clean-webpack-plugin": "^3.0.0",
|
||||||
|
"css-loader": "^3.6.0",
|
||||||
|
"cssnano": "latest",
|
||||||
|
"file-loader": "^5.1.0",
|
||||||
|
"html-webpack-inline-source-plugin": "0.0.10",
|
||||||
|
"html-webpack-plugin": "^3.2.0",
|
||||||
|
"image-webpack-loader": "^6.0.0",
|
||||||
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||||
|
"postcss-loader": "^3.0.0",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
|
"resolve-url-loader": "^3.1.1",
|
||||||
|
"responsive-loader": "^1.2.0",
|
||||||
|
"sass": "^1.26.10",
|
||||||
|
"sass-loader": "^8.0.2",
|
||||||
|
"sharp": "^0.23.4",
|
||||||
|
"style-loader": "^1.2.1",
|
||||||
|
"svg-url-loader": "^3.0.3",
|
||||||
|
"terser-webpack-plugin": "^2.3.8",
|
||||||
|
"ts-loader": "^6.2.2",
|
||||||
|
"typescript": "^3.9.7",
|
||||||
|
"webpack": "^4.44.1",
|
||||||
|
"webpack-cli": "^3.3.12",
|
||||||
|
"webpack-dev-server": "^3.11.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/helper/create-image.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { ResponsiveImage } from '../model/responsive-image';
|
||||||
|
import { last } from './last';
|
||||||
|
|
||||||
|
export const createImage = (
|
||||||
|
image: ResponsiveImage,
|
||||||
|
alt: string,
|
||||||
|
tabIndex: number,
|
||||||
|
imageScreenRatio = 0.25
|
||||||
|
): HTMLImageElement => {
|
||||||
|
const img = new Image(image.width, image.height);
|
||||||
|
|
||||||
|
img.tabIndex = tabIndex;
|
||||||
|
img.srcset = image.srcSet;
|
||||||
|
img.sizes =
|
||||||
|
image.images
|
||||||
|
.map(d => `(max-width: ${d.width / imageScreenRatio}px) ${d.width}px,`)
|
||||||
|
.join('\n') + `\n${last(image.images).width}px`;
|
||||||
|
img.src = last(image.images)?.path;
|
||||||
|
img.alt = alt;
|
||||||
|
|
||||||
|
return img;
|
||||||
|
};
|
||||||
2
src/helper/last.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const last = <T>(list: ArrayLike<T>): T =>
|
||||||
|
list.length >= 1 ? list[list.length - 1] : undefined;
|
||||||
24
src/index.html
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Photos - András Schmelczer</title>
|
||||||
|
<meta name="description" content="A very simple page where I showcase some of my photos. Although I am not a professional photographer, I truly love capturing joyous moments.">
|
||||||
|
<meta name="theme-color" content="#A63446">
|
||||||
|
<meta name="viewport" content="initial-scale=1.0" />
|
||||||
|
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Roboto:100" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>András Schmelczer</h1>
|
||||||
|
</header>
|
||||||
|
<main id="images">
|
||||||
|
<section id="portraits" class="cluster"></section>
|
||||||
|
<div id="frame-container">
|
||||||
|
<img src="" alt="opened photo" id="frame-image"/>
|
||||||
|
</div>
|
||||||
|
<section id="landscapes" class="cluster"></section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
239
src/index.scss
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
@mixin center-children() {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin portrait() {
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin landscape() {
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$text-color: #ffffff;
|
||||||
|
$accent-color: #a63446;
|
||||||
|
$background-color: #eeeeee;
|
||||||
|
$border-width: 5px;
|
||||||
|
$normal-margin: 24px;
|
||||||
|
$small-margin: 6px;
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
outline: $accent-color solid $border-width * 1.1;
|
||||||
|
outline-offset: -$border-width * 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: $border-width solid $text-color;
|
||||||
|
box-sizing: content-box;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background-color: $background-color;
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@include portrait {
|
||||||
|
height: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include landscape {
|
||||||
|
&::-webkit-scrollbar-track,
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
background-color: transparent;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: $accent-color;
|
||||||
|
border-radius: 1000px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
header {
|
||||||
|
font-family: 'Roboto', serif;
|
||||||
|
font-weight: 100;
|
||||||
|
|
||||||
|
@include landscape {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include portrait {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
@include center-children;
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
@include portrait {
|
||||||
|
width: 100%;
|
||||||
|
height: 2.15ch;
|
||||||
|
padding: $normal-margin 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include landscape {
|
||||||
|
width: 2.15ch;
|
||||||
|
height: 100vh;
|
||||||
|
padding: 0 $normal-margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: $text-color;
|
||||||
|
background-color: $accent-color;
|
||||||
|
padding: $normal-margin 0;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
z-index: 2;
|
||||||
|
position: fixed;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@include portrait {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include landscape {
|
||||||
|
width: 101vh;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main#images {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
@include portrait {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cluster {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
img {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&#portraits > img {
|
||||||
|
@include portrait {
|
||||||
|
height: 12vmin;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
@include landscape {
|
||||||
|
width: 8vmin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&#landscapes > img {
|
||||||
|
@include portrait {
|
||||||
|
height: 8vmin;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
@include landscape {
|
||||||
|
width: 12vmin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
@include portrait {
|
||||||
|
margin: 0 $small-margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include portrait {
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
margin: $small-margin 0;
|
||||||
|
padding: $small-margin 0;
|
||||||
|
object-fit: contain;
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
margin-left: 2 * $small-margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 2 * $small-margin !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include landscape {
|
||||||
|
flex-direction: column;
|
||||||
|
margin: $normal-margin;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin: $small-margin 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#frame-container {
|
||||||
|
@include center-children;
|
||||||
|
margin: 0 $border-width;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
@include landscape {
|
||||||
|
}
|
||||||
|
|
||||||
|
@include portrait {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%) translateX(-50%);
|
||||||
|
|
||||||
|
@include portrait {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100% !important; // undo js
|
||||||
|
max-height: 100%;
|
||||||
|
left: 50% !important; // undo js
|
||||||
|
}
|
||||||
|
|
||||||
|
@include landscape {
|
||||||
|
position: fixed;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import './index.scss';
|
||||||
|
import { Photos } from './photos';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const images = require.context('./pictures', true, /.jpg$/);
|
||||||
|
const imagePath = name => images(name, true);
|
||||||
|
|
||||||
|
const addSupportForTabNavigation = () =>
|
||||||
|
(document.onkeydown = e => {
|
||||||
|
if (e.key === ' ') {
|
||||||
|
(document.activeElement as HTMLElement)?.click();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sizeFrame = () => {
|
||||||
|
const container = document.querySelector('#frame-container') as HTMLElement;
|
||||||
|
const image = container.querySelector('img');
|
||||||
|
|
||||||
|
image.style.maxWidth = container.clientWidth + 'px';
|
||||||
|
image.style.left = container.offsetLeft + container.clientWidth / 2 + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
new Photos(
|
||||||
|
images.keys().map(k => imagePath(k)),
|
||||||
|
document.querySelector('#landscapes'),
|
||||||
|
document.querySelector('#portraits'),
|
||||||
|
document.querySelector('#frame-image')
|
||||||
|
);
|
||||||
|
|
||||||
|
addSupportForTabNavigation();
|
||||||
|
window.addEventListener('resize', sizeFrame);
|
||||||
|
sizeFrame();
|
||||||
12
src/model/responsive-image.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export type ResponsiveImage = {
|
||||||
|
srcSet: string;
|
||||||
|
src: string;
|
||||||
|
placeholder: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
images: Array<{
|
||||||
|
path: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
109
src/photos.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { ResponsiveImage } from './model/responsive-image';
|
||||||
|
import { createImage } from './helper/create-image';
|
||||||
|
|
||||||
|
export class Photos {
|
||||||
|
private readonly images: Array<HTMLImageElement>;
|
||||||
|
private _selectedId: number;
|
||||||
|
private timerId: NodeJS.Timeout = null;
|
||||||
|
private shouldNotChange = false;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
images: Array<ResponsiveImage>,
|
||||||
|
private readonly landscapesContainer: HTMLElement,
|
||||||
|
private readonly portraitsContainer: HTMLElement,
|
||||||
|
private readonly pictureFrame: HTMLImageElement
|
||||||
|
) {
|
||||||
|
this.images = this.convertImages(images);
|
||||||
|
this.placeImages();
|
||||||
|
this.addEventListeners();
|
||||||
|
this.startAutoChange();
|
||||||
|
this.selectedId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get selectedId(): number {
|
||||||
|
return this._selectedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private set selectedId(value: number) {
|
||||||
|
if (value < 0) {
|
||||||
|
value += this.images.length - 1;
|
||||||
|
}
|
||||||
|
this._selectedId = value % this.images.length;
|
||||||
|
this.loadToFrame(this.images[this.selectedId]);
|
||||||
|
this.images[this.selectedId].focus();
|
||||||
|
this.shouldNotChange = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertImages(images: Array<ResponsiveImage>) {
|
||||||
|
let portraits = 0; // evens
|
||||||
|
let landscapes = 1; // odds
|
||||||
|
|
||||||
|
const imgs = images.map((image, i) =>
|
||||||
|
createImage(
|
||||||
|
image,
|
||||||
|
'a photo',
|
||||||
|
Photos.isLandscape(image) ? (landscapes += 2) : (portraits += 2)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return imgs.sort((a, b) => a.tabIndex - b.tabIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private placeImages() {
|
||||||
|
this.images.forEach(img => {
|
||||||
|
if (Photos.isLandscape(img)) {
|
||||||
|
img.classList.add('landscape');
|
||||||
|
this.landscapesContainer.appendChild(img);
|
||||||
|
} else {
|
||||||
|
img.classList.add('portrait');
|
||||||
|
this.portraitsContainer.appendChild(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addEventListeners() {
|
||||||
|
window.document.addEventListener('keydown', e => {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
this.selectedId--;
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
this.selectedId++;
|
||||||
|
break;
|
||||||
|
case 'Tab':
|
||||||
|
if (this.timerId !== null) {
|
||||||
|
clearInterval(this.timerId);
|
||||||
|
this.timerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.document.addEventListener(
|
||||||
|
'scroll',
|
||||||
|
() => (this.shouldNotChange = true)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.images.forEach((image, i) =>
|
||||||
|
image.addEventListener('click', () => (this.selectedId = i))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAutoChange() {
|
||||||
|
this.timerId = setInterval(() => {
|
||||||
|
if (!this.shouldNotChange) {
|
||||||
|
this.selectedId++;
|
||||||
|
}
|
||||||
|
this.shouldNotChange = false;
|
||||||
|
}, 7000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadToFrame(img: HTMLImageElement) {
|
||||||
|
this.pictureFrame.src = img.src;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isLandscape(
|
||||||
|
image: HTMLImageElement | ResponsiveImage
|
||||||
|
): boolean {
|
||||||
|
return image.width > image.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/pictures/_DSC1190-Edit.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/pictures/_DSC1484.jpg
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
src/pictures/_DSC1834.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
src/pictures/_DSC2313.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
src/pictures/_DSC2362.jpg
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/pictures/_DSC2674.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/pictures/_DSC2692.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/pictures/_DSC8718-Edit.jpg
Normal file
|
After Width: | Height: | Size: 9.6 MiB |
BIN
src/pictures/adam.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
src/pictures/dorka-1.jpg
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
src/pictures/kezdetazarovonalbol.jpg
Normal file
|
After Width: | Height: | Size: 6.7 MiB |
BIN
src/pictures/nap-2.jpg
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
src/pictures/nyomtatás-10.jpg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
src/pictures/nyomtatás-8.jpg
Normal file
|
After Width: | Height: | Size: 7.6 MiB |
BIN
src/pictures/web-1-2.jpg
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
src/pictures/web-1.jpg
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
src/pictures/web-10.jpg
Normal file
|
After Width: | Height: | Size: 193 KiB |
BIN
src/pictures/web-11.jpg
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
src/pictures/web-12.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
src/pictures/web-13.jpg
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
src/pictures/web-14.jpg
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
src/pictures/web-17.jpg
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
src/pictures/web-18.jpg
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
src/pictures/web-19.jpg
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
src/pictures/web-2-2.jpg
Normal file
|
After Width: | Height: | Size: 413 KiB |
BIN
src/pictures/web-2.jpg
Normal file
|
After Width: | Height: | Size: 353 KiB |
BIN
src/pictures/web-20.jpg
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
src/pictures/web-22.jpg
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
src/pictures/web-23.jpg
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
src/pictures/web-24.jpg
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
src/pictures/web-25.jpg
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
src/pictures/web-26.jpg
Normal file
|
After Width: | Height: | Size: 297 KiB |
BIN
src/pictures/web-3-2.jpg
Normal file
|
After Width: | Height: | Size: 313 KiB |
BIN
src/pictures/web-3.jpg
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
src/pictures/web-4-2.jpg
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
src/pictures/web-42.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/pictures/web-5.jpg
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
src/pictures/web-6.jpg
Normal file
|
After Width: | Height: | Size: 465 KiB |
BIN
src/pictures/web-7.jpg
Normal file
|
After Width: | Height: | Size: 670 KiB |
BIN
src/pictures/web-8.jpg
Normal file
|
After Width: | Height: | Size: 810 KiB |
11
tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist/",
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"module": "es6",
|
||||||
|
"target": "es5",
|
||||||
|
"sourceMap": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"downlevelIteration": true
|
||||||
|
}
|
||||||
|
}
|
||||||
174
webpack.config.js
Normal file
|
|
@ -0,0 +1,174 @@
|
||||||
|
const path = require('path');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||||
|
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||||
|
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');
|
||||||
|
const Sharp = require('responsive-loader/sharp');
|
||||||
|
const Sass = require('sass');
|
||||||
|
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
watchOptions: {
|
||||||
|
ignored: /node_modules/,
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
disableHostCheck: true,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserJSPlugin({
|
||||||
|
sourceMap: !isProduction,
|
||||||
|
}),
|
||||||
|
new OptimizeCSSAssetsPlugin({}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CleanWebpackPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
xhtml: true,
|
||||||
|
template: './src/index.html',
|
||||||
|
minify: {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeComments: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
removeScriptTypeAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
},
|
||||||
|
inlineSource: '.(js|css)$',
|
||||||
|
}),
|
||||||
|
new HtmlWebpackInlineSourcePlugin(),
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: '[name].[contenthash].css',
|
||||||
|
chunkFilename: '[id].[contenthash].css',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
entry: {
|
||||||
|
index: './src/index.ts',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(jpe?g|png)$/i,
|
||||||
|
loader: 'responsive-loader',
|
||||||
|
options: {
|
||||||
|
adapter: Sharp,
|
||||||
|
quality: 90,
|
||||||
|
outputPath: 'static/',
|
||||||
|
sizes: [300, 800, 1200, 2000],
|
||||||
|
placeholder: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(webm|mp4|gif)$/i,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'file-loader',
|
||||||
|
query: {
|
||||||
|
outputPath: 'static/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'image-webpack-loader',
|
||||||
|
options: {
|
||||||
|
disable: !isProduction,
|
||||||
|
mozjpeg: {
|
||||||
|
progressive: true,
|
||||||
|
quality: 65,
|
||||||
|
},
|
||||||
|
optipng: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
pngquant: {
|
||||||
|
quality: [0.65, 0.9],
|
||||||
|
speed: 4,
|
||||||
|
},
|
||||||
|
gifsicle: {
|
||||||
|
interlaced: false,
|
||||||
|
},
|
||||||
|
webp: {
|
||||||
|
quality: 65,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
loader: 'svg-url-loader',
|
||||||
|
options: {
|
||||||
|
limit: 10 * 1024,
|
||||||
|
noquotes: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(pdf)$/i,
|
||||||
|
use: {
|
||||||
|
loader: 'file-loader',
|
||||||
|
query: {
|
||||||
|
outputPath: 'static/',
|
||||||
|
name: '[name].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ico$/i,
|
||||||
|
use: {
|
||||||
|
loader: 'file-loader',
|
||||||
|
query: {
|
||||||
|
outputPath: '/',
|
||||||
|
name: '[name].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/i,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
'css-loader',
|
||||||
|
'postcss-loader',
|
||||||
|
{
|
||||||
|
loader: 'resolve-url-loader',
|
||||||
|
options: {
|
||||||
|
keepQuery: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
sourceMap: true,
|
||||||
|
implementation: Sass,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff2?|ttf|eot|svg)(?:[?#].+)?$/,
|
||||||
|
use: {
|
||||||
|
loader: 'file-loader',
|
||||||
|
options: {
|
||||||
|
name: '[name].[ext]',
|
||||||
|
outputPath: 'static/fonts/',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: /fonts/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.ts$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.js'],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: '[name].[contenthash].js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
};
|
||||||