- Add bioTag1, bioTag2, bioTag3 fields to AuthorProfile model and defaultData - Add interactive Schlagworte editing fields to AdminPanel - Update Renee, SciFi, Erotica, and Clara portfolio templates to render dynamic bio tags - Sanitize domain routing logic to automatically match www and naked domain variants
439 lines
23 KiB
TypeScript
439 lines
23 KiB
TypeScript
import React, { useState } from "react";
|
||
import Markdown from "react-markdown";
|
||
import { AuthorProfile, Book, Project } from "../types";
|
||
import {
|
||
Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun, Music
|
||
} from "lucide-react";
|
||
|
||
interface ReneePortfolioProps {
|
||
data: AuthorProfile;
|
||
}
|
||
|
||
export default function ReneePortfolio({ data }: ReneePortfolioProps) {
|
||
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
|
||
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
|
||
|
||
const accentColor = data.accentColor || "#eab308";
|
||
const secondaryColor = data.secondaryColor || "#ec4899";
|
||
const bgColor = data.backgroundColor || "#0f172a";
|
||
const cardBgColor = data.cardBgColor || "#1e293b";
|
||
|
||
const fontClass = data.fontFamily === "serif" ? "font-serif" : data.fontFamily === "mono" ? "font-mono" : "font-sans";
|
||
|
||
return (
|
||
<div
|
||
className={`min-h-screen text-slate-100 ${fontClass} selection:bg-pink-500 selection:text-white overflow-x-hidden relative`}
|
||
style={{ backgroundColor: bgColor }}
|
||
>
|
||
{/* Hero Banner Background if present */}
|
||
{data.heroBannerUrl && (
|
||
<div className="absolute top-0 inset-x-0 h-96 overflow-hidden pointer-events-none -z-5 opacity-25">
|
||
<img src={data.heroBannerUrl} alt="" className="w-full h-full object-cover blur-sm" />
|
||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-slate-900" style={{ backgroundImage: `linear-gradient(to bottom, transparent, ${bgColor})` }} />
|
||
</div>
|
||
)}
|
||
|
||
{/* Decorative Yellow & Pink Warm Glows */}
|
||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-yellow-500/10 via-transparent to-transparent -z-10 pointer-events-none" />
|
||
<div className="absolute top-1/4 left-1/4 w-96 h-96 rounded-full blur-3xl pointer-events-none opacity-20" style={{ backgroundColor: accentColor }} />
|
||
<div className="absolute bottom-1/3 right-1/3 w-96 h-96 rounded-full blur-3xl pointer-events-none opacity-20" style={{ backgroundColor: secondaryColor }} />
|
||
|
||
{/* Header / Hero */}
|
||
<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="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 }}
|
||
>
|
||
<Heart className="w-3.5 h-3.5 fill-current animate-pulse" style={{ color: secondaryColor }} />
|
||
<span>{data.badgeText || "Herzklopfen & Gefühlsstark"}</span>
|
||
</div>
|
||
|
||
<h1 className="text-4xl md:text-6xl tracking-wide text-white uppercase font-bold drop-shadow-sm">
|
||
{data.heroTitle || data.name || "RENEE HEART"}
|
||
</h1>
|
||
<p className="font-sans text-xs md:text-sm tracking-[0.3em] uppercase font-semibold" style={{ color: secondaryColor }}>
|
||
{data.heroSubtitle || "HERZENSGESCHICHTEN & EMOTIONALE ROMANE"}
|
||
</p>
|
||
<div className="h-[2px] w-36 mx-auto mt-6 rounded-full" style={{ background: `linear-gradient(to right, transparent, ${accentColor}, ${secondaryColor}, transparent)` }} />
|
||
|
||
{/* Quick Section Navigation */}
|
||
<nav className="mt-8 flex flex-wrap justify-center gap-3 text-xs font-sans">
|
||
<a
|
||
href="#ueber-mich"
|
||
className="px-4 py-2 rounded-full border border-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"}
|
||
</a>
|
||
<a
|
||
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"
|
||
>
|
||
✍️ {data.projectsTitle || "Aus der Schreibstube"}
|
||
</a>
|
||
<a
|
||
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"
|
||
>
|
||
📖 {data.booksTitle || "Gefühlvolle Romane"}
|
||
</a>
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
{/* Main Content Area */}
|
||
<main className="max-w-6xl mx-auto px-4 py-16 space-y-24">
|
||
|
||
{/* Author Bio Section */}
|
||
<section id="ueber-mich" className="grid grid-cols-1 md:grid-cols-12 gap-12 items-center">
|
||
<div className="md:col-span-4 flex justify-center">
|
||
<div className="relative group">
|
||
{/* Outer 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="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
|
||
src={data.avatarUrl || "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=400"}
|
||
alt={data.name}
|
||
className="w-full h-full object-cover group-hover:scale-105 transition duration-500"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="md:col-span-8 space-y-6">
|
||
<h2 className="text-2xl md:text-3xl font-bold text-white flex items-center gap-3">
|
||
<Sparkles className="w-6 h-6" style={{ color: accentColor }} />
|
||
<span>{data.aboutTitle || "Über Renee Heart"}</span>
|
||
</h2>
|
||
<div className="prose prose-invert max-w-none text-slate-300 leading-relaxed space-y-4">
|
||
<p className="whitespace-pre-wrap">{data.bio}</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 pt-4 text-xs">
|
||
{(data.bioTag1Label || data.bioTag1Value) && (
|
||
<div className="p-3 border rounded-xl flex items-center gap-3 shadow-sm" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}>
|
||
<Heart className="w-5 h-5 fill-current shrink-0" style={{ color: secondaryColor }} />
|
||
<div>
|
||
<div className="text-slate-400 uppercase tracking-wider text-[10px]">{data.bioTag1Label || "Genre"}</div>
|
||
<div className="text-slate-200 font-semibold">{data.bioTag1Value || "Emotionale Romance"}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{(data.bioTag2Label || data.bioTag2Value) && (
|
||
<div className="p-3 border rounded-xl flex items-center gap-3 shadow-sm" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}>
|
||
<Sun className="w-5 h-5 shrink-0" style={{ color: accentColor }} />
|
||
<div>
|
||
<div className="text-slate-400 uppercase tracking-wider text-[10px]">{data.bioTag2Label || "Tonalität"}</div>
|
||
<div className="text-slate-200 font-semibold">{data.bioTag2Value || "Herzlich & Hoffnungsvoll"}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{(data.bioTag3Label || data.bioTag3Value) && (
|
||
<div className="p-3 border rounded-xl flex items-center gap-3 col-span-2 md:col-span-1 shadow-sm" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}>
|
||
<Sparkles className="w-5 h-5 shrink-0" style={{ color: secondaryColor }} />
|
||
<div>
|
||
<div className="text-slate-400 uppercase tracking-wider text-[10px]">{data.bioTag3Label || "Fokus"}</div>
|
||
<div className="text-slate-200 font-semibold">{data.bioTag3Value || "Gefühl & Tiefe"}</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Dynamic ongoing work writing progress */}
|
||
<section id="projekte" className="space-y-8">
|
||
<div className="flex flex-col md:flex-row md:items-end justify-between gap-4 border-b border-slate-800 pb-4">
|
||
<div>
|
||
<h2 className="text-2xl md:text-3xl font-bold text-white flex items-center gap-3">
|
||
<Heart className="w-6 h-6" style={{ color: secondaryColor }} />
|
||
<span>{data.projectsTitle || "Aus der Schreibstube"}</span>
|
||
</h2>
|
||
<p className="text-slate-400 text-sm mt-1">{data.projectsSubtitle || "Ein Blick auf meine aktuellen Geschichten in Entstehung – voller Hoffnung und Herzklopfen."}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
{data.projects.map((project) => (
|
||
<button
|
||
key={project.id}
|
||
onClick={() => setSelectedProject(project)}
|
||
className="p-6 border rounded-xl transition-all duration-300 flex flex-col text-left w-full justify-between group cursor-pointer hover:scale-[1.01] focus:outline-none shadow-lg"
|
||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}25` }}
|
||
>
|
||
<div className="space-y-4 w-full">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-[10px] uppercase font-sans font-bold px-2.5 py-1 rounded border tracking-wider" style={{ color: accentColor, backgroundColor: `${accentColor}15`, borderColor: `${accentColor}30` }}>
|
||
{project.status || "In Arbeit"}
|
||
</span>
|
||
<span className="text-xs text-slate-400 italic">
|
||
{project.progress}% fertiggestellt
|
||
</span>
|
||
</div>
|
||
<h3 className="text-lg font-bold text-white group-hover:text-yellow-300 transition-colors">
|
||
{project.title}
|
||
</h3>
|
||
<div className="text-slate-300 text-sm line-clamp-3 markdown-body [&_p]:inline [&_p]:m-0">
|
||
<Markdown
|
||
components={{
|
||
a: ({ node, ...props }) => (
|
||
<a
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
||
onClick={(e) => e.stopPropagation()}
|
||
{...props}
|
||
/>
|
||
)
|
||
}}
|
||
>
|
||
{project.description}
|
||
</Markdown>
|
||
</div>
|
||
<p className="font-sans text-[11px] mt-2 group-hover:translate-x-1 transition-transform flex items-center gap-1 font-semibold" style={{ color: secondaryColor }}>
|
||
Details einsehen <ArrowRight className="w-3 h-3" />
|
||
</p>
|
||
</div>
|
||
<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-full rounded-full transition-all duration-1000 ease-out shadow-sm"
|
||
style={{ width: `${project.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Bookshelf display */}
|
||
<section id="buecherregal" className="space-y-8">
|
||
<div className="border-b border-slate-800 pb-4">
|
||
<h2 className="text-2xl md:text-3xl font-bold text-white flex items-center gap-3">
|
||
<BookOpen className="w-6 h-6" style={{ color: accentColor }} />
|
||
<span>{data.booksTitle || "Gefühlvolle Romane"}</span>
|
||
</h2>
|
||
<p className="text-slate-400 text-sm mt-1">{data.booksSubtitle || "Geschichten zum Mitfühlen, Träumen und Genießen."}</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-6 justify-items-center">
|
||
{data.books.map((book) => (
|
||
<button
|
||
key={book.id}
|
||
onClick={() => setSelectedBook(book)}
|
||
className="group relative focus:outline-none text-left w-full max-w-[210px] aspect-[2/3] rounded-xl overflow-hidden border transition-all duration-300 hover:shadow-2xl cursor-pointer"
|
||
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" />
|
||
<img
|
||
src={book.coverUrl || "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=300"}
|
||
alt={book.title}
|
||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||
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">
|
||
<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 }}>
|
||
Entdecken <ArrowRight className="inline-block w-2.5 h-2.5" />
|
||
</p>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* Custom Highlight Section if present */}
|
||
{(data.customSectionTitle || data.customSectionContent) && (
|
||
<section id="herzenspost" className="p-8 rounded-2xl border relative overflow-hidden max-w-4xl mx-auto shadow-xl" style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}30` }}>
|
||
{data.customSectionTitle && (
|
||
<h2 className="text-2xl font-bold text-white mb-4 flex items-center gap-2">
|
||
<Heart className="w-5 h-5" style={{ color: secondaryColor }} />
|
||
<span>{data.customSectionTitle}</span>
|
||
</h2>
|
||
)}
|
||
{data.customSectionContent && (
|
||
<div className="text-slate-300 text-sm md:text-base leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||
<Markdown
|
||
components={{
|
||
a: ({ node, ...props }) => (
|
||
<a
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
||
{...props}
|
||
/>
|
||
)
|
||
}}
|
||
>
|
||
{data.customSectionContent}
|
||
</Markdown>
|
||
</div>
|
||
)}
|
||
</section>
|
||
)}
|
||
|
||
</main>
|
||
|
||
{/* Book Details Modal */}
|
||
{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="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` }}
|
||
>
|
||
<button
|
||
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"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
|
||
<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">
|
||
<img
|
||
src={selectedBook.coverUrl}
|
||
alt={selectedBook.title}
|
||
className="w-full h-full object-cover"
|
||
referrerPolicy="no-referrer"
|
||
/>
|
||
</div>
|
||
<div className="md:col-span-7 space-y-4">
|
||
<div>
|
||
<span className="text-xs uppercase tracking-wider font-bold" style={{ color: accentColor }}>Erscheinungsjahr {selectedBook.publishedYear}</span>
|
||
<h3 className="text-2xl font-bold text-white">{selectedBook.title}</h3>
|
||
{selectedBook.subtitle && (
|
||
<p className="text-sm text-slate-400 italic">{selectedBook.subtitle}</p>
|
||
)}
|
||
</div>
|
||
|
||
{selectedBook.genres && selectedBook.genres.length > 0 && (
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{selectedBook.genres.map((g, idx) => (
|
||
<span key={idx} className="px-2.5 py-0.5 rounded-full bg-slate-800 text-[10px] text-slate-300 border border-slate-700">
|
||
{g}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||
<Markdown
|
||
components={{
|
||
a: ({ node, ...props }) => (
|
||
<a
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
||
{...props}
|
||
/>
|
||
)
|
||
}}
|
||
>
|
||
{selectedBook.description}
|
||
</Markdown>
|
||
</div>
|
||
|
||
{/* Spotify Playlist player if configured */}
|
||
{selectedBook.spotifyPlaylistId && (
|
||
<div className="pt-2">
|
||
<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 && (
|
||
<div className="pt-2">
|
||
<a
|
||
href={selectedBook.buyLink}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl font-bold text-slate-950 transition-all text-xs cursor-pointer shadow-md"
|
||
style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||
>
|
||
<span>Buch kaufen / Lesen</span>
|
||
<ExternalLink className="w-4 h-4" />
|
||
</a>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Project Details Modal */}
|
||
{selectedProject && (
|
||
<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="relative w-full max-w-lg border rounded-2xl p-6 md:p-8 space-y-6 shadow-2xl"
|
||
style={{ backgroundColor: cardBgColor, borderColor: `${accentColor}40` }}
|
||
>
|
||
<button
|
||
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"
|
||
>
|
||
<X className="w-5 h-5" />
|
||
</button>
|
||
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-xs uppercase font-bold px-2.5 py-1 rounded border tracking-wider" style={{ color: accentColor, backgroundColor: `${accentColor}15`, borderColor: `${accentColor}30` }}>
|
||
Status: {selectedProject.status}
|
||
</span>
|
||
<span className="text-xs text-slate-400 italic">
|
||
Fortschritt: {selectedProject.progress}%
|
||
</span>
|
||
</div>
|
||
|
||
<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-full rounded-full transition-all duration-500"
|
||
style={{ width: `${selectedProject.progress}%`, background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }}
|
||
/>
|
||
</div>
|
||
|
||
<div className="text-slate-300 text-sm leading-relaxed markdown-body [&_p]:mb-4 [&_p:last-child]:mb-0">
|
||
<Markdown
|
||
components={{
|
||
a: ({ node, ...props }) => (
|
||
<a
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="underline text-yellow-400 hover:text-pink-400 font-medium"
|
||
{...props}
|
||
/>
|
||
)
|
||
}}
|
||
>
|
||
{selectedProject.description || "Für dieses Schreibprojekt wurden noch keine weiteren Details abgelegt."}
|
||
</Markdown>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Footer */}
|
||
<footer className="border-t border-slate-800 py-12 px-4 text-center text-xs text-slate-500 font-sans" style={{ backgroundColor: bgColor }}>
|
||
<div className="max-w-7xl mx-auto space-y-2">
|
||
<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>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
);
|
||
}
|