feat: require consent for Spotify embeds
This commit is contained in:
parent
91953bf162
commit
3abaaba869
10 changed files with 597 additions and 599 deletions
33
server.ts
33
server.ts
|
|
@ -15,7 +15,22 @@ const PORT = 3000;
|
||||||
const DATA_DIR = path.resolve(process.env.DATA_DIR || path.join(process.cwd(), "data"));
|
const DATA_DIR = path.resolve(process.env.DATA_DIR || path.join(process.cwd(), "data"));
|
||||||
const DATA_FILE = path.join(DATA_DIR, "database.json");
|
const DATA_FILE = path.join(DATA_DIR, "database.json");
|
||||||
const BACKUP_DIR = path.join(DATA_DIR, "backups");
|
const BACKUP_DIR = path.join(DATA_DIR, "backups");
|
||||||
const CURRENT_SCHEMA_VERSION = 1;
|
const CURRENT_SCHEMA_VERSION = 2;
|
||||||
|
const LEGACY_DEFAULT_IMAGE_URLS = new Set([
|
||||||
|
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=300",
|
||||||
|
"https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1518709268805-4e9042af9f23?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=300",
|
||||||
|
"https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=300",
|
||||||
|
"https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1448375240586-882707db888b?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=300",
|
||||||
|
"https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400",
|
||||||
|
]);
|
||||||
let writeQueue: Promise<void> = Promise.resolve();
|
let writeQueue: Promise<void> = Promise.resolve();
|
||||||
let serverReady = false;
|
let serverReady = false;
|
||||||
|
|
||||||
|
|
@ -83,6 +98,7 @@ async function initDatabase(): Promise<AuthorData> {
|
||||||
}
|
}
|
||||||
// Legacy files may not contain all newer profiles yet; migrations below add them.
|
// Legacy files may not contain all newer profiles yet; migrations below add them.
|
||||||
const parsed = rawParsed as AuthorData;
|
const parsed = rawParsed as AuthorData;
|
||||||
|
const loadedSchemaVersion = parsed.schemaVersion ?? 0;
|
||||||
let modified = false;
|
let modified = false;
|
||||||
|
|
||||||
// Ensure all dynamic static text and design properties exist for all profiles
|
// Ensure all dynamic static text and design properties exist for all profiles
|
||||||
|
|
@ -129,7 +145,20 @@ async function initDatabase(): Promise<AuthorData> {
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.schemaVersion === undefined) {
|
// Version 2 removes only the exact historical demo images. User-uploaded
|
||||||
|
// and other explicitly configured image URLs remain untouched.
|
||||||
|
if (loadedSchemaVersion < 2) {
|
||||||
|
for (const key of profileKeys) {
|
||||||
|
const profile = parsed[key];
|
||||||
|
if (LEGACY_DEFAULT_IMAGE_URLS.has(profile.avatarUrl)) profile.avatarUrl = "";
|
||||||
|
for (const book of profile.books) {
|
||||||
|
if (LEGACY_DEFAULT_IMAGE_URLS.has(book.coverUrl)) book.coverUrl = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
||||||
parsed.schemaVersion = CURRENT_SCHEMA_VERSION;
|
parsed.schemaVersion = CURRENT_SCHEMA_VERSION;
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { Book, Project, AuthorProfile, LegalDocument } from "../types";
|
import { Book, Project, AuthorProfile, LegalDocument } from "../types";
|
||||||
import { Leaf, Feather, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
import { Leaf, Feather, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
||||||
import SpotlightSection from "./SpotlightSection";
|
import SpotlightSection from "./SpotlightSection";
|
||||||
import LegalModal from "./LegalModal";
|
import LegalModal from "./LegalModal";
|
||||||
import ProjectDetailMedia from "./ProjectDetailMedia";
|
import ProjectDetailMedia from "./ProjectDetailMedia";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface ClaraPortfolioProps {
|
interface ClaraPortfolioProps {
|
||||||
data: AuthorProfile;
|
data: AuthorProfile;
|
||||||
|
|
@ -27,7 +28,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
const fontClass = data.fontFamily === "sans" ? "font-sans" : data.fontFamily === "mono" ? "font-mono" : "font-serif";
|
const fontClass = data.fontFamily === "sans" ? "font-sans" : data.fontFamily === "mono" ? "font-mono" : "font-serif";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`min-h-screen text-stone-200 ${fontClass} selection:bg-[#B39EAF]/30 selection:text-white overflow-x-hidden relative`}
|
className={`min-h-screen text-stone-200 ${fontClass} selection:bg-[#B39EAF]/30 selection:text-white overflow-x-hidden relative`}
|
||||||
style={{ backgroundColor: bgColor }}
|
style={{ backgroundColor: bgColor }}
|
||||||
>
|
>
|
||||||
|
|
@ -47,14 +48,14 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
{/* Header / Hero */}
|
{/* Header / Hero */}
|
||||||
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-stone-800/80">
|
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-stone-800/80">
|
||||||
<div className="text-center max-w-4xl mx-auto space-y-6">
|
<div className="text-center max-w-4xl mx-auto space-y-6">
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center gap-2 px-4 py-1.5 bg-stone-900/80 border rounded-full text-xs tracking-wider mb-2 shadow-sm"
|
className="inline-flex items-center gap-2 px-4 py-1.5 bg-stone-900/80 border rounded-full text-xs tracking-wider mb-2 shadow-sm"
|
||||||
style={{ borderColor: `${accentColor}40`, color: accentColor }}
|
style={{ borderColor: `${accentColor}40`, color: accentColor }}
|
||||||
>
|
>
|
||||||
<Leaf className="w-3.5 h-3.5 animate-pulse" style={{ color: accentColor }} />
|
<Leaf className="w-3.5 h-3.5 animate-pulse" style={{ color: accentColor }} />
|
||||||
<span className="font-serif italic text-stone-300">{data.badgeText || "Lyrik & Naturphilosophie"}</span>
|
<span className="font-serif italic text-stone-300">{data.badgeText || "Lyrik & Naturphilosophie"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-6xl font-serif tracking-widest text-stone-100 uppercase font-light drop-shadow-sm">
|
<h1 className="text-4xl md:text-6xl font-serif tracking-widest text-stone-100 uppercase font-light drop-shadow-sm">
|
||||||
{data.heroTitle || data.name || "CLARA FINCH"}
|
{data.heroTitle || data.name || "CLARA FINCH"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -65,19 +66,19 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
|
|
||||||
{/* Quick Section Navigation */}
|
{/* Quick Section Navigation */}
|
||||||
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
||||||
<a
|
<a
|
||||||
href="#ueber-mich"
|
href="#ueber-mich"
|
||||||
className="px-4 py-2 rounded-full border border-stone-800 bg-stone-900/60 hover:border-stone-500 transition-all text-stone-300 uppercase tracking-wider"
|
className="px-4 py-2 rounded-full border border-stone-800 bg-stone-900/60 hover:border-stone-500 transition-all text-stone-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
🌿 {data.aboutTitle || "Über mich"}
|
🌿 {data.aboutTitle || "Über mich"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#projekte"
|
href="#projekte"
|
||||||
className="px-4 py-2 rounded-full border border-stone-800 bg-stone-900/60 hover:border-stone-500 transition-all text-stone-300 uppercase tracking-wider"
|
className="px-4 py-2 rounded-full border border-stone-800 bg-stone-900/60 hover:border-stone-500 transition-all text-stone-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
✍️ {data.projectsTitle || "Notizbücher"}
|
✍️ {data.projectsTitle || "Notizbücher"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#buecherregal"
|
href="#buecherregal"
|
||||||
className="px-4 py-2 rounded-full border border-stone-800 bg-stone-900/60 hover:border-stone-500 transition-all text-stone-300 uppercase tracking-wider"
|
className="px-4 py-2 rounded-full border border-stone-800 bg-stone-900/60 hover:border-stone-500 transition-all text-stone-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
|
|
@ -89,7 +90,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
|
|
||||||
{/* Main Grid */}
|
{/* Main Grid */}
|
||||||
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
||||||
|
|
||||||
{/* About writer */}
|
{/* About writer */}
|
||||||
<section id="ueber-mich" className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
<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="md:col-span-4 flex justify-center">
|
||||||
|
|
@ -97,9 +98,9 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
{/* Outer soft natural glow */}
|
{/* Outer soft natural glow */}
|
||||||
<div className="absolute -inset-1.5 rounded-2xl blur opacity-20 group-hover:opacity-40 transition duration-1000 group-hover:duration-200" style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }} />
|
<div className="absolute -inset-1.5 rounded-2xl blur opacity-20 group-hover:opacity-40 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-stone-800" style={{ backgroundColor: cardBgColor }}>
|
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-stone-800" style={{ backgroundColor: cardBgColor }}>
|
||||||
<img
|
<img
|
||||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=400"}
|
src={data.avatarUrl || undefined}
|
||||||
alt={data.name}
|
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"
|
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"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
@ -114,7 +115,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
<div className="prose prose-invert prose-stone max-w-none text-stone-300 leading-relaxed font-sans space-y-4">
|
<div className="prose prose-invert prose-stone max-w-none text-stone-300 leading-relaxed font-sans space-y-4">
|
||||||
<p className="whitespace-pre-wrap italic">{data.bio}</p>
|
<p className="whitespace-pre-wrap italic">{data.bio}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
||||||
{(data.bioTag1Label || data.bioTag1Value) && (
|
{(data.bioTag1Label || data.bioTag1Value) && (
|
||||||
<div className="p-3 border rounded-xl flex items-center gap-3" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
|
<div className="p-3 border rounded-xl flex items-center gap-3" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
|
||||||
|
|
@ -149,7 +150,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
|
|
||||||
{/* Section 1.5: Spotlight / Neuerscheinung */}
|
{/* Section 1.5: Spotlight / Neuerscheinung */}
|
||||||
{data.books?.find(b => b.isSpotlight) && (
|
{data.books?.find(b => b.isSpotlight) && (
|
||||||
<SpotlightSection
|
<SpotlightSection
|
||||||
book={data.books.find(b => b.isSpotlight)!}
|
book={data.books.find(b => b.isSpotlight)!}
|
||||||
title={data.spotlightTitle}
|
title={data.spotlightTitle}
|
||||||
subtitle={data.spotlightSubtitle}
|
subtitle={data.spotlightSubtitle}
|
||||||
|
|
@ -173,8 +174,8 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{data.projects.map((project) => (
|
{data.projects.map((project) => (
|
||||||
<button
|
<button
|
||||||
key={project.id}
|
key={project.id}
|
||||||
onClick={() => setSelectedProject(project)}
|
onClick={() => setSelectedProject(project)}
|
||||||
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-md"
|
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-md"
|
||||||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}
|
||||||
|
|
@ -192,17 +193,17 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
{project.title}
|
{project.title}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-stone-300 text-sm italic line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
<div className="text-stone-300 text-sm italic line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-[#8FA797] hover:text-stone-200 font-medium"
|
className="underline text-[#8FA797] hover:text-stone-200 font-medium"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{project.description}
|
{project.description}
|
||||||
|
|
@ -214,8 +215,8 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 space-y-1.5 w-full">
|
<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-1 w-full bg-stone-900 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
||||||
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -244,13 +245,13 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-stone-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-stone-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl || "https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=300"}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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">
|
<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-stone-100 text-xs font-serif truncate">{book.title}</p>
|
<p className="text-stone-100 text-xs font-serif truncate">{book.title}</p>
|
||||||
<p className="text-[10px] font-sans mt-0.5 italic" style={{ color: accentColor }}>Verweilen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
<p className="text-[10px] font-sans mt-0.5 italic" style={{ color: accentColor }}>Verweilen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
||||||
|
|
@ -271,16 +272,16 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
)}
|
)}
|
||||||
{data.customSectionContent && (
|
{data.customSectionContent && (
|
||||||
<div className="text-stone-300 text-sm md:text-base leading-relaxed italic markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
<div className="text-stone-300 text-sm md:text-base leading-relaxed italic markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-[#8FA797] hover:text-stone-200 font-medium"
|
className="underline text-[#8FA797] hover:text-stone-200 font-medium"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data.customSectionContent}
|
{data.customSectionContent}
|
||||||
|
|
@ -295,15 +296,15 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
{/* Book details modular modal */}
|
{/* Book details modular modal */}
|
||||||
{selectedBook && (
|
{selectedBook && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-stone-950/85 backdrop-blur-md transition-opacity"
|
className="fixed inset-0 bg-stone-950/85 backdrop-blur-md transition-opacity"
|
||||||
onClick={() => setSelectedBook(null)}
|
onClick={() => setSelectedBook(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
<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">
|
<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
|
<button
|
||||||
onClick={() => setSelectedBook(null)}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
@ -311,19 +312,19 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
|
||||||
|
|
||||||
{/* Book Cover */}
|
{/* Book Cover */}
|
||||||
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
<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">
|
<div className="rounded-xl overflow-hidden shadow-2xl border border-[#4A5D50]/30 max-w-[260px] aspect-[2/3] w-full">
|
||||||
<img
|
<img
|
||||||
src={selectedBook.coverUrl}
|
src={selectedBook.coverUrl || undefined}
|
||||||
alt={selectedBook.title}
|
alt={selectedBook.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{selectedBook.buyLink && (
|
{selectedBook.buyLink && (
|
||||||
<a
|
<a
|
||||||
href={selectedBook.buyLink}
|
href={selectedBook.buyLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
@ -352,7 +353,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
<h3 className="text-3xl font-serif text-stone-100">
|
<h3 className="text-3xl font-serif text-stone-100">
|
||||||
{selectedBook.title}
|
{selectedBook.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{selectedBook.subtitle && (
|
{selectedBook.subtitle && (
|
||||||
<h4 className="text-sm font-sans italic text-[#B59EB1]">
|
<h4 className="text-sm font-sans italic text-[#B59EB1]">
|
||||||
{selectedBook.subtitle}
|
{selectedBook.subtitle}
|
||||||
|
|
@ -366,31 +367,15 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Embedded Spotify player (Only if playlist is non-empty) */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||||
<div className="space-y-2">
|
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -399,15 +384,15 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
{/* Project details modular modal */}
|
{/* Project details modular modal */}
|
||||||
{selectedProject && (
|
{selectedProject && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-stone-950/85 backdrop-blur-md transition-opacity"
|
className="fixed inset-0 bg-stone-950/85 backdrop-blur-md transition-opacity"
|
||||||
onClick={() => setSelectedProject(null)}
|
onClick={() => setSelectedProject(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
<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-5xl p-6 md:p-8 space-y-6">
|
<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-5xl p-6 md:p-8 space-y-6">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedProject(null)}
|
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
@ -436,8 +421,8 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
<span className="font-bold" style={{ color: accentColor }}>{selectedProject.progress}%</span>
|
<span className="font-bold" style={{ color: accentColor }}>{selectedProject.progress}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 w-full bg-stone-900 rounded-full overflow-hidden border border-stone-800">
|
<div className="h-2 w-full bg-stone-900 rounded-full overflow-hidden border border-stone-800">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000 ease-out shadow-md"
|
className="h-full rounded-full transition-all duration-1000 ease-out shadow-md"
|
||||||
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -461,7 +446,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -472,7 +457,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
<div className="max-w-7xl mx-auto space-y-3">
|
<div className="max-w-7xl mx-auto space-y-3">
|
||||||
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
||||||
<p className="text-stone-500">{data.footerText || "Betrachtungen, verwoben mit dem Rhythmus der wachsenden Natur."}</p>
|
<p className="text-stone-500">{data.footerText || "Betrachtungen, verwoben mit dem Rhythmus der wachsenden Natur."}</p>
|
||||||
|
|
||||||
{relevantLegalDocs.length > 0 && (
|
{relevantLegalDocs.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-6 pt-3 border-t border-stone-800/60 max-w-md mx-auto not-italic">
|
<div className="flex flex-wrap items-center justify-center gap-6 pt-3 border-t border-stone-800/60 max-w-md mx-auto not-italic">
|
||||||
{relevantLegalDocs.map((doc) => (
|
{relevantLegalDocs.map((doc) => (
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { Book, Project, AuthorProfile, LegalDocument } from "../types";
|
import { Book, Project, AuthorProfile, LegalDocument } from "../types";
|
||||||
import { Heart, Key, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
import { Heart, Key, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
||||||
import SpotlightSection from "./SpotlightSection";
|
import SpotlightSection from "./SpotlightSection";
|
||||||
import LegalModal from "./LegalModal";
|
import LegalModal from "./LegalModal";
|
||||||
import ProjectDetailMedia from "./ProjectDetailMedia";
|
import ProjectDetailMedia from "./ProjectDetailMedia";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface EroticaPortfolioProps {
|
interface EroticaPortfolioProps {
|
||||||
data: AuthorProfile;
|
data: AuthorProfile;
|
||||||
|
|
@ -27,7 +28,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
const fontClass = data.fontFamily === "sans" ? "font-sans" : data.fontFamily === "mono" ? "font-mono" : "font-serif";
|
const fontClass = data.fontFamily === "sans" ? "font-sans" : data.fontFamily === "mono" ? "font-mono" : "font-serif";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`min-h-screen text-neutral-200 ${fontClass} selection:bg-orange-500 selection:text-black overflow-x-hidden relative`}
|
className={`min-h-screen text-neutral-200 ${fontClass} selection:bg-orange-500 selection:text-black overflow-x-hidden relative`}
|
||||||
style={{ backgroundColor: bgColor }}
|
style={{ backgroundColor: bgColor }}
|
||||||
>
|
>
|
||||||
|
|
@ -47,14 +48,14 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
{/* Header / Hero */}
|
{/* Header / Hero */}
|
||||||
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-neutral-800/80">
|
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-neutral-800/80">
|
||||||
<div className="text-center max-w-4xl mx-auto space-y-6">
|
<div className="text-center max-w-4xl mx-auto space-y-6">
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center gap-2 px-4 py-1.5 bg-neutral-900/80 border rounded-full text-xs tracking-wider mb-2 shadow-sm"
|
className="inline-flex items-center gap-2 px-4 py-1.5 bg-neutral-900/80 border rounded-full text-xs tracking-wider mb-2 shadow-sm"
|
||||||
style={{ borderColor: `${accentColor}40`, color: accentColor }}
|
style={{ borderColor: `${accentColor}40`, color: accentColor }}
|
||||||
>
|
>
|
||||||
<Heart className="w-3.5 h-3.5 fill-current animate-pulse" style={{ color: accentColor }} />
|
<Heart className="w-3.5 h-3.5 fill-current animate-pulse" style={{ color: accentColor }} />
|
||||||
<span>{data.badgeText || "Stilvolle Sinnlichkeit & Romantik"}</span>
|
<span>{data.badgeText || "Stilvolle Sinnlichkeit & Romantik"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-6xl tracking-wide text-amber-100 uppercase font-light drop-shadow-sm font-serif">
|
<h1 className="text-4xl md:text-6xl tracking-wide text-amber-100 uppercase font-light drop-shadow-sm font-serif">
|
||||||
{data.heroTitle || data.name || "M. S. VELVET"}
|
{data.heroTitle || data.name || "M. S. VELVET"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -65,19 +66,19 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
|
|
||||||
{/* Quick Section Navigation */}
|
{/* Quick Section Navigation */}
|
||||||
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
||||||
<a
|
<a
|
||||||
href="#ueber-mich"
|
href="#ueber-mich"
|
||||||
className="px-4 py-2 rounded-full border border-neutral-800 bg-neutral-900/60 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
className="px-4 py-2 rounded-full border border-neutral-800 bg-neutral-900/60 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
🍑 {data.aboutTitle || "Über mich"}
|
🍑 {data.aboutTitle || "Über mich"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#projekte"
|
href="#projekte"
|
||||||
className="px-4 py-2 rounded-full border border-neutral-800 bg-neutral-900/60 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
className="px-4 py-2 rounded-full border border-neutral-800 bg-neutral-900/60 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
✍️ {data.projectsTitle || "In der Entstehung"}
|
✍️ {data.projectsTitle || "In der Entstehung"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#buecherregal"
|
href="#buecherregal"
|
||||||
className="px-4 py-2 rounded-full border border-neutral-800 bg-neutral-900/60 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
className="px-4 py-2 rounded-full border border-neutral-800 bg-neutral-900/60 hover:border-orange-400 hover:text-orange-300 transition-all text-neutral-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
|
|
@ -89,7 +90,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
|
|
||||||
{/* Main Grid */}
|
{/* Main Grid */}
|
||||||
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
||||||
|
|
||||||
{/* About writer */}
|
{/* About writer */}
|
||||||
<section id="ueber-mich" className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
<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="md:col-span-4 flex justify-center">
|
||||||
|
|
@ -97,9 +98,9 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
{/* Outer warm glow */}
|
{/* Outer warm glow */}
|
||||||
<div className="absolute -inset-1.5 rounded-2xl blur opacity-25 group-hover:opacity-60 transition duration-1000 group-hover:duration-200" style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }} />
|
<div className="absolute -inset-1.5 rounded-2xl blur opacity-25 group-hover:opacity-60 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-neutral-800" style={{ backgroundColor: cardBgColor }}>
|
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-neutral-800" style={{ backgroundColor: cardBgColor }}>
|
||||||
<img
|
<img
|
||||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=400"}
|
src={data.avatarUrl || undefined}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
className="w-full h-full object-cover grayscale brightness-90 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-1000"
|
className="w-full h-full object-cover grayscale brightness-90 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-1000"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
@ -114,7 +115,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
<div className="prose prose-invert prose-stone max-w-none text-neutral-300 leading-relaxed font-sans space-y-4">
|
<div className="prose prose-invert prose-stone max-w-none text-neutral-300 leading-relaxed font-sans space-y-4">
|
||||||
<p className="whitespace-pre-wrap italic">{data.bio}</p>
|
<p className="whitespace-pre-wrap italic">{data.bio}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
||||||
{(data.bioTag1Label || data.bioTag1Value) && (
|
{(data.bioTag1Label || data.bioTag1Value) && (
|
||||||
<div className="p-3 border rounded-xl flex items-center gap-3" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
|
<div className="p-3 border rounded-xl flex items-center gap-3" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
|
||||||
|
|
@ -149,7 +150,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
|
|
||||||
{/* Section 1.5: Spotlight / Neuerscheinung */}
|
{/* Section 1.5: Spotlight / Neuerscheinung */}
|
||||||
{data.books?.find(b => b.isSpotlight) && (
|
{data.books?.find(b => b.isSpotlight) && (
|
||||||
<SpotlightSection
|
<SpotlightSection
|
||||||
book={data.books.find(b => b.isSpotlight)!}
|
book={data.books.find(b => b.isSpotlight)!}
|
||||||
title={data.spotlightTitle}
|
title={data.spotlightTitle}
|
||||||
subtitle={data.spotlightSubtitle}
|
subtitle={data.spotlightSubtitle}
|
||||||
|
|
@ -173,8 +174,8 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{data.projects.map((project) => (
|
{data.projects.map((project) => (
|
||||||
<button
|
<button
|
||||||
key={project.id}
|
key={project.id}
|
||||||
onClick={() => setSelectedProject(project)}
|
onClick={() => setSelectedProject(project)}
|
||||||
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"
|
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` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}25` }}
|
||||||
|
|
@ -192,17 +193,17 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
{project.title}
|
{project.title}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-neutral-300 text-sm italic line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
<div className="text-neutral-300 text-sm italic line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-orange-400 hover:text-amber-300 font-medium"
|
className="underline text-orange-400 hover:text-amber-300 font-medium"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{project.description}
|
{project.description}
|
||||||
|
|
@ -214,8 +215,8 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 space-y-1.5 w-full">
|
<div className="mt-6 space-y-1.5 w-full">
|
||||||
<div className="h-1 w-full bg-neutral-900 rounded-full overflow-hidden">
|
<div className="h-1 w-full bg-neutral-900 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
||||||
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -244,13 +245,13 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-orange-950/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-orange-950/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl || "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=300"}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-neutral-950 via-neutral-950/90 to-transparent p-4 opacity-80 group-hover:opacity-100 transition-all duration-350 z-20">
|
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-neutral-950 via-neutral-950/90 to-transparent p-4 opacity-80 group-hover:opacity-100 transition-all duration-350 z-20">
|
||||||
<p className="text-amber-100 text-xs font-serif truncate">{book.title}</p>
|
<p className="text-amber-100 text-xs font-serif truncate">{book.title}</p>
|
||||||
<p className="text-[10px] font-sans mt-0.5 italic" style={{ color: accentColor }}>Eintauchen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
<p className="text-[10px] font-sans mt-0.5 italic" style={{ color: accentColor }}>Eintauchen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
||||||
|
|
@ -271,16 +272,16 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
)}
|
)}
|
||||||
{data.customSectionContent && (
|
{data.customSectionContent && (
|
||||||
<div className="text-neutral-300 text-sm md:text-base leading-relaxed italic markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
<div className="text-neutral-300 text-sm md:text-base leading-relaxed italic markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-orange-400 hover:text-amber-300 font-medium"
|
className="underline text-orange-400 hover:text-amber-300 font-medium"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data.customSectionContent}
|
{data.customSectionContent}
|
||||||
|
|
@ -295,15 +296,15 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
{/* Book details modular modal */}
|
{/* Book details modular modal */}
|
||||||
{selectedBook && (
|
{selectedBook && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-neutral-950/85 backdrop-blur-md transition-opacity"
|
className="fixed inset-0 bg-neutral-950/85 backdrop-blur-md transition-opacity"
|
||||||
onClick={() => setSelectedBook(null)}
|
onClick={() => setSelectedBook(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
<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-neutral-950 border border-neutral-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">
|
<div className="relative transform overflow-hidden rounded-2xl bg-neutral-950 border border-neutral-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
|
<button
|
||||||
onClick={() => setSelectedBook(null)}
|
onClick={() => setSelectedBook(null)}
|
||||||
className="absolute top-4 right-4 text-neutral-400 hover:text-amber-100 hover:bg-orange-950/80 px-2.5 py-1 rounded-md text-xs font-sans border border-neutral-800 transition-all cursor-pointer"
|
className="absolute top-4 right-4 text-neutral-400 hover:text-amber-100 hover:bg-orange-950/80 px-2.5 py-1 rounded-md text-xs font-sans border border-neutral-800 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
|
|
@ -311,19 +312,19 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
|
||||||
|
|
||||||
{/* Book Cover */}
|
{/* Book Cover */}
|
||||||
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
||||||
<div className="rounded-xl overflow-hidden shadow-2xl border border-orange-950/50 max-w-[260px] aspect-[2/3] w-full">
|
<div className="rounded-xl overflow-hidden shadow-2xl border border-orange-950/50 max-w-[260px] aspect-[2/3] w-full">
|
||||||
<img
|
<img
|
||||||
src={selectedBook.coverUrl}
|
src={selectedBook.coverUrl || undefined}
|
||||||
alt={selectedBook.title}
|
alt={selectedBook.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{selectedBook.buyLink && (
|
{selectedBook.buyLink && (
|
||||||
<a
|
<a
|
||||||
href={selectedBook.buyLink}
|
href={selectedBook.buyLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
@ -352,7 +353,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
<h3 className="text-3xl font-serif text-amber-100">
|
<h3 className="text-3xl font-serif text-amber-100">
|
||||||
{selectedBook.title}
|
{selectedBook.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{selectedBook.subtitle && (
|
{selectedBook.subtitle && (
|
||||||
<h4 className="text-sm font-sans italic text-orange-400">
|
<h4 className="text-sm font-sans italic text-orange-400">
|
||||||
{selectedBook.subtitle}
|
{selectedBook.subtitle}
|
||||||
|
|
@ -366,31 +367,15 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Embedded Spotify player */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||||
<div className="space-y-2">
|
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
|
||||||
<p className="text-xs font-sans text-orange-400 flex items-center gap-2 italic">
|
|
||||||
<Heart className="w-3.5 h-3.5 fill-orange-400 text-orange-400" />
|
|
||||||
<span>Sinnliche Begleitmusik (Spotify Playlist)</span>
|
|
||||||
</p>
|
|
||||||
<div className="rounded-xl overflow-hidden bg-neutral-900 border border-neutral-900">
|
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -399,15 +384,15 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
{/* Project details modular modal */}
|
{/* Project details modular modal */}
|
||||||
{selectedProject && (
|
{selectedProject && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-neutral-950/85 backdrop-blur-md transition-opacity"
|
className="fixed inset-0 bg-neutral-950/85 backdrop-blur-md transition-opacity"
|
||||||
onClick={() => setSelectedProject(null)}
|
onClick={() => setSelectedProject(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
<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-neutral-950 border border-neutral-900 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-5xl p-6 md:p-8 space-y-6">
|
<div className="relative transform overflow-hidden rounded-2xl bg-neutral-950 border border-neutral-900 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-5xl p-6 md:p-8 space-y-6">
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedProject(null)}
|
onClick={() => setSelectedProject(null)}
|
||||||
className="absolute top-4 right-4 text-neutral-400 hover:text-amber-100 hover:bg-orange-950/80 px-2.5 py-1 rounded-md text-xs font-sans border border-neutral-800 transition-all cursor-pointer"
|
className="absolute top-4 right-4 text-neutral-400 hover:text-amber-100 hover:bg-orange-950/80 px-2.5 py-1 rounded-md text-xs font-sans border border-neutral-800 transition-all cursor-pointer"
|
||||||
>
|
>
|
||||||
|
|
@ -436,8 +421,8 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
<span className="font-bold" style={{ color: accentColor }}>{selectedProject.progress}%</span>
|
<span className="font-bold" style={{ color: accentColor }}>{selectedProject.progress}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 w-full bg-neutral-900 rounded-full overflow-hidden border border-neutral-800">
|
<div className="h-2 w-full bg-neutral-900 rounded-full overflow-hidden border border-neutral-800">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000 ease-out shadow-md"
|
className="h-full rounded-full transition-all duration-1000 ease-out shadow-md"
|
||||||
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -461,7 +446,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -472,7 +457,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
<div className="max-w-7xl mx-auto space-y-3">
|
<div className="max-w-7xl mx-auto space-y-3">
|
||||||
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
||||||
<p className="text-neutral-500">{data.footerText || "Geschlossenes Refugium für feine, hochästhetische Phantasiereisen."}</p>
|
<p className="text-neutral-500">{data.footerText || "Geschlossenes Refugium für feine, hochästhetische Phantasiereisen."}</p>
|
||||||
|
|
||||||
{relevantLegalDocs.length > 0 && (
|
{relevantLegalDocs.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-6 pt-3 border-t border-neutral-800/60 max-w-md mx-auto not-italic">
|
<div className="flex flex-wrap items-center justify-center gap-6 pt-3 border-t border-neutral-800/60 max-w-md mx-auto not-italic">
|
||||||
{relevantLegalDocs.map((doc) => (
|
{relevantLegalDocs.map((doc) => (
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,13 @@
|
||||||
import { Music } from "lucide-react";
|
|
||||||
import { Project } from "../types";
|
import { Project } from "../types";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface ProjectDetailMediaProps {
|
interface ProjectDetailMediaProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
accentColor: string;
|
accentColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSpotifyPlaylistId(value?: string): string | null {
|
|
||||||
if (!value) return null;
|
|
||||||
const trimmed = value.trim();
|
|
||||||
const urlMatch = trimmed.match(/open\.spotify\.com\/playlist\/([a-zA-Z0-9]+)/);
|
|
||||||
const candidate = urlMatch?.[1] || trimmed;
|
|
||||||
return /^[a-zA-Z0-9]+$/.test(candidate) ? candidate : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProjectDetailMedia({ project, accentColor }: ProjectDetailMediaProps) {
|
export default function ProjectDetailMedia({ project, accentColor }: ProjectDetailMediaProps) {
|
||||||
const playlistId = normalizeSpotifyPlaylistId(project.spotifyPlaylistId);
|
if (!project.imageUrl && !project.spotifyPlaylistId) return null;
|
||||||
|
|
||||||
if (!project.imageUrl && !playlistId) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
|
@ -33,22 +23,12 @@ export default function ProjectDetailMedia({ project, accentColor }: ProjectDeta
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{playlistId && (
|
{project.spotifyPlaylistId && (
|
||||||
<div className="space-y-2">
|
<SpotifyConsentPlayer
|
||||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider" style={{ color: accentColor }}>
|
playlist={project.spotifyPlaylistId}
|
||||||
<Music className="h-4 w-4" />
|
title={`Spotify-Playlist zu ${project.title}`}
|
||||||
<span>Playlist zum Schreibprojekt</span>
|
accentColor={accentColor}
|
||||||
</div>
|
/>
|
||||||
<iframe
|
|
||||||
title={`Spotify-Playlist zu ${project.title}`}
|
|
||||||
src={`https://open.spotify.com/embed/playlist/${playlistId}?utm_source=generator&theme=0`}
|
|
||||||
width="100%"
|
|
||||||
height="152"
|
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
||||||
loading="lazy"
|
|
||||||
className="rounded-xl border-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { AuthorProfile, Book, Project, LegalDocument } from "../types";
|
import { AuthorProfile, Book, Project, LegalDocument } from "../types";
|
||||||
import {
|
import {
|
||||||
Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun, Music
|
Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import SpotlightSection from "./SpotlightSection";
|
import SpotlightSection from "./SpotlightSection";
|
||||||
import LegalModal from "./LegalModal";
|
import LegalModal from "./LegalModal";
|
||||||
import ProjectDetailMedia from "./ProjectDetailMedia";
|
import ProjectDetailMedia from "./ProjectDetailMedia";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface ReneePortfolioProps {
|
interface ReneePortfolioProps {
|
||||||
data: AuthorProfile;
|
data: AuthorProfile;
|
||||||
|
|
@ -29,7 +30,7 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
const fontClass = data.fontFamily === "serif" ? "font-serif" : data.fontFamily === "mono" ? "font-mono" : "font-sans";
|
const fontClass = data.fontFamily === "serif" ? "font-serif" : data.fontFamily === "mono" ? "font-mono" : "font-sans";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`min-h-screen text-slate-100 ${fontClass} selection:bg-pink-500 selection:text-white overflow-x-hidden relative`}
|
className={`min-h-screen text-slate-100 ${fontClass} selection:bg-pink-500 selection:text-white overflow-x-hidden relative`}
|
||||||
style={{ backgroundColor: bgColor }}
|
style={{ backgroundColor: bgColor }}
|
||||||
>
|
>
|
||||||
|
|
@ -49,14 +50,14 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
{/* Header / Hero */}
|
{/* Header / Hero */}
|
||||||
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-slate-800">
|
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-slate-800">
|
||||||
<div className="text-center max-w-4xl mx-auto space-y-6">
|
<div className="text-center max-w-4xl mx-auto space-y-6">
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center gap-2 px-4 py-1.5 bg-slate-800/80 border rounded-full text-xs tracking-wider mb-2 shadow-sm"
|
className="inline-flex items-center gap-2 px-4 py-1.5 bg-slate-800/80 border rounded-full text-xs tracking-wider mb-2 shadow-sm"
|
||||||
style={{ borderColor: `${accentColor}40`, color: accentColor }}
|
style={{ borderColor: `${accentColor}40`, color: accentColor }}
|
||||||
>
|
>
|
||||||
<Heart className="w-3.5 h-3.5 fill-current animate-pulse" style={{ color: secondaryColor }} />
|
<Heart className="w-3.5 h-3.5 fill-current animate-pulse" style={{ color: secondaryColor }} />
|
||||||
<span>{data.badgeText || "Herzklopfen & Gefühlsstark"}</span>
|
<span>{data.badgeText || "Herzklopfen & Gefühlsstark"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-4xl md:text-6xl tracking-wide text-white uppercase font-bold drop-shadow-sm">
|
<h1 className="text-4xl md:text-6xl tracking-wide text-white uppercase font-bold drop-shadow-sm">
|
||||||
{data.heroTitle || data.name || "RENEE HEART"}
|
{data.heroTitle || data.name || "RENEE HEART"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
@ -67,19 +68,19 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
|
|
||||||
{/* Quick Section Navigation */}
|
{/* Quick Section Navigation */}
|
||||||
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
||||||
<a
|
<a
|
||||||
href="#ueber-mich"
|
href="#ueber-mich"
|
||||||
className="px-4 py-2 rounded-full border border-slate-700 bg-slate-800/60 hover:border-yellow-400 hover:text-yellow-300 transition-all text-slate-200 uppercase tracking-wider font-medium"
|
className="px-4 py-2 rounded-full border border-slate-700 bg-slate-800/60 hover:border-yellow-400 hover:text-yellow-300 transition-all text-slate-200 uppercase tracking-wider font-medium"
|
||||||
>
|
>
|
||||||
💖 {data.aboutTitle || "Über Renee Heart"}
|
💖 {data.aboutTitle || "Über Renee Heart"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#projekte"
|
href="#projekte"
|
||||||
className="px-4 py-2 rounded-full border border-slate-700 bg-slate-800/60 hover:border-yellow-400 hover:text-yellow-300 transition-all text-slate-200 uppercase tracking-wider font-medium"
|
className="px-4 py-2 rounded-full border border-slate-700 bg-slate-800/60 hover:border-yellow-400 hover:text-yellow-300 transition-all text-slate-200 uppercase tracking-wider font-medium"
|
||||||
>
|
>
|
||||||
✍️ {data.projectsTitle || "Aus der Schreibstube"}
|
✍️ {data.projectsTitle || "Aus der Schreibstube"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#buecherregal"
|
href="#buecherregal"
|
||||||
className="px-4 py-2 rounded-full border border-slate-700 bg-slate-800/60 hover:border-yellow-400 hover:text-yellow-300 transition-all text-slate-200 uppercase tracking-wider font-medium"
|
className="px-4 py-2 rounded-full border border-slate-700 bg-slate-800/60 hover:border-yellow-400 hover:text-yellow-300 transition-all text-slate-200 uppercase tracking-wider font-medium"
|
||||||
>
|
>
|
||||||
|
|
@ -91,7 +92,7 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<main className="max-w-6xl mx-auto px-4 py-16 space-y-24">
|
<main className="max-w-6xl mx-auto px-4 py-16 space-y-24">
|
||||||
|
|
||||||
{/* Author Bio Section */}
|
{/* Author Bio Section */}
|
||||||
<section id="ueber-mich" className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
<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="md:col-span-4 flex justify-center">
|
||||||
|
|
@ -99,9 +100,9 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
{/* Outer yellow/pink glow */}
|
{/* Outer yellow/pink glow */}
|
||||||
<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="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-700 shadow-xl" style={{ backgroundColor: cardBgColor }}>
|
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-slate-700 shadow-xl" style={{ backgroundColor: cardBgColor }}>
|
||||||
<img
|
<img
|
||||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=400"}
|
src={data.avatarUrl || undefined}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition duration-500"
|
className="w-full h-full object-cover group-hover:scale-105 transition duration-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -115,7 +116,7 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
<div className="prose prose-invert max-w-none text-slate-300 leading-relaxed space-y-4">
|
<div className="prose prose-invert max-w-none text-slate-300 leading-relaxed space-y-4">
|
||||||
<p className="whitespace-pre-wrap">{data.bio}</p>
|
<p className="whitespace-pre-wrap">{data.bio}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
||||||
{(data.bioTag1Label || data.bioTag1Value) && (
|
{(data.bioTag1Label || data.bioTag1Value) && (
|
||||||
<div className="p-3 border rounded-xl flex items-center gap-3 shadow-sm" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}>
|
<div className="p-3 border rounded-xl flex items-center gap-3 shadow-sm" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}>
|
||||||
|
|
@ -150,7 +151,7 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
|
|
||||||
{/* Section 1.5: Spotlight / Neuerscheinung */}
|
{/* Section 1.5: Spotlight / Neuerscheinung */}
|
||||||
{data.books?.find(b => b.isSpotlight) && (
|
{data.books?.find(b => b.isSpotlight) && (
|
||||||
<SpotlightSection
|
<SpotlightSection
|
||||||
book={data.books.find(b => b.isSpotlight)!}
|
book={data.books.find(b => b.isSpotlight)!}
|
||||||
title={data.spotlightTitle}
|
title={data.spotlightTitle}
|
||||||
subtitle={data.spotlightSubtitle}
|
subtitle={data.spotlightSubtitle}
|
||||||
|
|
@ -174,8 +175,8 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{data.projects.map((project) => (
|
{data.projects.map((project) => (
|
||||||
<button
|
<button
|
||||||
key={project.id}
|
key={project.id}
|
||||||
onClick={() => setSelectedProject(project)}
|
onClick={() => setSelectedProject(project)}
|
||||||
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"
|
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` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}25` }}
|
||||||
|
|
@ -193,17 +194,17 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
{project.title}
|
{project.title}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-slate-300 text-sm line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
<div className="text-slate-300 text-sm line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{project.description}
|
{project.description}
|
||||||
|
|
@ -215,8 +216,8 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 space-y-1.5 w-full">
|
<div className="mt-6 space-y-1.5 w-full">
|
||||||
<div className="h-1.5 w-full bg-slate-900 rounded-full overflow-hidden">
|
<div className="h-1.5 w-full bg-slate-900 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
||||||
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -245,13 +246,13 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-yellow-500/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-yellow-500/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl || "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=300"}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
referrerPolicy="no-referrer"
|
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-90 group-hover:opacity-100 transition-all duration-300 z-20">
|
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-slate-950 via-slate-950/90 to-transparent p-4 opacity-90 group-hover:opacity-100 transition-all duration-300 z-20">
|
||||||
<p className="text-white text-xs font-bold truncate">{book.title}</p>
|
<p className="text-white text-xs font-bold truncate">{book.title}</p>
|
||||||
<p className="text-[10px] font-sans mt-0.5 font-semibold flex items-center gap-1" style={{ color: secondaryColor }}>
|
<p className="text-[10px] font-sans mt-0.5 font-semibold flex items-center gap-1" style={{ color: secondaryColor }}>
|
||||||
|
|
@ -274,16 +275,16 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
)}
|
)}
|
||||||
{data.customSectionContent && (
|
{data.customSectionContent && (
|
||||||
<div className="text-slate-300 text-sm md:text-base leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
<div className="text-slate-300 text-sm md:text-base leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data.customSectionContent}
|
{data.customSectionContent}
|
||||||
|
|
@ -298,11 +299,11 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
{/* Book Details Modal */}
|
{/* Book Details Modal */}
|
||||||
{selectedBook && (
|
{selectedBook && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md animate-fade-in">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-md animate-fade-in">
|
||||||
<div
|
<div
|
||||||
className="relative w-full max-w-2xl border rounded-2xl p-6 md:p-8 space-y-6 max-h-[90vh] overflow-y-auto shadow-2xl"
|
className="relative w-full max-w-2xl border rounded-2xl p-6 md:p-8 space-y-6 max-h-[90vh] overflow-y-auto shadow-2xl"
|
||||||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}40` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}40` }}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedBook(null)}
|
onClick={() => setSelectedBook(null)}
|
||||||
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-white bg-slate-800/80 rounded-full transition-colors cursor-pointer"
|
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-white bg-slate-800/80 rounded-full transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
|
|
@ -311,8 +312,8 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-start">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-start">
|
||||||
<div className="md:col-span-5 aspect-[2/3] rounded-xl overflow-hidden border border-slate-700 shadow-md">
|
<div className="md:col-span-5 aspect-[2/3] rounded-xl overflow-hidden border border-slate-700 shadow-md">
|
||||||
<img
|
<img
|
||||||
src={selectedBook.coverUrl}
|
src={selectedBook.coverUrl || undefined}
|
||||||
alt={selectedBook.title}
|
alt={selectedBook.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -338,44 +339,30 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedBook.description}
|
{selectedBook.description}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spotify Playlist player if configured */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{selectedBook.spotifyPlaylistId && (
|
{selectedBook.spotifyPlaylistId && (
|
||||||
<div className="pt-2">
|
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
|
||||||
<div className="flex items-center gap-1.5 text-xs text-slate-400 mb-2">
|
|
||||||
<Music className="w-3.5 h-3.5 text-pink-400" />
|
|
||||||
<span>Klangwelt zum Buch auf Spotify</span>
|
|
||||||
</div>
|
|
||||||
<iframe
|
|
||||||
style={{ borderRadius: "12px" }}
|
|
||||||
src={`https://open.spotify.com/embed/playlist/${selectedBook.spotifyPlaylistId}?utm_source=generator&theme=0`}
|
|
||||||
width="100%"
|
|
||||||
height="152"
|
|
||||||
frameBorder="0"
|
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedBook.buyLink && (
|
{selectedBook.buyLink && (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<a
|
<a
|
||||||
href={selectedBook.buyLink}
|
href={selectedBook.buyLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
@ -397,11 +384,11 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
{selectedProject && (
|
{selectedProject && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/80 backdrop-blur-md animate-fade-in">
|
<div className="fixed inset-0 z-50 overflow-y-auto bg-slate-950/80 backdrop-blur-md animate-fade-in">
|
||||||
<div className="flex min-h-full items-center justify-center p-4">
|
<div className="flex min-h-full items-center justify-center p-4">
|
||||||
<div
|
<div
|
||||||
className="relative w-full max-w-5xl border rounded-2xl p-6 md:p-8 my-8 space-y-6 shadow-2xl"
|
className="relative w-full max-w-5xl border rounded-2xl p-6 md:p-8 my-8 space-y-6 shadow-2xl"
|
||||||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}40` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}40` }}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedProject(null)}
|
onClick={() => setSelectedProject(null)}
|
||||||
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-white bg-slate-800/80 rounded-full transition-colors cursor-pointer"
|
className="absolute top-4 right-4 p-2 text-slate-400 hover:text-white bg-slate-800/80 rounded-full transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
|
|
@ -421,8 +408,8 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
<h3 className="text-2xl font-bold text-white">{selectedProject.title}</h3>
|
<h3 className="text-2xl font-bold text-white">{selectedProject.title}</h3>
|
||||||
|
|
||||||
<div className="h-2 w-full bg-slate-900 rounded-full overflow-hidden">
|
<div className="h-2 w-full bg-slate-900 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-500"
|
className="h-full rounded-full transition-all duration-500"
|
||||||
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -442,16 +429,16 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
</h4>
|
</h4>
|
||||||
)}
|
)}
|
||||||
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedProject.description || "Für dieses Schreibprojekt wurden noch keine weiteren Details abgelegt."}
|
{selectedProject.description || "Für dieses Schreibprojekt wurden noch keine weiteren Details abgelegt."}
|
||||||
|
|
@ -470,7 +457,7 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
<div className="max-w-7xl mx-auto space-y-3">
|
<div className="max-w-7xl mx-auto space-y-3">
|
||||||
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
||||||
<p className="text-slate-500">{data.footerText || "Geschrieben mit viel Gefühl und Herzblut."}</p>
|
<p className="text-slate-500">{data.footerText || "Geschrieben mit viel Gefühl und Herzblut."}</p>
|
||||||
|
|
||||||
{relevantLegalDocs.length > 0 && (
|
{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">
|
<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) => (
|
{relevantLegalDocs.map((doc) => (
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, M
|
||||||
import SpotlightSection from "./SpotlightSection";
|
import SpotlightSection from "./SpotlightSection";
|
||||||
import LegalModal from "./LegalModal";
|
import LegalModal from "./LegalModal";
|
||||||
import ProjectDetailMedia from "./ProjectDetailMedia";
|
import ProjectDetailMedia from "./ProjectDetailMedia";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface SciFiPortfolioProps {
|
interface SciFiPortfolioProps {
|
||||||
data: AuthorProfile;
|
data: AuthorProfile;
|
||||||
|
|
@ -27,7 +28,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
const fontClass = data.fontFamily === "serif" ? "font-serif" : data.fontFamily === "mono" ? "font-mono" : "font-sans";
|
const fontClass = data.fontFamily === "serif" ? "font-serif" : data.fontFamily === "mono" ? "font-mono" : "font-sans";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`min-h-screen text-slate-100 ${fontClass} selection:bg-cyan-500 selection:text-white overflow-x-hidden relative`}
|
className={`min-h-screen text-slate-100 ${fontClass} selection:bg-cyan-500 selection:text-white overflow-x-hidden relative`}
|
||||||
style={{ backgroundColor: bgColor }}
|
style={{ backgroundColor: bgColor }}
|
||||||
>
|
>
|
||||||
|
|
@ -41,11 +42,11 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
|
|
||||||
{/* Decorative starry background with magical aura */}
|
{/* 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 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
|
<div
|
||||||
className="absolute top-1/4 left-1/4 w-96 h-96 rounded-full blur-3xl pointer-events-none opacity-20"
|
className="absolute top-1/4 left-1/4 w-96 h-96 rounded-full blur-3xl pointer-events-none opacity-20"
|
||||||
style={{ backgroundColor: accentColor }}
|
style={{ backgroundColor: accentColor }}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="absolute top-2/3 right-1/4 w-96 h-96 rounded-full blur-3xl pointer-events-none opacity-20"
|
className="absolute top-2/3 right-1/4 w-96 h-96 rounded-full blur-3xl pointer-events-none opacity-20"
|
||||||
style={{ backgroundColor: secondaryColor }}
|
style={{ backgroundColor: secondaryColor }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -54,40 +55,40 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
<header className="relative py-24 px-4 max-w-7xl mx-auto border-b border-slate-800/80">
|
<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">
|
<div className="text-center max-w-4xl mx-auto">
|
||||||
{/* Subtle magical/futuristic accent */}
|
{/* Subtle magical/futuristic accent */}
|
||||||
<div
|
<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"
|
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 }}
|
style={{ borderColor: `${accentColor}40`, color: accentColor }}
|
||||||
>
|
>
|
||||||
<Star className="w-3.5 h-3.5 animate-pulse" style={{ color: accentColor }} />
|
<Star className="w-3.5 h-3.5 animate-pulse" style={{ color: accentColor }} />
|
||||||
<span>{data.badgeText || "Cyber-Mystisches Archive"}</span>
|
<span>{data.badgeText || "Cyber-Mystisches Archive"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 className="text-5xl md:text-7xl tracking-tight text-white mb-4 uppercase drop-shadow-md font-bold">
|
<h1 className="text-5xl md:text-7xl tracking-tight text-white mb-4 uppercase drop-shadow-md font-bold">
|
||||||
{data.heroTitle || data.name || "DANIEL HESSE"}
|
{data.heroTitle || data.name || "DANIEL HESSE"}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="font-mono text-xs md:text-sm tracking-widest uppercase mb-8" style={{ color: accentColor }}>
|
<p className="font-mono text-xs md:text-sm tracking-widest uppercase mb-8" style={{ color: accentColor }}>
|
||||||
{data.heroSubtitle || "WELTENWANDLER & STERNENZEICHNER"}
|
{data.heroSubtitle || "WELTENWANDLER & STERNENZEICHNER"}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
className="h-0.5 w-24 mx-auto rounded-full"
|
className="h-0.5 w-24 mx-auto rounded-full"
|
||||||
style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Quick Section Navigation */}
|
{/* Quick Section Navigation */}
|
||||||
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-mono">
|
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-mono">
|
||||||
<a
|
<a
|
||||||
href="#ueber-mich"
|
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"
|
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"}
|
🌌 {data.aboutTitle || "Über mich"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#projekte"
|
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"
|
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"}
|
🛰️ {data.projectsTitle || "Aktuelle Projekte"}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href="#buecherregal"
|
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
@ -99,20 +100,20 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
|
|
||||||
{/* Main Container */}
|
{/* Main Container */}
|
||||||
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
<main className="max-w-7xl mx-auto px-4 py-16 space-y-24">
|
||||||
|
|
||||||
{/* Section 1: Über mich */}
|
{/* Section 1: Über mich */}
|
||||||
<section id="ueber-mich" className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
<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="md:col-span-4 flex justify-center">
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
{/* Outer neon ring */}
|
{/* Outer neon ring */}
|
||||||
<div
|
<div
|
||||||
className="absolute -inset-1.5 rounded-2xl blur opacity-30 group-hover:opacity-70 transition duration-1000 group-hover:duration-200"
|
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})` }}
|
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 }}>
|
<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
|
<img
|
||||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=400"}
|
src={data.avatarUrl || undefined}
|
||||||
alt={data.name}
|
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"
|
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"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
@ -127,7 +128,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
<div className="prose prose-invert max-w-none text-slate-300 leading-relaxed space-y-4">
|
<div className="prose prose-invert max-w-none text-slate-300 leading-relaxed space-y-4">
|
||||||
<p className="whitespace-pre-wrap">{data.bio}</p>
|
<p className="whitespace-pre-wrap">{data.bio}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 font-mono text-xs">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 font-mono text-xs">
|
||||||
{(data.bioTag1Label || data.bioTag1Value) && (
|
{(data.bioTag1Label || data.bioTag1Value) && (
|
||||||
<div className="p-3 border rounded-lg flex items-center gap-3" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
|
<div className="p-3 border rounded-lg flex items-center gap-3" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}20` }}>
|
||||||
|
|
@ -162,7 +163,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
|
|
||||||
{/* Section 1.5: Spotlight / Neuerscheinung */}
|
{/* Section 1.5: Spotlight / Neuerscheinung */}
|
||||||
{data.books?.find(b => b.isSpotlight) && (
|
{data.books?.find(b => b.isSpotlight) && (
|
||||||
<SpotlightSection
|
<SpotlightSection
|
||||||
book={data.books.find(b => b.isSpotlight)!}
|
book={data.books.find(b => b.isSpotlight)!}
|
||||||
title={data.spotlightTitle}
|
title={data.spotlightTitle}
|
||||||
subtitle={data.spotlightSubtitle}
|
subtitle={data.spotlightSubtitle}
|
||||||
|
|
@ -189,8 +190,8 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{data.projects.map((project) => (
|
{data.projects.map((project) => (
|
||||||
<button
|
<button
|
||||||
key={project.id}
|
key={project.id}
|
||||||
onClick={() => setSelectedProject(project)}
|
onClick={() => setSelectedProject(project)}
|
||||||
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"
|
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` }}
|
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}25` }}
|
||||||
|
|
@ -208,17 +209,17 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
{project.title}
|
{project.title}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="text-slate-300 text-sm leading-relaxed line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
<div className="text-slate-300 text-sm leading-relaxed line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-cyan-400 hover:text-cyan-300 font-medium"
|
className="underline text-cyan-400 hover:text-cyan-300 font-medium"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{project.description}
|
{project.description}
|
||||||
|
|
@ -231,8 +232,8 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
{/* Custom glowing progress bar */}
|
{/* Custom glowing progress bar */}
|
||||||
<div className="mt-6 space-y-1.5 w-full">
|
<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-1.5 w-full bg-slate-950/80 rounded-full overflow-hidden border border-slate-800">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
className="h-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
||||||
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -261,13 +262,13 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}
|
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" />
|
<div className="absolute inset-0 bg-cyan-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl || "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=300"}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 z-0"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 z-0"
|
||||||
referrerPolicy="no-referrer"
|
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">
|
<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>
|
<p className="text-white text-xs font-bold truncate">{book.title}</p>
|
||||||
<p className="text-[10px] font-mono mt-0.5" style={{ color: accentColor }}>Details auslesen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
<p className="text-[10px] font-mono mt-0.5" style={{ color: accentColor }}>Details auslesen <ArrowRight className="inline-block w-2.5 h-2.5 ml-0.5" /></p>
|
||||||
|
|
@ -291,16 +292,16 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
)}
|
)}
|
||||||
{data.customSectionContent && (
|
{data.customSectionContent && (
|
||||||
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline text-cyan-400 hover:text-cyan-300 font-medium"
|
className="underline text-cyan-400 hover:text-cyan-300 font-medium"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data.customSectionContent}
|
{data.customSectionContent}
|
||||||
|
|
@ -316,16 +317,16 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
{selectedBook && (
|
{selectedBook && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||||
{/* Overlay background */}
|
{/* Overlay background */}
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-slate-950/80 backdrop-blur-md transition-opacity"
|
className="fixed inset-0 bg-slate-950/80 backdrop-blur-md transition-opacity"
|
||||||
onClick={() => setSelectedBook(null)}
|
onClick={() => setSelectedBook(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
<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-slate-900 border border-slate-800 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-4xl p-6 md:p-8 space-y-6">
|
<div className="relative transform overflow-hidden rounded-2xl bg-slate-900 border border-slate-800 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-4xl p-6 md:p-8 space-y-6">
|
||||||
|
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedBook(null)}
|
onClick={() => setSelectedBook(null)}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
@ -333,19 +334,19 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 pt-4">
|
||||||
|
|
||||||
{/* Book Cover */}
|
{/* Book Cover */}
|
||||||
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
<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">
|
<div className="rounded-xl overflow-hidden shadow-2xl border border-indigo-900/30 max-w-[260px] aspect-[2/3] w-full">
|
||||||
<img
|
<img
|
||||||
src={selectedBook.coverUrl}
|
src={selectedBook.coverUrl || undefined}
|
||||||
alt={selectedBook.title}
|
alt={selectedBook.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{selectedBook.buyLink && (
|
{selectedBook.buyLink && (
|
||||||
<a
|
<a
|
||||||
href={selectedBook.buyLink}
|
href={selectedBook.buyLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
|
@ -374,7 +375,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
<h3 className="text-3xl font-extrabold text-white">
|
<h3 className="text-3xl font-extrabold text-white">
|
||||||
{selectedBook.title}
|
{selectedBook.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{selectedBook.subtitle && (
|
{selectedBook.subtitle && (
|
||||||
<h4 className="text-sm font-mono text-indigo-400 uppercase tracking-wider">
|
<h4 className="text-sm font-mono text-indigo-400 uppercase tracking-wider">
|
||||||
{selectedBook.subtitle}
|
{selectedBook.subtitle}
|
||||||
|
|
@ -388,32 +389,15 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Integrated Spotify web player playlist if available */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||||
<div className="space-y-2">
|
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
|
||||||
<p className="text-xs font-mono text-indigo-400 flex items-center gap-2">
|
|
||||||
<Star className="w-3.5 h-3.5 animate-spin" />
|
|
||||||
<span>INTEGRIERTES ATMOSPHÄREN-SYSTEM (SPOTIFY PLAYLIST)</span>
|
|
||||||
</p>
|
|
||||||
<div className="rounded-xl overflow-hidden bg-slate-950 border border-slate-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"
|
|
||||||
className="bg-slate-950"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -423,16 +407,16 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
{selectedProject && (
|
{selectedProject && (
|
||||||
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
<div className="fixed inset-0 z-50 overflow-y-auto" role="dialog" aria-modal="true">
|
||||||
{/* Overlay background */}
|
{/* Overlay background */}
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-slate-950/80 backdrop-blur-md transition-opacity"
|
className="fixed inset-0 bg-slate-950/80 backdrop-blur-md transition-opacity"
|
||||||
onClick={() => setSelectedProject(null)}
|
onClick={() => setSelectedProject(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
<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-slate-900 border border-slate-800 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-5xl p-6 md:p-8 space-y-6">
|
<div className="relative transform overflow-hidden rounded-2xl bg-slate-900 border border-slate-800 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-5xl p-6 md:p-8 space-y-6">
|
||||||
|
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedProject(null)}
|
onClick={() => setSelectedProject(null)}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
@ -461,8 +445,8 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
<span className="font-bold" style={{ color: accentColor }}>{selectedProject.progress}%</span>
|
<span className="font-bold" style={{ color: accentColor }}>{selectedProject.progress}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-2 w-full bg-slate-950 rounded-full overflow-hidden border border-slate-800">
|
<div className="h-2 w-full bg-slate-950 rounded-full overflow-hidden border border-slate-800">
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-1000 ease-out shadow-md"
|
className="h-full rounded-full transition-all duration-1000 ease-out shadow-md"
|
||||||
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -486,7 +470,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -497,7 +481,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
<div className="max-w-7xl mx-auto space-y-4">
|
<div className="max-w-7xl mx-auto space-y-4">
|
||||||
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
<p>© {new Date().getFullYear()} {data.name}. Alle Rechte vorbehalten.</p>
|
||||||
<p className="text-slate-500">{data.footerText || "Dokumentation, Fragmente und stellare Chiffren. Verschlüsselt mit moderner Node.JS Technologie."}</p>
|
<p className="text-slate-500">{data.footerText || "Dokumentation, Fragmente und stellare Chiffren. Verschlüsselt mit moderner Node.JS Technologie."}</p>
|
||||||
|
|
||||||
{relevantLegalDocs.length > 0 && (
|
{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">
|
<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) => (
|
{relevantLegalDocs.map((doc) => (
|
||||||
|
|
|
||||||
68
src/components/SpotifyConsentPlayer.tsx
Normal file
68
src/components/SpotifyConsentPlayer.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Music } from "lucide-react";
|
||||||
|
|
||||||
|
interface SpotifyConsentPlayerProps {
|
||||||
|
playlist: string;
|
||||||
|
title: string;
|
||||||
|
accentColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlaylistId(value: string): string | null {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const urlMatch = trimmed.match(/open\.spotify\.com\/playlist\/([a-zA-Z0-9]+)/);
|
||||||
|
const candidate = urlMatch?.[1] || trimmed;
|
||||||
|
return /^[a-zA-Z0-9]+$/.test(candidate) ? candidate : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpotifyConsentPlayer({ playlist, title, accentColor = "#34d399" }: SpotifyConsentPlayerProps) {
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
const playlistId = normalizePlaylistId(playlist);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoaded(false);
|
||||||
|
}, [playlist]);
|
||||||
|
|
||||||
|
if (!playlistId) return null;
|
||||||
|
|
||||||
|
if (isLoaded) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<iframe
|
||||||
|
title={title}
|
||||||
|
src={`https://open.spotify.com/embed/playlist/${playlistId}?utm_source=generator&theme=0`}
|
||||||
|
width="100%"
|
||||||
|
height="152"
|
||||||
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
|
loading="lazy"
|
||||||
|
className="rounded-xl border-0"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsLoaded(false)}
|
||||||
|
className="text-[10px] text-slate-500 underline underline-offset-2 hover:text-slate-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
Spotify-Player wieder deaktivieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-black/20 p-4 space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider" style={{ color: accentColor }}>
|
||||||
|
<Music className="h-4 w-4" />
|
||||||
|
<span>Spotify-Playlist</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs leading-relaxed text-slate-400">
|
||||||
|
Der Player wird erst nach Ihrer Zustimmung von Spotify geladen. Dabei wird eine Verbindung zu Spotify hergestellt.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsLoaded(true)}
|
||||||
|
className="rounded-lg border border-white/15 bg-white/10 px-4 py-2 text-xs font-semibold text-white hover:bg-white/15 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Spotify-Player laden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { Book } from "../types";
|
import { Book } from "../types";
|
||||||
import { Sparkles, Calendar, ShoppingBag, Music, ExternalLink, Tag } from "lucide-react";
|
import { Sparkles, Calendar, ShoppingBag, ExternalLink, Tag } from "lucide-react";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface SpotlightSectionProps {
|
interface SpotlightSectionProps {
|
||||||
book: Book;
|
book: Book;
|
||||||
|
|
@ -20,13 +21,11 @@ export default function SpotlightSection({
|
||||||
secondaryColor,
|
secondaryColor,
|
||||||
cardBgColor,
|
cardBgColor,
|
||||||
}: SpotlightSectionProps) {
|
}: SpotlightSectionProps) {
|
||||||
const [showSpotify, setShowSpotify] = useState(false);
|
|
||||||
|
|
||||||
// Helper to check if a release date is in the future
|
// Helper to check if a release date is in the future
|
||||||
const isFutureRelease = (): boolean => {
|
const isFutureRelease = (): boolean => {
|
||||||
if (!book.publishedYear) return false;
|
if (!book.publishedYear) return false;
|
||||||
const str = String(book.publishedYear).trim();
|
const str = String(book.publishedYear).trim();
|
||||||
|
|
||||||
// Check if it's a 4-digit year like "2027"
|
// Check if it's a 4-digit year like "2027"
|
||||||
if (/^\d{4}$/.test(str)) {
|
if (/^\d{4}$/.test(str)) {
|
||||||
return parseInt(str) > new Date().getFullYear();
|
return parseInt(str) > new Date().getFullYear();
|
||||||
|
|
@ -56,7 +55,7 @@ export default function SpotlightSection({
|
||||||
return (
|
return (
|
||||||
<section id="spotlight" className="space-y-6 my-12">
|
<section id="spotlight" className="space-y-6 my-12">
|
||||||
<div className="flex items-center gap-3 border-b border-slate-800 pb-3">
|
<div className="flex items-center gap-3 border-b border-slate-800 pb-3">
|
||||||
<div
|
<div
|
||||||
className="p-2 rounded-xl flex items-center justify-center shadow-md"
|
className="p-2 rounded-xl flex items-center justify-center shadow-md"
|
||||||
style={{ backgroundColor: `${accentColor}20`, color: accentColor }}
|
style={{ backgroundColor: `${accentColor}20`, color: accentColor }}
|
||||||
>
|
>
|
||||||
|
|
@ -70,16 +69,16 @@ export default function SpotlightSection({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="relative overflow-hidden border rounded-2xl p-6 md:p-8 shadow-2xl transition-all"
|
className="relative overflow-hidden border rounded-2xl p-6 md:p-8 shadow-2xl transition-all"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: cardBgColor,
|
backgroundColor: cardBgColor,
|
||||||
borderColor: `${accentColor}40`,
|
borderColor: `${accentColor}40`,
|
||||||
boxShadow: `0 20px 50px -10px ${accentColor}15`
|
boxShadow: `0 20px 50px -10px ${accentColor}15`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Subtle Ambient Background Gradient */}
|
{/* Subtle Ambient Background Gradient */}
|
||||||
<div
|
<div
|
||||||
className="absolute -right-20 -bottom-20 w-80 h-80 rounded-full blur-3xl pointer-events-none opacity-20"
|
className="absolute -right-20 -bottom-20 w-80 h-80 rounded-full blur-3xl pointer-events-none opacity-20"
|
||||||
style={{ backgroundColor: accentColor }}
|
style={{ backgroundColor: accentColor }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -88,14 +87,14 @@ export default function SpotlightSection({
|
||||||
{/* Cover image column */}
|
{/* Cover image column */}
|
||||||
<div className="md:col-span-4 flex flex-col items-center space-y-4">
|
<div className="md:col-span-4 flex flex-col items-center space-y-4">
|
||||||
<div className="relative group w-full max-w-[260px]">
|
<div className="relative group w-full max-w-[260px]">
|
||||||
<div
|
<div
|
||||||
className="absolute -inset-1 rounded-2xl blur-lg opacity-40 group-hover:opacity-75 transition duration-500"
|
className="absolute -inset-1 rounded-2xl blur-lg opacity-40 group-hover:opacity-75 transition duration-500"
|
||||||
style={{ backgroundColor: accentColor }}
|
style={{ backgroundColor: accentColor }}
|
||||||
/>
|
/>
|
||||||
<div className="relative aspect-[2/3] w-full rounded-xl overflow-hidden border border-slate-700/60 shadow-2xl bg-slate-900">
|
<div className="relative aspect-[2/3] w-full rounded-xl overflow-hidden border border-slate-700/60 shadow-2xl bg-slate-900">
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
/>
|
/>
|
||||||
|
|
@ -120,10 +119,10 @@ export default function SpotlightSection({
|
||||||
{/* Details column */}
|
{/* Details column */}
|
||||||
<div className="md:col-span-8 space-y-5">
|
<div className="md:col-span-8 space-y-5">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className="px-3 py-1 rounded-full text-xs font-mono font-bold tracking-wide uppercase border flex items-center gap-1.5 shadow-sm"
|
className="px-3 py-1 rounded-full text-xs font-mono font-bold tracking-wide uppercase border flex items-center gap-1.5 shadow-sm"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isFuture ? `${secondaryColor}25` : `${accentColor}25`,
|
backgroundColor: isFuture ? `${secondaryColor}25` : `${accentColor}25`,
|
||||||
borderColor: isFuture ? secondaryColor : accentColor,
|
borderColor: isFuture ? secondaryColor : accentColor,
|
||||||
color: isFuture ? "#fff" : accentColor
|
color: isFuture ? "#fff" : accentColor
|
||||||
}}
|
}}
|
||||||
|
|
@ -156,8 +155,8 @@ export default function SpotlightSection({
|
||||||
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
||||||
<Tag className="w-3.5 h-3.5 text-slate-500 mr-1" />
|
<Tag className="w-3.5 h-3.5 text-slate-500 mr-1" />
|
||||||
{book.genres.map((g, idx) => (
|
{book.genres.map((g, idx) => (
|
||||||
<span
|
<span
|
||||||
key={idx}
|
key={idx}
|
||||||
className="px-2.5 py-0.5 rounded-md text-[11px] font-mono border bg-slate-900/80 text-slate-300 border-slate-800"
|
className="px-2.5 py-0.5 rounded-md text-[11px] font-mono border bg-slate-900/80 text-slate-300 border-slate-800"
|
||||||
>
|
>
|
||||||
{g}
|
{g}
|
||||||
|
|
@ -168,46 +167,27 @@ export default function SpotlightSection({
|
||||||
|
|
||||||
{/* Synopsis */}
|
{/* Synopsis */}
|
||||||
<div className="prose prose-invert max-w-none text-slate-300 text-sm leading-relaxed space-y-3 pt-2 border-t border-slate-800/60">
|
<div className="prose prose-invert max-w-none text-slate-300 text-sm leading-relaxed space-y-3 pt-2 border-t border-slate-800/60">
|
||||||
<Markdown
|
<Markdown
|
||||||
components={{
|
components={{
|
||||||
a: ({ node, ...props }) => (
|
a: ({ node, ...props }) => (
|
||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="underline font-medium hover:opacity-80"
|
className="underline font-medium hover:opacity-80"
|
||||||
style={{ color: accentColor }}
|
style={{ color: accentColor }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{book.description}
|
{book.description}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spotify Player Toggle if present */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{book.spotifyPlaylistId && (
|
{book.spotifyPlaylistId && (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<button
|
<SpotifyConsentPlayer playlist={book.spotifyPlaylistId} title={`Spotify-Playlist zu ${book.title}`} accentColor={accentColor} />
|
||||||
onClick={() => setShowSpotify(!showSpotify)}
|
|
||||||
className="px-4 py-2 bg-slate-900 hover:bg-slate-850 border border-slate-700 rounded-xl text-xs font-mono text-slate-300 flex items-center gap-2 transition-all cursor-pointer"
|
|
||||||
>
|
|
||||||
<Music className="w-4 h-4 text-emerald-400" />
|
|
||||||
<span>{showSpotify ? "Playlist einklappen" : "Buch-Soundtrack (Spotify) anhören"}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showSpotify && (
|
|
||||||
<div className="mt-3 rounded-xl overflow-hidden border border-slate-800 shadow-xl bg-slate-950">
|
|
||||||
<iframe
|
|
||||||
src={`https://open.spotify.com/embed/playlist/${book.spotifyPlaylistId}?utm_source=generator&theme=0`}
|
|
||||||
width="100%"
|
|
||||||
height="152"
|
|
||||||
frameBorder="0"
|
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { AuthorData } from "./types";
|
||||||
export const defaultAuthorData: AuthorData = {
|
export const defaultAuthorData: AuthorData = {
|
||||||
scifi: {
|
scifi: {
|
||||||
name: "Daniel Hesse",
|
name: "Daniel Hesse",
|
||||||
avatarUrl: "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=300",
|
avatarUrl: "",
|
||||||
heroTitle: "DANIEL HESSE",
|
heroTitle: "DANIEL HESSE",
|
||||||
heroSubtitle: "WELTENWANDLER & STERNENZEICHNER",
|
heroSubtitle: "WELTENWANDLER & STERNENZEICHNER",
|
||||||
bio: "Herzlich willkommen! Ich bin Daniel Hesse. Ich schreibe epische Fantasy und tiefgründige Science-Fiction, die dich an die Grenzen der Realität und weit darüber hinaus entführen. Schon als Kind faszinierten mich die unendlichen Weiten des Kosmos und die alten Sagen vergessener Völker. In meinen Büchern verschmelzen kalte, fortschrittliche Technologie und uralter, mystischer Zauber zu einer einzigartigen Atmosphäre. Wenn ich nicht gerade neue Galaxien am Laptop erschaffe, wandere ich durch dunkle Wälder oder beobachte mit meinem Teleskop den Nachthimmel.",
|
bio: "Herzlich willkommen! Ich bin Daniel Hesse. Ich schreibe epische Fantasy und tiefgründige Science-Fiction, die dich an die Grenzen der Realität und weit darüber hinaus entführen. Schon als Kind faszinierten mich die unendlichen Weiten des Kosmos und die alten Sagen vergessener Völker. In meinen Büchern verschmelzen kalte, fortschrittliche Technologie und uralter, mystischer Zauber zu einer einzigartigen Atmosphäre. Wenn ich nicht gerade neue Galaxien am Laptop erschaffe, wandere ich durch dunkle Wälder oder beobachte mit meinem Teleskop den Nachthimmel.",
|
||||||
|
|
@ -28,7 +28,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_1",
|
id: "book_1",
|
||||||
title: "Caelum: Ruf der Sterne",
|
title: "Caelum: Ruf der Sterne",
|
||||||
subtitle: "Arche-Chroniken I",
|
subtitle: "Arche-Chroniken I",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Am Rande der bekannten Galaxie sucht die Besatzung der 'Caelum' nach einer neuen Heimat für die letzten Überlebenden der Erde. Doch der leere Raum zwischen den Sternen ist nicht so unbewohnt, wie sie dachten. Es wartet eine Bedrohung, die älter ist als die Zeit selbst, aber auch eine Macht, die das Verständnis der Menschheit revolutionieren könnte.",
|
description: "Am Rande der bekannten Galaxie sucht die Besatzung der 'Caelum' nach einer neuen Heimat für die letzten Überlebenden der Erde. Doch der leere Raum zwischen den Sternen ist nicht so unbewohnt, wie sie dachten. Es wartet eine Bedrohung, die älter ist als die Zeit selbst, aber auch eine Macht, die das Verständnis der Menschheit revolutionieren könnte.",
|
||||||
publishedYear: 2024,
|
publishedYear: 2024,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -41,7 +41,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_2",
|
id: "book_2",
|
||||||
title: "Die Schatten von ThalÃassia",
|
title: "Die Schatten von ThalÃassia",
|
||||||
subtitle: "Einzelband",
|
subtitle: "Einzelband",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "In der versunkenen Unterwasser-Metropole Thalassia ringen Magokraten und Kybernetiker um ein uraltes Artefakt. Ein junger Taucher stößt in den abyssalen Gräben auf ein grauenvolles Geheimnis, das das Schicksal beider Fraktionen besiegeln könnte.",
|
description: "In der versunkenen Unterwasser-Metropole Thalassia ringen Magokraten und Kybernetiker um ein uraltes Artefakt. Ein junger Taucher stößt in den abyssalen Gräben auf ein grauenvolles Geheimnis, das das Schicksal beider Fraktionen besiegeln könnte.",
|
||||||
publishedYear: 2023,
|
publishedYear: 2023,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -80,7 +80,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
},
|
},
|
||||||
erotica: {
|
erotica: {
|
||||||
name: "Annie Slone",
|
name: "Annie Slone",
|
||||||
avatarUrl: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=300",
|
avatarUrl: "",
|
||||||
heroTitle: "ANNIE SLONE",
|
heroTitle: "ANNIE SLONE",
|
||||||
heroSubtitle: "ELEGANT-SINNLICHE LITERATUR",
|
heroSubtitle: "ELEGANT-SINNLICHE LITERATUR",
|
||||||
bio: "Willkommen in meiner privaten Welt der Versuchung. Unter dem Pseudonym Annie Slone schreibe ich stilvolle, hocherotische Romane und prickelnde Erzählungen. Meine Leidenschaft liegt darin, das feine Spiel zwischen Verlangen und Hingabe, Macht und Vertrauen in all seinen schillernden Facetten zu beleuchten. Meine Geschichten richten sich an Kenner anspruchsvoller Erotikliteratur, die Wert auf tiefgründige Charaktere, knisternde Spannung und ästhetische Beschreibungen legen. Treten Sie näher und lassen Sie sich entführen.",
|
bio: "Willkommen in meiner privaten Welt der Versuchung. Unter dem Pseudonym Annie Slone schreibe ich stilvolle, hocherotische Romane und prickelnde Erzählungen. Meine Leidenschaft liegt darin, das feine Spiel zwischen Verlangen und Hingabe, Macht und Vertrauen in all seinen schillernden Facetten zu beleuchten. Meine Geschichten richten sich an Kenner anspruchsvoller Erotikliteratur, die Wert auf tiefgründige Charaktere, knisternde Spannung und ästhetische Beschreibungen legen. Treten Sie näher und lassen Sie sich entführen.",
|
||||||
|
|
@ -105,7 +105,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_er_1",
|
id: "book_er_1",
|
||||||
title: "Flüstern im Samt",
|
title: "Flüstern im Samt",
|
||||||
subtitle: "Velvet Trust I",
|
subtitle: "Velvet Trust I",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Ein Blick in den überfüllten Ballsaal reicht aus, um Sophias wohlgeordnete Welt ins Wanken zu bringen. Der geheimnisvolle Kunsthändler Alexander bietet ihr einen Deal an, dem sie nicht widerstehen kann. Doch hinter den luxuriösen Fassaden verbirgt sich ein leidenschaftliches Abenteuer voller Geheimnisse, das ihre tiefsten Tabus herausfordern wird.",
|
description: "Ein Blick in den überfüllten Ballsaal reicht aus, um Sophias wohlgeordnete Welt ins Wanken zu bringen. Der geheimnisvolle Kunsthändler Alexander bietet ihr einen Deal an, dem sie nicht widerstehen kann. Doch hinter den luxuriösen Fassaden verbirgt sich ein leidenschaftliches Abenteuer voller Geheimnisse, das ihre tiefsten Tabus herausfordern wird.",
|
||||||
publishedYear: 2025,
|
publishedYear: 2025,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -116,7 +116,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_er_2",
|
id: "book_er_2",
|
||||||
title: "Die Maske des Begehrens",
|
title: "Die Maske des Begehrens",
|
||||||
subtitle: "Erotischer Thriller",
|
subtitle: "Erotischer Thriller",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "In Venedigs geheimem Karnevalskreisen kreuzen sich die Wege zweier Fremder, versteckt hinter venezianischen Seidenmasken. Ein sinnliches Versteckspiel beginnt, bei dem Identitäten verblassen und nur noch die unkontrollierbare sexuelle Spannung regiert.",
|
description: "In Venedigs geheimem Karnevalskreisen kreuzen sich die Wege zweier Fremder, versteckt hinter venezianischen Seidenmasken. Ein sinnliches Versteckspiel beginnt, bei dem Identitäten verblassen und nur noch die unkontrollierbare sexuelle Spannung regiert.",
|
||||||
publishedYear: 2024,
|
publishedYear: 2024,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -155,7 +155,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
},
|
},
|
||||||
clara: {
|
clara: {
|
||||||
name: "Clara Finch",
|
name: "Clara Finch",
|
||||||
avatarUrl: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=300",
|
avatarUrl: "",
|
||||||
heroTitle: "CLARA FINCH",
|
heroTitle: "CLARA FINCH",
|
||||||
heroSubtitle: "LYRIK, ESSAYS & NATURPHILOSOPHIE",
|
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.",
|
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.",
|
||||||
|
|
@ -180,7 +180,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_cl_1",
|
id: "book_cl_1",
|
||||||
title: "Die Stille der Halme",
|
title: "Die Stille der Halme",
|
||||||
subtitle: "Gedichte",
|
subtitle: "Gedichte",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
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.",
|
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,
|
publishedYear: 2025,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -191,7 +191,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_cl_2",
|
id: "book_cl_2",
|
||||||
title: "Knospen im Frost",
|
title: "Knospen im Frost",
|
||||||
subtitle: "Prosa-Miniaturen",
|
subtitle: "Prosa-Miniaturen",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1448375240586-882707db888b?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
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.",
|
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,
|
publishedYear: 2024,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -230,7 +230,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
},
|
},
|
||||||
renee: {
|
renee: {
|
||||||
name: "Renee Heart",
|
name: "Renee Heart",
|
||||||
avatarUrl: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=300",
|
avatarUrl: "",
|
||||||
heroTitle: "RENEE HEART",
|
heroTitle: "RENEE HEART",
|
||||||
heroSubtitle: "HERZENSGESCHICHTEN & EMOTIONALE ROMANE",
|
heroSubtitle: "HERZENSGESCHICHTEN & EMOTIONALE ROMANE",
|
||||||
bio: "Herzlich willkommen in meinem literarischen Zuhause. Ich bin Renee Heart. Ich schreibe herzerwärmende, gefühlvolle Romane über die großen Wendepunkte im Leben, tiefe Freundschaften, unvergessliche Liebesgeschichten und den Mut, dem eigenen Herzen zu folgen. Meine Geschichten handeln von echten Emotionen, leisen wie auch stürmischen Momenten und Charakteren, die man sofort ins Herz schließt. Lassen Sie sich von meinen Büchern berühren und begleiten!",
|
bio: "Herzlich willkommen in meinem literarischen Zuhause. Ich bin Renee Heart. Ich schreibe herzerwärmende, gefühlvolle Romane über die großen Wendepunkte im Leben, tiefe Freundschaften, unvergessliche Liebesgeschichten und den Mut, dem eigenen Herzen zu folgen. Meine Geschichten handeln von echten Emotionen, leisen wie auch stürmischen Momenten und Charakteren, die man sofort ins Herz schließt. Lassen Sie sich von meinen Büchern berühren und begleiten!",
|
||||||
|
|
@ -255,7 +255,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_rh_1",
|
id: "book_rh_1",
|
||||||
title: "Wo das Herz Zuhause ist",
|
title: "Wo das Herz Zuhause ist",
|
||||||
subtitle: "Herzensroman",
|
subtitle: "Herzensroman",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Als Maya das alte Haus ihrer Großmutter an der Küste erbt, ahnt sie nicht, dass der Neuanfang ihr ganzes Leben verändern wird. Zwischen stürmischen Wellen und warmem Tee findet sie nicht nur vergessene Familiengeheimnisse, sondern auch die Liebe ihres Lebens.",
|
description: "Als Maya das alte Haus ihrer Großmutter an der Küste erbt, ahnt sie nicht, dass der Neuanfang ihr ganzes Leben verändern wird. Zwischen stürmischen Wellen und warmem Tee findet sie nicht nur vergessene Familiengeheimnisse, sondern auch die Liebe ihres Lebens.",
|
||||||
publishedYear: 2025,
|
publishedYear: 2025,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -266,7 +266,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_rh_2",
|
id: "book_rh_2",
|
||||||
title: "Sterne über den Dächern",
|
title: "Sterne über den Dächern",
|
||||||
subtitle: "Romantische Komödie",
|
subtitle: "Romantische Komödie",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Eine humorvolle und tiefgründige Geschichte über zwei Gegensätze, die sich in einer nächtlichen Sternwarte näherkommen als je geahnt.",
|
description: "Eine humorvolle und tiefgründige Geschichte über zwei Gegensätze, die sich in einer nächtlichen Sternwarte näherkommen als je geahnt.",
|
||||||
publishedYear: 2024,
|
publishedYear: 2024,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue