From 4bf0e2221307b29ae847ab408fc61b774405b87b Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:20:56 +0200 Subject: [PATCH 1/3] chore: add a formatter, matching the style this repo already writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit singleQuote=true was chosen by counting this repo's own imports, not by fleet decree. The fleet is genuinely split and the two repos that already had a .prettierrc disagreed with each other, so there was no standard to restore. Quote style does not cross repo boundaries; having a gate does. Markdown is ignored for now — prettier rewraps prose, which would bury the real diff. Co-Authored-By: Claude Opus 5 --- .prettierignore | 21 +++++++++++++++++++++ .prettierrc | 9 +++++++++ package-lock.json | 20 ++++++++++++++++++++ package.json | 7 +++++-- 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1ed5ea5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,21 @@ +# Build output and vendored trees — formatting these is noise. +node_modules +.next +dist +build +out +coverage +.turbo +.vercel +*.min.js +*.min.css + +# Lockfiles are generated; prettier would rewrite them wholesale. +package-lock.json +pnpm-lock.yaml +yarn.lock + +# Markdown is deliberately out of scope for now. Prettier rewraps prose, which +# is where it is most opinionated and least useful, and it would bury the real +# diff. Remove this line when you want docs formatted too. +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..616247a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "all", + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/package-lock.json b/package-lock.json index 340b96e..3c21a66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,12 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.3.3", + "prettier": "3.9.6", "typescript": "^5", "vitest": "^4.1.11" + }, + "engines": { + "node": ">=20" } }, "node_modules/@babel/code-frame": { @@ -5834,6 +5838,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/package.json b/package.json index fad0d77..4804916 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ "start": "next start", "lint": "eslint", "typecheck": "tsc --noEmit", - "verify": "npm run lint && npm run typecheck && npm run test && npm run build", + "verify": "npm run format:check && npm run lint && npm run typecheck && npm run test && npm run build", "media": "python3 scripts/process-media.py", - "test": "vitest run" + "test": "vitest run", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "next": "16.3.3", @@ -26,6 +28,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "16.3.3", + "prettier": "3.9.6", "typescript": "^5", "vitest": "^4.1.11" } From b13c7d4ef9f9dd3a3014901c05406046c691deec Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:21:58 +0200 Subject: [PATCH 2/3] style: format with prettier (20 files) Mechanical. No behaviour change. This SHA is listed in .git-blame-ignore-revs so `git blame` skips it. Co-Authored-By: Claude Opus 5 --- .github/dependabot.yml | 4 +- app/api/enquiry/route.ts | 7 +-- app/sitemap.ts | 4 +- components/Book.tsx | 4 +- components/BookingForm.tsx | 27 +++++--- components/Gallery.tsx | 7 +-- components/Nav.tsx | 6 +- components/StickyBook.tsx | 4 +- content/works.json | 110 +++++++-------------------------- eslint.config.mjs | 14 ++--- lib/enquiry.ts | 9 ++- lib/i18n/dictionaries/de-CH.ts | 6 +- lib/i18n/dictionaries/fr.ts | 9 +-- lib/i18n/dictionaries/ja.ts | 3 +- lib/i18n/dictionaries/ko.ts | 3 +- lib/i18n/dictionaries/tr.ts | 3 +- proxy.ts | 3 +- scripts/shots.mjs | 46 ++++++++------ tests/i18n.test.ts | 9 ++- tests/regressions.test.ts | 4 +- 20 files changed, 114 insertions(+), 168 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a646416..554fa37 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,7 +11,7 @@ version: 2 updates: - package-ecosystem: npm - directory: "/" + directory: '/' schedule: interval: weekly open-pull-requests-limit: 5 @@ -20,7 +20,7 @@ updates: update-types: [minor, patch] - package-ecosystem: github-actions - directory: "/" + directory: '/' schedule: interval: weekly open-pull-requests-limit: 3 diff --git a/app/api/enquiry/route.ts b/app/api/enquiry/route.ts index 4b01245..d409d5e 100644 --- a/app/api/enquiry/route.ts +++ b/app/api/enquiry/route.ts @@ -120,12 +120,7 @@ export async function POST(request: NextRequest) { // Log the whole enquiry, not just the failure. The send is the only copy in // flight, so without this a bad API key silently costs him the booking — // journald keeps it until someone can forward it by hand. - console.error( - '[enquiry] send failed:', - result.reason, - '\n', - formatEnquiry(values, locale), - ); + console.error('[enquiry] send failed:', result.reason, '\n', formatEnquiry(values, locale)); return NextResponse.json({ ok: false, error: 'sendFailed' }, { status: 502 }); } diff --git a/app/sitemap.ts b/app/sitemap.ts index 6012abf..2d9f315 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -11,9 +11,7 @@ import { SITE } from '@/lib/site'; * be indexed. */ export default function sitemap(): MetadataRoute.Sitemap { - const languages = Object.fromEntries( - LOCALES.map((l) => [HTML_LANG[l], `${SITE.url}/${l}`]), - ); + const languages = Object.fromEntries(LOCALES.map((l) => [HTML_LANG[l], `${SITE.url}/${l}`])); return LOCALES.map((locale) => ({ url: `${SITE.url}/${locale}`, diff --git a/components/Book.tsx b/components/Book.tsx index ae2731b..c714e93 100644 --- a/components/Book.tsx +++ b/components/Book.tsx @@ -59,9 +59,7 @@ export function Book({ t, locale }: { t: Dictionary; locale: Locale }) { target="_blank" rel="noreferrer noopener" > - - {String(i + 1).padStart(2, '0')} - + {String(i + 1).padStart(2, '0')} {d.label} {d.handle} {d.note} diff --git a/components/BookingForm.tsx b/components/BookingForm.tsx index e4faf6c..9db89c2 100644 --- a/components/BookingForm.tsx +++ b/components/BookingForm.tsx @@ -102,10 +102,13 @@ export function BookingForm({ t, locale }: { t: Dictionary; locale: Locale }) { } if (body.fields) setErrors(body.fields); setState('failed'); - setFailure(body.error === 'rateLimited' ? t.booking.errors.rateLimited - : body.error === 'fileTooLarge' || body.error === 'fileType' - ? t.booking.errors.fileRejected - : t.booking.errors.failed); + setFailure( + body.error === 'rateLimited' + ? t.booking.errors.rateLimited + : body.error === 'fileTooLarge' || body.error === 'fileType' + ? t.booking.errors.fileRejected + : t.booking.errors.failed, + ); } catch { setState('failed'); setFailure(t.booking.errors.failed); @@ -127,10 +130,14 @@ export function BookingForm({ t, locale }: { t: Dictionary; locale: Locale }) {
{ENQUIRY_FIELDS.map((field) => { const error = errors[field.id]; - const hint = field.id in t.booking.hints - ? t.booking.hints[field.id as keyof typeof t.booking.hints] - : undefined; - const describedBy = [hint && `${fieldId(field.id)}-hint`, error && `${fieldId(field.id)}-err`] + const hint = + field.id in t.booking.hints + ? t.booking.hints[field.id as keyof typeof t.booking.hints] + : undefined; + const describedBy = [ + hint && `${fieldId(field.id)}-hint`, + error && `${fieldId(field.id)}-err`, + ] .filter(Boolean) .join(' '); @@ -193,7 +200,9 @@ export function BookingForm({ t, locale }: { t: Dictionary; locale: Locale }) { type="file" multiple accept={REFERENCES.accept.join(',')} - onChange={(e) => setFiles([...(e.target.files ?? [])].slice(0, REFERENCES.max).map((f) => f.name))} + onChange={(e) => + setFiles([...(e.target.files ?? [])].slice(0, REFERENCES.max).map((f) => f.name)) + } /> {t.booking.referencesHint} {files.length > 0 ? ( diff --git a/components/Gallery.tsx b/components/Gallery.tsx index 591bd00..490d4fb 100644 --- a/components/Gallery.tsx +++ b/components/Gallery.tsx @@ -21,8 +21,7 @@ export function Gallery({ t, works }: { t: Dictionary; works: Work[] }) { const close = useCallback(() => setOpen(null), []); const step = useCallback( - (dir: 1 | -1) => - setOpen((i) => (i === null ? i : (i + dir + works.length) % works.length)), + (dir: 1 | -1) => setOpen((i) => (i === null ? i : (i + dir + works.length) % works.length)), [works.length], ); @@ -65,9 +64,7 @@ export function Gallery({ t, works }: { t: Dictionary; works: Work[] }) { - - {String(i + 1).padStart(2, '0')} - + {String(i + 1).padStart(2, '0')} {work.title} {work.style} / {work.placement} diff --git a/components/Nav.tsx b/components/Nav.tsx index 73ef07c..a8c8dfc 100644 --- a/components/Nav.tsx +++ b/components/Nav.tsx @@ -58,11 +58,7 @@ export function Nav({ t, locale }: { t: Dictionary; locale: Locale }) {
    {NAV_IDS.map((id) => (
  • - + {t.nav[id]}
  • diff --git a/components/StickyBook.tsx b/components/StickyBook.tsx index 9e1f1ab..3c9d0ec 100644 --- a/components/StickyBook.tsx +++ b/components/StickyBook.tsx @@ -24,9 +24,7 @@ export function StickyBook({ t }: { t: Dictionary }) { const update = () => { const pastHero = window.scrollY > window.innerHeight * 0.9; - const atBook = book - ? book.getBoundingClientRect().top < window.innerHeight * 0.85 - : false; + const atBook = book ? book.getBoundingClientRect().top < window.innerHeight * 0.85 : false; setShown(pastHero && !atBook); }; diff --git a/content/works.json b/content/works.json index afad7fc..5403b11 100644 --- a/content/works.json +++ b/content/works.json @@ -5,10 +5,7 @@ "source": "WhatsApp Image 2026-08-06 at 15.31.01.jpeg", "title": "Sami Tutar", "shape": "wide", - "focus": [ - 0.5, - 0.4 - ], + "focus": [0.5, 0.4], "zoom": 1.0, "grade": "portrait" }, @@ -21,10 +18,7 @@ "style": "Blackwork", "shape": "wide", "featured": true, - "focus": [ - 0.5, - 0.44 - ], + "focus": [0.5, 0.44], "zoom": 1.25 }, { @@ -34,10 +28,7 @@ "placement": "Shoulder", "style": "Blackwork", "shape": "portrait", - "focus": [ - 0.44, - 0.42 - ], + "focus": [0.44, 0.42], "zoom": 1.35 }, { @@ -47,10 +38,7 @@ "placement": "Forearm", "style": "Ornamental", "shape": "portrait", - "focus": [ - 0.45, - 0.53 - ], + "focus": [0.45, 0.53], "zoom": 1.9 }, { @@ -60,10 +48,7 @@ "placement": "Forearm", "style": "Fine line", "shape": "portrait", - "focus": [ - 0.45, - 0.55 - ], + "focus": [0.45, 0.55], "zoom": 1.4 }, { @@ -73,10 +58,7 @@ "placement": "Forearm", "style": "Blackwork", "shape": "portrait", - "focus": [ - 0.45, - 0.42 - ], + "focus": [0.45, 0.42], "zoom": 1.5 }, { @@ -86,10 +68,7 @@ "placement": "Ribs", "style": "Lettering", "shape": "portrait", - "focus": [ - 0.5, - 0.5 - ], + "focus": [0.5, 0.5], "zoom": 1.4 }, { @@ -99,10 +78,7 @@ "placement": "Chest", "style": "Fine line", "shape": "wide", - "focus": [ - 0.56, - 0.5 - ], + "focus": [0.56, 0.5], "zoom": 1.45 }, { @@ -112,10 +88,7 @@ "placement": "Ribs", "style": "Fine line", "shape": "portrait", - "focus": [ - 0.5, - 0.38 - ], + "focus": [0.5, 0.38], "zoom": 2.0 }, { @@ -125,10 +98,7 @@ "placement": "Forearm", "style": "Blackwork", "shape": "portrait", - "focus": [ - 0.45, - 0.45 - ], + "focus": [0.45, 0.45], "zoom": 1.2 }, { @@ -138,10 +108,7 @@ "placement": "Forearm", "style": "Ornamental", "shape": "portrait", - "focus": [ - 0.55, - 0.5 - ], + "focus": [0.55, 0.5], "zoom": 1.35 }, { @@ -151,10 +118,7 @@ "placement": "Forearm", "style": "Fine line", "shape": "portrait", - "focus": [ - 0.47, - 0.36 - ], + "focus": [0.47, 0.36], "zoom": 1.5 }, { @@ -165,10 +129,7 @@ "style": "Lettering", "shape": "wide", "rotate": 180, - "focus": [ - 0.49, - 0.475 - ], + "focus": [0.49, 0.475], "zoom": 1.6 }, { @@ -178,10 +139,7 @@ "placement": "Shin", "style": "Blackwork", "shape": "portrait", - "focus": [ - 0.57, - 0.495 - ], + "focus": [0.57, 0.495], "zoom": 1.85 }, { @@ -191,10 +149,7 @@ "placement": "Forearm", "style": "Fine line", "shape": "portrait", - "focus": [ - 0.5, - 0.45 - ], + "focus": [0.5, 0.45], "zoom": 1.15 }, { @@ -204,10 +159,7 @@ "placement": "Hand", "style": "Blackwork", "shape": "portrait", - "focus": [ - 0.57, - 0.52 - ], + "focus": [0.57, 0.52], "zoom": 1.8 }, { @@ -217,10 +169,7 @@ "placement": "Calf", "style": "Lettering", "shape": "portrait", - "focus": [ - 0.615, - 0.455 - ], + "focus": [0.615, 0.455], "zoom": 2.0 }, { @@ -230,10 +179,7 @@ "placement": "Shoulder", "style": "Fine line", "shape": "wide", - "focus": [ - 0.5, - 0.63 - ], + "focus": [0.5, 0.63], "zoom": 1.2 }, { @@ -243,10 +189,7 @@ "placement": "Calf", "style": "Fine line", "shape": "portrait", - "focus": [ - 0.47, - 0.32 - ], + "focus": [0.47, 0.32], "zoom": 2.2 }, { @@ -256,10 +199,7 @@ "placement": "Forearm", "style": "Fine line", "shape": "portrait", - "focus": [ - 0.42, - 0.48 - ], + "focus": [0.42, 0.48], "zoom": 1.45 }, { @@ -269,10 +209,7 @@ "placement": "Forearm", "style": "Blackwork", "shape": "portrait", - "focus": [ - 0.42, - 0.36 - ], + "focus": [0.42, 0.36], "zoom": 1.7 }, { @@ -282,10 +219,7 @@ "placement": "Forearm", "style": "Lettering", "shape": "wide", - "focus": [ - 0.47, - 0.36 - ], + "focus": [0.47, 0.36], "zoom": 1.35 } ], diff --git a/eslint.config.mjs b/eslint.config.mjs index 05e726d..626ca82 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,6 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; +import { defineConfig, globalIgnores } from 'eslint/config'; +import nextVitals from 'eslint-config-next/core-web-vitals'; +import nextTs from 'eslint-config-next/typescript'; const eslintConfig = defineConfig([ ...nextVitals, @@ -8,10 +8,10 @@ const eslintConfig = defineConfig([ // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", + '.next/**', + 'out/**', + 'build/**', + 'next-env.d.ts', ]), ]); diff --git a/lib/enquiry.ts b/lib/enquiry.ts index 88808b0..6c5fb38 100644 --- a/lib/enquiry.ts +++ b/lib/enquiry.ts @@ -79,7 +79,9 @@ export const AGE_FIELD = 'age'; export type EnquiryValues = Record; -export type EnquiryErrors = Partial>; +export type EnquiryErrors = Partial< + Record +>; const EMAIL_SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; @@ -88,7 +90,10 @@ const EMAIL_SHAPE = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/; * truth. Returns error *codes*, not sentences — the caller translates them, so * this file never has to know which language the visitor is reading. */ -export function validateEnquiry(values: Partial, ageConfirmed: boolean): EnquiryErrors { +export function validateEnquiry( + values: Partial, + ageConfirmed: boolean, +): EnquiryErrors { const errors: EnquiryErrors = {}; for (const field of ENQUIRY_FIELDS) { diff --git a/lib/i18n/dictionaries/de-CH.ts b/lib/i18n/dictionaries/de-CH.ts index 63cbe78..94b0816 100644 --- a/lib/i18n/dictionaries/de-CH.ts +++ b/lib/i18n/dictionaries/de-CH.ts @@ -61,7 +61,8 @@ const deCH: Dictionary = { sending: 'Wird gesendet…', sentTitle: 'Gesendet.', sentBody: 'Er liest jede Nachricht selbst und antwortet innert weniger Tage.', - privacy: 'Deine Nachricht geht direkt in sein Postfach. Auf dieser Seite wird nichts gespeichert.', + privacy: + 'Deine Nachricht geht direkt in sein Postfach. Auf dieser Seite wird nichts gespeichert.', orEmail: 'Oder schreib ihm direkt', errors: { required: 'Erforderlich', @@ -76,7 +77,8 @@ const deCH: Dictionary = { copy: 'Adresse kopieren', copied: 'Kopiert', mailSubject: 'Tattoo-Anfrage — S.Ink', - mailBody: 'Hoi Sami,\n\nReferenz: \nStelle: \nUngefähre Grösse: \nMeine Verfügbarkeit: \n\nDanke', + mailBody: + 'Hoi Sami,\n\nReferenz: \nStelle: \nUngefähre Grösse: \nMeine Verfügbarkeit: \n\nDanke', }, footer: { backToTop: 'Nach oben' }, labels: { diff --git a/lib/i18n/dictionaries/fr.ts b/lib/i18n/dictionaries/fr.ts index 5467bae..1982aaa 100644 --- a/lib/i18n/dictionaries/fr.ts +++ b/lib/i18n/dictionaries/fr.ts @@ -54,8 +54,8 @@ const fr: Dictionary = { size: 'Environ, en cm.', }, references: 'Images de référence', - referencesHint: 'Jusqu\'à 3. Captures, croquis ou l\'endroit sur le corps.', - age: 'J\'ai 18 ans ou plus.', + referencesHint: "Jusqu'à 3. Captures, croquis ou l'endroit sur le corps.", + age: "J'ai 18 ans ou plus.", submit: 'Envoyer la demande', sending: 'Envoi…', sentTitle: 'Envoyé.', @@ -68,14 +68,15 @@ const fr: Dictionary = { tooLong: 'Trop long', fileRejected: 'Images uniquement, 5 Mo maximum chacune.', rateLimited: 'Trop de messages. Réessayez plus tard.', - failed: 'Échec de l\'envoi. Écrivez-lui directement :', + failed: "Échec de l'envoi. Écrivez-lui directement :", }, }, contact: { copy: 'Copier l’adresse', copied: 'Copié', mailSubject: 'Demande de tatouage — S.Ink', - mailBody: 'Salut Sami,\n\nRéférence : \nEmplacement : \nTaille approximative : \nMes disponibilités : \n\nMerci', + mailBody: + 'Salut Sami,\n\nRéférence : \nEmplacement : \nTaille approximative : \nMes disponibilités : \n\nMerci', }, footer: { backToTop: 'Haut de page' }, labels: { diff --git a/lib/i18n/dictionaries/ja.ts b/lib/i18n/dictionaries/ja.ts index 3afcb08..b1fc281 100644 --- a/lib/i18n/dictionaries/ja.ts +++ b/lib/i18n/dictionaries/ja.ts @@ -82,7 +82,8 @@ const ja: Dictionary = { copy: 'アドレスをコピー', copied: 'コピーしました', mailSubject: 'タトゥーのご相談 — S.Ink', - mailBody: 'Sami さん\n\n参考画像:\n部位:\nおおよそのサイズ:\n希望日程:\n\nよろしくお願いします', + mailBody: + 'Sami さん\n\n参考画像:\n部位:\nおおよそのサイズ:\n希望日程:\n\nよろしくお願いします', }, footer: { backToTop: 'トップへ' }, labels: { diff --git a/lib/i18n/dictionaries/ko.ts b/lib/i18n/dictionaries/ko.ts index b3254e5..7adb4db 100644 --- a/lib/i18n/dictionaries/ko.ts +++ b/lib/i18n/dictionaries/ko.ts @@ -75,7 +75,8 @@ const ko: Dictionary = { copy: '주소 복사', copied: '복사됨', mailSubject: '타투 문의 — S.Ink', - mailBody: '안녕하세요 Sami,\n\n레퍼런스: \n위치: \n대략적인 크기: \n가능한 일정: \n\n감사합니다', + mailBody: + '안녕하세요 Sami,\n\n레퍼런스: \n위치: \n대략적인 크기: \n가능한 일정: \n\n감사합니다', }, footer: { backToTop: '맨 위로' }, labels: { diff --git a/lib/i18n/dictionaries/tr.ts b/lib/i18n/dictionaries/tr.ts index 589a58a..a087d34 100644 --- a/lib/i18n/dictionaries/tr.ts +++ b/lib/i18n/dictionaries/tr.ts @@ -75,7 +75,8 @@ const tr: Dictionary = { copy: 'Adresi kopyala', copied: 'Kopyalandı', mailSubject: 'Dövme talebi — S.Ink', - mailBody: 'Merhaba Sami,\n\nReferans: \nYer: \nYaklaşık boyut: \nMüsait olduğum zamanlar: \n\nTeşekkürler', + mailBody: + 'Merhaba Sami,\n\nReferans: \nYer: \nYaklaşık boyut: \nMüsait olduğum zamanlar: \n\nTeşekkürler', }, footer: { backToTop: 'Başa dön' }, labels: { diff --git a/proxy.ts b/proxy.ts index 9dc070a..5c8d9f6 100644 --- a/proxy.ts +++ b/proxy.ts @@ -22,8 +22,7 @@ export function proxy(request: NextRequest) { // Already carrying a language — leave it alone. if (isLocale(segments[0] ?? '')) return NextResponse.next(); - const isSection = - segments.length === 1 && (NAV_IDS as readonly string[]).includes(segments[0]); + const isSection = segments.length === 1 && (NAV_IDS as readonly string[]).includes(segments[0]); // Everything else genuinely does not exist, and saying so is the honest // answer: bouncing every unknown URL to the front page is a soft 404, which // search engines treat as a lie and which leaves a real 404 page unreachable. diff --git a/scripts/shots.mjs b/scripts/shots.mjs index fc80fea..d33000b 100644 --- a/scripts/shots.mjs +++ b/scripts/shots.mjs @@ -55,31 +55,37 @@ for (const s of SHOTS) { // Wait on the page's own state rather than on a guessed delay: every image in // view decoded, and every reveal finished rather than merely started. - await page.waitForFunction(() => { - const imagesReady = [...document.images].every((i) => !i.complete || i.naturalWidth > 0); - const revealsSettled = [...document.querySelectorAll('.reveal')].every((el) => { - const r = el.getBoundingClientRect(); - // Only demand a settled opacity from elements that are actually due to - // reveal — same trigger line the observer uses. An element peeking in at - // the very bottom edge is correctly still hidden, and waiting on it hangs. - const due = r.bottom > 0 && r.top < window.innerHeight * 0.88; - return !due || getComputedStyle(el).opacity === '1'; - }); - return imagesReady && revealsSettled; - }, { timeout: 15000 }); + await page.waitForFunction( + () => { + const imagesReady = [...document.images].every((i) => !i.complete || i.naturalWidth > 0); + const revealsSettled = [...document.querySelectorAll('.reveal')].every((el) => { + const r = el.getBoundingClientRect(); + // Only demand a settled opacity from elements that are actually due to + // reveal — same trigger line the observer uses. An element peeking in at + // the very bottom edge is correctly still hidden, and waiting on it hangs. + const due = r.bottom > 0 && r.top < window.innerHeight * 0.88; + return !due || getComputedStyle(el).opacity === '1'; + }); + return imagesReady && revealsSettled; + }, + { timeout: 15000 }, + ); // Finally wait for text to stop moving. The wordmark scrambles itself on load, // so a shot taken the moment the images are ready catches it mid-flight and // photographs "5Z / AAA" where the brand should be. Watching the text rather // than naming the component keeps this honest for anything animated later. - await page.waitForFunction(() => { - const now = document.body.innerText; - const w = /** @type {any} */ (window); - const stable = w.__prevText === now ? (w.__stableFor ?? 0) + 1 : 0; - w.__prevText = now; - w.__stableFor = stable; - return stable >= 3; - }, { timeout: 15000, polling: 150 }); + await page.waitForFunction( + () => { + const now = document.body.innerText; + const w = /** @type {any} */ (window); + const stable = w.__prevText === now ? (w.__stableFor ?? 0) + 1 : 0; + w.__prevText = now; + w.__stableFor = stable; + return stable >= 3; + }, + { timeout: 15000, polling: 150 }, + ); await page.screenshot({ path: `${out}/${s.name}.jpg`, quality: 88, type: 'jpeg' }); await page.close(); diff --git a/tests/i18n.test.ts b/tests/i18n.test.ts index 9c3364a..99354d5 100644 --- a/tests/i18n.test.ts +++ b/tests/i18n.test.ts @@ -28,7 +28,14 @@ describe('dictionaries', () => { for (const field of ENQUIRY_FIELDS) { expect(t.booking.fields[field.id], `missing label: ${field.id}`).toBeTruthy(); } - for (const code of ['required', 'email', 'tooLong', 'fileRejected', 'rateLimited', 'failed'] as const) { + for (const code of [ + 'required', + 'email', + 'tooLong', + 'fileRejected', + 'rateLimited', + 'failed', + ] as const) { expect(t.booking.errors[code], `missing error: ${code}`).toBeTruthy(); } }); diff --git a/tests/regressions.test.ts b/tests/regressions.test.ts index b8f7ef8..26e0858 100644 --- a/tests/regressions.test.ts +++ b/tests/regressions.test.ts @@ -38,9 +38,7 @@ describe('regressions', () => { // the metadata value paints the browser chrome around it. They drift apart // silently and only show up as a seam on mobile. const token = /--primitive-void:\s*(#[0-9a-f]{3,8})/i.exec(read('app/globals.css'))?.[1]; - const theme = /themeColor:\s*'(#[0-9a-f]{3,8})'/i.exec( - read('app/[locale]/layout.tsx'), - )?.[1]; + const theme = /themeColor:\s*'(#[0-9a-f]{3,8})'/i.exec(read('app/[locale]/layout.tsx'))?.[1]; expect(token).toBeDefined(); expect(theme?.toLowerCase()).toBe(token?.toLowerCase()); }); From 0d2908b18038b6eed948f4ad281be1cc2af56324 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+catomean@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:22:00 +0200 Subject: [PATCH 3/3] chore: teach git blame to skip the reformat Co-Authored-By: Claude Opus 5 --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..1c8de4a --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Bulk reformats. `git config blame.ignoreRevsFile .git-blame-ignore-revs` +b13c7d4ef9f9dd3a3014901c05406046c691deec # prettier, 20 files