feat: plakat redesign for subpages (projects, books, legal, privacy)
- Shared SubpageShell (HeaderClient paper strip + ink footer) and SubpageIntro, btn/ghost/card helpers in app/_ui/subpage.tsx - Projects list: poster cards with hard ink shadows, filter pills, ink search field; no fake 800ms skeleton and no framer-motion opacity:0 in SSR - Project detail: poster hero, .plakat-prose markdown, links/stack aside, back link works without JS - Books: server-side fetch (visible without JS), cover + rating pills - Legal notice / privacy: moved to app/_ui/*Client.tsx, root routes removed (middleware always redirects to /[locale]), consent reset via useConsent - ProjectThumbnail rebuilt on Plakat tokens - Old Header.tsx + test removed; brown/cream/sand palettes and unused glass-panel/admin-glass(-light)/liquid-bg-blob/float CSS dropped (liquid palette, glass-card, admin-glass-card stay for admin + 404) - .plakat-prose replaces the missing typography plugin Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+89
-125
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ExternalLink, ArrowLeft, Github as GithubIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import ProjectThumbnail from "@/app/components/ProjectThumbnail";
|
||||
import { BackArrow, SubpageIntro, btn, card, frame, ghost, monoLabel } from "./subpage";
|
||||
|
||||
export type ProjectDetailData = {
|
||||
id: number;
|
||||
@@ -39,137 +39,101 @@ export default function ProjectDetailClient({
|
||||
const tCommon = useTranslations("common");
|
||||
const tDetail = useTranslations("projects.detail");
|
||||
const router = useRouter();
|
||||
const [canGoBack, setCanGoBack] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prüfen, ob wir eine History haben (von Home gekommen)
|
||||
if (typeof window !== 'undefined' && window.history.length > 1) {
|
||||
setCanGoBack(true);
|
||||
}
|
||||
|
||||
}, []);
|
||||
|
||||
const handleBack = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
// Wenn wir direkt auf die Seite gekommen sind (Deep Link), gehen wir zur Projektliste
|
||||
// Ansonsten nutzen wir den Browser-Back, um an die exakte Stelle der Home oder Liste zurückzukehren
|
||||
if (canGoBack) {
|
||||
const listHref = `/${locale}/projects`;
|
||||
const handleBack = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
// Kommt man von der eigenen Seite, geht es per Browser-Zurück an die exakte Stelle
|
||||
// (Startseite oder Liste). Bei Deep Links, ohne JS oder mit Modifier-Taste gilt der Link selbst.
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
|
||||
const fromHere = document.referrer && new URL(document.referrer).origin === window.location.origin;
|
||||
if (fromHere && window.history.length > 1) {
|
||||
e.preventDefault();
|
||||
router.back();
|
||||
} else {
|
||||
router.push(`/${locale}/projects`);
|
||||
}
|
||||
};
|
||||
|
||||
const liveUrl = project.live && project.live !== "#" ? project.live : null;
|
||||
const githubUrl = project.github && project.github !== "#" ? project.github : null;
|
||||
const rawDate = project.date || project.created_at;
|
||||
const year = rawDate && !Number.isNaN(new Date(rawDate).getFullYear()) ? String(new Date(rawDate).getFullYear()) : null;
|
||||
const stack = project.technologies?.length ? project.technologies : project.tags;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50 dark:bg-stone-950 pt-32 pb-20 px-6 transition-colors duration-500">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
|
||||
{/* Navigation - Intelligent Back */}
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-12 group bg-transparent border-none cursor-pointer"
|
||||
>
|
||||
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform duration-200 ease-out" />
|
||||
<span className="font-bold uppercase tracking-widest text-xs">
|
||||
{tCommon("back")}
|
||||
</span>
|
||||
</button>
|
||||
<article>
|
||||
<SubpageIntro
|
||||
back={
|
||||
<Link href={listHref} onClick={handleBack} className={ghost}>
|
||||
<BackArrow /> {tCommon("back")}
|
||||
</Link>
|
||||
}
|
||||
kicker={[project.category, year].filter(Boolean).join(" · ")}
|
||||
title={project.title}
|
||||
>
|
||||
<p>{project.description}</p>
|
||||
</SubpageIntro>
|
||||
|
||||
{/* Title Section */}
|
||||
<div className="mb-20">
|
||||
<h1 className="text-6xl md:text-[10rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase mb-8">
|
||||
{project.title}<span className="text-liquid-mint">.</span>
|
||||
</h1>
|
||||
<p className="text-xl md:text-3xl font-light text-stone-500 dark:text-stone-400 max-w-4xl leading-snug tracking-tight">
|
||||
{project.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Feature Image Box */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-4 md:p-8 border border-stone-200/60 dark:border-stone-800/60 shadow-sm mb-12 overflow-hidden">
|
||||
<div className="relative aspect-video rounded-[2rem] overflow-hidden border-4 border-stone-50 dark:border-stone-800 shadow-2xl">
|
||||
{project.imageUrl ? (
|
||||
<Image src={project.imageUrl} alt={project.title} fill className="object-cover" priority sizes="100vw" />
|
||||
) : (
|
||||
<ProjectThumbnail
|
||||
title={project.title}
|
||||
category={project.category}
|
||||
tags={project.tags}
|
||||
slug={project.slug}
|
||||
size="hero"
|
||||
/>
|
||||
)}
|
||||
<div className="relative mt-8 aspect-[4/3] overflow-hidden rounded-[18px] border-2 border-ink shadow-[8px_8px_0_rgb(var(--plakat-ink))] sm:aspect-video md:mt-12">
|
||||
{project.imageUrl ? (
|
||||
<Image src={project.imageUrl} alt={project.title} fill className="object-cover" priority sizes="(max-width: 1400px) 100vw, 1400px" />
|
||||
) : (
|
||||
<div aria-hidden="true">
|
||||
<ProjectThumbnail title={project.title} category={project.category} tags={project.tags} slug={project.slug} size="hero" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
<div className="lg:col-span-8 space-y-8">
|
||||
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 md:p-16 border border-stone-200/60 dark:border-stone-800/60 shadow-sm">
|
||||
<div className="prose prose-stone dark:prose-invert max-w-none text-lg md:text-xl font-light leading-relaxed">
|
||||
<ReactMarkdown>{project.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-4 space-y-8">
|
||||
|
||||
|
||||
|
||||
{/* Quick Links Box - Only show if links exist */}
|
||||
|
||||
{((project.live && project.live !== "#") || (project.github && project.github !== "#")) && (
|
||||
|
||||
<div className="bg-stone-900 dark:bg-stone-800 rounded-[3rem] p-10 border border-stone-800 dark:border-stone-700 shadow-2xl text-white">
|
||||
|
||||
<h3 className="text-xl font-black mb-8 uppercase tracking-widest text-liquid-mint">Links</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
{project.live && project.live !== "#" && (
|
||||
|
||||
<a href={project.live} target="_blank" rel="noopener noreferrer" className="flex items-center justify-between w-full p-5 bg-white text-stone-900 rounded-2xl font-black active:scale-[0.97] transition-transform duration-150 ease-out group">
|
||||
|
||||
<span>{project.button_live_label || tDetail("liveDemo")}</span>
|
||||
|
||||
<ExternalLink size={20} className="group-hover:translate-x-1 transition-transform duration-200 ease-out" />
|
||||
|
||||
</a>
|
||||
|
||||
)}
|
||||
|
||||
{project.github && project.github !== "#" && (
|
||||
|
||||
<a href={project.github} target="_blank" rel="noopener noreferrer" className="flex items-center justify-between w-full p-5 bg-stone-800 text-white border border-stone-700 rounded-2xl font-black hover:bg-stone-700 transition-colors group">
|
||||
|
||||
<span>{project.button_github_label || tDetail("viewSource")}</span>
|
||||
|
||||
<GithubIcon size={20} className="group-hover:rotate-12 transition-transform" />
|
||||
|
||||
</a>
|
||||
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm">
|
||||
<h3 className="text-xl font-black mb-8 uppercase tracking-widest text-stone-400">Stack</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<span key={tag} className="px-4 py-2 bg-stone-50 dark:bg-stone-800 rounded-xl text-xs font-bold border border-stone-100 dark:border-stone-700">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-12 grid gap-10 lg:grid-cols-12 lg:gap-12">
|
||||
<div className="min-w-0 lg:col-span-8">
|
||||
{project.content ? (
|
||||
<div className="plakat-prose max-w-[68ch]">
|
||||
<ReactMarkdown>{project.content}</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<p className="max-w-[68ch] text-lg leading-relaxed">{project.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<aside className="grid content-start gap-6 lg:sticky lg:top-24 lg:col-span-4 lg:self-start">
|
||||
{(liveUrl || githubUrl) && (
|
||||
<section className={`${frame} bg-signal p-5 text-ink md:p-6`} aria-labelledby="project-links">
|
||||
<h2 id="project-links" className={monoLabel}>
|
||||
{tDetail("links")}
|
||||
</h2>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{liveUrl && (
|
||||
<a href={liveUrl} target="_blank" rel="noopener noreferrer" className={btn}>
|
||||
{project.button_live_label || tDetail("liveDemo")} <span aria-hidden="true">↗</span>
|
||||
</a>
|
||||
)}
|
||||
{githubUrl && (
|
||||
<a href={githubUrl} target="_blank" rel="noopener noreferrer" className={`${ghost} px-5 py-3 text-[15px] font-semibold`}>
|
||||
{project.button_github_label || tDetail("viewSource")} <span aria-hidden="true">↗</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{stack.length > 0 && (
|
||||
<section className={`${card} p-5 md:p-6`} aria-labelledby="project-stack">
|
||||
<h2 id="project-stack" className={monoLabel}>
|
||||
{tDetail("techStack")}
|
||||
</h2>
|
||||
<ul className="mt-4 flex flex-wrap gap-2">
|
||||
{stack.map((tag) => (
|
||||
<li key={tag} className="rounded-full px-3 py-1.5 text-sm font-medium shadow-[inset_0_0_0_1.5px_rgb(var(--plakat-ink))]">
|
||||
{tag}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<Link href={listHref} className={`${ghost} w-fit`}>
|
||||
<BackArrow /> {tCommon("backToProjects")}
|
||||
</Link>
|
||||
</aside>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user