Added spotlight section #10

Merged
Dada1981 merged 1 commit from dev into main 2026-07-26 20:13:35 +00:00
9 changed files with 388 additions and 15 deletions
Showing only changes of commit 4a70e27548 - Show all commits

View file

@ -48,7 +48,8 @@ async function initDatabase(): Promise<AuthorData> {
"bioTag1Label", "bioTag1Value", "bioTag1Label", "bioTag1Value",
"bioTag2Label", "bioTag2Value", "bioTag2Label", "bioTag2Value",
"bioTag3Label", "bioTag3Value", "bioTag3Label", "bioTag3Value",
"badgeText", "aboutTitle", "projectsTitle", "projectsSubtitle", "badgeText", "aboutTitle", "spotlightTitle", "spotlightSubtitle",
"projectsTitle", "projectsSubtitle",
"booksTitle", "booksSubtitle", "footerText", "booksTitle", "booksSubtitle", "footerText",
"accentColor", "secondaryColor", "backgroundColor", "cardBgColor", "accentColor", "secondaryColor", "backgroundColor", "cardBgColor",
"fontFamily", "heroBannerUrl" "fontFamily", "heroBannerUrl"

View file

@ -30,6 +30,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
// Editing forms state // Editing forms state
const [editingBookId, setEditingBookId] = useState<string | null>(null); const [editingBookId, setEditingBookId] = useState<string | null>(null);
const [bookForm, setBookForm] = useState<Partial<Book>>({}); const [bookForm, setBookForm] = useState<Partial<Book>>({});
const [genreInputRaw, setGenreInputRaw] = useState("");
const [editingProjectId, setEditingProjectId] = useState<string | null>(null); const [editingProjectId, setEditingProjectId] = useState<string | null>(null);
const [projectForm, setProjectForm] = useState<Partial<Project>>({}); const [projectForm, setProjectForm] = useState<Partial<Project>>({});
@ -225,7 +226,14 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
const handleSaveBook = async () => { const handleSaveBook = async () => {
if (!authorData || !profile) return; if (!authorData || !profile) return;
const parsedGenres = genreInputRaw.split(",").map(g => g.trim()).filter(Boolean);
let updatedBooks = [...profile.books]; let updatedBooks = [...profile.books];
// If current book is marked as spotlight, clear spotlight flag on all other books
if (bookForm.isSpotlight) {
updatedBooks = updatedBooks.map(b => ({ ...b, isSpotlight: false }));
}
if (editingBookId === "new") { if (editingBookId === "new") {
const newBook: Book = { const newBook: Book = {
id: "book_" + Date.now(), id: "book_" + Date.now(),
@ -236,12 +244,20 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
publishedYear: bookForm.publishedYear || new Date().getFullYear(), publishedYear: bookForm.publishedYear || new Date().getFullYear(),
buyLink: bookForm.buyLink || "", buyLink: bookForm.buyLink || "",
spotifyPlaylistId: bookForm.spotifyPlaylistId || "", spotifyPlaylistId: bookForm.spotifyPlaylistId || "",
genres: bookForm.genres || [] genres: parsedGenres,
isSpotlight: !!bookForm.isSpotlight,
spotlightBadge: bookForm.spotlightBadge || ""
}; };
updatedBooks.push(newBook); updatedBooks.push(newBook);
} else { } else {
updatedBooks = updatedBooks.map(b => updatedBooks = updatedBooks.map(b =>
b.id === editingBookId ? { ...b, ...bookForm } as Book : b b.id === editingBookId ? {
...b,
...bookForm,
genres: parsedGenres,
isSpotlight: !!bookForm.isSpotlight,
spotlightBadge: bookForm.spotlightBadge || ""
} as Book : b
); );
} }
@ -806,6 +822,31 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
/> />
</div> </div>
<div className="space-y-1">
<label className="text-xs font-mono text-slate-400 uppercase font-bold text-amber-400 flex items-center gap-1">
<Sparkles className="w-3.5 h-3.5" />
<span>Spotlight / Neuerscheinung Sektionsüberschrift</span>
</label>
<input
type="text"
value={profile.spotlightTitle || ""}
onChange={(e) => updateProfileField("spotlightTitle", e.target.value)}
placeholder="z.B. Spotlight & Neuerscheinung"
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-amber-500 text-sm"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-mono text-slate-400 uppercase text-amber-400">Spotlight Untertitel / Beschreibung</label>
<input
type="text"
value={profile.spotlightSubtitle || ""}
onChange={(e) => updateProfileField("spotlightSubtitle", e.target.value)}
placeholder="z.B. Besonders hervorgehobenes Werk im Fokus"
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-amber-500 text-sm"
/>
</div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-mono text-slate-400 uppercase">Projekte Sektionsüberschrift</label> <label className="text-xs font-mono text-slate-400 uppercase">Projekte Sektionsüberschrift</label>
<input <input
@ -1229,8 +1270,9 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
setBookForm({ setBookForm({
title: "", subtitle: "", coverUrl: "", description: "", title: "", subtitle: "", coverUrl: "", description: "",
publishedYear: new Date().getFullYear(), buyLink: "", publishedYear: new Date().getFullYear(), buyLink: "",
spotifyPlaylistId: "", genres: [] spotifyPlaylistId: "", genres: [], isSpotlight: false, spotlightBadge: ""
}); });
setGenreInputRaw("");
// Preset Assistant blurb writer values to blank // Preset Assistant blurb writer values to blank
setAiTitle(""); setAiTitle("");
setAiIdeas(""); setAiIdeas("");
@ -1250,13 +1292,18 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
profile.books.map((b) => ( profile.books.map((b) => (
<div key={b.id} className="p-4 bg-slate-950 border border-slate-800 rounded-xl space-y-3 flex flex-col justify-between"> <div key={b.id} className="p-4 bg-slate-950 border border-slate-800 rounded-xl space-y-3 flex flex-col justify-between">
<div className="space-y-3"> <div className="space-y-3">
<div className="w-24 aspect-[2/3] mx-auto rounded-lg overflow-hidden border border-slate-800 bg-slate-900"> <div className="w-24 aspect-[2/3] mx-auto rounded-lg overflow-hidden border border-slate-800 bg-slate-900 relative">
<img src={b.coverUrl} alt={b.title} className="w-full h-full object-cover" referrerPolicy="no-referrer" /> <img src={b.coverUrl} alt={b.title} className="w-full h-full object-cover" referrerPolicy="no-referrer" />
{b.isSpotlight && (
<span className="absolute top-1 right-1 bg-amber-500 text-slate-950 p-1 rounded-full shadow-md" title="Spotlight / Neuerscheinung">
<Sparkles className="w-3 h-3" />
</span>
)}
</div> </div>
<div className="text-center space-y-0.5"> <div className="text-center space-y-0.5">
<h4 className="text-sm font-bold text-white line-clamp-1">{b.title}</h4> <h4 className="text-sm font-bold text-white line-clamp-1">{b.title}</h4>
<p className="text-[10px] text-indigo-400 font-mono italic uppercase">{b.subtitle || "Einzelband"}</p> <p className="text-[10px] text-indigo-400 font-mono italic uppercase">{b.subtitle || "Einzelband"}</p>
<span className="text-[10px] text-slate-500 font-mono">Jahr: {b.publishedYear}</span> <span className="text-[10px] text-slate-500 font-mono">Datum/Jahr: {b.publishedYear}</span>
</div> </div>
</div> </div>
@ -1265,6 +1312,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
onClick={() => { onClick={() => {
setEditingBookId(b.id); setEditingBookId(b.id);
setBookForm(b); setBookForm(b);
setGenreInputRaw(b.genres ? b.genres.join(", ") : "");
setAiTitle(b.title || ""); setAiTitle(b.title || "");
setAiResult(""); setAiResult("");
}} }}
@ -1275,7 +1323,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
</button> </button>
<button <button
onClick={() => handleDeleteBook(b.id)} onClick={() => handleDeleteBook(b.id)}
className="p-1.5 hover:bg-rose-950 text-rose-450 text-rose-400 rounded-lg transition-all border border-transparent hover:border-rose-900 cursor-pointer" className="p-1.5 hover:bg-rose-950 text-rose-400 rounded-lg transition-all border border-transparent hover:border-rose-900 cursor-pointer"
title="Löschen" title="Löschen"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
@ -1298,6 +1346,42 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Spotlight Section Toggle inside Book Form */}
<div className="md:col-span-2 p-4 bg-amber-950/30 border border-amber-500/40 rounded-xl space-y-3">
<div className="flex items-center gap-3">
<input
type="checkbox"
id="isSpotlightCheckbox"
checked={!!bookForm.isSpotlight}
onChange={(e) => setBookForm({ ...bookForm, isSpotlight: e.target.checked })}
className="w-4 h-4 rounded accent-amber-500 cursor-pointer"
/>
<label htmlFor="isSpotlightCheckbox" className="text-sm font-bold text-amber-300 cursor-pointer flex items-center gap-2">
<Sparkles className="w-4 h-4 text-amber-400" />
<span>Als Neuerscheinung / Buch-Spotlight auf der Homepage hervorheben</span>
</label>
</div>
<p className="text-xs text-slate-400 pl-7">
Wenn aktiviert, wird dieses Werk in einer prominenten Highlight-Kachel zwischen "Über mich" und "Aktuelle Projekte" platziert.
</p>
{bookForm.isSpotlight && (
<div className="pl-7 pt-1 space-y-1">
<label className="text-[10px] font-mono text-amber-400 uppercase font-semibold">Individueller Badge-Text (optional)</label>
<input
type="text"
value={bookForm.spotlightBadge || ""}
onChange={(e) => setBookForm({ ...bookForm, spotlightBadge: e.target.value })}
placeholder="z.B. Neuerscheinung, Vorbestellbar, Herbst-Highlight..."
className="w-full px-3 py-1.5 bg-slate-950 border border-slate-800 rounded-lg text-white outline-none focus:border-amber-500 text-xs"
/>
<p className="text-[10px] text-slate-500">
Wenn leer, wird automatisch "Erscheint am [Datum]" (bei Zukunftsdatum) oder "Neuerscheinung" verwendet.
</p>
</div>
)}
</div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-mono text-slate-400 uppercase">Buchtitel</label> <label className="text-xs font-mono text-slate-400 uppercase">Buchtitel</label>
<input <input
@ -1331,23 +1415,26 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
/> />
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-mono text-slate-400 uppercase">Veröffentlichungsjahr</label> <label className="text-xs font-mono text-slate-400 uppercase">Veröffentlichungsdatum / -jahr</label>
<input <input
type="number" type="text"
value={bookForm.publishedYear || ""} value={bookForm.publishedYear || ""}
onChange={(e) => setBookForm({ ...bookForm, publishedYear: parseInt(e.target.value) })} onChange={(e) => setBookForm({ ...bookForm, publishedYear: e.target.value })}
placeholder="z.B. 15.10.2026 oder 2025"
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm"
/> />
<p className="text-[10px] text-slate-500">Unterstützt Jahreszahlen (2026) und konkrete Daten (15.10.2026).</p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-mono text-slate-400 uppercase flex items-center gap-1"> <label className="text-xs font-mono text-slate-400 uppercase flex items-center gap-1">
<Eye className="w-3.5 h-3.5" /> <Eye className="w-3.5 h-3.5" />
<span>Buy-Link (z.B. Amazon, Thalia)</span> <span>Buy-Link / Shop-Link (z.B. Amazon, Thalia)</span>
</label> </label>
<input <input
type="text" type="text"
value={bookForm.buyLink || ""} value={bookForm.buyLink || ""}
onChange={(e) => setBookForm({ ...bookForm, buyLink: e.target.value })} onChange={(e) => setBookForm({ ...bookForm, buyLink: e.target.value })}
placeholder="https://..."
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm"
/> />
</div> </div>
@ -1369,11 +1456,12 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
<label className="text-xs font-mono text-slate-400 uppercase">Genres (Kommagetrennt)</label> <label className="text-xs font-mono text-slate-400 uppercase">Genres (Kommagetrennt)</label>
<input <input
type="text" type="text"
value={bookForm.genres?.join(", ") || ""} value={genreInputRaw}
onChange={(e) => setBookForm({ ...bookForm, genres: e.target.value.split(",").map(g => g.trim()).filter(g => g !== "") })} onChange={(e) => setGenreInputRaw(e.target.value)}
placeholder="z.B. Science-Fiction, Cyberpunk, Drama" placeholder="z.B. Science-Fiction, Cyberpunk, Drama"
className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm" className="w-full px-3 py-2 bg-slate-950 border border-slate-800 rounded-xl text-white outline-none focus:border-indigo-500 text-sm"
/> />
<p className="text-[10px] text-slate-500">Trennen Sie mehrere Genres einfach durch Kommas.</p>
</div> </div>
{/* Buch-Klappentext Synopsis / Description */} {/* Buch-Klappentext Synopsis / Description */}

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { Book, Project, AuthorProfile } from "../types"; import { Book, Project, AuthorProfile } from "../types";
import { Leaf, Feather, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react"; import { Leaf, Feather, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
import SpotlightSection from "./SpotlightSection";
interface ClaraPortfolioProps { interface ClaraPortfolioProps {
data: AuthorProfile; data: AuthorProfile;
@ -139,6 +140,18 @@ export default function ClaraPortfolio({ data }: ClaraPortfolioProps) {
</div> </div>
</section> </section>
{/* Section 1.5: Spotlight / Neuerscheinung */}
{data.books?.find(b => b.isSpotlight) && (
<SpotlightSection
book={data.books.find(b => b.isSpotlight)!}
title={data.spotlightTitle}
subtitle={data.spotlightSubtitle}
accentColor={accentColor}
secondaryColor={secondaryColor}
cardBgColor={cardBgColor}
/>
)}
{/* Dynamic ongoing work writing progress */} {/* Dynamic ongoing work writing progress */}
<section id="projekte" className="space-y-8"> <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-stone-800 pb-4"> <div className="flex flex-col md:flex-row md:items-end justify-between gap-4 border-b border-stone-800 pb-4">

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { Book, Project, AuthorProfile } from "../types"; import { Book, Project, AuthorProfile } from "../types";
import { Heart, Key, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react"; import { Heart, Key, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
import SpotlightSection from "./SpotlightSection";
interface EroticaPortfolioProps { interface EroticaPortfolioProps {
data: AuthorProfile; data: AuthorProfile;
@ -139,6 +140,18 @@ export default function EroticaPortfolio({ data }: EroticaPortfolioProps) {
</div> </div>
</section> </section>
{/* Section 1.5: Spotlight / Neuerscheinung */}
{data.books?.find(b => b.isSpotlight) && (
<SpotlightSection
book={data.books.find(b => b.isSpotlight)!}
title={data.spotlightTitle}
subtitle={data.spotlightSubtitle}
accentColor={accentColor}
secondaryColor={secondaryColor}
cardBgColor={cardBgColor}
/>
)}
{/* Dynamic ongoing work writing progress */} {/* Dynamic ongoing work writing progress */}
<section id="projekte" className="space-y-8"> <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-neutral-800 pb-4"> <div className="flex flex-col md:flex-row md:items-end justify-between gap-4 border-b border-neutral-800 pb-4">

View file

@ -4,6 +4,7 @@ import { AuthorProfile, Book, Project } from "../types";
import { import {
Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun, Music Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun, Music
} from "lucide-react"; } from "lucide-react";
import SpotlightSection from "./SpotlightSection";
interface ReneePortfolioProps { interface ReneePortfolioProps {
data: AuthorProfile; data: AuthorProfile;
@ -140,6 +141,18 @@ export default function ReneePortfolio({ data }: ReneePortfolioProps) {
</div> </div>
</section> </section>
{/* Section 1.5: Spotlight / Neuerscheinung */}
{data.books?.find(b => b.isSpotlight) && (
<SpotlightSection
book={data.books.find(b => b.isSpotlight)!}
title={data.spotlightTitle}
subtitle={data.spotlightSubtitle}
accentColor={accentColor}
secondaryColor={secondaryColor}
cardBgColor={cardBgColor}
/>
)}
{/* Dynamic ongoing work writing progress */} {/* Dynamic ongoing work writing progress */}
<section id="projekte" className="space-y-8"> <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 className="flex flex-col md:flex-row md:items-end justify-between gap-4 border-b border-slate-800 pb-4">

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { Book, Project, AuthorProfile } from "../types"; import { Book, Project, AuthorProfile } from "../types";
import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, Moon } from "lucide-react"; import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, Moon } from "lucide-react";
import SpotlightSection from "./SpotlightSection";
interface SciFiPortfolioProps { interface SciFiPortfolioProps {
data: AuthorProfile; data: AuthorProfile;
@ -152,6 +153,18 @@ export default function SciFiPortfolio({ data }: SciFiPortfolioProps) {
</div> </div>
</section> </section>
{/* Section 1.5: Spotlight / Neuerscheinung */}
{data.books?.find(b => b.isSpotlight) && (
<SpotlightSection
book={data.books.find(b => b.isSpotlight)!}
title={data.spotlightTitle}
subtitle={data.spotlightSubtitle}
accentColor={accentColor}
secondaryColor={secondaryColor}
cardBgColor={cardBgColor}
/>
)}
{/* Section 2: Aktuelle Projekte */} {/* Section 2: Aktuelle Projekte */}
<section id="projekte" className="space-y-8"> <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 className="flex flex-col md:flex-row md:items-end justify-between gap-4 border-b border-slate-800 pb-4">

View file

@ -0,0 +1,218 @@
import React, { useState } from "react";
import Markdown from "react-markdown";
import { Book } from "../types";
import { Sparkles, Calendar, ShoppingBag, Music, ExternalLink, Tag } from "lucide-react";
interface SpotlightSectionProps {
book: Book;
title?: string;
subtitle?: string;
accentColor: string;
secondaryColor: string;
cardBgColor: string;
}
export default function SpotlightSection({
book,
title,
subtitle,
accentColor,
secondaryColor,
cardBgColor,
}: SpotlightSectionProps) {
const [showSpotify, setShowSpotify] = useState(false);
// Helper to check if a release date is in the future
const isFutureRelease = (): boolean => {
if (!book.publishedYear) return false;
const str = String(book.publishedYear).trim();
// Check if it's a 4-digit year like "2027"
if (/^\d{4}$/.test(str)) {
return parseInt(str) > new Date().getFullYear();
}
// Check if it's a German date e.g. "15.10.2026" or "15.10.26"
const germanDateMatch = str.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2,4})$/);
if (germanDateMatch) {
let [, day, month, year] = germanDateMatch;
if (year.length === 2) year = "20" + year;
const parsedDate = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
return parsedDate > new Date();
}
// Standard JS Date parse
const parsed = Date.parse(str);
if (!isNaN(parsed)) {
return parsed > Date.now();
}
return false;
};
const isFuture = isFutureRelease();
const badgeText = book.spotlightBadge || (isFuture ? `Erscheint am ${book.publishedYear}` : "Neuerscheinung");
return (
<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="p-2 rounded-xl flex items-center justify-center shadow-md"
style={{ backgroundColor: `${accentColor}20`, color: accentColor }}
>
<Sparkles className="w-6 h-6 animate-pulse" />
</div>
<div>
<h2 className="text-2xl md:text-3xl font-bold tracking-tight text-white flex items-center gap-2">
<span>{title || "Spotlight & Neuerscheinung"}</span>
</h2>
<p className="text-xs text-slate-400">{subtitle || "Besonders hervorgehobenes Werk im Fokus"}</p>
</div>
</div>
<div
className="relative overflow-hidden border rounded-2xl p-6 md:p-8 shadow-2xl transition-all"
style={{
backgroundColor: cardBgColor,
borderColor: `${accentColor}40`,
boxShadow: `0 20px 50px -10px ${accentColor}15`
}}
>
{/* Subtle Ambient Background Gradient */}
<div
className="absolute -right-20 -bottom-20 w-80 h-80 rounded-full blur-3xl pointer-events-none opacity-20"
style={{ backgroundColor: accentColor }}
/>
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 items-center relative z-10">
{/* Cover image column */}
<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="absolute -inset-1 rounded-2xl blur-lg opacity-40 group-hover:opacity-75 transition duration-500"
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">
<img
src={book.coverUrl}
alt={book.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
referrerPolicy="no-referrer"
/>
</div>
</div>
{book.buyLink && (
<a
href={book.buyLink}
target="_blank"
rel="noopener noreferrer"
className="w-full max-w-[260px] py-3 px-4 rounded-xl font-bold text-xs uppercase tracking-wider transition-all duration-300 flex items-center justify-center gap-2 shadow-lg cursor-pointer hover:scale-[1.02]"
style={{ backgroundColor: accentColor, color: "#000" }}
>
<ShoppingBag className="w-4 h-4" />
<span>{isFuture ? "Jetzt vorbestellen" : "Jetzt im Handel"}</span>
<ExternalLink className="w-3.5 h-3.5 ml-auto opacity-70" />
</a>
)}
</div>
{/* Details column */}
<div className="md:col-span-8 space-y-5">
<div className="flex flex-wrap items-center gap-2">
<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"
style={{
backgroundColor: isFuture ? `${secondaryColor}25` : `${accentColor}25`,
borderColor: isFuture ? secondaryColor : accentColor,
color: isFuture ? "#fff" : accentColor
}}
>
<Sparkles className="w-3.5 h-3.5" />
<span>{badgeText}</span>
</span>
{book.publishedYear && (
<span className="px-3 py-1 bg-slate-800/80 border border-slate-700 text-slate-300 rounded-full text-xs font-mono flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5 text-slate-400" />
<span>{isFuture ? "Erscheinungsdatum:" : "Veröffentlichung:"} {book.publishedYear}</span>
</span>
)}
</div>
<div>
<h3 className="text-2xl md:text-4xl font-extrabold text-white tracking-tight">
{book.title}
</h3>
{book.subtitle && (
<p className="text-base md:text-lg font-medium mt-1 italic" style={{ color: secondaryColor }}>
{book.subtitle}
</p>
)}
</div>
{/* Genres */}
{book.genres && book.genres.length > 0 && (
<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" />
{book.genres.map((g, idx) => (
<span
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"
>
{g}
</span>
))}
</div>
)}
{/* 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">
<Markdown
components={{
a: ({ node, ...props }) => (
<a
target="_blank"
rel="noopener noreferrer"
className="underline font-medium hover:opacity-80"
style={{ color: accentColor }}
{...props}
/>
)
}}
>
{book.description}
</Markdown>
</div>
{/* Spotify Player Toggle if present */}
{book.spotifyPlaylistId && (
<div className="pt-2">
<button
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>
</section>
);
}

View file

@ -33,7 +33,9 @@ export const defaultAuthorData: AuthorData = {
publishedYear: 2024, publishedYear: 2024,
buyLink: "https://amazon.de", buyLink: "https://amazon.de",
spotifyPlaylistId: "37i9dQZF1DX10zK7J2Y7gO", spotifyPlaylistId: "37i9dQZF1DX10zK7J2Y7gO",
genres: ["Science-Fiction", "Space Opera", "Techno-Fantasy"] genres: ["Science-Fiction", "Space Opera", "Techno-Fantasy"],
isSpotlight: true,
spotlightBadge: "Große Neuerscheinung"
}, },
{ {
id: "book_2", id: "book_2",
@ -51,6 +53,8 @@ export const defaultAuthorData: AuthorData = {
customSectionContent: "Besuchen Sie mich auf der nächsten Buchmesse am Stand für spekulative Phantastik. Ich freue mich auf den intergalaktischen Austausch mit meinen Leserinnen und Lesern!", customSectionContent: "Besuchen Sie mich auf der nächsten Buchmesse am Stand für spekulative Phantastik. Ich freue mich auf den intergalaktischen Austausch mit meinen Leserinnen und Lesern!",
badgeText: "Cyber-Mystisches Archive", badgeText: "Cyber-Mystisches Archive",
aboutTitle: "Über den Autor", aboutTitle: "Über den Autor",
spotlightTitle: "Spotlight & Neuerscheinung",
spotlightSubtitle: "Besonders hervorgehobenes Werk im Fokus",
bioTag1Label: "Fokus", bioTag1Label: "Fokus",
bioTag1Value: "Sci-Fi & Fantasy", bioTag1Value: "Sci-Fi & Fantasy",
bioTag2Label: "Status", bioTag2Label: "Status",
@ -121,6 +125,8 @@ export const defaultAuthorData: AuthorData = {
customSectionContent: "Tragen Sie sich in meinen privaten Briefverteiler ein, um exklusive Einladungen zu meinen intimen Salon-Lesungen in Berlin und Wien zu erhalten. Die Plätze sind streng limitiert.", customSectionContent: "Tragen Sie sich in meinen privaten Briefverteiler ein, um exklusive Einladungen zu meinen intimen Salon-Lesungen in Berlin und Wien zu erhalten. Die Plätze sind streng limitiert.",
badgeText: "Stilvolle Sinnlichkeit & Romantik", badgeText: "Stilvolle Sinnlichkeit & Romantik",
aboutTitle: "Das Geheimnis hinter der Feder", aboutTitle: "Das Geheimnis hinter der Feder",
spotlightTitle: "Spotlight & Neuerscheinung",
spotlightSubtitle: "Besonders hervorgehobenes Werk im Fokus",
bioTag1Label: "Genre", bioTag1Label: "Genre",
bioTag1Value: "Sinnliche Romance", bioTag1Value: "Sinnliche Romance",
bioTag2Label: "Tonalität", bioTag2Label: "Tonalität",
@ -191,6 +197,8 @@ export const defaultAuthorData: AuthorData = {
customSectionContent: "Auf meinen geführten Waldspaziergängen und Schreib-Retreats lade ich Sie ein, die Stille des Waldes neu zu erfahren und Ihre Gedanken auf das Papier fließen zu lassen. Schreiben Sie mir gern für Termine und Buchungsanfragen.", customSectionContent: "Auf meinen geführten Waldspaziergängen und Schreib-Retreats lade ich Sie ein, die Stille des Waldes neu zu erfahren und Ihre Gedanken auf das Papier fließen zu lassen. Schreiben Sie mir gern für Termine und Buchungsanfragen.",
badgeText: "Lyrik, Essays & Naturphilosophie", badgeText: "Lyrik, Essays & Naturphilosophie",
aboutTitle: "Aus meiner Schreibstube", aboutTitle: "Aus meiner Schreibstube",
spotlightTitle: "Spotlight & Neuerscheinung",
spotlightSubtitle: "Besonders hervorgehobenes Werk im Fokus",
bioTag1Label: "Genre", bioTag1Label: "Genre",
bioTag1Value: "Lyrik & Essays", bioTag1Value: "Lyrik & Essays",
bioTag2Label: "Ästhetik", bioTag2Label: "Ästhetik",
@ -261,6 +269,8 @@ export const defaultAuthorData: AuthorData = {
customSectionContent: "Möchten Sie vorab Leseproben erhalten und mich auf meinem Schreibweg begleiten? Melden Sie sich für meine monatliche Herzenspost an!", customSectionContent: "Möchten Sie vorab Leseproben erhalten und mich auf meinem Schreibweg begleiten? Melden Sie sich für meine monatliche Herzenspost an!",
badgeText: "Herzklopfen & Gefühlsstark", badgeText: "Herzklopfen & Gefühlsstark",
aboutTitle: "Über Renee Heart", aboutTitle: "Über Renee Heart",
spotlightTitle: "Spotlight & Neuerscheinung",
spotlightSubtitle: "Besonders hervorgehobenes Werk im Fokus",
bioTag1Label: "Genre", bioTag1Label: "Genre",
bioTag1Value: "Emotionale Romance", bioTag1Value: "Emotionale Romance",
bioTag2Label: "Tonalität", bioTag2Label: "Tonalität",

View file

@ -12,10 +12,12 @@ export interface Book {
subtitle?: string; subtitle?: string;
coverUrl: string; coverUrl: string;
description: string; description: string;
publishedYear: number; publishedYear: number | string; // Accepts year (2026) or full date string (15.10.2026, Herbst 2026)
buyLink?: string; buyLink?: string;
spotifyPlaylistId?: string; // Just the playlist ID (e.g., "47R6ZAdk7Xf6M4i9mR0fP1") spotifyPlaylistId?: string; // Just the playlist ID (e.g., "47R6ZAdk7Xf6M4i9mR0fP1")
genres?: string[]; genres?: string[];
isSpotlight?: boolean; // Highlighted as spotlight / new release
spotlightBadge?: string; // Optional custom badge text (e.g. "Neuerscheinung", "Vorbestellbar")
} }
export interface AuthorProfile { export interface AuthorProfile {
@ -36,6 +38,8 @@ export interface AuthorProfile {
// Custom static texts // Custom static texts
badgeText?: string; badgeText?: string;
aboutTitle?: string; aboutTitle?: string;
spotlightTitle?: string;
spotlightSubtitle?: string;
// Dynamic Bio Highlights / Schlagworte (Über mich) // Dynamic Bio Highlights / Schlagworte (Über mich)
bioTag1Label?: string; bioTag1Label?: string;