Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Failing after 10m3s
Added rate limiting to APIs, cleaned up docs, implemented fallback logic for reviews without text, and added comprehensive n8n guide.
160 lines
5.9 KiB
TypeScript
160 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import { motion } from "framer-motion";
|
|
import { BookOpen } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
import Image from "next/image";
|
|
|
|
interface CurrentlyReading {
|
|
title: string;
|
|
authors: string[];
|
|
image: string | null;
|
|
progress: number;
|
|
startedAt: string | null;
|
|
}
|
|
|
|
const CurrentlyReading = () => {
|
|
const t = useTranslations("home.about.currentlyReading");
|
|
const [books, setBooks] = useState<CurrentlyReading[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
// Nur einmal beim Laden der Seite
|
|
const fetchCurrentlyReading = async () => {
|
|
try {
|
|
const res = await fetch("/api/n8n/hardcover/currently-reading", {
|
|
cache: "default",
|
|
});
|
|
|
|
if (!res.ok) {
|
|
throw new Error("Failed to fetch");
|
|
}
|
|
|
|
const data = await res.json();
|
|
// Handle both single book and array of books
|
|
if (data.currentlyReading) {
|
|
const booksArray = Array.isArray(data.currentlyReading)
|
|
? data.currentlyReading
|
|
: [data.currentlyReading];
|
|
setBooks(booksArray);
|
|
} else {
|
|
setBooks([]);
|
|
}
|
|
} catch (error) {
|
|
if (process.env.NODE_ENV === "development") {
|
|
console.error("Error fetching currently reading:", error);
|
|
}
|
|
setBooks([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchCurrentlyReading();
|
|
}, []); // Leeres Array = nur einmal beim Mount
|
|
|
|
// Zeige nichts wenn kein Buch gelesen wird oder noch geladen wird
|
|
if (loading || books.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<BookOpen size={18} className="text-stone-600 dark:text-stone-300 flex-shrink-0" />
|
|
<h3 className="text-lg font-bold text-stone-900 dark:text-stone-100">
|
|
{t("title")} {books.length > 1 && `(${books.length})`}
|
|
</h3>
|
|
</div>
|
|
|
|
{/* Books List */}
|
|
{books.map((book, index) => (
|
|
<motion.div
|
|
key={`${book.title}-${index}`}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: "-50px" }}
|
|
transition={{ duration: 0.6, delay: index * 0.1, ease: [0.25, 0.1, 0.25, 1] }}
|
|
whileHover={{
|
|
scale: 1.02,
|
|
transition: { duration: 0.4, ease: "easeOut" },
|
|
}}
|
|
className="relative overflow-hidden bg-gradient-to-br from-liquid-lavender/15 via-liquid-pink/10 to-liquid-rose/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-lavender/30 dark:border-stone-700 rounded-xl p-6 backdrop-blur-sm hover:border-liquid-lavender/50 dark:hover:border-stone-600 hover:from-liquid-lavender/20 hover:via-liquid-pink/15 hover:to-liquid-rose/20 transition-all duration-500 ease-out"
|
|
>
|
|
{/* Background Blob Animation */}
|
|
<motion.div
|
|
className="absolute -top-10 -right-10 w-32 h-32 bg-gradient-to-br from-liquid-lavender/20 to-liquid-pink/20 dark:from-stone-700 dark:to-stone-600 rounded-full blur-2xl"
|
|
animate={{
|
|
scale: [1, 1.2, 1],
|
|
opacity: [0.3, 0.5, 0.3],
|
|
}}
|
|
transition={{
|
|
duration: 8,
|
|
repeat: Infinity,
|
|
ease: "easeInOut",
|
|
delay: index * 0.5,
|
|
}}
|
|
/>
|
|
|
|
<div className="relative z-10 flex flex-col sm:flex-row gap-4 items-start">
|
|
{/* Book Cover */}
|
|
{book.image && (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }}
|
|
className="flex-shrink-0"
|
|
>
|
|
<div className="relative w-24 h-36 sm:w-28 sm:h-40 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600">
|
|
<Image
|
|
src={book.image}
|
|
alt={book.title}
|
|
fill
|
|
className="object-cover"
|
|
sizes="(max-width: 640px) 96px, 112px"
|
|
/>
|
|
{/* Glossy Overlay */}
|
|
<div className="absolute inset-0 bg-gradient-to-tr from-white/20 via-transparent to-white/10 pointer-events-none" />
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Book Info */}
|
|
<div className="flex-1 min-w-0">
|
|
{/* Title */}
|
|
<h4 className="text-lg font-bold text-stone-900 dark:text-stone-100 mb-1 line-clamp-2">
|
|
{book.title}
|
|
</h4>
|
|
|
|
{/* Authors */}
|
|
<p className="text-sm text-stone-600 dark:text-stone-400 mb-4 line-clamp-1">
|
|
{book.authors.join(", ")}
|
|
</p>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between text-xs text-stone-600 dark:text-stone-400">
|
|
<span>{t("progress")}</span>
|
|
<span className="font-semibold">{book.progress}%</span>
|
|
</div>
|
|
<div className="relative h-2 bg-white/50 dark:bg-stone-700 rounded-full overflow-hidden border border-white/70 dark:border-stone-600">
|
|
<motion.div
|
|
initial={{ width: 0 }}
|
|
animate={{ width: `${book.progress}%` }}
|
|
transition={{ duration: 1, delay: 0.3 + index * 0.1, ease: "easeOut" }}
|
|
className="absolute left-0 top-0 h-full bg-gradient-to-r from-liquid-lavender via-liquid-pink to-liquid-rose rounded-full shadow-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CurrentlyReading;
|