From 4a70e275489f182379a4be60dc9542b7c597a6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20He=C3=9Fe?= Date: Sun, 26 Jul 2026 22:12:41 +0200 Subject: [PATCH] Added spotlight section --- server.ts | 3 +- src/components/AdminPanel.tsx | 112 ++++++++++++-- src/components/ClaraPortfolio.tsx | 13 ++ src/components/EroticaPortfolio.tsx | 13 ++ src/components/ReneePortfolio.tsx | 13 ++ src/components/SciFiPortfolio.tsx | 13 ++ src/components/SpotlightSection.tsx | 218 ++++++++++++++++++++++++++++ src/defaultData.ts | 12 +- src/types.ts | 6 +- 9 files changed, 388 insertions(+), 15 deletions(-) create mode 100644 src/components/SpotlightSection.tsx diff --git a/server.ts b/server.ts index ec19b60..6daa6c0 100644 --- a/server.ts +++ b/server.ts @@ -48,7 +48,8 @@ async function initDatabase(): Promise { "bioTag1Label", "bioTag1Value", "bioTag2Label", "bioTag2Value", "bioTag3Label", "bioTag3Value", - "badgeText", "aboutTitle", "projectsTitle", "projectsSubtitle", + "badgeText", "aboutTitle", "spotlightTitle", "spotlightSubtitle", + "projectsTitle", "projectsSubtitle", "booksTitle", "booksSubtitle", "footerText", "accentColor", "secondaryColor", "backgroundColor", "cardBgColor", "fontFamily", "heroBannerUrl" diff --git a/src/components/AdminPanel.tsx b/src/components/AdminPanel.tsx index 3c231b3..0e71f6e 100644 --- a/src/components/AdminPanel.tsx +++ b/src/components/AdminPanel.tsx @@ -30,6 +30,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { // Editing forms state const [editingBookId, setEditingBookId] = useState(null); const [bookForm, setBookForm] = useState>({}); + const [genreInputRaw, setGenreInputRaw] = useState(""); const [editingProjectId, setEditingProjectId] = useState(null); const [projectForm, setProjectForm] = useState>({}); @@ -225,7 +226,14 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { const handleSaveBook = async () => { if (!authorData || !profile) return; + const parsedGenres = genreInputRaw.split(",").map(g => g.trim()).filter(Boolean); 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") { const newBook: Book = { id: "book_" + Date.now(), @@ -236,12 +244,20 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { publishedYear: bookForm.publishedYear || new Date().getFullYear(), buyLink: bookForm.buyLink || "", spotifyPlaylistId: bookForm.spotifyPlaylistId || "", - genres: bookForm.genres || [] + genres: parsedGenres, + isSpotlight: !!bookForm.isSpotlight, + spotlightBadge: bookForm.spotlightBadge || "" }; updatedBooks.push(newBook); } else { 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) { /> +
+ + 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" + /> +
+ +
+ + 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" + /> +
+
(
-
+
{b.title} + {b.isSpotlight && ( + + + + )}

{b.title}

{b.subtitle || "Einzelband"}

- Jahr: {b.publishedYear} + Datum/Jahr: {b.publishedYear}
@@ -1265,6 +1312,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { onClick={() => { setEditingBookId(b.id); setBookForm(b); + setGenreInputRaw(b.genres ? b.genres.join(", ") : ""); setAiTitle(b.title || ""); setAiResult(""); }} @@ -1275,7 +1323,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
+ {/* Spotlight Section Toggle inside Book Form */} +
+
+ setBookForm({ ...bookForm, isSpotlight: e.target.checked })} + className="w-4 h-4 rounded accent-amber-500 cursor-pointer" + /> + +
+

+ Wenn aktiviert, wird dieses Werk in einer prominenten Highlight-Kachel zwischen "Über mich" und "Aktuelle Projekte" platziert. +

+ + {bookForm.isSpotlight && ( +
+ + 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" + /> +

+ Wenn leer, wird automatisch "Erscheint am [Datum]" (bei Zukunftsdatum) oder "Neuerscheinung" verwendet. +

+
+ )} +
+
- + 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" /> +

Unterstützt Jahreszahlen (2026) und konkrete Daten (15.10.2026).

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" />
@@ -1369,11 +1456,12 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) { setBookForm({ ...bookForm, genres: e.target.value.split(",").map(g => g.trim()).filter(g => g !== "") })} + value={genreInputRaw} + onChange={(e) => setGenreInputRaw(e.target.value)} 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" /> +

Trennen Sie mehrere Genres einfach durch Kommas.

{/* Buch-Klappentext Synopsis / Description */} diff --git a/src/components/ClaraPortfolio.tsx b/src/components/ClaraPortfolio.tsx index f502c78..c06ac5e 100644 --- a/src/components/ClaraPortfolio.tsx +++ b/src/components/ClaraPortfolio.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import Markdown from "react-markdown"; import { Book, Project, AuthorProfile } from "../types"; import { Leaf, Feather, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react"; +import SpotlightSection from "./SpotlightSection"; interface ClaraPortfolioProps { data: AuthorProfile; @@ -139,6 +140,18 @@ export default function ClaraPortfolio({ data }: ClaraPortfolioProps) {
+ {/* Section 1.5: Spotlight / Neuerscheinung */} + {data.books?.find(b => b.isSpotlight) && ( + b.isSpotlight)!} + title={data.spotlightTitle} + subtitle={data.spotlightSubtitle} + accentColor={accentColor} + secondaryColor={secondaryColor} + cardBgColor={cardBgColor} + /> + )} + {/* Dynamic ongoing work writing progress */}
diff --git a/src/components/EroticaPortfolio.tsx b/src/components/EroticaPortfolio.tsx index affc9b6..e5b784d 100644 --- a/src/components/EroticaPortfolio.tsx +++ b/src/components/EroticaPortfolio.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import Markdown from "react-markdown"; import { Book, Project, AuthorProfile } from "../types"; import { Heart, Key, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react"; +import SpotlightSection from "./SpotlightSection"; interface EroticaPortfolioProps { data: AuthorProfile; @@ -139,6 +140,18 @@ export default function EroticaPortfolio({ data }: EroticaPortfolioProps) {
+ {/* Section 1.5: Spotlight / Neuerscheinung */} + {data.books?.find(b => b.isSpotlight) && ( + b.isSpotlight)!} + title={data.spotlightTitle} + subtitle={data.spotlightSubtitle} + accentColor={accentColor} + secondaryColor={secondaryColor} + cardBgColor={cardBgColor} + /> + )} + {/* Dynamic ongoing work writing progress */}
diff --git a/src/components/ReneePortfolio.tsx b/src/components/ReneePortfolio.tsx index 78dca97..c9103f1 100644 --- a/src/components/ReneePortfolio.tsx +++ b/src/components/ReneePortfolio.tsx @@ -4,6 +4,7 @@ import { AuthorProfile, Book, Project } from "../types"; import { Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun, Music } from "lucide-react"; +import SpotlightSection from "./SpotlightSection"; interface ReneePortfolioProps { data: AuthorProfile; @@ -140,6 +141,18 @@ export default function ReneePortfolio({ data }: ReneePortfolioProps) {
+ {/* Section 1.5: Spotlight / Neuerscheinung */} + {data.books?.find(b => b.isSpotlight) && ( + b.isSpotlight)!} + title={data.spotlightTitle} + subtitle={data.spotlightSubtitle} + accentColor={accentColor} + secondaryColor={secondaryColor} + cardBgColor={cardBgColor} + /> + )} + {/* Dynamic ongoing work writing progress */}
diff --git a/src/components/SciFiPortfolio.tsx b/src/components/SciFiPortfolio.tsx index 7917cc4..4254ea2 100644 --- a/src/components/SciFiPortfolio.tsx +++ b/src/components/SciFiPortfolio.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import Markdown from "react-markdown"; import { Book, Project, AuthorProfile } from "../types"; import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, Moon } from "lucide-react"; +import SpotlightSection from "./SpotlightSection"; interface SciFiPortfolioProps { data: AuthorProfile; @@ -152,6 +153,18 @@ export default function SciFiPortfolio({ data }: SciFiPortfolioProps) {
+ {/* Section 1.5: Spotlight / Neuerscheinung */} + {data.books?.find(b => b.isSpotlight) && ( + b.isSpotlight)!} + title={data.spotlightTitle} + subtitle={data.spotlightSubtitle} + accentColor={accentColor} + secondaryColor={secondaryColor} + cardBgColor={cardBgColor} + /> + )} + {/* Section 2: Aktuelle Projekte */}
diff --git a/src/components/SpotlightSection.tsx b/src/components/SpotlightSection.tsx new file mode 100644 index 0000000..fd38d7b --- /dev/null +++ b/src/components/SpotlightSection.tsx @@ -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 ( +
+
+
+ +
+
+

+ {title || "Spotlight & Neuerscheinung"} +

+

{subtitle || "Besonders hervorgehobenes Werk im Fokus"}

+
+
+ +
+ {/* Subtle Ambient Background Gradient */} +
+ +
+ {/* Cover image column */} +
+
+
+
+ {book.title} +
+
+ + {book.buyLink && ( + + + {isFuture ? "Jetzt vorbestellen" : "Jetzt im Handel"} + + + )} +
+ + {/* Details column */} +
+
+ + + {badgeText} + + + {book.publishedYear && ( + + + {isFuture ? "Erscheinungsdatum:" : "Veröffentlichung:"} {book.publishedYear} + + )} +
+ +
+

+ {book.title} +

+ {book.subtitle && ( +

+ {book.subtitle} +

+ )} +
+ + {/* Genres */} + {book.genres && book.genres.length > 0 && ( +
+ + {book.genres.map((g, idx) => ( + + {g} + + ))} +
+ )} + + {/* Synopsis */} + + + {/* Spotify Player Toggle if present */} + {book.spotifyPlaylistId && ( +
+ + + {showSpotify && ( +
+