feat: require consent for Spotify embeds #13
10 changed files with 597 additions and 599 deletions
33
server.ts
33
server.ts
|
|
@ -15,7 +15,22 @@ const PORT = 3000;
|
||||||
const DATA_DIR = path.resolve(process.env.DATA_DIR || path.join(process.cwd(), "data"));
|
const DATA_DIR = path.resolve(process.env.DATA_DIR || path.join(process.cwd(), "data"));
|
||||||
const DATA_FILE = path.join(DATA_DIR, "database.json");
|
const DATA_FILE = path.join(DATA_DIR, "database.json");
|
||||||
const BACKUP_DIR = path.join(DATA_DIR, "backups");
|
const BACKUP_DIR = path.join(DATA_DIR, "backups");
|
||||||
const CURRENT_SCHEMA_VERSION = 1;
|
const CURRENT_SCHEMA_VERSION = 2;
|
||||||
|
const LEGACY_DEFAULT_IMAGE_URLS = new Set([
|
||||||
|
"https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=300",
|
||||||
|
"https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1518709268805-4e9042af9f23?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=300",
|
||||||
|
"https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=300",
|
||||||
|
"https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1448375240586-882707db888b?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=300",
|
||||||
|
"https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&q=80&w=600",
|
||||||
|
"https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400",
|
||||||
|
]);
|
||||||
let writeQueue: Promise<void> = Promise.resolve();
|
let writeQueue: Promise<void> = Promise.resolve();
|
||||||
let serverReady = false;
|
let serverReady = false;
|
||||||
|
|
||||||
|
|
@ -83,6 +98,7 @@ async function initDatabase(): Promise<AuthorData> {
|
||||||
}
|
}
|
||||||
// Legacy files may not contain all newer profiles yet; migrations below add them.
|
// Legacy files may not contain all newer profiles yet; migrations below add them.
|
||||||
const parsed = rawParsed as AuthorData;
|
const parsed = rawParsed as AuthorData;
|
||||||
|
const loadedSchemaVersion = parsed.schemaVersion ?? 0;
|
||||||
let modified = false;
|
let modified = false;
|
||||||
|
|
||||||
// Ensure all dynamic static text and design properties exist for all profiles
|
// Ensure all dynamic static text and design properties exist for all profiles
|
||||||
|
|
@ -129,7 +145,20 @@ async function initDatabase(): Promise<AuthorData> {
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.schemaVersion === undefined) {
|
// Version 2 removes only the exact historical demo images. User-uploaded
|
||||||
|
// and other explicitly configured image URLs remain untouched.
|
||||||
|
if (loadedSchemaVersion < 2) {
|
||||||
|
for (const key of profileKeys) {
|
||||||
|
const profile = parsed[key];
|
||||||
|
if (LEGACY_DEFAULT_IMAGE_URLS.has(profile.avatarUrl)) profile.avatarUrl = "";
|
||||||
|
for (const book of profile.books) {
|
||||||
|
if (LEGACY_DEFAULT_IMAGE_URLS.has(book.coverUrl)) book.coverUrl = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
||||||
parsed.schemaVersion = CURRENT_SCHEMA_VERSION;
|
parsed.schemaVersion = CURRENT_SCHEMA_VERSION;
|
||||||
modified = true;
|
modified = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
||||||
id: "book_" + Date.now(),
|
id: "book_" + Date.now(),
|
||||||
title: bookForm.title || "Unbenanntes Buch",
|
title: bookForm.title || "Unbenanntes Buch",
|
||||||
subtitle: bookForm.subtitle || "",
|
subtitle: bookForm.subtitle || "",
|
||||||
coverUrl: bookForm.coverUrl || "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=400",
|
coverUrl: bookForm.coverUrl || "",
|
||||||
description: bookForm.description || "",
|
description: bookForm.description || "",
|
||||||
publishedYear: bookForm.publishedYear || new Date().getFullYear(),
|
publishedYear: bookForm.publishedYear || new Date().getFullYear(),
|
||||||
buyLink: bookForm.buyLink || "",
|
buyLink: bookForm.buyLink || "",
|
||||||
|
|
@ -598,7 +598,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
||||||
<h3 className="text-xs font-mono text-indigo-400 uppercase tracking-wider">Aktive Identität</h3>
|
<h3 className="text-xs font-mono text-indigo-400 uppercase tracking-wider">Aktive Identität</h3>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<img
|
<img
|
||||||
src={profile.avatarUrl}
|
src={profile.avatarUrl || undefined}
|
||||||
alt={profile.name}
|
alt={profile.name}
|
||||||
className="w-10 h-10 rounded-full object-cover border border-slate-700"
|
className="w-10 h-10 rounded-full object-cover border border-slate-700"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -1479,7 +1479,7 @@ export default function AdminPanel({ onLogout }: AdminPanelProps) {
|
||||||
<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 relative">
|
<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 || undefined} alt={b.title} className="w-full h-full object-cover" referrerPolicy="no-referrer" />
|
||||||
{b.isSpotlight && (
|
{b.isSpotlight && (
|
||||||
<span className="absolute top-1 right-1 bg-amber-500 text-slate-950 p-1 rounded-full shadow-md" title="Spotlight / Neuerscheinung">
|
<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" />
|
<Sparkles className="w-3 h-3" />
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { Book, Project, AuthorProfile, LegalDocument } from "../types";
|
import { Book, Project, AuthorProfile, LegalDocument } from "../types";
|
||||||
import { Leaf, Feather, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
import { Leaf, Feather, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
||||||
import SpotlightSection from "./SpotlightSection";
|
import SpotlightSection from "./SpotlightSection";
|
||||||
import LegalModal from "./LegalModal";
|
import LegalModal from "./LegalModal";
|
||||||
import ProjectDetailMedia from "./ProjectDetailMedia";
|
import ProjectDetailMedia from "./ProjectDetailMedia";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface ClaraPortfolioProps {
|
interface ClaraPortfolioProps {
|
||||||
data: AuthorProfile;
|
data: AuthorProfile;
|
||||||
|
|
@ -98,7 +99,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
<div className="absolute -inset-1.5 rounded-2xl blur opacity-20 group-hover:opacity-40 transition duration-1000 group-hover:duration-200" style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }} />
|
<div className="absolute -inset-1.5 rounded-2xl blur opacity-20 group-hover:opacity-40 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-stone-800" style={{ backgroundColor: cardBgColor }}>
|
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-stone-800" style={{ backgroundColor: cardBgColor }}>
|
||||||
<img
|
<img
|
||||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=400"}
|
src={data.avatarUrl || undefined}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
className="w-full h-full object-cover grayscale brightness-95 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-1000"
|
className="w-full h-full object-cover grayscale brightness-95 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-1000"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -245,7 +246,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-stone-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-stone-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl || "https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=300"}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -316,7 +317,7 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
||||||
<div className="rounded-xl overflow-hidden shadow-2xl border border-[#4A5D50]/30 max-w-[260px] aspect-[2/3] w-full">
|
<div className="rounded-xl overflow-hidden shadow-2xl border border-[#4A5D50]/30 max-w-[260px] aspect-[2/3] w-full">
|
||||||
<img
|
<img
|
||||||
src={selectedBook.coverUrl}
|
src={selectedBook.coverUrl || undefined}
|
||||||
alt={selectedBook.title}
|
alt={selectedBook.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -366,25 +367,9 @@ export default function ClaraPortfolio({ data, legalDocuments = [] }: ClaraPortf
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Embedded Spotify player (Only if playlist is non-empty) */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||||
<div className="space-y-2">
|
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
|
||||||
<p className="text-xs font-sans text-[#8FA797] flex items-center gap-2 italic">
|
|
||||||
<Leaf className="w-3.5 h-3.5 text-[#8FA797]" />
|
|
||||||
<span>Klanglandschaft (Begleitende Playlist)</span>
|
|
||||||
</p>
|
|
||||||
<div className="rounded-xl overflow-hidden bg-stone-900 border border-stone-800">
|
|
||||||
<iframe
|
|
||||||
src={`https://open.spotify.com/embed/playlist/${selectedBook.spotifyPlaylistId}?utm_source=generator&theme=0`}
|
|
||||||
width="100%"
|
|
||||||
height="152"
|
|
||||||
frameBorder="0"
|
|
||||||
allowFullScreen={false}
|
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { Book, Project, AuthorProfile, LegalDocument } from "../types";
|
import { Book, Project, AuthorProfile, LegalDocument } from "../types";
|
||||||
import { Heart, Key, Star, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
import { Heart, Key, Sparkles, BookOpen, ExternalLink, ArrowRight } from "lucide-react";
|
||||||
import SpotlightSection from "./SpotlightSection";
|
import SpotlightSection from "./SpotlightSection";
|
||||||
import LegalModal from "./LegalModal";
|
import LegalModal from "./LegalModal";
|
||||||
import ProjectDetailMedia from "./ProjectDetailMedia";
|
import ProjectDetailMedia from "./ProjectDetailMedia";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface EroticaPortfolioProps {
|
interface EroticaPortfolioProps {
|
||||||
data: AuthorProfile;
|
data: AuthorProfile;
|
||||||
|
|
@ -98,7 +99,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
<div className="absolute -inset-1.5 rounded-2xl blur opacity-25 group-hover:opacity-60 transition duration-1000 group-hover:duration-200" style={{ background: `linear-gradient(to right, ${accentColor}, ${secondaryColor})` }} />
|
<div className="absolute -inset-1.5 rounded-2xl blur opacity-25 group-hover:opacity-60 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-neutral-800" style={{ backgroundColor: cardBgColor }}>
|
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-neutral-800" style={{ backgroundColor: cardBgColor }}>
|
||||||
<img
|
<img
|
||||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=400"}
|
src={data.avatarUrl || undefined}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
className="w-full h-full object-cover grayscale brightness-90 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-1000"
|
className="w-full h-full object-cover grayscale brightness-90 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-1000"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -245,7 +246,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-orange-950/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-orange-950/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl || "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=300"}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700 z-0"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -316,7 +317,7 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
||||||
<div className="rounded-xl overflow-hidden shadow-2xl border border-orange-950/50 max-w-[260px] aspect-[2/3] w-full">
|
<div className="rounded-xl overflow-hidden shadow-2xl border border-orange-950/50 max-w-[260px] aspect-[2/3] w-full">
|
||||||
<img
|
<img
|
||||||
src={selectedBook.coverUrl}
|
src={selectedBook.coverUrl || undefined}
|
||||||
alt={selectedBook.title}
|
alt={selectedBook.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -366,25 +367,9 @@ export default function EroticaPortfolio({ data, legalDocuments = [] }: EroticaP
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Embedded Spotify player */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||||
<div className="space-y-2">
|
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
|
||||||
<p className="text-xs font-sans text-orange-400 flex items-center gap-2 italic">
|
|
||||||
<Heart className="w-3.5 h-3.5 fill-orange-400 text-orange-400" />
|
|
||||||
<span>Sinnliche Begleitmusik (Spotify Playlist)</span>
|
|
||||||
</p>
|
|
||||||
<div className="rounded-xl overflow-hidden bg-neutral-900 border border-neutral-900">
|
|
||||||
<iframe
|
|
||||||
src={`https://open.spotify.com/embed/playlist/${selectedBook.spotifyPlaylistId}?utm_source=generator&theme=0`}
|
|
||||||
width="100%"
|
|
||||||
height="152"
|
|
||||||
frameBorder="0"
|
|
||||||
allowFullScreen={false}
|
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,13 @@
|
||||||
import { Music } from "lucide-react";
|
|
||||||
import { Project } from "../types";
|
import { Project } from "../types";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface ProjectDetailMediaProps {
|
interface ProjectDetailMediaProps {
|
||||||
project: Project;
|
project: Project;
|
||||||
accentColor: string;
|
accentColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSpotifyPlaylistId(value?: string): string | null {
|
|
||||||
if (!value) return null;
|
|
||||||
const trimmed = value.trim();
|
|
||||||
const urlMatch = trimmed.match(/open\.spotify\.com\/playlist\/([a-zA-Z0-9]+)/);
|
|
||||||
const candidate = urlMatch?.[1] || trimmed;
|
|
||||||
return /^[a-zA-Z0-9]+$/.test(candidate) ? candidate : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProjectDetailMedia({ project, accentColor }: ProjectDetailMediaProps) {
|
export default function ProjectDetailMedia({ project, accentColor }: ProjectDetailMediaProps) {
|
||||||
const playlistId = normalizeSpotifyPlaylistId(project.spotifyPlaylistId);
|
if (!project.imageUrl && !project.spotifyPlaylistId) return null;
|
||||||
|
|
||||||
if (!project.imageUrl && !playlistId) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
|
|
@ -33,22 +23,12 @@ export default function ProjectDetailMedia({ project, accentColor }: ProjectDeta
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{playlistId && (
|
{project.spotifyPlaylistId && (
|
||||||
<div className="space-y-2">
|
<SpotifyConsentPlayer
|
||||||
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider" style={{ color: accentColor }}>
|
playlist={project.spotifyPlaylistId}
|
||||||
<Music className="h-4 w-4" />
|
title={`Spotify-Playlist zu ${project.title}`}
|
||||||
<span>Playlist zum Schreibprojekt</span>
|
accentColor={accentColor}
|
||||||
</div>
|
/>
|
||||||
<iframe
|
|
||||||
title={`Spotify-Playlist zu ${project.title}`}
|
|
||||||
src={`https://open.spotify.com/embed/playlist/${playlistId}?utm_source=generator&theme=0`}
|
|
||||||
width="100%"
|
|
||||||
height="152"
|
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
||||||
loading="lazy"
|
|
||||||
className="rounded-xl border-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,12 @@ import React, { useState } from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { AuthorProfile, Book, Project, LegalDocument } from "../types";
|
import { AuthorProfile, Book, Project, LegalDocument } from "../types";
|
||||||
import {
|
import {
|
||||||
Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun, Music
|
Heart, Sparkles, BookOpen, ExternalLink, X, ArrowRight, Sun
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import SpotlightSection from "./SpotlightSection";
|
import SpotlightSection from "./SpotlightSection";
|
||||||
import LegalModal from "./LegalModal";
|
import LegalModal from "./LegalModal";
|
||||||
import ProjectDetailMedia from "./ProjectDetailMedia";
|
import ProjectDetailMedia from "./ProjectDetailMedia";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface ReneePortfolioProps {
|
interface ReneePortfolioProps {
|
||||||
data: AuthorProfile;
|
data: AuthorProfile;
|
||||||
|
|
@ -100,7 +101,7 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
<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="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 }}>
|
<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
|
<img
|
||||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=400"}
|
src={data.avatarUrl || undefined}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition duration-500"
|
className="w-full h-full object-cover group-hover:scale-105 transition duration-500"
|
||||||
/>
|
/>
|
||||||
|
|
@ -246,7 +247,7 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-yellow-500/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-yellow-500/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl || "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=300"}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -312,7 +313,7 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 items-start">
|
<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">
|
<div className="md:col-span-5 aspect-[2/3] rounded-xl overflow-hidden border border-slate-700 shadow-md">
|
||||||
<img
|
<img
|
||||||
src={selectedBook.coverUrl}
|
src={selectedBook.coverUrl || undefined}
|
||||||
alt={selectedBook.title}
|
alt={selectedBook.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -354,23 +355,9 @@ export default function ReneePortfolio({ data, legalDocuments = [] }: ReneePortf
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spotify Playlist player if configured */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{selectedBook.spotifyPlaylistId && (
|
{selectedBook.spotifyPlaylistId && (
|
||||||
<div className="pt-2">
|
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
|
||||||
<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 && (
|
{selectedBook.buyLink && (
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { BookOpen, Star, HelpCircle, Award, Compass, ArrowRight, ExternalLink, M
|
||||||
import SpotlightSection from "./SpotlightSection";
|
import SpotlightSection from "./SpotlightSection";
|
||||||
import LegalModal from "./LegalModal";
|
import LegalModal from "./LegalModal";
|
||||||
import ProjectDetailMedia from "./ProjectDetailMedia";
|
import ProjectDetailMedia from "./ProjectDetailMedia";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface SciFiPortfolioProps {
|
interface SciFiPortfolioProps {
|
||||||
data: AuthorProfile;
|
data: AuthorProfile;
|
||||||
|
|
@ -111,7 +112,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
/>
|
/>
|
||||||
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-slate-800" style={{ backgroundColor: cardBgColor }}>
|
<div className="relative w-64 h-64 md:w-80 md:h-80 rounded-2xl overflow-hidden border border-slate-800" style={{ backgroundColor: cardBgColor }}>
|
||||||
<img
|
<img
|
||||||
src={data.avatarUrl || "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=400"}
|
src={data.avatarUrl || undefined}
|
||||||
alt={data.name}
|
alt={data.name}
|
||||||
className="w-full h-full object-cover grayscale brightness-90 contrast-110 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-700"
|
className="w-full h-full object-cover grayscale brightness-90 contrast-110 group-hover:scale-105 group-hover:grayscale-0 transition-all duration-700"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -262,7 +263,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
>
|
>
|
||||||
<div className="absolute inset-0 bg-cyan-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
<div className="absolute inset-0 bg-cyan-900/10 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none" />
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl || "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=300"}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 z-0"
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 z-0"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -338,7 +339,7 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
<div className="col-span-1 md:col-span-4 flex flex-col items-center">
|
||||||
<div className="rounded-xl overflow-hidden shadow-2xl border border-indigo-900/30 max-w-[260px] aspect-[2/3] w-full">
|
<div className="rounded-xl overflow-hidden shadow-2xl border border-indigo-900/30 max-w-[260px] aspect-[2/3] w-full">
|
||||||
<img
|
<img
|
||||||
src={selectedBook.coverUrl}
|
src={selectedBook.coverUrl || undefined}
|
||||||
alt={selectedBook.title}
|
alt={selectedBook.title}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -388,26 +389,9 @@ export default function SciFiPortfolio({ data, legalDocuments = [] }: SciFiPortf
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Integrated Spotify web player playlist if available */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
{selectedBook.spotifyPlaylistId && selectedBook.spotifyPlaylistId.trim() !== "" && (
|
||||||
<div className="space-y-2">
|
<SpotifyConsentPlayer playlist={selectedBook.spotifyPlaylistId} title={`Spotify-Playlist zu ${selectedBook.title}`} accentColor={accentColor} />
|
||||||
<p className="text-xs font-mono text-indigo-400 flex items-center gap-2">
|
|
||||||
<Star className="w-3.5 h-3.5 animate-spin" />
|
|
||||||
<span>INTEGRIERTES ATMOSPHÄREN-SYSTEM (SPOTIFY PLAYLIST)</span>
|
|
||||||
</p>
|
|
||||||
<div className="rounded-xl overflow-hidden bg-slate-950 border border-slate-800">
|
|
||||||
<iframe
|
|
||||||
src={`https://open.spotify.com/embed/playlist/${selectedBook.spotifyPlaylistId}?utm_source=generator&theme=0`}
|
|
||||||
width="100%"
|
|
||||||
height="152"
|
|
||||||
frameBorder="0"
|
|
||||||
allowFullScreen={false}
|
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
||||||
loading="lazy"
|
|
||||||
className="bg-slate-950"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
68
src/components/SpotifyConsentPlayer.tsx
Normal file
68
src/components/SpotifyConsentPlayer.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Music } from "lucide-react";
|
||||||
|
|
||||||
|
interface SpotifyConsentPlayerProps {
|
||||||
|
playlist: string;
|
||||||
|
title: string;
|
||||||
|
accentColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePlaylistId(value: string): string | null {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
const urlMatch = trimmed.match(/open\.spotify\.com\/playlist\/([a-zA-Z0-9]+)/);
|
||||||
|
const candidate = urlMatch?.[1] || trimmed;
|
||||||
|
return /^[a-zA-Z0-9]+$/.test(candidate) ? candidate : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SpotifyConsentPlayer({ playlist, title, accentColor = "#34d399" }: SpotifyConsentPlayerProps) {
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
const playlistId = normalizePlaylistId(playlist);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoaded(false);
|
||||||
|
}, [playlist]);
|
||||||
|
|
||||||
|
if (!playlistId) return null;
|
||||||
|
|
||||||
|
if (isLoaded) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<iframe
|
||||||
|
title={title}
|
||||||
|
src={`https://open.spotify.com/embed/playlist/${playlistId}?utm_source=generator&theme=0`}
|
||||||
|
width="100%"
|
||||||
|
height="152"
|
||||||
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
|
loading="lazy"
|
||||||
|
className="rounded-xl border-0"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsLoaded(false)}
|
||||||
|
className="text-[10px] text-slate-500 underline underline-offset-2 hover:text-slate-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
Spotify-Player wieder deaktivieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-white/10 bg-black/20 p-4 space-y-3">
|
||||||
|
<div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-wider" style={{ color: accentColor }}>
|
||||||
|
<Music className="h-4 w-4" />
|
||||||
|
<span>Spotify-Playlist</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs leading-relaxed text-slate-400">
|
||||||
|
Der Player wird erst nach Ihrer Zustimmung von Spotify geladen. Dabei wird eine Verbindung zu Spotify hergestellt.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsLoaded(true)}
|
||||||
|
className="rounded-lg border border-white/15 bg-white/10 px-4 py-2 text-xs font-semibold text-white hover:bg-white/15 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Spotify-Player laden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import { Book } from "../types";
|
import { Book } from "../types";
|
||||||
import { Sparkles, Calendar, ShoppingBag, Music, ExternalLink, Tag } from "lucide-react";
|
import { Sparkles, Calendar, ShoppingBag, ExternalLink, Tag } from "lucide-react";
|
||||||
|
import SpotifyConsentPlayer from "./SpotifyConsentPlayer";
|
||||||
|
|
||||||
interface SpotlightSectionProps {
|
interface SpotlightSectionProps {
|
||||||
book: Book;
|
book: Book;
|
||||||
|
|
@ -20,8 +21,6 @@ export default function SpotlightSection({
|
||||||
secondaryColor,
|
secondaryColor,
|
||||||
cardBgColor,
|
cardBgColor,
|
||||||
}: SpotlightSectionProps) {
|
}: SpotlightSectionProps) {
|
||||||
const [showSpotify, setShowSpotify] = useState(false);
|
|
||||||
|
|
||||||
// Helper to check if a release date is in the future
|
// Helper to check if a release date is in the future
|
||||||
const isFutureRelease = (): boolean => {
|
const isFutureRelease = (): boolean => {
|
||||||
if (!book.publishedYear) return false;
|
if (!book.publishedYear) return false;
|
||||||
|
|
@ -94,7 +93,7 @@ export default function SpotlightSection({
|
||||||
/>
|
/>
|
||||||
<div className="relative aspect-[2/3] w-full rounded-xl overflow-hidden border border-slate-700/60 shadow-2xl bg-slate-900">
|
<div className="relative aspect-[2/3] w-full rounded-xl overflow-hidden border border-slate-700/60 shadow-2xl bg-slate-900">
|
||||||
<img
|
<img
|
||||||
src={book.coverUrl}
|
src={book.coverUrl || undefined}
|
||||||
alt={book.title}
|
alt={book.title}
|
||||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
referrerPolicy="no-referrer"
|
referrerPolicy="no-referrer"
|
||||||
|
|
@ -185,29 +184,10 @@ export default function SpotlightSection({
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spotify Player Toggle if present */}
|
{/* Privacy-friendly Spotify player */}
|
||||||
{book.spotifyPlaylistId && (
|
{book.spotifyPlaylistId && (
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
<button
|
<SpotifyConsentPlayer playlist={book.spotifyPlaylistId} title={`Spotify-Playlist zu ${book.title}`} accentColor={accentColor} />
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { AuthorData } from "./types";
|
||||||
export const defaultAuthorData: AuthorData = {
|
export const defaultAuthorData: AuthorData = {
|
||||||
scifi: {
|
scifi: {
|
||||||
name: "Daniel Hesse",
|
name: "Daniel Hesse",
|
||||||
avatarUrl: "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=300",
|
avatarUrl: "",
|
||||||
heroTitle: "DANIEL HESSE",
|
heroTitle: "DANIEL HESSE",
|
||||||
heroSubtitle: "WELTENWANDLER & STERNENZEICHNER",
|
heroSubtitle: "WELTENWANDLER & STERNENZEICHNER",
|
||||||
bio: "Herzlich willkommen! Ich bin Daniel Hesse. Ich schreibe epische Fantasy und tiefgründige Science-Fiction, die dich an die Grenzen der Realität und weit darüber hinaus entführen. Schon als Kind faszinierten mich die unendlichen Weiten des Kosmos und die alten Sagen vergessener Völker. In meinen Büchern verschmelzen kalte, fortschrittliche Technologie und uralter, mystischer Zauber zu einer einzigartigen Atmosphäre. Wenn ich nicht gerade neue Galaxien am Laptop erschaffe, wandere ich durch dunkle Wälder oder beobachte mit meinem Teleskop den Nachthimmel.",
|
bio: "Herzlich willkommen! Ich bin Daniel Hesse. Ich schreibe epische Fantasy und tiefgründige Science-Fiction, die dich an die Grenzen der Realität und weit darüber hinaus entführen. Schon als Kind faszinierten mich die unendlichen Weiten des Kosmos und die alten Sagen vergessener Völker. In meinen Büchern verschmelzen kalte, fortschrittliche Technologie und uralter, mystischer Zauber zu einer einzigartigen Atmosphäre. Wenn ich nicht gerade neue Galaxien am Laptop erschaffe, wandere ich durch dunkle Wälder oder beobachte mit meinem Teleskop den Nachthimmel.",
|
||||||
|
|
@ -28,7 +28,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_1",
|
id: "book_1",
|
||||||
title: "Caelum: Ruf der Sterne",
|
title: "Caelum: Ruf der Sterne",
|
||||||
subtitle: "Arche-Chroniken I",
|
subtitle: "Arche-Chroniken I",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1451187580459-43490279c0fa?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Am Rande der bekannten Galaxie sucht die Besatzung der 'Caelum' nach einer neuen Heimat für die letzten Überlebenden der Erde. Doch der leere Raum zwischen den Sternen ist nicht so unbewohnt, wie sie dachten. Es wartet eine Bedrohung, die älter ist als die Zeit selbst, aber auch eine Macht, die das Verständnis der Menschheit revolutionieren könnte.",
|
description: "Am Rande der bekannten Galaxie sucht die Besatzung der 'Caelum' nach einer neuen Heimat für die letzten Überlebenden der Erde. Doch der leere Raum zwischen den Sternen ist nicht so unbewohnt, wie sie dachten. Es wartet eine Bedrohung, die älter ist als die Zeit selbst, aber auch eine Macht, die das Verständnis der Menschheit revolutionieren könnte.",
|
||||||
publishedYear: 2024,
|
publishedYear: 2024,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -41,7 +41,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_2",
|
id: "book_2",
|
||||||
title: "Die Schatten von ThalÃassia",
|
title: "Die Schatten von ThalÃassia",
|
||||||
subtitle: "Einzelband",
|
subtitle: "Einzelband",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "In der versunkenen Unterwasser-Metropole Thalassia ringen Magokraten und Kybernetiker um ein uraltes Artefakt. Ein junger Taucher stößt in den abyssalen Gräben auf ein grauenvolles Geheimnis, das das Schicksal beider Fraktionen besiegeln könnte.",
|
description: "In der versunkenen Unterwasser-Metropole Thalassia ringen Magokraten und Kybernetiker um ein uraltes Artefakt. Ein junger Taucher stößt in den abyssalen Gräben auf ein grauenvolles Geheimnis, das das Schicksal beider Fraktionen besiegeln könnte.",
|
||||||
publishedYear: 2023,
|
publishedYear: 2023,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -80,7 +80,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
},
|
},
|
||||||
erotica: {
|
erotica: {
|
||||||
name: "Annie Slone",
|
name: "Annie Slone",
|
||||||
avatarUrl: "https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&q=80&w=300",
|
avatarUrl: "",
|
||||||
heroTitle: "ANNIE SLONE",
|
heroTitle: "ANNIE SLONE",
|
||||||
heroSubtitle: "ELEGANT-SINNLICHE LITERATUR",
|
heroSubtitle: "ELEGANT-SINNLICHE LITERATUR",
|
||||||
bio: "Willkommen in meiner privaten Welt der Versuchung. Unter dem Pseudonym Annie Slone schreibe ich stilvolle, hocherotische Romane und prickelnde Erzählungen. Meine Leidenschaft liegt darin, das feine Spiel zwischen Verlangen und Hingabe, Macht und Vertrauen in all seinen schillernden Facetten zu beleuchten. Meine Geschichten richten sich an Kenner anspruchsvoller Erotikliteratur, die Wert auf tiefgründige Charaktere, knisternde Spannung und ästhetische Beschreibungen legen. Treten Sie näher und lassen Sie sich entführen.",
|
bio: "Willkommen in meiner privaten Welt der Versuchung. Unter dem Pseudonym Annie Slone schreibe ich stilvolle, hocherotische Romane und prickelnde Erzählungen. Meine Leidenschaft liegt darin, das feine Spiel zwischen Verlangen und Hingabe, Macht und Vertrauen in all seinen schillernden Facetten zu beleuchten. Meine Geschichten richten sich an Kenner anspruchsvoller Erotikliteratur, die Wert auf tiefgründige Charaktere, knisternde Spannung und ästhetische Beschreibungen legen. Treten Sie näher und lassen Sie sich entführen.",
|
||||||
|
|
@ -105,7 +105,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_er_1",
|
id: "book_er_1",
|
||||||
title: "Flüstern im Samt",
|
title: "Flüstern im Samt",
|
||||||
subtitle: "Velvet Trust I",
|
subtitle: "Velvet Trust I",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1517841905240-472988babdf9?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Ein Blick in den überfüllten Ballsaal reicht aus, um Sophias wohlgeordnete Welt ins Wanken zu bringen. Der geheimnisvolle Kunsthändler Alexander bietet ihr einen Deal an, dem sie nicht widerstehen kann. Doch hinter den luxuriösen Fassaden verbirgt sich ein leidenschaftliches Abenteuer voller Geheimnisse, das ihre tiefsten Tabus herausfordern wird.",
|
description: "Ein Blick in den überfüllten Ballsaal reicht aus, um Sophias wohlgeordnete Welt ins Wanken zu bringen. Der geheimnisvolle Kunsthändler Alexander bietet ihr einen Deal an, dem sie nicht widerstehen kann. Doch hinter den luxuriösen Fassaden verbirgt sich ein leidenschaftliches Abenteuer voller Geheimnisse, das ihre tiefsten Tabus herausfordern wird.",
|
||||||
publishedYear: 2025,
|
publishedYear: 2025,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -116,7 +116,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_er_2",
|
id: "book_er_2",
|
||||||
title: "Die Maske des Begehrens",
|
title: "Die Maske des Begehrens",
|
||||||
subtitle: "Erotischer Thriller",
|
subtitle: "Erotischer Thriller",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1508700115892-45ecd05ae2ad?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "In Venedigs geheimem Karnevalskreisen kreuzen sich die Wege zweier Fremder, versteckt hinter venezianischen Seidenmasken. Ein sinnliches Versteckspiel beginnt, bei dem Identitäten verblassen und nur noch die unkontrollierbare sexuelle Spannung regiert.",
|
description: "In Venedigs geheimem Karnevalskreisen kreuzen sich die Wege zweier Fremder, versteckt hinter venezianischen Seidenmasken. Ein sinnliches Versteckspiel beginnt, bei dem Identitäten verblassen und nur noch die unkontrollierbare sexuelle Spannung regiert.",
|
||||||
publishedYear: 2024,
|
publishedYear: 2024,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -155,7 +155,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
},
|
},
|
||||||
clara: {
|
clara: {
|
||||||
name: "Clara Finch",
|
name: "Clara Finch",
|
||||||
avatarUrl: "https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=300",
|
avatarUrl: "",
|
||||||
heroTitle: "CLARA FINCH",
|
heroTitle: "CLARA FINCH",
|
||||||
heroSubtitle: "LYRIK, ESSAYS & NATURPHILOSOPHIE",
|
heroSubtitle: "LYRIK, ESSAYS & NATURPHILOSOPHIE",
|
||||||
bio: "Willkommen auf meiner kreativen Lichtung. Ich bin Clara Finch. Unter diesem Namen widme ich mich der lyrischen Poesie, den feinen Essays und naturphilosophischen Betrachtungen über die Verbindung von Mensch und Erde. Meine Texte entstehen oft auf langen Spaziergängen durch neblige Täler, dichte Wälder und karge Küstenstreifen. Sie fangen die Vergänglichkeit des Augenblicks ein – sanft wie Malve, tief verwurzelt wie Salbei. Hier teile ich meine gedruckten Werke, laufenden Notizbücher und klanglichen Inspirationen mit Ihnen. Treten Sie leise ein und verweilen Sie einen Moment.",
|
bio: "Willkommen auf meiner kreativen Lichtung. Ich bin Clara Finch. Unter diesem Namen widme ich mich der lyrischen Poesie, den feinen Essays und naturphilosophischen Betrachtungen über die Verbindung von Mensch und Erde. Meine Texte entstehen oft auf langen Spaziergängen durch neblige Täler, dichte Wälder und karge Küstenstreifen. Sie fangen die Vergänglichkeit des Augenblicks ein – sanft wie Malve, tief verwurzelt wie Salbei. Hier teile ich meine gedruckten Werke, laufenden Notizbücher und klanglichen Inspirationen mit Ihnen. Treten Sie leise ein und verweilen Sie einen Moment.",
|
||||||
|
|
@ -180,7 +180,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_cl_1",
|
id: "book_cl_1",
|
||||||
title: "Die Stille der Halme",
|
title: "Die Stille der Halme",
|
||||||
subtitle: "Gedichte",
|
subtitle: "Gedichte",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1501504905252-473c47e087f8?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Mein erster veröffentlichter Gedichtband, der den Leser auf eine kontemplative Reise durch unberührte Landschaften, stille Morgenstunden und das leise Rauschen der Felder entführt. Jedes Wort ist sorgsam wie ein seltener Kieselstein ausgewählt, um das Unsagbare im Alltäglichen spürbar zu machen.",
|
description: "Mein erster veröffentlichter Gedichtband, der den Leser auf eine kontemplative Reise durch unberührte Landschaften, stille Morgenstunden und das leise Rauschen der Felder entführt. Jedes Wort ist sorgsam wie ein seltener Kieselstein ausgewählt, um das Unsagbare im Alltäglichen spürbar zu machen.",
|
||||||
publishedYear: 2025,
|
publishedYear: 2025,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -191,7 +191,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_cl_2",
|
id: "book_cl_2",
|
||||||
title: "Knospen im Frost",
|
title: "Knospen im Frost",
|
||||||
subtitle: "Prosa-Miniaturen",
|
subtitle: "Prosa-Miniaturen",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1448375240586-882707db888b?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Eine Sammlung feinsinniger Prosa-Skizzen, die sich mit der Resilienz des Lebens inmitten winterlicher Kälte befassen. Metaphern aus Flora und Fauna zeichnen ein zärtliches Porträt des menschlichen Geistes in Zeiten des Umbruchs.",
|
description: "Eine Sammlung feinsinniger Prosa-Skizzen, die sich mit der Resilienz des Lebens inmitten winterlicher Kälte befassen. Metaphern aus Flora und Fauna zeichnen ein zärtliches Porträt des menschlichen Geistes in Zeiten des Umbruchs.",
|
||||||
publishedYear: 2024,
|
publishedYear: 2024,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -230,7 +230,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
},
|
},
|
||||||
renee: {
|
renee: {
|
||||||
name: "Renee Heart",
|
name: "Renee Heart",
|
||||||
avatarUrl: "https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&q=80&w=300",
|
avatarUrl: "",
|
||||||
heroTitle: "RENEE HEART",
|
heroTitle: "RENEE HEART",
|
||||||
heroSubtitle: "HERZENSGESCHICHTEN & EMOTIONALE ROMANE",
|
heroSubtitle: "HERZENSGESCHICHTEN & EMOTIONALE ROMANE",
|
||||||
bio: "Herzlich willkommen in meinem literarischen Zuhause. Ich bin Renee Heart. Ich schreibe herzerwärmende, gefühlvolle Romane über die großen Wendepunkte im Leben, tiefe Freundschaften, unvergessliche Liebesgeschichten und den Mut, dem eigenen Herzen zu folgen. Meine Geschichten handeln von echten Emotionen, leisen wie auch stürmischen Momenten und Charakteren, die man sofort ins Herz schließt. Lassen Sie sich von meinen Büchern berühren und begleiten!",
|
bio: "Herzlich willkommen in meinem literarischen Zuhause. Ich bin Renee Heart. Ich schreibe herzerwärmende, gefühlvolle Romane über die großen Wendepunkte im Leben, tiefe Freundschaften, unvergessliche Liebesgeschichten und den Mut, dem eigenen Herzen zu folgen. Meine Geschichten handeln von echten Emotionen, leisen wie auch stürmischen Momenten und Charakteren, die man sofort ins Herz schließt. Lassen Sie sich von meinen Büchern berühren und begleiten!",
|
||||||
|
|
@ -255,7 +255,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_rh_1",
|
id: "book_rh_1",
|
||||||
title: "Wo das Herz Zuhause ist",
|
title: "Wo das Herz Zuhause ist",
|
||||||
subtitle: "Herzensroman",
|
subtitle: "Herzensroman",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Als Maya das alte Haus ihrer Großmutter an der Küste erbt, ahnt sie nicht, dass der Neuanfang ihr ganzes Leben verändern wird. Zwischen stürmischen Wellen und warmem Tee findet sie nicht nur vergessene Familiengeheimnisse, sondern auch die Liebe ihres Lebens.",
|
description: "Als Maya das alte Haus ihrer Großmutter an der Küste erbt, ahnt sie nicht, dass der Neuanfang ihr ganzes Leben verändern wird. Zwischen stürmischen Wellen und warmem Tee findet sie nicht nur vergessene Familiengeheimnisse, sondern auch die Liebe ihres Lebens.",
|
||||||
publishedYear: 2025,
|
publishedYear: 2025,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
@ -266,7 +266,7 @@ export const defaultAuthorData: AuthorData = {
|
||||||
id: "book_rh_2",
|
id: "book_rh_2",
|
||||||
title: "Sterne über den Dächern",
|
title: "Sterne über den Dächern",
|
||||||
subtitle: "Romantische Komödie",
|
subtitle: "Romantische Komödie",
|
||||||
coverUrl: "https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&q=80&w=600",
|
coverUrl: "",
|
||||||
description: "Eine humorvolle und tiefgründige Geschichte über zwei Gegensätze, die sich in einer nächtlichen Sternwarte näherkommen als je geahnt.",
|
description: "Eine humorvolle und tiefgründige Geschichte über zwei Gegensätze, die sich in einer nächtlichen Sternwarte näherkommen als je geahnt.",
|
||||||
publishedYear: 2024,
|
publishedYear: 2024,
|
||||||
buyLink: "https://amazon.de",
|
buyLink: "https://amazon.de",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue