618 lines
33 KiB
TypeScript
618 lines
33 KiB
TypeScript
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>
|
||
);
|
||
}
|