feat: add Clara Finch portfolio identity
Expand the application to support a third author persona, "Clara Finch," including dedicated routing, component architecture, database schema updates, and admin panel integration.
This commit is contained in:
parent
29c086779b
commit
25326bb97a
6 changed files with 507 additions and 19 deletions
24
server.ts
24
server.ts
|
|
@ -30,7 +30,27 @@ async function initDatabase(): Promise<AuthorData> {
|
|||
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||
try {
|
||||
const existingData = await fs.readFile(DATA_FILE, "utf-8");
|
||||
return JSON.parse(existingData) as AuthorData;
|
||||
const parsed = JSON.parse(existingData) as AuthorData;
|
||||
|
||||
// Auto-heal database: make sure all profiles from defaultAuthorData exist
|
||||
let modified = false;
|
||||
if (!parsed.scifi) {
|
||||
parsed.scifi = defaultAuthorData.scifi;
|
||||
modified = true;
|
||||
}
|
||||
if (!parsed.erotica) {
|
||||
parsed.erotica = defaultAuthorData.erotica;
|
||||
modified = true;
|
||||
}
|
||||
if (!parsed.clara) {
|
||||
parsed.clara = defaultAuthorData.clara;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
await fs.writeFile(DATA_FILE, JSON.stringify(parsed, null, 2), "utf-8");
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
// If file doesn't exist, seed it with default data
|
||||
await fs.writeFile(DATA_FILE, JSON.stringify(defaultAuthorData, null, 2), "utf-8");
|
||||
|
|
@ -116,7 +136,7 @@ app.post("/api/admin/login", (req, res) => {
|
|||
// 3. Save modified profile configurations (About, Projects, Books)
|
||||
app.post("/api/admin/save-profile", verifyToken, async (req, res) => {
|
||||
const { profileKey, profileData } = req.body;
|
||||
if (profileKey !== "scifi" && profileKey !== "erotica") {
|
||||
if (profileKey !== "scifi" && profileKey !== "erotica" && profileKey !== "clara") {
|
||||
res.status(400).json({ error: "Ungültiger Profilschlüssel." });
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
61
src/App.tsx
61
src/App.tsx
|
|
@ -2,8 +2,9 @@ import { useState, useEffect } from "react";
|
|||
import { AuthorData } from "./types";
|
||||
import SciFiPortfolio from "./components/SciFiPortfolio";
|
||||
import EroticaPortfolio from "./components/EroticaPortfolio";
|
||||
import ClaraPortfolio from "./components/ClaraPortfolio";
|
||||
import AdminPanel from "./components/AdminPanel";
|
||||
import { Lock, Eye, Sparkles, RefreshCw } from "lucide-react";
|
||||
import { Lock, Eye, Sparkles, RefreshCw, Leaf } from "lucide-react";
|
||||
|
||||
export default function App() {
|
||||
const [authorData, setAuthorData] = useState<AuthorData | null>(null);
|
||||
|
|
@ -71,18 +72,26 @@ export default function App() {
|
|||
return <AdminPanel onLogout={handleAdminLogout} />;
|
||||
}
|
||||
|
||||
// 2. Sensual/Erotica Portfolio pseudonym - accessed ONLY via /sensual-moments (Strict content separation)
|
||||
if (currentPath === "/sensual-moments" || currentPath.startsWith("/sensual-moments/")) {
|
||||
return <EroticaPortfolio data={authorData.erotica} />;
|
||||
}
|
||||
// 2. Sensual/Erotica Portfolio pseudonym
|
||||
// 3. Poetic Clara Finch Portfolio pseudonym
|
||||
// 4. SciFi Daniel Hesse Portfolio
|
||||
|
||||
const renderActivePortfolio = () => {
|
||||
if (currentPath === "/sensual-moments" || currentPath.startsWith("/sensual-moments/")) {
|
||||
return <EroticaPortfolio data={authorData.erotica} />;
|
||||
}
|
||||
if (currentPath === "/clara-finch" || currentPath.startsWith("/clara-finch/")) {
|
||||
return <ClaraPortfolio data={authorData.clara} />;
|
||||
}
|
||||
return <SciFiPortfolio data={authorData.scifi} />;
|
||||
};
|
||||
|
||||
// 3. Fallback: SciFi & Fantasy portfolio - accessed via `/` (Daniel Hesse identity)
|
||||
return (
|
||||
<>
|
||||
<SciFiPortfolio data={authorData.scifi} />
|
||||
{renderActivePortfolio()}
|
||||
|
||||
{/* Absolute floating administration bypass link hidden on the bottom right for convenience of testing/admin accessing */}
|
||||
<div className="fixed bottom-3 left-3 opacity-0 hover:opacity-100 transition-opacity z-40 bg-slate-950/80 border border-slate-800 p-2.5 rounded-xl flex items-center gap-4 text-xs font-mono">
|
||||
{/* Absolute floating administration and pseudonym portal switcher on the bottom left (hover to show) */}
|
||||
<div className="fixed bottom-3 left-3 opacity-0 hover:opacity-100 focus-within:opacity-100 transition-opacity duration-300 z-40 bg-neutral-950/90 border border-neutral-800 p-2.5 rounded-xl flex flex-wrap items-center gap-4 text-xs font-mono shadow-xl backdrop-blur-sm">
|
||||
<a
|
||||
href="/admin"
|
||||
onClick={(e) => {
|
||||
|
|
@ -90,12 +99,25 @@ export default function App() {
|
|||
window.history.pushState({}, "", "/admin");
|
||||
setCurrentPath("/admin");
|
||||
}}
|
||||
className="text-slate-500 hover:text-cyan-400 font-bold flex items-center gap-1"
|
||||
className="text-stone-500 hover:text-cyan-400 font-bold flex items-center gap-1 transition-colors"
|
||||
>
|
||||
<Lock className="w-3.5 h-3.5" />
|
||||
<span>Admin-Zentrale</span>
|
||||
</a>
|
||||
<div className="w-px h-3 bg-slate-800" />
|
||||
<div className="w-px h-3 bg-neutral-800" />
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.history.pushState({}, "", "/");
|
||||
setCurrentPath("/");
|
||||
}}
|
||||
className={`font-bold flex items-center gap-1 transition-colors ${currentPath === "/" ? "text-cyan-400" : "text-stone-500 hover:text-cyan-400"}`}
|
||||
>
|
||||
<Sparkles className="w-3.5 h-3.5" />
|
||||
<span>Sci-Fi & Fantasy</span>
|
||||
</a>
|
||||
<div className="w-px h-3 bg-neutral-800" />
|
||||
<a
|
||||
href="/sensual-moments"
|
||||
onClick={(e) => {
|
||||
|
|
@ -103,10 +125,23 @@ export default function App() {
|
|||
window.history.pushState({}, "", "/sensual-moments");
|
||||
setCurrentPath("/sensual-moments");
|
||||
}}
|
||||
className="text-slate-500 hover:text-rose-400 font-bold flex items-center gap-1"
|
||||
className={`font-bold flex items-center gap-1 transition-colors ${currentPath === "/sensual-moments" ? "text-orange-400" : "text-stone-500 hover:text-orange-400"}`}
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
<span>Sinnlich</span>
|
||||
<span>Sinnlich (M.S. Velvet)</span>
|
||||
</a>
|
||||
<div className="w-px h-3 bg-neutral-800" />
|
||||
<a
|
||||
href="/clara-finch"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.history.pushState({}, "", "/clara-finch");
|
||||
setCurrentPath("/clara-finch");
|
||||
}}
|
||||
className={`font-bold flex items-center gap-1 transition-colors ${currentPath === "/clara-finch" ? "text-emerald-400" : "text-stone-500 hover:text-emerald-400"}`}
|
||||
>
|
||||
<Leaf className="w-3.5 h-3.5" />
|
||||
<span>Naturlyrik (Clara Finch)</span>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
|||
// Author Data State
|
||||
const [authorData, setAuthorData] = useState<AuthorData | null>(null);
|
||||
|
||||
// Selected Profile state ("scifi" vs "erotica")
|
||||
const [activeProfile, setActiveProfile] = useState<"scifi" | "erotica">("scifi");
|
||||
// Selected Profile state ("scifi" vs "erotica" vs "clara")
|
||||
const [activeProfile, setActiveProfile] = useState<"scifi" | "erotica" | "clara">("scifi");
|
||||
|
||||
// Sub-tab selection state ("profile" | "books" | "projects")
|
||||
const [activeTab, setActiveTab] = useState<"profile" | "books" | "projects">("profile");
|
||||
|
|
@ -97,7 +97,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
|||
};
|
||||
|
||||
// Profile save handler
|
||||
const handleSaveProfileInfo = async (profileKey: "scifi" | "erotica") => {
|
||||
const handleSaveProfileInfo = async (profileKey: "scifi" | "erotica" | "clara") => {
|
||||
if (!authorData) return;
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
|
@ -419,10 +419,16 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => { setActiveProfile("erotica"); setActiveTab("profile"); }}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold cursor-pointer transition-all ${activeProfile === "erotica" ? "bg-rose-650 bg-rose-800 text-white" : "text-slate-400 hover:text-white"}`}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold cursor-pointer transition-all ${activeProfile === "erotica" ? "bg-rose-800 text-white" : "text-slate-400 hover:text-white"}`}
|
||||
>
|
||||
Erotik (Marc Velvet)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setActiveProfile("clara"); setActiveTab("profile"); }}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold cursor-pointer transition-all ${activeProfile === "clara" ? "bg-emerald-700 text-white" : "text-slate-400 hover:text-white"}`}
|
||||
>
|
||||
Naturlyrik (Clara Finch)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
|
|||
379
src/components/ClaraPortfolio.tsx
Normal file
379
src/components/ClaraPortfolio.tsx
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
import { useState } from "react";
|
||||
import { Book, Project, AuthorProfile } from "../types";
|
||||
import { Leaf, Feather, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
||||
|
||||
interface ClaraPortfolioProps {
|
||||
data: AuthorProfile;
|
||||
}
|
||||
|
||||
export default function ClaraPortfolio({ data }: ClaraPortfolioProps) {
|
||||
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
|
||||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-950 text-stone-200 font-sans selection:bg-[#B39EAF]/30 selection:text-white overflow-x-hidden relative">
|
||||
{/* Delicate organic gradients - Sage (Salbei) and Mauve (Malve) */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-[#4A5D50]/15 via-stone-950 to-stone-950 -z-10 pointer-events-none" />
|
||||
<div className="absolute top-1/4 left-1/4 w-96 h-96 bg-[#8FA797]/5 rounded-full blur-3xl pointer-events-none" />
|
||||
<div className="absolute bottom-1/3 right-1/3 w-96 h-96 bg-[#B59EB1]/5 rounded-full blur-3xl pointer-events-none" />
|
||||
|
||||
{/* Header / Hero */}
|
||||
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-[#4A5D50]/20">
|
||||
<div className="text-center max-w-4xl mx-auto space-y-6">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-[#4A5D50]/10 border border-[#8FA797]/20 rounded-full text-[#8FA797] text-xs tracking-wider mb-2">
|
||||
<Leaf className="w-3.5 h-3.5 text-[#8FA797] animate-pulse" />
|
||||
<span className="font-serif italic text-stone-300">Lyrik & Naturphilosophie</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-4xl md:text-6xl font-serif tracking-widest text-stone-100 uppercase font-light drop-shadow-[0_2px_10px_rgba(143,167,151,0.1)]">
|
||||
{data.heroTitle || "CLARA FINCH"}
|
||||
</h1>
|
||||
<p className="text-[#B59EB1] font-sans text-xs md:text-sm tracking-[0.4em] uppercase">
|
||||
{data.heroSubtitle || "LYRIK, ESSAYS & NATURPHILOSOPHIE"}
|
||||
</p>
|
||||
<div className="h-[1px] w-36 bg-gradient-to-r from-transparent via-[#8FA797]/55 via-[#B59EB1]/50 to-transparent mx-auto mt-6" />
|
||||
|
||||
{/* Quick Section Navigation (Sage & Mauve Styled) */}
|
||||
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
||||
<a
|
||||
href="#ueber-mich"
|
||||
className="px-4 py-2 rounded-full border border-[#4A5D50]/30 bg-[#4A5D50]/10 hover:border-[#8FA797] hover:text-[#8FA797] transition-all text-stone-300 uppercase tracking-wider"
|
||||
>
|
||||
🌿 Über mich
|
||||
</a>
|
||||
<a
|
||||
href="#projekte"
|
||||
className="px-4 py-2 rounded-full border border-[#4A5D50]/30 bg-[#4A5D50]/10 hover:border-[#8FA797] hover:text-[#8FA797] transition-all text-stone-300 uppercase tracking-wider"
|
||||
>
|
||||
✍️ Notizbücher
|
||||
</a>
|
||||
<a
|
||||
href="#buecherregal"
|
||||
className="px-4 py-2 rounded-full border border-[#4A5D50]/30 bg-[#4A5D50]/10 hover:border-[#8FA797] hover:text-[#8FA797] transition-all text-stone-300 uppercase tracking-wider"
|
||||
>
|
||||
📖 Gedruckte Werke
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Grid */}
|
||||
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
||||
|
||||
{/* About writer */}
|
||||
<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 soft natural glow */}
|
||||
<div className="absolute -inset-1.5 bg-gradient-to-r from-[#8FA797] to-[#B59EB1] rounded-2xl blur opacity-20 group-hover:opacity-40 transition duration-1000 group-hover:duration-200"></div>
|
||||
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-[#4A5D50]/30 bg-stone-900">
|
||||
<img
|
||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=400"}
|
||||
alt={data.name}
|
||||
className="w-full h-full object-cover grayscale brightness-95 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-1000"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-8 space-y-6">
|
||||
<h2 className="text-2xl md:text-3xl font-serif text-stone-100 flex items-center gap-3">
|
||||
<Feather className="w-6 h-6 text-[#8FA797]" />
|
||||
<span>Das Atmen zwischen den Zeilen</span>
|
||||
</h2>
|
||||
<div className="prose prose-invert prose-stone max-w-none text-stone-350 text-stone-300 leading-relaxed font-sans space-y-4">
|
||||
<p className="whitespace-pre-wrap italic">{data.bio}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
||||
<div className="p-3 bg-stone-900/50 border border-stone-900 rounded-xl flex items-center gap-3">
|
||||
<Leaf className="w-5 h-5 text-[#8FA797]" />
|
||||
<div>
|
||||
<div className="text-stone-500 uppercase tracking-wider text-[10px]">Genre</div>
|
||||
<div className="text-stone-200 font-semibold">Lyrik & Essays</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-stone-900/50 border border-stone-900 rounded-xl flex items-center gap-3">
|
||||
<Sparkles className="w-5 h-5 text-[#B59EB1]" />
|
||||
<div>
|
||||
<div className="text-stone-500 uppercase tracking-wider text-[10px]">Ästhetik</div>
|
||||
<div className="text-stone-200 font-semibold">Malve & Salbei</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 bg-stone-900/50 border border-stone-900 rounded-xl flex items-center gap-3 col-span-2 md:col-span-1">
|
||||
<Feather className="w-5 h-5 text-[#8FA797]" />
|
||||
<div>
|
||||
<div className="text-stone-500 uppercase tracking-wider text-[10px]">Fokus</div>
|
||||
<div className="text-stone-200 font-semibold">Naturkontemplation</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Dynamic ongoing work writing progress */}
|
||||
<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-[#4A5D50]/20 pb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl md:text-3xl font-serif text-stone-100 flex items-center gap-3">
|
||||
<Feather className="w-6 h-6 text-[#8FA797]" />
|
||||
<span>Aus den Notizbüchern</span>
|
||||
</h2>
|
||||
<p className="text-stone-400 text-sm mt-1">Gedanken im Entwurf, Fragmente und Sammlungen, die bald das Licht der Welt erblicken.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{data.projects.map((project) => (
|
||||
<button
|
||||
key={project.id}
|
||||
onClick={() => setSelectedProject(project)}
|
||||
className="p-6 bg-[#4A5D50]/5 border border-[#4A5D50]/20 hover:border-[#8FA797]/40 rounded-xl transition-all duration-300 flex flex-col text-left w-full justify-between group cursor-pointer hover:shadow-[0_0_20px_rgba(143,167,151,0.15)] focus:outline-none"
|
||||
>
|
||||
<div className="space-y-4 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[10px] uppercase font-sans text-[#8FA797] bg-[#4A5D50]/20 px-2.5 py-1 rounded border border-[#8FA797]/20 tracking-wider">
|
||||
{project.status || "In Arbeit"}
|
||||
</span>
|
||||
<span className="text-xs text-stone-400 italic">
|
||||
Reifegrad: {project.progress}%
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-serif text-stone-100 group-hover:text-[#B59EB1] transition-colors">
|
||||
{project.title}
|
||||
</h3>
|
||||
<p className="text-stone-300 text-sm italic line-clamp-3">
|
||||
{project.description}
|
||||
</p>
|
||||
<p className="text-[#8FA797] font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 italic">
|
||||
Details einsehen <ArrowRight className="w-3 h-3" />
|
||||
</p>
|
||||
</div>
|
||||
{/* Sage & Mauve loading progress meter */}
|
||||
<div className="mt-6 space-y-1.5 w-full">
|
||||
<div className="h-1 w-full bg-stone-900 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#4A5D50] via-[#8FA797] to-[#B59EB1] rounded-full transition-all duration-1000 ease-out shadow-[0_0_8px_rgba(143,167,151,0.3)]"
|
||||
style={{ width: `${project.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bookshelf display */}
|
||||
<section id="buecherregal" className="space-y-8">
|
||||
<div className="border-b border-[#4A5D50]/20 pb-4">
|
||||
<h2 className="text-2xl md:text-3xl font-serif text-stone-100 flex items-center gap-3">
|
||||
<BookOpen className="w-6 h-6 text-[#8FA797]" />
|
||||
<span>Gedruckte Werke</span>
|
||||
</h2>
|
||||
<p className="text-stone-400 text-sm mt-1">Erschlossene Wortlandschaften. Wählen Sie einen Band, um tiefer hineinzuhören.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 justify-items-center">
|
||||
{data.books.map((book) => (
|
||||
<button
|
||||
key={book.id}
|
||||
onClick={() => setSelectedBook(book)}
|
||||
className="group relative focus:outline-none text-left w-full max-w-[210px] aspect-[2/3] rounded-lg overflow-hidden border border-stone-900 bg-stone-900 hover:border-[#8FA797]/50 transition-all duration-300 hover:shadow-[0_0_20px_rgba(143,167,151,0.15)] cursor-pointer"
|
||||
>
|
||||
<div className="absolute inset-0 bg-[#4A5D50]/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||
<img
|
||||
src={book.coverUrl || "https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=300"}
|
||||
alt={book.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
|
||||
{/* Readable, beautiful overlay on top of cover, slightly translucent, fully visible on hover */}
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-stone-950 via-stone-950/90 to-transparent p-4 opacity-80 group-hover:opacity-100 transition-all duration-350 z-20">
|
||||
<p className="text-[#E6E0E5] text-xs font-serif truncate">{book.title}</p>
|
||||
<p className="text-[#8FA797] text-[10px] font-sans mt-0.5 italic">Verweilen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
{/* Book details modular modal */}
|
||||
{selectedBook && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="fixed inset-0 bg-stone-950/85 backdrop-blur-md transition-opacity"
|
||||
onClick={() => setSelectedBook(null)}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-stone-950 border border-stone-900 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-4xl p-6 md:p-8 space-y-6">
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedBook(null)}
|
||||
className="absolute top-4 right-4 text-stone-400 hover:text-stone-100 hover:bg-[#4A5D50]/80 px-2.5 py-1 rounded-md text-xs font-sans border border-stone-800 transition-all cursor-pointer"
|
||||
>
|
||||
SCHLIEẞEN
|
||||
</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-[#4A5D50]/30 max-w-[260px] aspect-[2/3] w-full">
|
||||
<img
|
||||
src={selectedBook.coverUrl}
|
||||
alt={selectedBook.title}
|
||||
className="w-full h-full object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
</div>
|
||||
{selectedBook.buyLink && (
|
||||
<a
|
||||
href={selectedBook.buyLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-6 w-full max-w-[260px] py-3 text-center bg-gradient-to-r from-[#8FA797] to-[#B59EB1] hover:from-[#9fbca9] hover:to-[#cbb5c7] text-stone-950 font-bold font-sans rounded-xl flex items-center justify-center gap-2 transition-all hover:scale-[1.02] shadow-[0_4px_12px_rgba(143,167,151,0.2)] text-sm"
|
||||
>
|
||||
<span>Werk erwerben</span>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Details text area */}
|
||||
<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-[10px] font-sans text-[#8FA797] bg-[#4A5D50]/20 px-2.5 py-0.5 rounded border border-[#8FA797]/30">
|
||||
Erschienen: {selectedBook.publishedYear}
|
||||
</span>
|
||||
{selectedBook.genres?.map((genre) => (
|
||||
<span key={genre} className="text-[10px] font-sans text-[#B59EB1] bg-[#B59EB1]/10 px-2.5 py-0.5 rounded border border-[#B59EB1]/20">
|
||||
{genre}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-serif text-stone-100">
|
||||
{selectedBook.title}
|
||||
</h3>
|
||||
|
||||
{selectedBook.subtitle && (
|
||||
<h4 className="text-sm font-sans italic text-[#B59EB1]">
|
||||
{selectedBook.subtitle}
|
||||
</h4>
|
||||
)}
|
||||
|
||||
<div className="h-[1px] bg-[#4A5D50]/30" />
|
||||
|
||||
<p className="text-stone-300 text-sm leading-relaxed whitespace-pre-wrap italic">
|
||||
{selectedBook.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Embedded Spotify player (Only if playlist is non-empty) */}
|
||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs font-sans text-[#8FA797] flex items-center gap-2 italic">
|
||||
<Leaf className="w-3.5 h-3.5 text-[#8FA797]" />
|
||||
<span>Klanglandschaft (Begleitende Playlist)</span>
|
||||
</p>
|
||||
<div className="rounded-xl overflow-hidden bg-stone-900 border border-stone-800">
|
||||
<iframe
|
||||
src={`https://open.spotify.com/embed/playlist/${selectedBook.spotifyPlaylistId}?utm_source=generator&theme=0`}
|
||||
width="100%"
|
||||
height="152"
|
||||
frameBorder="0"
|
||||
allowFullScreen={false}
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project details modular modal */}
|
||||
{selectedProject && (
|
||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||
<div
|
||||
className="fixed inset-0 bg-stone-950/85 backdrop-blur-md transition-opacity"
|
||||
onClick={() => setSelectedProject(null)}
|
||||
/>
|
||||
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<div className="relative transform overflow-hidden rounded-2xl bg-stone-950 border border-stone-900 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-2xl p-6 md:p-8 space-y-6">
|
||||
|
||||
<button
|
||||
onClick={() => setSelectedProject(null)}
|
||||
className="absolute top-4 right-4 text-stone-400 hover:text-stone-100 hover:bg-[#4A5D50]/80 px-2.5 py-1 rounded-md text-xs font-sans border border-stone-800 transition-all cursor-pointer"
|
||||
>
|
||||
SCHLIEẞEN
|
||||
</button>
|
||||
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-[10px] font-sans text-[#8FA797] bg-[#4A5D50]/20 px-2.5 py-0.5 rounded border border-[#8FA797]/30">
|
||||
Status: {selectedProject.status || "In Arbeit"}
|
||||
</span>
|
||||
<span className="text-[10px] font-sans text-[#B59EB1] bg-[#B59EB1]/10 px-2.5 py-0.5 rounded border border-[#B59EB1]/20">
|
||||
Notizbucheintrag
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-serif text-stone-100">
|
||||
{selectedProject.title}
|
||||
</h3>
|
||||
|
||||
<div className="h-[1px] bg-[#4A5D50]/30" />
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center text-xs font-sans text-stone-400">
|
||||
<span>ENTWURF-FORTSCHRITT:</span>
|
||||
<span className="text-[#8FA797] font-bold">{selectedProject.progress}%</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-stone-900 rounded-full overflow-hidden border border-stone-800">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#4A5D50] via-[#8FA797] to-[#B59EB1] rounded-full transition-all duration-1000 ease-out shadow-[0_0_8px_rgba(143,167,151,0.25)]"
|
||||
style={{ width: `${selectedProject.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#4A5D50]/5 p-5 rounded-xl border border-[#4A5D50]/20 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-2 text-stone-700 text-[10px] font-mono select-none">
|
||||
FRAGMENT_LOG_CLARA
|
||||
</div>
|
||||
<h4 className="text-xs font-sans text-[#8FA797] uppercase tracking-wider mb-3 italic">Konzeptuelle Notizen & Auszüge</h4>
|
||||
<p className="text-stone-200 text-sm leading-relaxed whitespace-pre-wrap italic">
|
||||
{selectedProject.description || "Für dieses Buchprojekt wurden noch keine lyrischen Gedanken freigegeben."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-stone-950 border-t border-[#4A5D50]/20 py-12 px-4 text-center text-xs text-stone-500 italic">
|
||||
<div className="max-w-7xl mx-auto space-y-2">
|
||||
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
||||
<p className="text-stone-600">Betrachtungen, verwoben mit dem Rhythmus der wachsenden Natur.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -94,5 +94,52 @@ export const defaultAuthorData: AuthorData = {
|
|||
genres: ["Sinnlicher Thriller", "BDSM-Touch", "Maskerade"]
|
||||
}
|
||||
]
|
||||
},
|
||||
clara: {
|
||||
name: "Clara Finch",
|
||||
avatarUrl: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=300",
|
||||
heroTitle: "CLARA FINCH",
|
||||
heroSubtitle: "LYRIK, ESSAYS & NATURPHILOSOPHIE",
|
||||
bio: "Willkommen auf meiner kreativen Lichtung. Ich bin Clara Finch. Unter diesem Namen widme ich mich der lyrischen Poesie, den feinen Essays und naturphilosophischen Betrachtungen über die Verbindung von Mensch und Erde. Meine Texte entstehen oft auf langen Spaziergängen durch neblige Täler, dichte Wälder und karge Küstenstreifen. Sie fangen die Vergänglichkeit des Augenblicks ein – sanft wie Malve, tief verwurzelt wie Salbei. Hier teile ich meine gedruckten Werke, laufenden Notizbücher und klanglichen Inspirationen mit Ihnen. Treten Sie leise ein und verweilen Sie einen Moment.",
|
||||
projects: [
|
||||
{
|
||||
id: "proj_cl_1",
|
||||
title: "Echos des Unterholzes (Gedichtband)",
|
||||
status: "Schreibphase",
|
||||
progress: 75,
|
||||
description: "Eine lyrische Spurensuche im Moos und im Wind. Gedichte über das Schweigen der Bäume und die Weisheit des Bodens."
|
||||
},
|
||||
{
|
||||
id: "proj_cl_2",
|
||||
title: "Vom Atmen der Landschaft",
|
||||
status: "Konzeptionell",
|
||||
progress: 40,
|
||||
description: "Eine Sammlung naturphilosophischer Essays über unsere tiefe Verbundenheit mit dem Rhythmus der Jahreszeiten."
|
||||
}
|
||||
],
|
||||
books: [
|
||||
{
|
||||
id: "book_cl_1",
|
||||
title: "Die Stille der Halme",
|
||||
subtitle: "Gedichte",
|
||||
coverUrl: "https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=600",
|
||||
description: "Mein erster veröffentlichter Gedichtband, der den Leser auf eine kontemplative Reise durch unberührte Landschaften, stille Morgenstunden und das leise Rauschen der Felder entführt. Jedes Wort ist sorgsam wie ein seltener Kieselstein ausgewählt, um das Unsagbare im Alltäglichen spürbar zu machen.",
|
||||
publishedYear: 2025,
|
||||
buyLink: "https://amazon.de",
|
||||
spotifyPlaylistId: "37i9dQZF1DX2sQHbtM6v7h",
|
||||
genres: ["Lyrik", "Poesie", "Naturphilosophie"]
|
||||
},
|
||||
{
|
||||
id: "book_cl_2",
|
||||
title: "Knospen im Frost",
|
||||
subtitle: "Prosa-Miniaturen",
|
||||
coverUrl: "https://images.unsplash.com/photo-1448375240586-882707db888b?auto=format&fit=crop&q=80&w=600",
|
||||
description: "Eine Sammlung feinsinniger Prosa-Skizzen, die sich mit der Resilienz des Lebens inmitten winterlicher Kälte befassen. Metaphern aus Flora und Fauna zeichnen ein zärtliches Porträt des menschlichen Geistes in Zeiten des Umbruchs.",
|
||||
publishedYear: 2024,
|
||||
buyLink: "https://amazon.de",
|
||||
spotifyPlaylistId: "37i9dQZF1DWZqd5YICuS9g",
|
||||
genres: ["Prosa", "Natur-Essays", "Minimalismus"]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface AuthorProfile {
|
|||
export interface AuthorData {
|
||||
scifi: AuthorProfile;
|
||||
erotica: AuthorProfile;
|
||||
clara: AuthorProfile;
|
||||
}
|
||||
|
||||
export interface AdminSettings {
|
||||
|
|
|
|||
Loading…
Reference in a new issue