Clean up
|
|
@ -7,12 +7,15 @@ on:
|
|||
branches: ['main']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: 'pages'
|
||||
cancel-in-progress: false
|
||||
group: 'photos-${{ github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
validate:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
|
|
@ -21,18 +24,46 @@ jobs:
|
|||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '12'
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm install
|
||||
run: npm ci
|
||||
|
||||
- name: Check
|
||||
run: npm run check
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
npm run build
|
||||
run: npm run build
|
||||
|
||||
- name: Copy files to nginx pages mount
|
||||
- name: Install Playwright browser
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: End-to-end smoke test
|
||||
run: npm run test:e2e
|
||||
|
||||
deploy:
|
||||
runs-on: docker
|
||||
needs: validate
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Copy build to host pages mount
|
||||
run: |
|
||||
apt update && apt install -y rsync
|
||||
rsync -a --delete --mkpath --chmod=ugo=rwx dist/ /pages/photos/
|
||||
mkdir -p /pages/photos
|
||||
rsync -a --delete dist/ /pages/photos/
|
||||
|
|
|
|||
1
.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
|||
og-image.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
5
.gitignore
vendored
|
|
@ -1,5 +1,8 @@
|
|||
node_modules
|
||||
dist
|
||||
target
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
public/static/photos
|
||||
src/generated
|
||||
playwright-report
|
||||
test-results
|
||||
|
|
|
|||
1
.nvmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
22.13.0
|
||||
26
custom.d.ts
vendored
|
|
@ -1,26 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
||||
declare module '*.txt' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.html' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
38
eslint.config.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'dist/**',
|
||||
'public/static/photos/**',
|
||||
'src/generated/**',
|
||||
'playwright-report/**',
|
||||
'test-results/**',
|
||||
],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.js', '**/*.mjs'],
|
||||
languageOptions: {
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
BIN
og-image.jpg
|
Before Width: | Height: | Size: 131 B After Width: | Height: | Size: 243 KiB |
5495
package-lock.json
generated
Normal file
BIN
public/og-image.jpg
Normal file
|
After Width: | Height: | Size: 243 KiB |
4
public/robots.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: https://schmelczer.dev/photos/sitemap.xml
|
||||
15
public/site.webmanifest
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "Photos - András Schmelczer",
|
||||
"short_name": "Photos",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#eeeeee",
|
||||
"theme_color": "#a63446",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.svg",
|
||||
"sizes": "64x64",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
]
|
||||
}
|
||||
6
public/sitemap.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://schmelczer.dev/photos/</loc>
|
||||
</url>
|
||||
</urlset>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
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;
|
||||
};
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export const last = <T>(list: ArrayLike<T>): T =>
|
||||
list.length >= 1 ? list[list.length - 1] : undefined;
|
||||
44
src/index.ts
|
|
@ -1,35 +1,19 @@
|
|||
import './index.scss';
|
||||
import './no-change/404.html';
|
||||
import './no-change/robots.txt';
|
||||
import { Photos } from './photos';
|
||||
import { photos } from './generated/photos';
|
||||
import { PhotoGallery, requiredElement } from './photos';
|
||||
|
||||
// @ts-ignore
|
||||
const images = require.context('./pictures', true, /.jpg$/);
|
||||
const imagePath = name => images(name, true);
|
||||
document.documentElement.classList.add('js');
|
||||
|
||||
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')
|
||||
const gallery = requiredElement<HTMLElement>('#gallery', HTMLElement);
|
||||
const frame = requiredElement<HTMLElement>('#frame-content', HTMLElement);
|
||||
const toggle = requiredElement<HTMLButtonElement>(
|
||||
'#slideshow-toggle',
|
||||
HTMLButtonElement
|
||||
);
|
||||
|
||||
addSupportForTabNavigation();
|
||||
window.addEventListener('resize', sizeFrame);
|
||||
sizeFrame();
|
||||
new PhotoGallery({
|
||||
photos,
|
||||
gallery,
|
||||
frame,
|
||||
toggle,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
export type ResponsiveImage = {
|
||||
srcSet: string;
|
||||
src: string;
|
||||
placeholder: string;
|
||||
width: number;
|
||||
height: number;
|
||||
images: Array<{
|
||||
path: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}>;
|
||||
};
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
|
||||
<title>Not found</title>
|
||||
<meta name="theme-color" content="#b7455e" />
|
||||
<meta name="viewport" content="initial-scale=1.0" />
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #b7455e;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: "Roboto", "Helvetica Neue", sans-serif;
|
||||
font-weight: 100;
|
||||
font-size: 3rem;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>The requested resource cannot be found.</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
User-agent: *
|
||||
Allow: /
|
||||
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 3 MiB After Width: | Height: | Size: 3 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 9.6 MiB After Width: | Height: | Size: 7.4 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 6.7 MiB After Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 6.5 MiB After Width: | Height: | Size: 2 MiB |
|
Before Width: | Height: | Size: 7.6 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 265 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 413 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 429 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 381 KiB After Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 356 KiB After Width: | Height: | Size: 392 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 437 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 820 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 136 KiB |
|
Before Width: | Height: | Size: 465 KiB After Width: | Height: | Size: 572 KiB |
|
Before Width: | Height: | Size: 670 KiB After Width: | Height: | Size: 691 KiB |
|
Before Width: | Height: | Size: 810 KiB After Width: | Height: | Size: 920 KiB |
|
|
@ -1,11 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"noImplicitAny": false,
|
||||
"module": "es6",
|
||||
"target": "es5",
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
"downlevelIteration": true
|
||||
}
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"types": ["vite/client", "vitest/globals"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.json",
|
||||
"vite.config.ts",
|
||||
"eslint.config.js"
|
||||
]
|
||||
}
|
||||
|
|
|
|||