Files
portfolio/app/components/tisch/TischSection.tsx
T
denshooter 70f378e720 feat: plakat redesign phase 2 (desk with throwable objects, mobile card deck)
- New "Tisch" section replaces the old projects block on the home page
- Objects: project cards from Directus (flip on tap, link to detail page),
  thesis ticket with countdown, server card, currently-reading book from
  Hardcover (with skeleton), polaroid photo, sticker
- Desktop: Framer Motion drag with momentum, elastic table edges, tilt that
  follows throw velocity, bring-to-front, "Tidy up" and "Mess it up"
- Mobile: card deck, swipe the top card away (distance or velocity),
  prev/next buttons, live counter, tap to flip
- Both layouts are server rendered and switched by CSS, so content is
  visible without JavaScript and nothing jumps on hydration
- Taps after a drag no longer flip cards
- New named colors polaroid/bookcover instead of raw hex classes
- Texts in de/en, i18n key check extended

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:47:42 +02:00

105 lines
3.9 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"),
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,
};
});
// 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: "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>
);
}