Added spotlight section #10
9 changed files with 388 additions and 15 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 */}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
218
src/components/SpotlightSection.tsx
Normal file
218
src/components/SpotlightSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue