From 20b719893cbdd7645ee137bd5a9ca2045b8caef5 Mon Sep 17 00:00:00 2001 From: denshooter Date: Fri, 16 Oct 2026 00:00:58 +0200 Subject: [PATCH] feat: plakat redesign for subpages (projects, books, legal, privacy) - Shared SubpageShell (HeaderClient paper strip + ink footer) and SubpageIntro, btn/ghost/card helpers in app/_ui/subpage.tsx - Projects list: poster cards with hard ink shadows, filter pills, ink search field; no fake 800ms skeleton and no framer-motion opacity:0 in SSR - Project detail: poster hero, .plakat-prose markdown, links/stack aside, back link works without JS - Books: server-side fetch (visible without JS), cover + rating pills - Legal notice / privacy: moved to app/_ui/*Client.tsx, root routes removed (middleware always redirects to /[locale]), consent reset via useConsent - ProjectThumbnail rebuilt on Plakat tokens - Old Header.tsx + test removed; brown/cream/sand palettes and unused glass-panel/admin-glass(-light)/liquid-bg-blob/float CSS dropped (liquid palette, glass-card, admin-glass-card stay for admin + 404) - .plakat-prose replaces the missing typography plugin Co-Authored-By: Claude Opus 5.5 --- .claude/rules/components.md | 14 +- app/[locale]/books/page.tsx | 213 ++++++++++--------- app/[locale]/legal-notice/page.tsx | 13 +- app/[locale]/privacy-policy/page.tsx | 13 +- app/[locale]/projects/[slug]/page.tsx | 5 +- app/[locale]/projects/page.tsx | 7 +- app/__tests__/components/Header.test.tsx | 34 --- app/__tests__/pre-push-check.test.ts | 14 +- app/_ui/LegalNoticeClient.tsx | 80 +++++++ app/_ui/PrivacyPolicyClient.tsx | 177 ++++++++++++++++ app/_ui/ProjectDetailClient.tsx | 214 ++++++++----------- app/_ui/ProjectsPageClient.tsx | 256 +++++++++++++---------- app/_ui/SubpageShell.tsx | 27 +++ app/_ui/subpage.tsx | 48 +++++ app/_ui/useCmsPage.ts | 29 +++ app/components/Header.tsx | 98 --------- app/components/ProjectThumbnail.tsx | 131 ++++-------- app/globals.css | 179 +++++++++++----- app/legal-notice/page.tsx | 135 ------------ app/privacy-policy/page.tsx | 216 ------------------- docs/CHANGING_TEXTS.md | 4 +- tailwind.config.ts | 16 +- 22 files changed, 918 insertions(+), 1005 deletions(-) delete mode 100644 app/__tests__/components/Header.test.tsx create mode 100644 app/_ui/LegalNoticeClient.tsx create mode 100644 app/_ui/PrivacyPolicyClient.tsx create mode 100644 app/_ui/SubpageShell.tsx create mode 100644 app/_ui/subpage.tsx create mode 100644 app/_ui/useCmsPage.ts delete mode 100644 app/components/Header.tsx delete mode 100644 app/legal-notice/page.tsx delete mode 100644 app/privacy-policy/page.tsx diff --git a/.claude/rules/components.md b/.claude/rules/components.md index d7976e9..6454563 100644 --- a/.claude/rules/components.md +++ b/.claude/rules/components.md @@ -18,11 +18,15 @@ import ScrollFadeIn from "@/app/components/ScrollFadeIn" `AnimatePresence` is fine for modals and overlays that only appear after user interaction. -## Design system -- Colors: only `liquid-*` tokens — no hardcoded hex or raw Tailwind palette colors -- Cards: `bg-gradient-to-br from-liquid-*/15 via-liquid-*/10 to-liquid-*/15 backdrop-blur-sm border-2 rounded-xl` -- Headlines: `uppercase tracking-tighter` with accent dot `.` -- Body text: `text-stone-600 dark:text-stone-400` — never `text-stone-400` alone (fails contrast) +## Design system (Plakat) +- Colors: only the Plakat tokens `signal`, `ink`, `paper`, `paper-2`, `ok` — no hex, no raw Tailwind palette, no `liquid-*` (left over only in admin and 404) +- Ink and paper swap roles in dark mode on their own; `signal` stays red +- Fonts: `font-display` (Bricolage, `[font-variation-settings:'wdth'_80]` for posters), `font-sans` (Geist), `font-mono` (Geist Mono) +- Cards: `rounded-[18px] border-2 border-ink` + hard offset shadow `shadow-[6px_6px_0_rgb(var(--plakat-ink))]` +- Pills: `rounded-full` with `shadow-[inset_0_0_0_1.5px_rgb(var(--plakat-ink))]`, filled variant `bg-ink text-paper` +- Labels: `font-mono text-[13px]`; nothing below 12px +- Subpages: wrap in `app/_ui/SubpageShell.tsx`, reuse `SubpageIntro`, `btn`, `ghost`, `card` from `app/_ui/subpage.tsx`; long text in `.plakat-prose` +- Motion: `ease-out` / `ease-drawer`, press feedback `active:scale-[0.97]`, never `transition-all` ## Async components Every component that fetches data must have a Skeleton loading state shown while data loads. diff --git a/app/[locale]/books/page.tsx b/app/[locale]/books/page.tsx index 3cd33b8..490d9e8 100644 --- a/app/[locale]/books/page.tsx +++ b/app/[locale]/books/page.tsx @@ -1,114 +1,121 @@ -"use client"; - -import { Star, ArrowLeft } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; -import { useEffect, useState } from "react"; -import { useLocale, useTranslations } from "next-intl"; -import { Skeleton } from "@/app/components/ui/Skeleton"; -import { BookReview } from "@/lib/directus"; -import Header from "@/app/components/Header"; -import Footer from "@/app/components/Footer"; -import ScrollFadeIn from "@/app/components/ScrollFadeIn"; +import type { Metadata } from "next"; +import { getTranslations } from "next-intl/server"; +import { getBookReviews, type BookReview } from "@/lib/directus"; +import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; +import SubpageShell from "@/app/_ui/SubpageShell"; +import { BackArrow, SubpageIntro, card, ghost, monoLabel } from "@/app/_ui/subpage"; -export default function BooksPage() { - const locale = useLocale(); - const t = useTranslations("books"); - const common = useTranslations("common"); - const [reviews, setReviews] = useState([]); - const [loading, setLoading] = useState(true); +export const revalidate = 300; - useEffect(() => { - const fetchBooks = async () => { - try { - const res = await fetch(`/api/book-reviews?locale=${locale}`); - const data = await res.json(); - if (data.bookReviews) setReviews(data.bookReviews); - } catch { - if (process.env.NODE_ENV === "development") { - // console.error in dev only - } - } finally { - setLoading(false); - } - }; - fetchBooks(); - }, [locale]); +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + const isDe = locale === "de"; + return { + title: isDe ? "Bibliothek – Dennis Konkol" : "Library – Dennis Konkol", + alternates: { + canonical: toAbsoluteUrl(`/${locale}/books`), + languages: getLanguageAlternates({ pathWithoutLocale: "books" }), + }, + }; +} + +export default async function BooksPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "books" }); + const common = await getTranslations({ locale, namespace: "common" }); + + // Serverseitig laden, damit die Liste auch ohne JavaScript dasteht + let reviews: BookReview[] = []; + try { + reviews = (await getBookReviews(locale)) ?? []; + } catch (err) { + if (process.env.NODE_ENV === "development") console.error("Book reviews fetch failed:", err); + } return ( -
-
- +
+ {text && ( +
+ + {text} +
+ )} + ); -} \ No newline at end of file +} diff --git a/app/[locale]/legal-notice/page.tsx b/app/[locale]/legal-notice/page.tsx index 4ab1de6..c487780 100644 --- a/app/[locale]/legal-notice/page.tsx +++ b/app/[locale]/legal-notice/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; -export { default } from "../../legal-notice/page"; +import SubpageShell from "@/app/_ui/SubpageShell"; +import LegalNoticeClient from "@/app/_ui/LegalNoticeClient"; export async function generateMetadata({ params, @@ -10,6 +11,7 @@ export async function generateMetadata({ const { locale } = await params; const languages = getLanguageAlternates({ pathWithoutLocale: "legal-notice" }); return { + title: locale === "de" ? "Impressum – Dennis Konkol" : "Legal notice – Dennis Konkol", alternates: { canonical: toAbsoluteUrl(`/${locale}/legal-notice`), languages, @@ -17,3 +19,12 @@ export async function generateMetadata({ }; } + +export default async function Page({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + return ( + + + + ); +} diff --git a/app/[locale]/privacy-policy/page.tsx b/app/[locale]/privacy-policy/page.tsx index 1f5b0cd..65f005a 100644 --- a/app/[locale]/privacy-policy/page.tsx +++ b/app/[locale]/privacy-policy/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; -export { default } from "../../privacy-policy/page"; +import SubpageShell from "@/app/_ui/SubpageShell"; +import PrivacyPolicyClient from "@/app/_ui/PrivacyPolicyClient"; export async function generateMetadata({ params, @@ -10,6 +11,7 @@ export async function generateMetadata({ const { locale } = await params; const languages = getLanguageAlternates({ pathWithoutLocale: "privacy-policy" }); return { + title: locale === "de" ? "Datenschutz – Dennis Konkol" : "Privacy policy – Dennis Konkol", alternates: { canonical: toAbsoluteUrl(`/${locale}/privacy-policy`), languages, @@ -17,3 +19,12 @@ export async function generateMetadata({ }; } + +export default async function Page({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + return ( + + + + ); +} diff --git a/app/[locale]/projects/[slug]/page.tsx b/app/[locale]/projects/[slug]/page.tsx index 6416294..c60422d 100644 --- a/app/[locale]/projects/[slug]/page.tsx +++ b/app/[locale]/projects/[slug]/page.tsx @@ -1,5 +1,6 @@ import { prisma } from "@/lib/prisma"; import ProjectDetailClient from "@/app/_ui/ProjectDetailClient"; +import SubpageShell from "@/app/_ui/SubpageShell"; import { notFound } from "next/navigation"; import type { Metadata } from "next"; import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; @@ -107,13 +108,13 @@ export default async function ProjectPage({ }; return ( - <> +