9e6ae866d0
- 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>
110 lines
4.1 KiB
TypeScript
110 lines
4.1 KiB
TypeScript
import Link from "next/link";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { getProjects } from "@/lib/directus";
|
|
import Board from "./Board";
|
|
import Deck from "./Deck";
|
|
import { PROJECT_COLORS, type TischLabels, type TischObject } from "./types";
|
|
|
|
interface TischSectionProps {
|
|
locale: string;
|
|
}
|
|
|
|
// Abgabe der Masterarbeit, Grundlage für das Ticket
|
|
const THESIS_DEADLINE = new Date("2027-02-15T12:00:00+01:00");
|
|
|
|
/**
|
|
* Der Tisch: Projekte und ein paar Dinge aus meinem Alltag als Objekte.
|
|
* Desktop zeigt den freien Tisch, das Handy einen Kartenstapel. Beides wird
|
|
* serverseitig gerendert und per CSS umgeschaltet, damit nichts springt.
|
|
*/
|
|
export default async function TischSection({ locale }: TischSectionProps) {
|
|
const t = await getTranslations({ locale, namespace: "home.tisch" });
|
|
const projects = (await getProjects(locale, { published: true, limit: 6 }).catch(() => null)) ?? [];
|
|
const daysLeft = Math.max(0, Math.ceil((THESIS_DEADLINE.getTime() - Date.now()) / 86_400_000));
|
|
|
|
const labels: TischLabels = {
|
|
hintBoard: t("hintBoard"),
|
|
hintDeck: t("hintDeck"),
|
|
tidy: t("tidy"),
|
|
mess: t("mess"),
|
|
flip: t("flip"),
|
|
details: t("details"),
|
|
prev: t("prev"),
|
|
next: t("next"),
|
|
ticketTitle: t("ticketTitle"),
|
|
ticketDays: t("ticketDays"),
|
|
ticketJob: t("ticketJob"),
|
|
ticketStub: t("ticketStub"),
|
|
serverTitle: t("serverTitle"),
|
|
servers: [
|
|
{ name: t("servers.ingress.name"), detail: t("servers.ingress.detail") },
|
|
{ name: t("servers.mail.name"), detail: t("servers.mail.detail") },
|
|
{ name: t("servers.home.name"), detail: t("servers.home.detail") },
|
|
],
|
|
serverNote: t("serverNote"),
|
|
bookLabel: t("bookLabel"),
|
|
bookFallback: t("bookFallback"),
|
|
photoCaption: t("photoCaption"),
|
|
chatCard: t("chatCard"),
|
|
chatCardHint: t("chatCardHint"),
|
|
sticker: t("sticker"),
|
|
};
|
|
|
|
const projectObjects: TischObject[] = projects.map((p, i) => {
|
|
const color = PROJECT_COLORS[i % PROJECT_COLORS.length];
|
|
return {
|
|
kind: "project",
|
|
id: `project-${p.slug}`,
|
|
index: i,
|
|
title: p.title,
|
|
description: p.description,
|
|
year: p.created_at ? String(new Date(p.created_at).getFullYear()) : "",
|
|
tags: p.tags?.length ? p.tags : p.technologies ?? [],
|
|
href: `/${locale}/projects/${p.slug}`,
|
|
color: color.bg,
|
|
textColor: color.text,
|
|
liveUrl: p.live_url && p.live_url !== "#" ? p.live_url : undefined,
|
|
githubUrl: p.github_url && p.github_url !== "#" ? p.github_url : undefined,
|
|
};
|
|
});
|
|
|
|
// Reihenfolge = Startplätze auf dem Tisch; Projekte zuerst, damit sie oben liegen
|
|
const objects: TischObject[] = [
|
|
...projectObjects,
|
|
{ kind: "ticket", id: "ticket", daysLeft },
|
|
{ kind: "server", id: "server" },
|
|
{ kind: "book", id: "book", href: `/${locale}/books` },
|
|
{ kind: "photo", id: "photo" },
|
|
{ kind: "chat", id: "chat" },
|
|
{ kind: "sticker", id: "sticker" },
|
|
];
|
|
// Im Stapel wäre der runde Sticker eine Ellipse und wiederholt nur den Claim
|
|
const deckObjects = objects.filter((o) => o.kind !== "sticker");
|
|
|
|
return (
|
|
<section id="projects" className="border-b-2 border-ink pb-16 pt-14 md:pt-16" aria-labelledby="tisch-title">
|
|
<div className="flex flex-wrap items-end justify-between gap-4 px-4 pb-4 sm:px-6 lg:px-10">
|
|
<h2
|
|
id="tisch-title"
|
|
className="font-display text-[clamp(2.4rem,6vw,5rem)] font-extrabold leading-[0.92] tracking-[-0.045em] [font-variation-settings:'wdth'_82]"
|
|
>
|
|
{t("title")}
|
|
</h2>
|
|
<Link
|
|
href={`/${locale}/projects`}
|
|
className="inline-flex items-center gap-2 rounded-full px-5 py-3 text-[15px] 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]"
|
|
>
|
|
{t("allProjects")} <span aria-hidden="true">↗</span>
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="hidden md:block">
|
|
<Board objects={objects} labels={labels} />
|
|
</div>
|
|
<div className="md:hidden">
|
|
<Deck objects={deckObjects} labels={labels} />
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|