4f5089ce38
- AI chat moves into a window/sheet, opened from a new speech-bubble card on the desk; it now respects the chatbot consent and asks before sending anything to n8n (previously it ignored the consent) - Contact as red poster section with big mail address and the form on a paper card; validation errors are finally shown inline (aria-invalid, role=alert) - Footer as ink strip with dk0 wordmark, legal links and a button to reopen the privacy settings - "About me" in the header opens the about window on the home page - Remove the old About section and its parts (BentoChat, ActivityFeed, CurrentlyReading, ReadBooks), the unused ChatWidget, Projects and BentoGrid, their tests, wrappers, loaders, types and texts - scripts/directus-cleanup-redesign.mjs: dry run by default, --apply archives hobbies, tech stack categories and old content pages and deletes four unused message overrides after a full JSON backup; run only after the redesign is live on production Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
153 lines
6.6 KiB
TypeScript
153 lines
6.6 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Image from "next/image";
|
||
import Link from "next/link";
|
||
import type { TischLabels, TischObject } from "./types";
|
||
import { loadCurrentBook, type CurrentBook } from "./currentBook";
|
||
|
||
interface ObjectFaceProps {
|
||
obj: TischObject;
|
||
labels: TischLabels;
|
||
}
|
||
|
||
const pad = (n: number) => String(n).padStart(2, "0");
|
||
|
||
// Rand und Rundung, die alle Objekte teilen
|
||
const edge = "h-full w-full overflow-hidden rounded-2xl border-2 border-ink";
|
||
|
||
/** Zeichnet ein Objekt. Größe und Position bestimmt der Container (Tisch oder Stapel). */
|
||
export default function ObjectFace({ obj, labels }: ObjectFaceProps) {
|
||
switch (obj.kind) {
|
||
case "project":
|
||
return (
|
||
<div className={`${edge} flex flex-col justify-between p-4`} style={{ background: obj.color, color: obj.textColor }}>
|
||
<div className="flex justify-between font-mono text-xs">
|
||
<span>({pad(obj.index + 1)})</span>
|
||
<span>{obj.year}</span>
|
||
</div>
|
||
<h3 className="font-display text-[2.4rem] font-extrabold leading-[0.88] tracking-[-0.045em] [overflow-wrap:anywhere] [font-variation-settings:'wdth'_78] [color:inherit]">
|
||
{obj.title}
|
||
</h3>
|
||
<div className="font-mono text-xs leading-snug">
|
||
<div>{obj.tags.slice(0, 3).join(" · ")}</div>
|
||
<div className="mt-2 opacity-70">{labels.flip} ↗</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
case "ticket":
|
||
return (
|
||
<div className={`${edge} grid grid-cols-[1fr_76px] bg-ink text-paper`}>
|
||
<div className="flex flex-col justify-between p-4">
|
||
<span className="font-mono text-[11.5px] uppercase tracking-wide">{labels.ticketTitle}</span>
|
||
<div>
|
||
<div className="font-display text-[4.4rem] font-extrabold leading-[0.8] tracking-[-0.05em] text-signal [font-variant-numeric:tabular-nums]">
|
||
{obj.daysLeft}
|
||
</div>
|
||
<div className="mt-1 text-sm font-semibold">{labels.ticketDays}</div>
|
||
</div>
|
||
<span className="font-mono text-[11.5px] leading-snug">{labels.ticketJob}</span>
|
||
</div>
|
||
<div className="grid place-items-center border-l-2 border-dashed border-paper/30 font-mono text-[11px] uppercase tracking-[0.12em] text-signal [transform:rotate(180deg)] [writing-mode:vertical-rl]">
|
||
{labels.ticketStub}
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
case "server":
|
||
return (
|
||
<div className={`${edge} flex flex-col gap-3 bg-paper p-4 text-ink`}>
|
||
<h3 className="font-display text-2xl font-extrabold leading-none tracking-[-0.03em]">{labels.serverTitle}</h3>
|
||
{labels.servers.map((s) => (
|
||
<div key={s.name} className="grid grid-cols-[10px_1fr] items-center gap-2.5 text-sm">
|
||
<span className="h-2.5 w-2.5 rounded-full bg-ok shadow-[0_0_0_3px_rgb(var(--plakat-ok)/0.2)]" aria-hidden="true" />
|
||
<span>
|
||
{s.name}
|
||
<span className="block font-mono text-[11.5px] opacity-75">{s.detail}</span>
|
||
</span>
|
||
</div>
|
||
))}
|
||
<p className="mt-auto border-t-2 border-ink/15 pt-2.5 font-mono text-[11.5px] leading-snug">{labels.serverNote}</p>
|
||
</div>
|
||
);
|
||
|
||
case "book":
|
||
return <BookFace href={obj.href} labels={labels} />;
|
||
|
||
case "photo":
|
||
return (
|
||
<div className="flex h-full w-full flex-col rounded-md border-2 border-ink bg-polaroid p-2.5 pb-0 text-polaroid-ink">
|
||
<div className="relative w-full flex-1 overflow-hidden rounded-sm bg-paper-2">
|
||
<Image src="/images/me.jpg" alt="Dennis Konkol" fill sizes="220px" className="object-cover" draggable={false} />
|
||
</div>
|
||
<div className="py-2.5 text-center font-display text-lg font-bold">{labels.photoCaption}</div>
|
||
</div>
|
||
);
|
||
|
||
case "chat":
|
||
return (
|
||
<div className="flex h-full w-full flex-col justify-between rounded-[22px_22px_22px_6px] border-2 border-ink bg-ink p-4 text-paper">
|
||
<span className="font-mono text-xs uppercase tracking-wide text-signal">KI · n8n</span>
|
||
<span className="font-display text-[1.7rem] font-extrabold leading-[0.95] tracking-[-0.03em]">{labels.chatCard}</span>
|
||
<span className="font-mono text-xs opacity-70">{labels.chatCardHint} ↗</span>
|
||
</div>
|
||
);
|
||
|
||
case "sticker":
|
||
return (
|
||
<div className="grid h-full w-full place-items-center rounded-full border-2 border-ink bg-signal p-6 text-center font-display text-xl font-extrabold leading-[0.98] tracking-[-0.02em] text-ink">
|
||
{labels.sticker}
|
||
</div>
|
||
);
|
||
}
|
||
}
|
||
|
||
function BookFace({ href, labels }: { href: string; labels: TischLabels }) {
|
||
const [book, setBook] = useState<CurrentBook | null | undefined>(undefined);
|
||
|
||
useEffect(() => {
|
||
let alive = true;
|
||
loadCurrentBook().then((b) => {
|
||
if (alive) setBook(b);
|
||
});
|
||
return () => {
|
||
alive = false;
|
||
};
|
||
}, []);
|
||
|
||
const progress = book && Number.isFinite(book.progress) ? Math.max(0, Math.min(100, book.progress)) : 0;
|
||
|
||
return (
|
||
<div className="relative h-full w-full overflow-hidden rounded-[6px_14px_14px_6px] border-2 border-ink bg-bookcover text-bookcover-text shadow-[inset_6px_0_0_rgba(255,255,255,0.08)]">
|
||
{book?.image && (
|
||
<Image src={book.image} alt="" fill sizes="200px" className="object-cover opacity-40" draggable={false} />
|
||
)}
|
||
<div className="relative flex h-full flex-col justify-between p-4">
|
||
<span className="font-mono text-xs uppercase tracking-wide">{labels.bookLabel}</span>
|
||
{book === undefined ? (
|
||
<div className="space-y-2" aria-hidden="true">
|
||
<div className="h-7 w-4/5 animate-pulse rounded bg-white/15" />
|
||
<div className="h-4 w-1/2 animate-pulse rounded bg-white/10" />
|
||
</div>
|
||
) : book ? (
|
||
<div>
|
||
<div className="line-clamp-3 font-display text-[1.9rem] font-extrabold leading-[0.92] tracking-[-0.03em]">{book.title}</div>
|
||
<div className="mt-1.5 font-mono text-xs">{book.authors.join(", ")}</div>
|
||
</div>
|
||
) : (
|
||
<Link href={href} className="font-display text-2xl font-extrabold leading-tight underline decoration-2 underline-offset-4">
|
||
{labels.bookFallback}
|
||
</Link>
|
||
)}
|
||
<div>
|
||
<div className="font-mono text-xs">{book ? `${progress} %` : " "}</div>
|
||
<div className="mt-1.5 h-1 overflow-hidden rounded bg-white/20">
|
||
<div className="h-full origin-left bg-signal" style={{ transform: `scaleX(${progress / 100})` }} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|