webstack-author/src/components/PortfolioPage.tsx
2026-08-15 10:21:35 +02:00

618 lines
33 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState } from "react";
import Markdown from "react-markdown";
import { Book, Project, AuthorProfile, LegalDocument, PortfolioTheme } from "../types";
import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, Moon, Mail, Instagram, AtSign, Share2, FileText } from "lucide-react";
import SpotlightSection from "./SpotlightSection";
import LegalModal from "./LegalModal";
import ProjectDetailMedia from "./ProjectDetailMedia";
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
import AccessibleModal from "./AccessibleModal";
interface PortfolioPageProps {
data: AuthorProfile;
legalDocuments?: LegalDocument[];
theme: PortfolioTheme;
}
function safeExternalUrl(value: string): string | null {
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
} catch {
return null;
}
}
const themeDefaults: Record<PortfolioTheme, { accent: string; secondary: string; background: string; card: string; badge: string; subtitle: string }> = {
cosmic: { accent: "#06b6d4", secondary: "#a855f7", background: "#020617", card: "#0f172a", badge: "Geschichten & Welten", subtitle: "AUTORINNEN- UND AUTORENPORTFOLIO" },
velvet: { accent: "#f97316", secondary: "#f59e0b", background: "#0a0a0a", card: "#171717", badge: "Literatur & Leidenschaft", subtitle: "AUTORINNEN- UND AUTORENPORTFOLIO" },
botanical: { accent: "#8FA797", secondary: "#B59EB1", background: "#0c0a09", card: "#1c1917", badge: "Poesie & Gedanken", subtitle: "AUTORINNEN- UND AUTORENPORTFOLIO" },
romance: { accent: "#e11d48", secondary: "#f59e0b", background: "#10070b", card: "#1f1117", badge: "Geschichten & Gefühle", subtitle: "AUTORINNEN- UND AUTORENPORTFOLIO" },
};
export default function PortfolioPage({ data, legalDocuments = [], theme }: PortfolioPageProps) {
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [legalModalOpen, setLegalModalOpen] = useState(false);
const [activeLegalId, setActiveLegalId] = useState<string | null>(null);
const [shareNotice, setShareNotice] = useState("");
const relevantLegalDocs = legalDocuments;
const defaults = themeDefaults[theme];
const accentColor = data.accentColor || defaults.accent;
const secondaryColor = data.secondaryColor || defaults.secondary;
const bgColor = data.backgroundColor || defaults.background;
const cardBgColor = data.cardBgColor || defaults.card;
const fontClass = data.fontFamily === "serif" ? "font-serif" : data.fontFamily === "mono" ? "font-mono" : "font-sans";
const customSectionLinks = (data.customSectionLinks || []).slice(0, 3).flatMap((link) => {
const url = safeExternalUrl(link.url);
return link.label.trim() && url ? [{ label: link.label.trim(), url }] : [];
});
const displayedBooks = data.books.map((book, index) => ({ book, index })).sort((left, right) => {
if (!left.book.seriesName && !right.book.seriesName) return left.index - right.index;
if (!left.book.seriesName) return 1;
if (!right.book.seriesName) return -1;
const seriesComparison = left.book.seriesName.localeCompare(right.book.seriesName, "de");
return seriesComparison || Number(left.book.seriesNumber || 0) - Number(right.book.seriesNumber || 0) || left.index - right.index;
}).map(({ book }) => book);
const syncDetailFromUrl = () => {
const params = new URLSearchParams(window.location.search);
setSelectedBook(data.books.find((book) => book.id === params.get("book")) || null);
setSelectedProject(data.projects.find((project) => project.id === params.get("project")) || null);
};
useEffect(() => {
syncDetailFromUrl();
window.addEventListener("popstate", syncDetailFromUrl);
return () => window.removeEventListener("popstate", syncDetailFromUrl);
}, [data]);
const openDetail = (kind: "book" | "project", id: string) => {
const url = new URL(window.location.href);
url.search = "";
url.searchParams.set(kind, id);
window.history.pushState({}, "", url);
syncDetailFromUrl();
};
const closeDetail = () => {
const url = new URL(window.location.href);
url.search = "";
window.history.replaceState({}, "", url);
setSelectedBook(null);
setSelectedProject(null);
};
const shareDetail = async (title: string) => {
try {
if (navigator.share) await navigator.share({ title, url: window.location.href });
else {
await navigator.clipboard.writeText(window.location.href);
setShareNotice("Link wurde kopiert.");
window.setTimeout(() => setShareNotice(""), 3000);
}
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
setShareNotice("Der Link konnte nicht kopiert werden.");
}
};
return (
<div
className={`min-h-screen text-slate-100 ${fontClass} selection:bg-cyan-500 selection:text-white overflow-x-hidden relative`}
style={{ backgroundColor: bgColor }}
>
{/* Hero Banner Background if present */}
{data.heroBannerUrl && (
<div className="absolute top-0 inset-x-0 h-96 overflow-hidden pointer-events-none -z-5 opacity-25">
<img src={data.heroBannerUrl} alt="" className="w-full h-full object-cover blur-sm" />
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-slate-950" style={{ backgroundImage: `linear-gradient(to bottom, transparent, ${bgColor})` }} />
</div>
)}
{/* Decorative starry background with magical aura */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-indigo-950/40 via-transparent to-transparent -z-10 pointer-events-none" />
<div
className="absolute top-1/4 left-1/4 w-96 h-96 rounded-full blur-3xl pointer-events-none opacity-20"
style={{ backgroundColor: accentColor }}
/>
<div
className="absolute top-2/3 right-1/4 w-96 h-96 rounded-full blur-3xl pointer-events-none opacity-20"
style={{ backgroundColor: secondaryColor }}
/>
{/* Hero Section */}
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-slate-800/80">
<div className="text-center max-w-4xl mx-auto">
{/* Subtle magical/futuristic accent */}
<div
className="inline-flex items-center gap-2 px-3.5 py-1 bg-slate-900/80 border rounded-full text-xs font-mono mb-6 tracking-widest uppercase shadow-sm"
style={{ borderColor: `${accentColor}40`, color: accentColor }}
>
<Star className="w-3.5 h-3.5 animate-pulse" style={{ color: accentColor }} />
<span>{data.badgeText || defaults.badge}</span>
</div>
<h1 className="text-5xl md:text-7xl tracking-tight text-white mb-4 uppercase drop-shadow-md font-bold">
{data.heroTitle || data.name}
</h1>
<p className="font-mono text-xs md:text-sm tracking-widest uppercase mb-8" style={{ color: accentColor }}>
{data.heroSubtitle || defaults.subtitle}
</p>
<div
className="h-0.5 w-24 mx-auto rounded-full"
style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
/>
{/* Quick Section Navigation */}
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-mono">
<a
href="#ueber-mich"
className="px-4 py-2 rounded-full border border-slate-800 bg-slate-900/60 hover:border-cyan-400 hover:text-cyan-400 transition-all text-slate-300 uppercase tracking-wider"
>
🌌 {data.aboutTitle || "Über mich"}
</a>
<a
href="#projekte"
className="px-4 py-2 rounded-full border border-slate-800 bg-slate-900/60 hover:border-cyan-400 hover:text-cyan-400 transition-all text-slate-300 uppercase tracking-wider"
>
🛰️ {data.projectsTitle || "Aktuelle Projekte"}
</a>
<a
href="#buecherregal"
className="px-4 py-2 rounded-full border border-slate-800 bg-slate-900/60 hover:border-cyan-400 hover:text-cyan-400 transition-all text-slate-300 uppercase tracking-wider"
>
🪐 {data.booksTitle || "Bücherregal"}
</a>
</nav>
</div>
</header>
{/* Main Container */}
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
{/* Section 1: Über mich */}
<section id="ueber-mich" className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
<div className="md:col-span-4 flex justify-center">
<div className="relative group">
{/* Outer neon ring */}
<div
className="absolute -inset-1.5 rounded-2xl blur opacity-30 group-hover:opacity-70 transition duration-1000 group-hover:duration-200"
style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
/>
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-slate-800" style={{ backgroundColor: cardBgColor }}>
<img
src={data.avatarUrl || undefined}
alt={data.name}
className="w-full h-full object-cover grayscale brightness-90 contrast-110 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-700"
referrerPolicy="no-referrer"
/>
</div>
</div>
</div>
<div className="md:col-span-8 space-y-6">
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
<Compass className="w-7 h-7" style={{ color: accentColor }} />
<span>{data.aboutTitle || "Über den Autor"}</span>
</h2>
<div className="prose prose-invert max-w-none text-slate-300 leading-relaxed space-y-4">
<p className="whitespace-pre-wrap">{data.bio}</p>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 font-mono text-xs">
{(data.bioTag1Label || data.bioTag1Value) && (
<div className="p-3 border rounded-lg flex items-center gap-3" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
<BookOpen className="w-5 h-5 shrink-0" style={{ color: secondaryColor }} />
<div>
<div className="text-slate-500 uppercase tracking-widest">{data.bioTag1Label || "Fokus"}</div>
<div className="text-white font-semibold">{data.bioTag1Value || "Literatur"}</div>
</div>
</div>
)}
{(data.bioTag2Label || data.bioTag2Value) && (
<div className="p-3 border rounded-lg flex items-center gap-3" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
<Star className="w-5 h-5 shrink-0" style={{ color: accentColor }} />
<div>
<div className="text-slate-500 uppercase tracking-widest">{data.bioTag2Label || "Welten"}</div>
<div className="text-white font-semibold">{data.bioTag2Value || "Geschichten"}</div>
</div>
</div>
)}
{(data.bioTag3Label || data.bioTag3Value) && (
<div className="p-3 border rounded-lg flex items-center gap-3 col-span-2 md:col-span-1" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
<Award className="w-5 h-5 shrink-0" style={{ color: secondaryColor }} />
<div>
<div className="text-slate-500 uppercase tracking-widest">{data.bioTag3Label || "Stil"}</div>
<div className="text-white font-semibold">{data.bioTag3Value || "Atmosphärisch"}</div>
</div>
</div>
)}
</div>
</div>
</section>
{/* Section 1.5: Spotlight / Neuerscheinung */}
{data.books?.find(b => b.isSpotlight) && (
<SpotlightSection
book={data.books.find(b => b.isSpotlight)!}
title={data.spotlightTitle}
subtitle={data.spotlightSubtitle}
accentColor={accentColor}
secondaryColor={secondaryColor}
cardBgColor={cardBgColor}
/>
)}
{/* Section 2: Aktuelle Projekte */}
<section id="projekte" className="space-y-8">
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 border-b border-slate-800 pb-4">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
<Star className="w-7 h-7" style={{ color: accentColor }} />
<span>{data.projectsTitle || "Aktuelle Projekte"}</span>
</h2>
<p className="text-slate-400 text-sm mt-1">{data.projectsSubtitle || "Ein exklusiver Blick auf das, was sich gerade auf meinem Schreib-Amboss befindet."}</p>
</div>
<div className="text-xs font-mono px-3 py-1 rounded border" style={{ color: accentColor, backgroundColor: `${accentColor}10`, borderColor: `${accentColor}30` }}>
Aktuell in Arbeit
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{data.projects.map((project) => (
<button
key={project.id}
onClick={() => openDetail("project", project.id)}
className="p-6 border rounded-xl transition-all duration-300 flex flex-col text-left w-full justify-between group cursor-pointer hover:scale-[1.01] focus:outline-none shadow-lg"
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}25` }}
>
<div className="space-y-4 w-full">
<div className="flex items-center justify-between">
<span className="text-xs font-mono px-2.5 py-1 rounded border uppercase tracking-wider" style={{ color: accentColor, backgroundColor: `${accentColor}15`, borderColor: `${accentColor}30` }}>
{project.status || "In Arbeit"}
</span>
<span className="text-xs text-slate-400 font-mono">
Progression: {project.progress}%
</span>
</div>
<h3 className="text-xl font-bold text-white group-hover:text-cyan-300 transition-colors">
{project.title}
</h3>
<div className="text-slate-300 text-sm leading-relaxed line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
<Markdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-cyan-400 hover:text-cyan-300 font-medium"
onClick={(e) => e.stopPropagation()}
{...props}
/>
)
}}
>
{project.description}
</Markdown>
</div>
<p className="font-mono text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1" style={{ color: accentColor }}>
Details anzeigen <ArrowRight className="w-3 h-3" />
</p>
</div>
{/* Custom glowing progress bar */}
<div className="mt-6 space-y-1.5 w-full">
<div className="h-1.5 w-full bg-slate-950/80 rounded-full overflow-hidden border border-slate-800">
<div
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
/>
</div>
</div>
</button>
))}
</div>
</section>
{/* Section 3: Bücherregal */}
<section id="buecherregal" className="space-y-8">
<div className="border-b border-slate-800 pb-4">
<h2 className="text-3xl font-bold tracking-tight text-white flex items-center gap-3">
<BookOpen className="w-7 h-7" style={{ color: secondaryColor }} />
<span>{data.booksTitle || "Das Bücherregal"}</span>
</h2>
<p className="text-slate-400 text-sm mt-1">{data.booksSubtitle || "Veröffentlichte Meilensteine. Klicken oder Tippen Sie auf ein Cover, um Geheimnisse zu enthüllen."}</p>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 justify-items-center">
{displayedBooks.map((book) => (
<button
key={book.id}
onClick={() => openDetail("book", book.id)}
className="group relative focus:outline-none text-left w-full max-w-[210px] aspect-[2/3] rounded-xl overflow-hidden border hover:border-cyan-400 transition-all duration-300 hover:shadow-xl cursor-pointer"
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}
>
<div className="absolute inset-0 bg-cyan-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
<img
src={book.coverUrl || undefined}
alt={book.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 z-0"
referrerPolicy="no-referrer"
/>
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-slate-950 via-slate-950/90 to-transparent p-4 opacity-80 group-hover:opacity-100 transition-all duration-350 z-20">
<p className="text-white text-xs font-bold truncate">{book.title}</p>
{book.seriesName && <p className="text-[9px] text-slate-300 truncate">{book.seriesName}{book.seriesNumber !== undefined && book.seriesNumber !== "" ? ` · Band ${book.seriesNumber}` : ""}</p>}
<p className="text-[10px] font-mono mt-0.5" style={{ color: accentColor }}>Details anzeigen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
</div>
</button>
))}
</div>
</section>
{/* Section 4: Custom Highlight Section if present */}
{(data.customSectionTitle || data.customSectionContent || customSectionLinks.length > 0) && (
<section className="p-8 rounded-2xl border relative overflow-hidden" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}>
<div className="absolute top-0 right-0 p-3 opacity-10 font-mono text-xs uppercase" style={{ color: accentColor }}>
CUSTOM_INFO_MODULE
</div>
{data.customSectionTitle && (
<h2 className="text-2xl font-bold text-white mb-4 flex items-center gap-2">
<Star className="w-5 h-5" style={{ color: accentColor }} />
<span>{data.customSectionTitle}</span>
</h2>
)}
{data.customSectionContent && (
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
<Markdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline text-cyan-400 hover:text-cyan-300 font-medium"
{...props}
/>
)
}}
>
{data.customSectionContent}
</Markdown>
</div>
)}
{customSectionLinks.length > 0 && (
<div className="mt-6 flex flex-col sm:flex-row sm:flex-wrap gap-3">
{customSectionLinks.map((link) => (
<a
key={`${link.label}-${link.url}`}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 rounded-xl border px-5 py-3 text-sm font-semibold text-white transition-all hover:-translate-y-0.5 hover:shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950"
style={{
borderColor: `${accentColor}70`,
background: `linear-gradient(135deg, ${accentColor}35, ${secondaryColor}25)`,
boxShadow: `0 8px 24px ${accentColor}15`,
}}
>
<span>{link.label}</span>
<ExternalLink className="h-4 w-4" aria-hidden="true" />
</a>
))}
</div>
)}
</section>
)}
{(data.contactEmail || data.instagramUrl || data.threadsUrl) && (
<section className="rounded-2xl border p-8 text-center" style={{ backgroundColor: cardBgColor, borderColor: `${secondaryColor}35` }}>
<h2 className="text-2xl font-bold text-white">Kontakt & Social Media</h2>
<p className="mt-2 text-sm text-slate-400">Weitere Neuigkeiten, Einblicke und Kontaktmöglichkeiten.</p>
<div className="mt-6 flex flex-col sm:flex-row sm:flex-wrap justify-center gap-3">
{data.contactEmail && (
<a href={`mailto:${data.contactEmail}`} className="inline-flex items-center justify-center gap-2 rounded-xl border px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-white/10" style={{ borderColor: `${accentColor}60` }}>
<Mail className="h-4 w-4" /><span>E-Mail</span>
</a>
)}
{safeExternalUrl(data.instagramUrl || "") && (
<a href={safeExternalUrl(data.instagramUrl || "")!} target="_blank" rel="noopener noreferrer" className="inline-flex items-center justify-center gap-2 rounded-xl border px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-white/10" style={{ borderColor: `${accentColor}60` }}>
<Instagram className="h-4 w-4" /><span>Instagram</span>
</a>
)}
{safeExternalUrl(data.threadsUrl || "") && (
<a href={safeExternalUrl(data.threadsUrl || "")!} target="_blank" rel="noopener noreferrer" className="inline-flex items-center justify-center gap-2 rounded-xl border px-5 py-3 text-sm font-semibold text-white transition-colors hover:bg-white/10" style={{ borderColor: `${accentColor}60` }}>
<AtSign className="h-4 w-4" /><span>Threads</span>
</a>
)}
</div>
</section>
)}
</main>
{shareNotice && <div role="status" aria-live="polite" className="fixed bottom-5 left-1/2 z-[70] -translate-x-1/2 rounded-xl border border-slate-700 bg-slate-900 px-4 py-2 text-sm text-white shadow-xl">{shareNotice}</div>}
{/* Book details modular interface modal */}
{selectedBook && (
<AccessibleModal titleId="book-detail-title" onClose={closeDetail}>
{/* Close Button */}
<button
onClick={closeDetail}
className="absolute top-4 right-4 text-slate-400 hover:text-white hover:bg-slate-800/80 px-2.5 py-1 rounded-md text-xs font-mono border border-slate-800 transition-all cursor-pointer"
>
SCHLIESSEN [ESC]
</button>
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
{/* Book Cover */}
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
<div className="rounded-xl overflow-hidden shadow-2xl border border-indigo-900/30 max-w-[260px] aspect-[2/3] w-full">
<img
src={selectedBook.coverUrl || undefined}
alt={selectedBook.title}
className="w-full h-full object-cover"
referrerPolicy="no-referrer"
/>
</div>
<div className="mt-6 w-full max-w-[260px] space-y-2">
{(selectedBook.ebookLink || selectedBook.buyLink) && <a href={selectedBook.ebookLink || selectedBook.buyLink} target="_blank" rel="noopener noreferrer" className="w-full py-3 text-center bg-gradient-to-r from-cyan-500 to-indigo-600 text-slate-950 font-bold rounded-xl flex items-center justify-center gap-2 text-sm"><span>E-Book</span><ExternalLink className="w-4 h-4" /></a>}
{selectedBook.paperbackLink && <a href={selectedBook.paperbackLink} target="_blank" rel="noopener noreferrer" className="w-full py-3 text-center border border-cyan-700 bg-slate-950 text-white font-bold rounded-xl flex items-center justify-center gap-2 text-sm"><span>Taschenbuch</span><ExternalLink className="w-4 h-4" /></a>}
{selectedBook.samplePdfUrl && <a href={selectedBook.samplePdfUrl} className="w-full py-3 text-center border border-slate-700 bg-slate-800 text-white font-bold rounded-xl flex items-center justify-center gap-2 text-sm"><FileText className="w-4 h-4" /><span>Leseprobe (PDF)</span></a>}
</div>
</div>
{/* Book Details */}
<div className="col-span-1 md:col-span-8 flex flex-col justify-between space-y-6">
<div className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-mono text-cyan-400 bg-cyan-950/40 px-2.5 py-0.5 rounded border border-cyan-900/40">
Erschienen: {selectedBook.publishedYear}
</span>
{selectedBook.genres?.map((genre) => (
<span key={genre} className="text-[11px] font-mono text-purple-400 bg-purple-950/40 px-2.5 py-0.5 rounded border border-purple-900/40">
{genre}
</span>
))}
</div>
<h3 id="book-detail-title" className="text-3xl font-extrabold text-white">
{selectedBook.title}
</h3>
{selectedBook.subtitle && (
<h4 className="text-sm font-mono text-indigo-400 uppercase tracking-wider">
{selectedBook.subtitle}
</h4>
)}
{selectedBook.seriesName && (
<p className="text-sm font-semibold" style={{ color: accentColor }}>{selectedBook.seriesName}{selectedBook.seriesNumber !== undefined && selectedBook.seriesNumber !== "" ? ` · Band ${selectedBook.seriesNumber}` : ""}</p>
)}
<button type="button" onClick={() => shareDetail(selectedBook.title)} className="inline-flex w-fit items-center gap-2 rounded-lg border border-slate-700 px-3 py-2 text-xs text-slate-300 hover:bg-slate-800"><Share2 className="h-4 w-4" /><span>Teilen / Link kopieren</span></button>
<div className="h-px bg-indigo-950" />
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
<Markdown>{selectedBook.description}</Markdown>
</div>
</div>
{/* Privacy-friendly Spotify player */}
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
)}
</div>
</div>
</AccessibleModal>
)}
{/* Project details modular modal */}
{selectedProject && (
<AccessibleModal titleId="project-detail-title" onClose={closeDetail} maxWidthClass="sm:max-w-5xl">
{/* Close Button */}
<button
onClick={closeDetail}
className="absolute top-4 right-4 text-slate-400 hover:text-white hover:bg-slate-800/80 px-2.5 py-1 rounded-md text-xs font-mono border border-slate-800 transition-all cursor-pointer"
>
SCHLIESSEN [ESC]
</button>
<div className="space-y-6 pt-4">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-mono text-cyan-400 bg-cyan-950/40 px-2.5 py-0.5 rounded border border-cyan-900/40">
Status: {selectedProject.status || "In Arbeit"}
</span>
<span className="text-xs font-mono text-purple-400 bg-purple-950/40 px-2.5 py-0.5 rounded border border-purple-900/40">
Aktuelles Projekt
</span>
</div>
<h3 id="project-detail-title" className="text-3xl font-extrabold text-white">
{selectedProject.title}
</h3>
<button type="button" onClick={() => shareDetail(selectedProject.title)} className="inline-flex w-fit items-center gap-2 rounded-lg border border-slate-700 px-3 py-2 text-xs text-slate-300 hover:bg-slate-800"><Share2 className="h-4 w-4" /><span>Teilen / Link kopieren</span></button>
<div className="h-px bg-indigo-950" />
<div className="space-y-2">
<div className="flex justify-between items-center text-xs font-mono text-slate-400">
<span>{data.projectProgressLabel ? `${data.projectProgressLabel.toUpperCase()}:` : "FORTSCHRITT:"}</span>
<span className="font-bold" style={{ color: accentColor }}>{selectedProject.progress}%</span>
</div>
<div className="h-2 w-full bg-slate-950 rounded-full overflow-hidden border border-slate-800">
<div
className="h-full rounded-full transition-all duration-1000 ease-out shadow-md"
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
/>
</div>
</div>
<div className={`grid gap-6 ${(selectedProject.imageUrl || selectedProject.spotifyPlaylistId) ? "lg:grid-cols-[minmax(0,1fr)_minmax(0,1.15fr)] lg:items-start" : ""}`}>
<ProjectDetailMedia project={selectedProject} accentColor={accentColor} />
<div className="bg-slate-950/60 p-5 rounded-xl border border-slate-800/60 relative overflow-hidden">
{data.projectExcerptBadge && (
<div className="absolute top-0 right-0 p-2 text-slate-700 text-[10px] font-mono select-none">
{data.projectExcerptBadge}
</div>
)}
<h4 className="text-xs font-mono text-slate-500 uppercase tracking-widest mb-3">
{data.projectExcerptTitle || "Projekt-Exzerpt & Fortschrittsnotizen"}
</h4>
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
<Markdown>{selectedProject.description || "Für dieses Schreibprojekt wurden noch keine weiteren Details abgelegt."}</Markdown>
</div>
</div>
</div>
</div>
</AccessibleModal>
)}
{/* Styled Footer */}
<footer className="border-t border-slate-800 py-12 px-4 text-center text-xs text-slate-500 font-mono" style={{ backgroundColor: bgColor }}>
<div className="max-w-7xl mx-auto space-y-4">
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
<p className="text-slate-500">{data.footerText || "Geschichten, Projekte und Veröffentlichungen."}</p>
{relevantLegalDocs.length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-6 pt-3 border-t border-slate-800/60 max-w-md mx-auto">
{relevantLegalDocs.map((doc) => (
<button
key={doc.id}
onClick={() => {
setActiveLegalId(doc.id);
setLegalModalOpen(true);
}}
className="text-slate-400 hover:text-cyan-400 transition-colors underline underline-offset-4 cursor-pointer"
>
{doc.title}
</button>
))}
</div>
)}
</div>
</footer>
{/* Legal Modal */}
<LegalModal
isOpen={legalModalOpen}
onClose={() => setLegalModalOpen(false)}
documents={relevantLegalDocs}
initialDocId={activeLegalId}
accentColor={accentColor}
secondaryColor={secondaryColor}
fontFamily={data.fontFamily || "sans"}
/>
</div>
);
}