webstack-author/src/components/ReneePortfolio.tsx
Daniel Heße 7d08057636 feat: make bio highlight tags dynamic and refine domain routing for www subdomains
- 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
2026-07-22 22:24:49 +02:00

439 lines
23 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

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

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}