08e856120b
- Tapping an object on the desk opens a window (desktop) or a bottom sheet (mobile) instead of flipping: projects, now, servers, reading, about, contact - Desktop windows in poster style (2px ink rule, hard offset shadow), open from the tapped object, thrown by the title bar with momentum and elastic viewport edges, stack by focus, minimize into a tray, Escape closes the top window, focus moves to close and returns to the trigger - Mobile sheets via vaul 1.1.2: drag down to dismiss, overlay, focus trap - Project window shows live/source links from Directus when present - Hero "Write me" opens the contact window; still a plain #contact link without JavaScript - Shared current-book loader for desk, deck and reading window - Texts in de/en, i18n key check extended Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
4.0 KiB
TypeScript
107 lines
4.0 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,
|
|
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: "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>
|
|
);
|
|
}
|