diff --git a/app/[locale]/books/page.tsx b/app/[locale]/books/page.tsx index bc129c9..b8dc9e3 100644 --- a/app/[locale]/books/page.tsx +++ b/app/[locale]/books/page.tsx @@ -1,34 +1,33 @@ -import { getBookReviews } from "@/lib/directus"; +"use client"; + import { getLanguageAlternates, toAbsoluteUrl } from "@/lib/seo"; -import type { Metadata } from "next"; import { BookOpen, ArrowLeft, Star } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; +import { useEffect, useState } from "react"; +import { useLocale } from "next-intl"; +import { Skeleton } from "@/app/components/ui/Skeleton"; +import { BookReview } from "@/lib/directus"; -export const revalidate = 300; +export default function BooksPage() { + const locale = useLocale(); + const [reviews, setReviews] = useState([]); + const [loading, setLoading] = useState(true); -export async function generateMetadata({ - params, -}: { - params: Promise<{ locale: string }>; -}): Promise { - const { locale } = await params; - return { - title: locale === "de" ? "Meine Bibliothek" : "My Library", - alternates: { - canonical: toAbsoluteUrl(`/${locale}/books`), - languages: getLanguageAlternates({ pathWithoutLocale: "books" }), - }, - }; -} - -export default async function BooksPage({ - params, -}: { - params: Promise<{ locale: string }>; -}) { - const { locale } = await params; - const reviews = await getBookReviews(locale); + useEffect(() => { + const fetchBooks = async () => { + try { + const res = await fetch(`/api/book-reviews?locale=${locale}`); + const data = await res.json(); + if (data.bookReviews) setReviews(data.bookReviews); + } catch (error) { + console.error("Books fetch failed:", error); + } finally { + setLoading(false); + } + }; + fetchBooks(); + }, [locale]); return (
@@ -52,37 +51,49 @@ export default async function BooksPage({
- {reviews?.map((review) => ( -
- {review.book_image && ( -
- {review.book_title} + {loading ? ( + Array.from({ length: 6 }).map((_, i) => ( +
+ +
+ +
- )} -
-
-

{review.book_title}

- {review.rating && ( -
- - {review.rating} +
+ )) + ) : ( + reviews?.map((review) => ( +
+ {review.book_image && ( +
+ {review.book_title} +
+ )} +
+
+

{review.book_title}

+ {review.rating && ( +
+ + {review.rating} +
+ )} +
+

{review.book_author}

+ {review.review && ( +
+

+ “{review.review.replace(/<[^>]*>/g, '')}” +

)}
-

{review.book_author}

- {review.review && ( -
-

- “{review.review.replace(/<[^>]*>/g, '')}” -

-
- )}
-
- ))} + )) + )}
diff --git a/app/_ui/ProjectDetailClient.tsx b/app/_ui/ProjectDetailClient.tsx index 3b89aca..35cc24c 100644 --- a/app/_ui/ProjectDetailClient.tsx +++ b/app/_ui/ProjectDetailClient.tsx @@ -8,6 +8,7 @@ import ReactMarkdown from "react-markdown"; import { useTranslations } from "next-intl"; import Image from "next/image"; import { useRouter } from "next/navigation"; +import { Skeleton } from "../components/ui/Skeleton"; export type ProjectDetailData = { id: number; diff --git a/app/_ui/ProjectsPageClient.tsx b/app/_ui/ProjectsPageClient.tsx index 9bbbe83..b1009a1 100644 --- a/app/_ui/ProjectsPageClient.tsx +++ b/app/_ui/ProjectsPageClient.tsx @@ -6,6 +6,7 @@ import { ArrowUpRight, ArrowLeft, Search } from "lucide-react"; import Link from "next/link"; import { useLocale, useTranslations } from "next-intl"; import Image from "next/image"; +import { Skeleton } from "../components/ui/Skeleton"; export type ProjectListItem = { id: number; @@ -30,6 +31,13 @@ export default function ProjectsPageClient({ const [selectedCategory, setSelectedCategory] = useState("all"); const [searchQuery, setSearchQuery] = useState(""); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Simulate initial load for smoother entrance or handle actual fetch if needed + const timer = setTimeout(() => setLoading(false), 800); + return () => clearTimeout(timer); + }, []); const categories = useMemo(() => { const unique = Array.from(new Set(projects.map((p) => p.category))).filter(Boolean); @@ -103,7 +111,18 @@ export default function ProjectsPageClient({ {/* Grid */}
- {filteredProjects.map((project) => ( + {loading ? ( + Array.from({ length: 4 }).map((_, i) => ( +
+ +
+ + +
+
+ )) + ) : ( + filteredProjects.map((project) => (
diff --git a/app/components/About.tsx b/app/components/About.tsx index b3c5825..afbfd6b 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -11,6 +11,7 @@ import { TechStackCategory, Hobby, BookReview } from "@/lib/directus"; import Link from "next/link"; import ActivityFeed from "./ActivityFeed"; import BentoChat from "./BentoChat"; +import { Skeleton } from "./ui/Skeleton"; const iconMap: Record = { Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2 @@ -24,6 +25,7 @@ const About = () => { const [hobbies, setHobbies] = useState([]); const [reviewsCount, setReviewsCount] = useState(0); const [cmsMessages, setCmsMessages] = useState>({}); + const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchData = async () => { @@ -50,7 +52,11 @@ const About = () => { const booksData = await booksRes.json(); if (booksData?.bookReviews) setReviewsCount(booksData.bookReviews.length); - } catch (error) {} + } catch (error) { + console.error("About data fetch failed:", error); + } finally { + setIsLoading(false); + } }; fetchData(); }, [locale]); @@ -73,12 +79,22 @@ const About = () => { {t("title")}.
- {cmsDoc ? :

{t("p1")} {t("p2")}

} + {isLoading ? ( +
+ + + +
+ ) : cmsDoc ? ( + + ) : ( +

{t("p1")} {t("p2")}

+ )}

{t("funFactTitle")}

-

{t("funFactBody")}

+ {isLoading ? :

{t("funFactBody")}

}
@@ -170,7 +186,11 @@ const About = () => {
-

{reviewsCount}+ Books

+ {isLoading ? ( + + ) : ( +

{reviewsCount}+ Books

+ )}

Read and summarized in my personal collection.

@@ -191,20 +211,32 @@ const About = () => { Beyond Dev
- {hobbies.map((hobby) => { - const Icon = iconMap[hobby.icon] || Lightbulb; - return ( -
-
- -
-
-

{hobby.title}

-

Passion & Mindset

+ {isLoading ? ( + Array.from({ length: 4 }).map((_, i) => ( +
+ +
+ +
- ) - })} + )) + ) : ( + hobbies.map((hobby) => { + const Icon = iconMap[hobby.icon] || Lightbulb; + return ( +
+
+ +
+
+

{hobby.title}

+

Passion & Mindset

+
+
+ ) + }) + )}
diff --git a/app/components/CurrentlyReading.tsx b/app/components/CurrentlyReading.tsx index 36059ff..704416c 100644 --- a/app/components/CurrentlyReading.tsx +++ b/app/components/CurrentlyReading.tsx @@ -5,6 +5,7 @@ import { BookOpen } from "lucide-react"; import { useEffect, useState } from "react"; import { useTranslations } from "next-intl"; import Image from "next/image"; +import { Skeleton } from "./ui/Skeleton"; interface CurrentlyReading { title: string; @@ -54,8 +55,26 @@ const CurrentlyReading = () => { fetchCurrentlyReading(); }, []); // Leeres Array = nur einmal beim Mount + if (loading) { + return ( +
+
+ +
+ + +
+ + +
+
+
+
+ ); + } + // Zeige nichts wenn kein Buch gelesen wird oder noch geladen wird - if (loading || books.length === 0) { + if (books.length === 0) { return null; } diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index eb78c08..24196b0 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -6,6 +6,7 @@ import { ArrowUpRight } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; import { useLocale, useTranslations } from "next-intl"; +import { Skeleton } from "./ui/Skeleton"; interface Project { id: number; @@ -24,6 +25,7 @@ interface Project { const Projects = () => { const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); const locale = useLocale(); const t = useTranslations("home.projects"); @@ -35,7 +37,11 @@ const Projects = () => { const data = await response.json(); setProjects(data.projects || []); } - } catch (error) {} + } catch (error) { + console.error("Featured projects fetch failed:", error); + } finally { + setLoading(false); + } }; loadProjects(); }, []); @@ -45,20 +51,31 @@ const Projects = () => {
-

- Selected Work +

+ Selected Work.

-

+

Projects that pushed my boundaries.

- - View Archive + + View Archive
-
- {projects.map((project) => ( +
+ {loading ? ( + Array.from({ length: 2 }).map((_, i) => ( +
+ +
+ + +
+
+ )) + ) : ( + projects.map((project) => ( { }, [locale]); if (loading) { - return
Lade Buch-Bewertungen...
; + return ( +
+ {[1, 2].map((i) => ( +
+ +
+ + + + +
+
+ ))} +
+ ); } if (reviews.length === 0) { diff --git a/app/components/ui/Skeleton.tsx b/app/components/ui/Skeleton.tsx new file mode 100644 index 0000000..280cbe7 --- /dev/null +++ b/app/components/ui/Skeleton.tsx @@ -0,0 +1,16 @@ +import { cn } from "@/lib/utils"; + +export function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +}