From 4d56d4904aac4366f2c8a65a679e1a9dd15b54fc Mon Sep 17 00:00:00 2001 From: denshooter Date: Mon, 16 Feb 2026 03:48:46 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20pers=C3=B6nliche=20Gedenkseite=20?= =?UTF-8?q?=E2=80=93=20Tribute,=20Autoplay-Musik,=20Next.js=2016=20Fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TributeSection: zwei Perspektiven (Familie + Dennis), emotional und persönlich - MusicPlayer: Autoplay mit stummem Start + Fade-In bei Interaktion, nahtloser Crossfade-Loop (überspringt stille letzten 10s), kompakter Mute-Button - Alle API-Routes: export const runtime = 'nodejs' für node:sqlite in Next.js 16 - Admin: defensive res.ok Checks vor .json() Parsing - DB: mkdirSync erst zur Laufzeit, path.resolve für DATA_DIR - page.tsx: plain() Helper für null-prototype SQLite-Rows - erinnerungen.md: alle Familieninfos und Erinnerungen dokumentiert - Nav: Musik-Tab entfernt, "Über Oma" hinzugefügt Co-Authored-By: Claude Opus 4.6 --- erinnerungen.md | 62 +++ package-lock.json | 711 +++++++++++++++++++++++---- package.json | 2 +- src/app/admin/page.tsx | 6 +- src/app/api/auth/route.ts | 2 + src/app/api/files/[...path]/route.ts | 2 +- src/app/api/media/[id]/route.ts | 4 +- src/app/api/media/route.ts | 2 + src/app/api/memories/[id]/route.ts | 2 + src/app/api/memories/route.ts | 2 + src/app/api/upload/route.ts | 2 +- src/app/impressum/page.tsx | 4 +- src/app/page.tsx | 41 +- src/components/MusicPlayer.tsx | 584 +++++----------------- src/components/TributeSection.tsx | 248 ++++++++++ src/lib/db.ts | 14 +- tsconfig.json | 33 +- 17 files changed, 1121 insertions(+), 600 deletions(-) create mode 100644 erinnerungen.md create mode 100644 src/components/TributeSection.tsx diff --git a/erinnerungen.md b/erinnerungen.md new file mode 100644 index 0000000..e15db21 --- /dev/null +++ b/erinnerungen.md @@ -0,0 +1,62 @@ +# Maria Malejka — Erinnerungen + +## Lebensdaten +- **Geboren:** 29. November 1944 +- **Gestorben:** 10. Februar 2026 (Lungenembolie, im Krankenhaus) +- **Beerdigung:** 20. Februar 2026 (Donnerstag) +- **Urnenbestattung** (Verbrennung am 16. Februar 2026) + +## Familie +- **Ehemann:** Josef Malejka (diamantene Hochzeit, über 60 Jahre verheiratet) +- **Töchter:** Beate, Renate, Margret +- **Enkel:** Dennis (Ersteller dieser Seite), Jacky (Dennis' Schwester) +- **Jackys Ehemann:** Niklas +- **Jacky** ist schwanger, hat den Ultraschall mit in den Sarg gelegt +- **Herkunft:** Kam aus Polen nach Deutschland mit ihren drei Töchtern + +## Omas Haus +- Jacky und Niklas haben Omas Haus 2025 gekauft und renovieren es +- Neues Leben in alten Wänden, dort wird bald ein Kind aufwachsen + +## Charakter & Wesen +- Einfach gestrickt im besten Sinne, kein Schickimicki, kein großes Trara +- Hat nichts weggeworfen, alles wiederverwendet +- Tierlieb: hatte Vögel und ihren Hund **Ronny** +- Liebte den Familienhund **Pico** (Hund von Beate/Dennis' Familie). Pico ist 2025 ebenfalls verstorben +- Stur auf eine liebevolle Art, hat Dinge auf ihre Weise gemacht +- Direkt und persönlich, aber warmherzig und ruhig +- Hat die Familie zusammengehalten +- Hat es geliebt, zu geben +- Roch nach "Oma": dezent, warm, vertraut, nicht nach Parfum + +## Weihnachten bei Oma und Opa +- Immer heiß, der Kamin lief immer +- Karpfen in der Badewanne (polnische Tradition) +- Berge von Essen +- Geschenke im Wohnzimmer, aber die Tür blieb zu bis das Essen fertig war +- Kein Schummeln + +## Erinnerungen +- **Rahmsauce** nach der Schule, immer +- Omas Kochen war legendär, mit Liebe gemacht +- Hat mehr mit Pico geredet als mit den Menschen wenn er zu Besuch kam + +## Die letzten Tage +- Oberschenkelhalsbruch (Sturheit, wollte alles alleine machen) +- Nach der Hüft-OP ging es ihr gut, sie lief schon wieder +- Im Aufenthaltsraum der Geriatrie: Blumenvasen gesehen, "die könnte man ja mitnehmen" +- Sollte in einer Woche in die Reha +- Ihr Zimmer lag gegenüber dem Zimmer, in dem Beate (Dennis' Mutter) wochenlang lag als sie mit Dennis schwanger war +- Im selben Flur, in dem Dennis auf die Welt kam, ist Oma gegangen +- Lungenembolie, Kollaps im Zimmer, Reanimation, Intensivstation +- 10. Februar 2026, 13:13 Uhr: Nachricht von Jacky über Telegram: "Sie möchten die Reanimation abbrechen." + +## Persönliches (Dennis) +- "Ich habe es dir viel zu selten gesagt, Oma. Aber ich habe dich geliebt. Sehr." +- Es kam unerwartet, kein langer Abschied, kein letztes Gespräch +- Pico und Oma: vielleicht sind sie jetzt wieder zusammen + +## Tonalität der Gedenkseite +- Direkt und persönlich +- Liebevoll und ruhig +- So wie Oma war diff --git a/package-lock.json b/package-lock.json index 3e3044a..1a67710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "framer-motion": "^11.2.0", "lucide-react": "^0.400.0", - "next": "14.2.5", + "next": "^16.1.6", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -37,6 +37,482 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -77,15 +553,15 @@ } }, "node_modules/@next/env": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", - "integrity": "sha512-/zZGkrTOsraVfYjGP8uM0p6r0BDT6xWpkjdVbcz66PJVSpwXX3yNiRycxAuDfBKGWBrZBXRuK/YVlkNgxHGwmA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.5.tgz", - "integrity": "sha512-/9zVxJ+K9lrzSGli1///ujyRfon/ZneeZ+v4ptpiPoOU+GKZnm8Wj8ELWU1Pm7GHltYRBklmXMTUqM/DqQ99FQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", "cpu": [ "arm64" ], @@ -99,9 +575,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.5.tgz", - "integrity": "sha512-vXHOPCwfDe9qLDuq7U1OYM2wUY+KQ4Ex6ozwsKxp26BlJ6XXbHleOUldenM67JRyBfVjv371oneEvYd3H2gNSA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", "cpu": [ "x64" ], @@ -115,9 +591,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.5.tgz", - "integrity": "sha512-vlhB8wI+lj8q1ExFW8lbWutA4M2ZazQNvMWuEDqZcuJJc78iUnLdPPunBPX8rC4IgT6lIx/adB+Cwrl99MzNaA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", "cpu": [ "arm64" ], @@ -131,9 +607,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.5.tgz", - "integrity": "sha512-NpDB9NUR2t0hXzJJwQSGu1IAOYybsfeB+LxpGsXrRIb7QOrYmidJz3shzY8cM6+rO4Aojuef0N/PEaX18pi9OA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", "cpu": [ "arm64" ], @@ -147,9 +623,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.5.tgz", - "integrity": "sha512-8XFikMSxWleYNryWIjiCX+gU201YS+erTUidKdyOVYi5qUQo/gRxv/3N1oZFCgqpesN6FPeqGM72Zve+nReVXQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", "cpu": [ "x64" ], @@ -163,9 +639,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.5.tgz", - "integrity": "sha512-6QLwi7RaYiQDcRDSU/os40r5o06b5ue7Jsk5JgdRBGGp8l37RZEh9JsLSM8QF0YDsgcosSeHjglgqi25+m04IQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", "cpu": [ "x64" ], @@ -179,9 +655,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.5.tgz", - "integrity": "sha512-1GpG2VhbspO+aYoMOQPQiqc/tG3LzmsdBH0LhnDS3JrtDx2QmzXe0B6mSZZiN3Bq7IOMXxv1nlsjzoS1+9mzZw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", "cpu": [ "arm64" ], @@ -194,26 +670,10 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.5.tgz", - "integrity": "sha512-Igh9ZlxwvCDsu6438FXlQTHlRno4gFpJzqPjSIBZooD22tKeI4fE/YMRoHVJHmrQ2P5YL1DoZ0qaOKkbeFWeMg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.5.tgz", - "integrity": "sha512-tEQ7oinq1/CjSG9uSTerca3v4AZ+dFa+4Yu6ihaG8Ud8ddqLQgFGcnwYls13H5X5CPDPZJdYxyeMui6muOLd4g==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", "cpu": [ "x64" ], @@ -264,20 +724,13 @@ "node": ">= 8" } }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", - "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@types/node": { @@ -387,7 +840,6 @@ "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -453,17 +905,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -568,6 +1009,16 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -731,12 +1182,6 @@ "node": ">=10.13.0" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -939,42 +1384,41 @@ } }, "node_modules/next": { - "version": "14.2.5", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.5.tgz", - "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "license": "MIT", "dependencies": { - "@next/env": "14.2.5", - "@swc/helpers": "0.5.5", - "busboy": "1.6.0", + "@next/env": "16.1.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1" + "styled-jsx": "5.1.6" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=18.17.0" + "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.5", - "@next/swc-darwin-x64": "14.2.5", - "@next/swc-linux-arm64-gnu": "14.2.5", - "@next/swc-linux-arm64-musl": "14.2.5", - "@next/swc-linux-x64-gnu": "14.2.5", - "@next/swc-linux-x64-musl": "14.2.5", - "@next/swc-win32-arm64-msvc": "14.2.5", - "@next/swc-win32-ia32-msvc": "14.2.5", - "@next/swc-win32-x64-msvc": "14.2.5" + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", + "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "peerDependenciesMeta": { @@ -984,6 +1428,9 @@ "@playwright/test": { "optional": true }, + "babel-plugin-react-compiler": { + "optional": true + }, "sass": { "optional": true } @@ -1397,6 +1844,64 @@ "loose-envify": "^1.1.0" } }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1406,18 +1911,10 @@ "node": ">=0.10.0" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", "license": "MIT", "dependencies": { "client-only": "0.0.1" @@ -1426,7 +1923,7 @@ "node": ">= 12.0.0" }, "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "peerDependenciesMeta": { "@babel/core": { diff --git a/package.json b/package.json index 53e168b..2176aa0 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dependencies": { "framer-motion": "^11.2.0", "lucide-react": "^0.400.0", - "next": "14.2.5", + "next": "^16.1.6", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 9af94c3..33f9288 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -64,10 +64,8 @@ export default function AdminPage() { fetch('/api/memories'), fetch('/api/media'), ]) - const [memoriesData, mediaData] = await Promise.all([ - memoriesRes.json(), - mediaRes.json(), - ]) + const memoriesData = memoriesRes.ok ? await memoriesRes.json() : [] + const mediaData = mediaRes.ok ? await mediaRes.json() : [] setMemories(Array.isArray(memoriesData) ? memoriesData : []) const items: MediaItem[] = Array.isArray(mediaData) ? mediaData : [] setPhotos(items.filter((m) => m.type === 'photo')) diff --git a/src/app/api/auth/route.ts b/src/app/api/auth/route.ts index 6be3cbc..933d3c4 100644 --- a/src/app/api/auth/route.ts +++ b/src/app/api/auth/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server' import { createHash } from 'crypto' import { cookies } from 'next/headers' +export const runtime = 'nodejs' + function getExpectedToken() { return createHash('sha256') .update(process.env.ADMIN_PASSWORD || 'change-me') diff --git a/src/app/api/files/[...path]/route.ts b/src/app/api/files/[...path]/route.ts index 9f1e374..5bc338f 100644 --- a/src/app/api/files/[...path]/route.ts +++ b/src/app/api/files/[...path]/route.ts @@ -4,7 +4,7 @@ import path from 'path' export const runtime = 'nodejs' -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data') +const DATA_DIR = path.resolve(process.cwd(), process.env.DATA_DIR || 'data') const MIME: Record = { '.jpg': 'image/jpeg', diff --git a/src/app/api/media/[id]/route.ts b/src/app/api/media/[id]/route.ts index 3a5b597..2eaa212 100644 --- a/src/app/api/media/[id]/route.ts +++ b/src/app/api/media/[id]/route.ts @@ -6,6 +6,8 @@ import { unlink } from 'fs/promises' import { existsSync } from 'fs' import path from 'path' +export const runtime = 'nodejs' + async function isAdmin() { const cookieStore = await cookies() const token = cookieStore.get('admin_auth')?.value @@ -15,7 +17,7 @@ async function isAdmin() { return token === expected } -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data') +const DATA_DIR = path.resolve(process.cwd(), process.env.DATA_DIR || 'data') export async function DELETE( _req: NextRequest, diff --git a/src/app/api/media/route.ts b/src/app/api/media/route.ts index 3144e0a..7c986b5 100644 --- a/src/app/api/media/route.ts +++ b/src/app/api/media/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' import { getDb } from '@/lib/db' +export const runtime = 'nodejs' + export async function GET(req: NextRequest) { const type = req.nextUrl.searchParams.get('type') const db = getDb() diff --git a/src/app/api/memories/[id]/route.ts b/src/app/api/memories/[id]/route.ts index f7e8c00..2e4069c 100644 --- a/src/app/api/memories/[id]/route.ts +++ b/src/app/api/memories/[id]/route.ts @@ -3,6 +3,8 @@ import { getDb } from '@/lib/db' import { cookies } from 'next/headers' import { createHash } from 'crypto' +export const runtime = 'nodejs' + async function isAdmin() { const cookieStore = await cookies() const token = cookieStore.get('admin_auth')?.value diff --git a/src/app/api/memories/route.ts b/src/app/api/memories/route.ts index 4112c4b..c01b70a 100644 --- a/src/app/api/memories/route.ts +++ b/src/app/api/memories/route.ts @@ -1,6 +1,8 @@ import { NextRequest, NextResponse } from 'next/server' import { getDb } from '@/lib/db' +export const runtime = 'nodejs' + export async function GET() { const db = getDb() const memories = db diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 22251e9..f4616a5 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -17,7 +17,7 @@ async function isAdmin() { return token === expected } -const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data') +const DATA_DIR = path.resolve(process.cwd(), process.env.DATA_DIR || 'data') const MIME_TO_FOLDER: Record = { 'image/jpeg': 'photos', diff --git a/src/app/impressum/page.tsx b/src/app/impressum/page.tsx index e5ca647..661d02b 100644 --- a/src/app/impressum/page.tsx +++ b/src/app/impressum/page.tsx @@ -37,7 +37,7 @@ export default function ImpressumPage() { Angaben gemäß § 5 TMG
-

Dennis Malejka

+

Dennis Konkol

(Kontaktdaten auf Anfrage)

@@ -99,7 +99,7 @@ export default function ImpressumPage() {

Alle auf dieser Seite veröffentlichten Fotos und Medien sind privates - Eigentum der Familie Malejka. Eine Weitergabe oder Veröffentlichung ohne + Eigentum der Familie. Eine Weitergabe oder Veröffentlichung ohne ausdrückliche Genehmigung ist nicht gestattet.

diff --git a/src/app/page.tsx b/src/app/page.tsx index 678c766..60dfc9d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,24 +7,30 @@ import MemorySection from '@/components/MemorySection' import WriteSection from '@/components/WriteSection' import MusicPlayer from '@/components/MusicPlayer' import VideoGallery from '@/components/VideoGallery' +import TributeSection from '@/components/TributeSection' export const dynamic = 'force-dynamic' +// node:sqlite returns null-prototype objects; convert to plain objects for Client Components +function plain(rows: unknown[]): T[] { + return JSON.parse(JSON.stringify(rows)) +} + export default async function HomePage() { const db = getDb() - const photos = db - .prepare("SELECT * FROM media WHERE type = 'photo' ORDER BY sort_order, created_at") - .all() as MediaItem[] - const videos = db - .prepare("SELECT * FROM media WHERE type = 'video' ORDER BY sort_order, created_at") - .all() as MediaItem[] - const music = db - .prepare("SELECT * FROM media WHERE type = 'music' ORDER BY sort_order, created_at") - .all() as MediaItem[] - const memories = db - .prepare('SELECT * FROM memories ORDER BY created_at DESC') - .all() as Memory[] + const photos = plain( + db.prepare("SELECT * FROM media WHERE type = 'photo' ORDER BY sort_order, created_at").all() + ) + const videos = plain( + db.prepare("SELECT * FROM media WHERE type = 'video' ORDER BY sort_order, created_at").all() + ) + const music = plain( + db.prepare("SELECT * FROM media WHERE type = 'music' ORDER BY sort_order, created_at").all() + ) + const memories = plain( + db.prepare('SELECT * FROM memories ORDER BY created_at DESC').all() + ) return (
@@ -34,6 +40,9 @@ export default async function HomePage() { {/* Navigation */} + {/* Personal tribute */} + + {/* Photos */} {photos.length > 0 && (
@@ -84,7 +93,7 @@ export default async function HomePage() { {/* Videos */} - {/* Music – always rendered (ambient fallback when no tracks) */} + {/* Floating music player */} {/* Footer */} diff --git a/src/components/MusicPlayer.tsx b/src/components/MusicPlayer.tsx index acd3c3b..6cb9038 100644 --- a/src/components/MusicPlayer.tsx +++ b/src/components/MusicPlayer.tsx @@ -1,481 +1,161 @@ 'use client' import { useState, useRef, useEffect, useCallback } from 'react' -import { motion, AnimatePresence } from 'framer-motion' -import { - Play, - Pause, - SkipForward, - SkipBack, - Volume2, - VolumeX, - X, -} from 'lucide-react' +import { Volume2, VolumeX } from 'lucide-react' import type { MediaItem } from '@/lib/types' -// ─── helpers ──────────────────────────────────────────────────────────────── - -function formatTime(s: number) { - if (!s || isNaN(s) || !isFinite(s)) return '--:--' - const m = Math.floor(s / 60) - const sec = Math.floor(s % 60) - return `${m}:${sec.toString().padStart(2, '0')}` -} - -function WaveformBars({ playing }: { playing: boolean }) { - return ( -
- {[0.55, 1, 0.7, 0.9, 0.5].map((h, i) => ( - - ))} -
- ) -} - -// ─── ambient audio via Web Audio API ──────────────────────────────────────── -// -// "Singing bowls" synthesis: each tone is a struck resonance with -// natural exponential decay – sounds like real crystal/tibetan bowls, -// not oscillator drones. Appropriate and peaceful for a memorial. -// -// Scale: A-minor pentatonic (A3 C4 D4 E4 G4 A4 C5 E5) -// Timing: random gaps of 3–8 s between strikes, so it breathes naturally. -// Reverb: two long delay tails for a large, warm hall. - -function useAmbient() { - const ctxRef = useRef(null) - const timerRef = useRef[]>([]) - const [playing, setPlaying] = useState(false) - - const start = useCallback(() => { - if (ctxRef.current) return - const ctx = new AudioContext() - ctxRef.current = ctx - - // ── Signal chain ──────────────────────────────────────────────────── - const master = ctx.createGain() - master.gain.value = 0.55 - master.connect(ctx.destination) - - // Long hall reverb (two delay tails, no feedback) - const mkTail = (t: number, wet: number) => { - const d = ctx.createDelay(6.0) - d.delayTime.value = t - const g = ctx.createGain() - g.gain.value = wet - d.connect(g) - g.connect(ctx.destination) - return d - } - const tail1 = mkTail(2.4, 0.30) - const tail2 = mkTail(4.8, 0.18) - - // ── Bowl strike ────────────────────────────────────────────────────── - // Two slightly detuned sine waves → natural "shimmer beat" of a real bowl. - // Gain follows exponential decay → sounds exactly like a struck bowl. - const strike = (freq: number, vol: number) => { - if (!ctxRef.current) return - const now = ctx.currentTime - const decay = 7 + Math.random() * 5 // 7–12 s natural ring - const detune = freq * 0.0025 // 0.25 % → slow shimmer beat - - ;[freq, freq + detune].forEach((f, i) => { - const osc = ctx.createOscillator() - osc.type = 'sine' - osc.frequency.value = f - - const g = ctx.createGain() - // Instant attack (bowl is struck), then pure exponential decay - g.gain.setValueAtTime(vol * (i === 0 ? 1 : 0.6), now) - g.gain.exponentialRampToValueAtTime(0.0001, now + decay) - - osc.connect(g) - g.connect(master) - g.connect(tail1) - g.connect(tail2) - osc.start(now) - osc.stop(now + decay + 0.05) - }) - } - - // ── Scale ─────────────────────────────────────────────────────────── - // A-minor pentatonic – every note here sounds consonant with every other. - // Weighted toward lower, warmer tones (more prominent) vs. higher (softer). - const bowls: { freq: number; vol: number }[] = [ - { freq: 220.00, vol: 0.38 }, // A3 – deep, warm anchor - { freq: 261.63, vol: 0.32 }, // C4 - { freq: 293.66, vol: 0.28 }, // D4 - { freq: 329.63, vol: 0.30 }, // E4 - { freq: 392.00, vol: 0.26 }, // G4 - { freq: 440.00, vol: 0.22 }, // A4 - { freq: 523.25, vol: 0.18 }, // C5 – bright, light - { freq: 659.25, vol: 0.14 }, // E5 – delicate shimmer - ] - - const pickBowl = () => bowls[Math.floor(Math.random() * bowls.length)] - - // ── Scheduler ─────────────────────────────────────────────────────── - // Occasional harmonic pairs (two bowls together) feel more musical. - const scheduleNext = () => { - if (!ctxRef.current) return - - const gap = 3000 + Math.random() * 5000 // 3–8 s between strikes - - // ~30 % chance of a soft harmonic pair (two bowls a fifth apart) - const b = pickBowl() - strike(b.freq, b.vol) - if (Math.random() < 0.30) { - const fifth = b.freq * 1.5 // perfect fifth - strike(fifth, b.vol * 0.55) // softer companion tone - } - - timerRef.current.push(setTimeout(scheduleNext, gap)) - } - - // Open with three spaced strikes to fill the silence gently - strike(220.00, 0.38) // A3 immediately - timerRef.current.push(setTimeout(() => { if (ctxRef.current) strike(329.63, 0.30) }, 2800)) // E4 - timerRef.current.push(setTimeout(() => { if (ctxRef.current) strike(261.63, 0.28) }, 5200)) // C4 - timerRef.current.push(setTimeout(scheduleNext, 7500)) // then random - - setPlaying(true) - }, []) - - const stop = useCallback(() => { - timerRef.current.forEach(clearTimeout) - timerRef.current = [] - const ctx = ctxRef.current - if (ctx) { - // Brief master fade so the currently ringing bowls die out naturally - const fade = ctx.createGain() - fade.gain.setValueAtTime(1, ctx.currentTime) - fade.gain.linearRampToValueAtTime(0, ctx.currentTime + 2.5) - fade.connect(ctx.destination) - setTimeout(() => { ctx.close(); ctxRef.current = null }, 2700) - } - setPlaying(false) - }, []) - - const toggle = useCallback(() => { - if (playing) stop() - else start() - }, [playing, start, stop]) - - return { playing, toggle, start, stop } -} - -// ─── component ────────────────────────────────────────────────────────────── +const TAIL_SKIP = 10 +const CROSSFADE_DURATION = 3 +const VOLUME = 0.4 +const FADE_IN_DURATION = 2000 // ms to fade in after first interaction export default function MusicPlayer({ tracks }: { tracks: MediaItem[] }) { - const [current, setCurrent] = useState(0) + const track = tracks[0] ?? null + const src = track ? `/api/files/${track.filename}` : null + + const audioA = useRef(null) + const audioB = useRef(null) + const activeRef = useRef<'A' | 'B'>('A') + const fadingRef = useRef(false) + const unmutedRef = useRef(false) + + const [userMuted, setUserMuted] = useState(false) const [playing, setPlaying] = useState(false) - const [volume, setVolume] = useState(0.4) - const [muted, setMuted] = useState(false) - const [progress, setProgress] = useState(0) - const [duration, setDuration] = useState(0) - const [elapsed, setElapsed] = useState(0) - const [miniVisible, setMiniVisible] = useState(false) - const audioRef = useRef(null) - const ambient = useAmbient() - const hasTrack = tracks.length > 0 - const track = tracks[current] ?? null + const getActive = useCallback( + () => (activeRef.current === 'A' ? audioA.current : audioB.current), + [], + ) + const getInactive = useCallback( + () => (activeRef.current === 'A' ? audioB.current : audioA.current), + [], + ) - const trackName = (i: number) => - tracks[i]?.original_name?.replace(/\.[^/.]+$/, '') || - tracks[i]?.caption || - `Titel ${i + 1}` + const getVolume = useCallback( + () => (userMuted ? 0 : VOLUME), + [userMuted], + ) + // Crossfade to loop back + const crossfade = useCallback(() => { + if (fadingRef.current) return + fadingRef.current = true + + const out = getActive()! + const inp = getInactive()! + activeRef.current = activeRef.current === 'A' ? 'B' : 'A' + + inp.currentTime = 0 + inp.volume = 0 + inp.play().catch(() => {}) + + const startTime = performance.now() + const outStartVol = out.volume + const targetVol = getVolume() + + const step = () => { + const t = Math.min((performance.now() - startTime) / (CROSSFADE_DURATION * 1000), 1) + inp.volume = targetVol * t + out.volume = outStartVol * (1 - t) + if (t < 1) { + requestAnimationFrame(step) + } else { + out.pause() + out.currentTime = 0 + fadingRef.current = false + } + } + requestAnimationFrame(step) + }, [getVolume, getActive, getInactive]) + + // Monitor playback for crossfade trigger useEffect(() => { - const a = audioRef.current - if (!a) return - a.volume = muted ? 0 : volume - }, [volume, muted]) + if (!playing || !src) return + let id: number + const tick = () => { + const a = getActive() + if (a && a.duration) { + const remaining = a.duration - a.currentTime + if (remaining <= TAIL_SKIP && !fadingRef.current) { + crossfade() + } + } + id = requestAnimationFrame(tick) + } + id = requestAnimationFrame(tick) + return () => cancelAnimationFrame(id) + }, [playing, src, getActive, crossfade]) + // Fallback: if audio ends without crossfade, restart + const handleEnded = useCallback(() => { + const a = getActive() + if (a) { + a.currentTime = 0 + a.play().catch(() => {}) + } + }, [getActive]) + + // Sync volume when user toggles mute useEffect(() => { - const a = audioRef.current - if (!a || !track) return - a.src = `/api/files/${track.filename}` - a.volume = muted ? 0 : volume - setDuration(0); setElapsed(0); setProgress(0) - if (playing) a.play().catch(() => setPlaying(false)) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [current]) + if (!unmutedRef.current) return + const vol = getVolume() + const a = getActive() + const b = getInactive() + if (a && !fadingRef.current) a.volume = vol + if (b && !fadingRef.current && !b.paused) b.volume = vol + }, [userMuted, getVolume, getActive, getInactive]) - const togglePlay = () => { - const a = audioRef.current + // Autoplay strategy: + // 1. Start muted (browsers allow this) + // 2. On first user interaction (scroll/click/touch/key), fade volume in + useEffect(() => { + if (!src) return + const a = audioA.current if (!a) return - if (playing) a.pause() - else a.play().catch(() => setPlaying(false)) - } - const playTrack = (i: number) => { - if (i === current) togglePlay() - else { setCurrent(i); setPlaying(true) } - setMiniVisible(true) - } + // Start muted playback immediately + a.volume = 0 + a.play().then(() => setPlaying(true)).catch(() => {}) - const prev = () => setCurrent((c) => (c - 1 + tracks.length) % tracks.length) - const next = () => setCurrent((c) => (c + 1) % tracks.length) + // Fade in on first interaction + const fadeIn = () => { + if (unmutedRef.current) return + unmutedRef.current = true - const handleTimeUpdate = () => { - const a = audioRef.current - if (!a || !a.duration) return - setProgress((a.currentTime / a.duration) * 100) - setElapsed(a.currentTime) - } + const active = activeRef.current === 'A' ? audioA.current : audioB.current + if (!active) return - const handleSeek = (e: React.ChangeEvent) => { - const a = audioRef.current - if (!a || !a.duration) return - const pct = parseFloat(e.target.value) - a.currentTime = (pct / 100) * a.duration - setProgress(pct) - } + const startTime = performance.now() + const step = () => { + const t = Math.min((performance.now() - startTime) / FADE_IN_DURATION, 1) + active.volume = VOLUME * t + if (t < 1) requestAnimationFrame(step) + } + requestAnimationFrame(step) + } - // Decide what the mini-player shows - const miniLabel = hasTrack ? trackName(current) : 'Stille Begleitung' - const miniPlaying = hasTrack ? playing : ambient.playing + const events = ['scroll', 'click', 'touchstart', 'keydown'] as const + events.forEach((e) => window.addEventListener(e, fadeIn, { once: true, passive: true })) + + return () => { + events.forEach((e) => window.removeEventListener(e, fadeIn)) + } + }, [src]) + + if (!track || !src) return null return ( <> - {hasTrack && ( -