feat: improve book reviews, restore detailed privacy policy, fix header logo, add theme toggle, integrate boneyard-js
- Book reviews: add line-clamp for longer review text with expand/collapse per review - Privacy policy: restore full detailed DSGVO-compliant fallback content - Header (legal pages): change logo from 'dk' to 'dk0' in circle - Header (main page): add ThemeToggle for dark/light mode switching - Skeleton loading: integrate boneyard-js for ReadBooks, CurrentlyReading, Projects - Add boneyard.config.json and bones/registry.ts placeholder
This commit is contained in:
@@ -7,6 +7,7 @@ import { ToastProvider } from "@/components/Toast";
|
||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||
import { ConsentProvider } from "./ConsentProvider";
|
||||
import { ThemeProvider } from "./ThemeProvider";
|
||||
import "../../bones/registry";
|
||||
|
||||
const BackgroundBlobs = dynamic(
|
||||
() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })),
|
||||
|
||||
@@ -5,7 +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";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
|
||||
interface CurrentlyReading {
|
||||
title: string;
|
||||
@@ -55,31 +55,14 @@ const CurrentlyReading = () => {
|
||||
fetchCurrentlyReading();
|
||||
}, []); // Leeres Array = nur einmal beim Mount
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-start">
|
||||
<Skeleton className="w-24 h-36 sm:w-28 sm:h-40 rounded-lg shrink-0" />
|
||||
<div className="flex-1 space-y-3 w-full">
|
||||
<Skeleton className="h-6 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
<div className="space-y-2 pt-4">
|
||||
<Skeleton className="h-2 w-full" />
|
||||
<Skeleton className="h-2 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Zeige nichts wenn kein Buch gelesen wird oder noch geladen wird
|
||||
if (books.length === 0) {
|
||||
// Zeige nichts wenn kein Buch gelesen wird
|
||||
if (books.length === 0 && !loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Skeleton name="currently-reading" loading={loading} animate="shimmer" transition>
|
||||
<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" />
|
||||
@@ -170,8 +153,9 @@ const CurrentlyReading = () => {
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ const Header = () => {
|
||||
href={`/${locale}`}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 transition-transform hover:scale-105 active:scale-95 shadow-lg"
|
||||
>
|
||||
<span className="font-black text-xs tracking-tighter">dk</span>
|
||||
<span className="font-black text-xs tracking-tighter">dk0</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Menu */}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SiGithub, SiLinkedin } from "react-icons/si";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
import type { NavTranslations } from "@/types/translations";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
// Inline SVG icons to avoid loading the full lucide-react chunk (~116KB)
|
||||
const MenuIcon = ({ size = 24 }: { size?: number }) => (
|
||||
@@ -128,6 +129,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
||||
>
|
||||
DE
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -188,7 +190,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
||||
</nav>
|
||||
|
||||
{/* Language Switcher Mobile */}
|
||||
<div className="flex gap-2 mt-6 pt-6 border-t border-stone-200">
|
||||
<div className="flex items-center gap-2 mt-6 pt-6 border-t border-stone-200">
|
||||
<Link
|
||||
href={enHref}
|
||||
onClick={() => setIsOpen(false)}
|
||||
@@ -211,6 +213,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
|
||||
>
|
||||
DE
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<div className="mt-8 pt-6 border-t border-stone-200">
|
||||
|
||||
@@ -6,7 +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";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
|
||||
interface Project {
|
||||
id: number;
|
||||
@@ -63,18 +63,9 @@ const Projects = () => {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Skeleton name="projects-grid" loading={loading} animate="shimmer" transition>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-12">
|
||||
{loading ? (
|
||||
Array.from({ length: 2 }).map((_, i) => (
|
||||
<div key={i} className="space-y-6">
|
||||
<Skeleton className="aspect-[4/3] rounded-[2.5rem]" />
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-8 w-1/2" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : projects.length === 0 ? (
|
||||
{projects.length === 0 && !loading ? (
|
||||
<div className="col-span-2 py-12 text-center text-stone-400 dark:text-stone-600 text-sm">
|
||||
No projects yet.
|
||||
</div>
|
||||
@@ -125,6 +116,7 @@ const Projects = () => {
|
||||
</motion.div>
|
||||
)))}
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BookCheck, Star, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import { Skeleton } from "./ui/Skeleton";
|
||||
import { Skeleton } from "boneyard-js/react";
|
||||
|
||||
interface BookReview {
|
||||
id: string;
|
||||
@@ -48,6 +48,7 @@ const ReadBooks = () => {
|
||||
const [reviews, setReviews] = useState<BookReview[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [expandedReviews, setExpandedReviews] = useState<Set<string>>(new Set());
|
||||
|
||||
const INITIAL_SHOW = 3;
|
||||
|
||||
@@ -82,25 +83,7 @@ const ReadBooks = () => {
|
||||
fetchReviews();
|
||||
}, [locale]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="flex flex-col sm:flex-row gap-4 items-start">
|
||||
<Skeleton className="w-20 h-[7.5rem] sm:w-24 sm:h-32 rounded-lg shrink-0" />
|
||||
<div className="flex-1 space-y-2 w-full">
|
||||
<Skeleton className="h-5 w-1/2" />
|
||||
<Skeleton className="h-4 w-1/3" />
|
||||
<Skeleton className="h-3 w-1/4 pt-2" />
|
||||
<Skeleton className="h-12 w-full pt-2" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (reviews.length === 0) {
|
||||
if (reviews.length === 0 && !loading) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 py-4 text-sm text-stone-400 dark:text-stone-500">
|
||||
<BookCheck size={16} className="shrink-0" />
|
||||
@@ -113,7 +96,8 @@ const ReadBooks = () => {
|
||||
const hasMore = reviews.length > INITIAL_SHOW;
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Skeleton name="read-books" loading={loading} animate="shimmer" transition>
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<BookCheck size={18} className="text-stone-600 dark:text-stone-300 flex-shrink-0" />
|
||||
@@ -198,9 +182,27 @@ const ReadBooks = () => {
|
||||
|
||||
{/* Review Text (Optional) */}
|
||||
{review.review && (
|
||||
<p className="text-sm text-stone-700 dark:text-stone-300 leading-relaxed italic">
|
||||
“{stripHtml(review.review)}”
|
||||
</p>
|
||||
<div className="relative">
|
||||
<p className={`text-sm text-stone-700 dark:text-stone-300 leading-relaxed italic ${!expandedReviews.has(review.id) ? 'line-clamp-3' : ''}`}>
|
||||
“{stripHtml(review.review)}”
|
||||
</p>
|
||||
{stripHtml(review.review).length > 100 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setExpandedReviews(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(review.id)) next.delete(review.id);
|
||||
else next.add(review.id);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
className="text-xs text-liquid-sky hover:text-liquid-mint dark:text-liquid-sky dark:hover:text-liquid-mint font-medium mt-1 transition-colors"
|
||||
>
|
||||
{expandedReviews.has(review.id) ? t("collapseReview") : t("readMore")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Finished Date */}
|
||||
@@ -240,8 +242,8 @@ const ReadBooks = () => {
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user