Files
portfolio/app/components/tisch/TischSection.tsx
denshooter 9e6ae866d0
CI / CD / test-build (push) Successful in 11m13s
CI / CD / deploy-dev (push) Successful in 2m25s
CI / CD / deploy-production (push) Has been skipped
feat: plakat redesign phase 4 (chat window, contact, footer, cleanup) (#77)
- 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>
2026-10-15 23:49:56 +02:00

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>
);
}