diff --git a/app/__tests__/pre-push-check.test.ts b/app/__tests__/pre-push-check.test.ts index f62d2cf..b0f75b6 100644 --- a/app/__tests__/pre-push-check.test.ts +++ b/app/__tests__/pre-push-check.test.ts @@ -138,6 +138,7 @@ const STATIC_KEYS: Record> = { consent: { ConsentBanner: ["title", "description", "essential", "chat", "alwaysOn", "acceptAll", "acceptSelected", "rejectAll", "hide"] }, "home.hero": { Hero: ["kicker", "claim", "intro", "countdown", "ctaWork", "ctaContact"] }, "home.tisch": { TischSection: ["title", "allProjects", "hintBoard", "hintDeck", "tidy", "mess", "flip", "details", "prev", "next", "ticketTitle", "ticketDays", "ticketJob", "ticketStub", "serverTitle", "servers.ingress.name", "servers.ingress.detail", "servers.mail.name", "servers.mail.detail", "servers.home.name", "servers.home.detail", "serverNote", "bookLabel", "bookFallback", "photoCaption", "sticker"] }, + "home.windows": { HomePageServer: ["close", "minimize", "openWindows", "projectPage", "source", "live", "stack", "year", "now.title", "now.days", "now.study.label", "now.study.text", "now.work.label", "now.work.text", "now.side.label", "now.side.text", "server.title", "server.headline", "read.title", "read.progress", "read.empty", "read.library", "about.title", "about.p1", "about.p2", "about.facts.laptop.label", "about.facts.laptop.value", "about.facts.pc.label", "about.facts.pc.value", "about.facts.study.label", "about.facts.study.value", "about.facts.languages.label", "about.facts.languages.value", "contact.title", "contact.body", "contact.copy", "contact.copied", "contact.form"] }, "home.about": { About: ["title", "p1", "p2", "p3", "funFactTitle", "funFactBody", "hobbiesTitle"] }, "home.about.activity": { ActivityFeed: ["idleStatus", "codingNow", "gaming", "inGame", "listening"] }, "home.about.currentlyReading": { CurrentlyReading: ["title", "progress"] }, diff --git a/app/_ui/HomePageServer.tsx b/app/_ui/HomePageServer.tsx index bb9f4eb..adc141a 100644 --- a/app/_ui/HomePageServer.tsx +++ b/app/_ui/HomePageServer.tsx @@ -1,6 +1,8 @@ import Header from "../components/Header.server"; import Hero from "../components/Hero"; import TischSection from "../components/tisch/TischSection"; +import WindowProvider from "../components/fenster/WindowProvider"; +import { getWindowLabels } from "../components/fenster/getWindowLabels"; import ScrollFadeIn from "../components/ScrollFadeIn"; import Script from "next/script"; import { @@ -20,10 +22,11 @@ interface HomePageServerProps { export default async function HomePageServer({ locale }: HomePageServerProps) { // Parallel laden aller Translations (hero translations handled by Hero server component) - const [aboutT, contactT, footerT] = await Promise.all([ + const [aboutT, contactT, footerT, windowLabels] = await Promise.all([ getAboutTranslations(locale), getContactTranslations(locale), getFooterTranslations(locale), + getWindowLabels(locale), ]); return ( @@ -86,22 +89,23 @@ export default async function HomePageServer({ locale }: HomePageServerProps) { }} />
-
- - + +
+ + + + + + + + + +
- + - - - - - -
- - - + ); } diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 6ec1911..2807c47 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -1,5 +1,6 @@ import { getTranslations } from "next-intl/server"; import KineticWordmark from "./KineticWordmark"; +import OpenWindowLink from "./fenster/OpenWindowLink"; interface HeroProps { locale: string; @@ -39,12 +40,13 @@ export default async function Hero({ locale }: HeroProps) { > {t("ctaWork")} - {t("ctaContact")} - + diff --git a/app/components/fenster/OpenWindowLink.tsx b/app/components/fenster/OpenWindowLink.tsx new file mode 100644 index 0000000..de14d5e --- /dev/null +++ b/app/components/fenster/OpenWindowLink.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useOpenWindow } from "./WindowProvider"; +import type { WindowRequest } from "./types"; + +interface OpenWindowLinkProps extends React.AnchorHTMLAttributes { + href: string; + request: WindowRequest; +} + +/** + * Ein normaler Link (funktioniert ohne JavaScript), der mit JavaScript stattdessen + * ein Fenster bzw. Sheet öffnet. + */ +export default function OpenWindowLink({ request, onClick, ...props }: OpenWindowLinkProps) { + const openWindow = useOpenWindow(); + return ( + { + onClick?.(e); + if (!openWindow || e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey) return; + e.preventDefault(); + openWindow(request, e.currentTarget); + }} + /> + ); +} diff --git a/app/components/fenster/WindowContent.tsx b/app/components/fenster/WindowContent.tsx new file mode 100644 index 0000000..f71ac0f --- /dev/null +++ b/app/components/fenster/WindowContent.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { loadCurrentBook, type CurrentBook } from "../tisch/currentBook"; +import type { WindowLabels, WindowRequest } from "./types"; + +const EMAIL = "contact@dk0.dev"; + +const btn = + "inline-flex items-center gap-2 rounded-full bg-ink px-4 py-2.5 text-sm font-semibold text-paper transition-transform duration-150 ease-out active:scale-[0.97]"; +const ghost = + "inline-flex items-center gap-2 rounded-full px-4 py-2.5 text-sm font-semibold shadow-[inset_0_0_0_1.5px_rgb(var(--plakat-ink))] transition-[background-color,color,transform] duration-150 ease-out hover:bg-ink hover:text-paper active:scale-[0.97]"; + +/** Titelzeile eines Fensters, auch für die Ablage-Leiste. */ +export function windowTitle(req: WindowRequest, labels: WindowLabels): string { + switch (req.kind) { + case "project": + return req.project.year ? `${req.project.title} · ${req.project.year}` : req.project.title; + case "now": + return labels.now.title; + case "server": + return labels.server.title; + case "read": + return labels.read.title; + case "about": + return labels.about.title; + case "contact": + return labels.contact.title; + } +} + +/** Farbe des Punkts in der Ablage-Leiste */ +export function windowAccent(req: WindowRequest): string { + if (req.kind === "project") return req.project.color; + return { now: "rgb(var(--plakat-red))", server: "rgb(var(--plakat-ok))", read: "#7a5240", about: "#2f5bea", contact: "rgb(var(--plakat-red))" }[req.kind]; +} + +interface WindowContentProps { + req: WindowRequest; + labels: WindowLabels; + /** Schließt das Fenster, z. B. wenn ein Link auf der Seite weiterführt */ + onClose: () => void; +} + +export default function WindowContent({ req, labels, onClose }: WindowContentProps) { + switch (req.kind) { + case "project": { + const p = req.project; + return ( + <> +
+ {p.tags.slice(0, 4).join(" · ")} + + {p.title} + +
+

{p.description}

+
+ {p.year && {p.year}} + {p.tags.length > 0 && {p.tags.join(", ")}} +
+
+ + {labels.projectPage} + + {p.liveUrl && ( + + {labels.live} + + )} + {p.githubUrl && ( + + {labels.source} + + )} +
+ + ); + } + + case "now": + return ; + + case "server": + return ( + <> +

{labels.server.headline}

+
+ {labels.server.rows.map((r) => ( +
+
+ ))} +
+

{labels.server.note}

+ + ); + + case "read": + return ; + + case "about": + return ( + <> + {labels.about.paragraphs.map((text) => ( +

+ {text} +

+ ))} +
+ {labels.about.facts.map((f) => ( + + {f.value} + + ))} +
+ + ); + + case "contact": + return ; + } +} + +function Row({ label, children }: { label: string; children: React.ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +// Abgabe der Masterarbeit; auf dem Client gerechnet, damit das Fenster nie einen veralteten Stand zeigt +const THESIS_DEADLINE = new Date("2027-02-15T12:00:00+01:00"); + +function NowBody({ labels }: { labels: WindowLabels }) { + const days = Math.max(0, Math.ceil((THESIS_DEADLINE.getTime() - Date.now()) / 86_400_000)); + return ( + <> +
+
+ {days} +
+

{labels.now.days}

+
+
+ {labels.now.rows.map((r) => ( + + {r.text} + + ))} +
+ + ); +} + +function ReadBody({ labels }: { labels: WindowLabels }) { + const [book, setBook] = useState(undefined); + useEffect(() => { + let alive = true; + loadCurrentBook().then((b) => alive && setBook(b)); + return () => { + alive = false; + }; + }, []); + const progress = book && Number.isFinite(book.progress) ? Math.max(0, Math.min(100, book.progress)) : 0; + + return ( + <> + {book === undefined ? ( +