Compare commits

..

3 Commits

Author SHA1 Message Date
denshooter
4d5dc1f8f9 Merge branch 'dev' into production
All checks were successful
CI / CD / test-build (push) Successful in 10m15s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Successful in 24s
2026-04-15 15:53:22 +02:00
denshooter
32abc7f3ef fix: update tests for dk0 logo and boneyard-js mock, add jest moduleNameMapper
All checks were successful
CI / CD / test-build (push) Successful in 10m13s
CI / CD / deploy-dev (push) Successful in 1m48s
CI / CD / deploy-production (push) Has been skipped
2026-04-15 14:37:50 +02:00
denshooter
87e337a3a0 feat: improve book reviews, restore detailed privacy policy, fix header logo, add theme toggle, integrate boneyard-js
Some checks failed
CI / CD / test-build (push) Failing after 5m28s
CI / CD / deploy-dev (push) Has been skipped
CI / CD / deploy-production (push) Has been skipped
- 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
2026-04-15 14:26:08 +02:00
18 changed files with 212 additions and 76 deletions

3
.gitignore vendored
View File

@@ -58,6 +58,9 @@ coverage/
.idea/ .idea/
.vscode/ .vscode/
# boneyard generated bones
bones/*.bones.json
# OS # OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db

View File

@@ -0,0 +1,6 @@
import React from "react";
export function Skeleton({ children, loading }: { children: React.ReactNode; loading: boolean; name?: string; animate?: string; transition?: boolean | number }) {
if (loading) return <div data-testid="boneyard-skeleton">Loading...</div>;
return <>{children}</>;
}

View File

@@ -2,16 +2,13 @@ import { render, screen, waitFor } from "@testing-library/react";
import CurrentlyReadingComp from "@/app/components/CurrentlyReading"; import CurrentlyReadingComp from "@/app/components/CurrentlyReading";
import React from "react"; import React from "react";
// Mock next-intl completely to avoid ESM issues
jest.mock("next-intl", () => ({ jest.mock("next-intl", () => ({
useTranslations: () => (key: string) => key, useTranslations: () => (key: string) => key,
useLocale: () => "en", useLocale: () => "en",
})); }));
// Mock next/image
jest.mock("next/image", () => ({ jest.mock("next/image", () => ({
__esModule: true, __esModule: true,
// eslint-disable-next-line @next/next/no-img-element
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => <img {...props} alt={props.alt || ""} />, default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => <img {...props} alt={props.alt || ""} />,
})); }));
@@ -22,8 +19,8 @@ describe("CurrentlyReading Component", () => {
it("renders skeleton when loading", () => { it("renders skeleton when loading", () => {
(global.fetch as jest.Mock).mockReturnValue(new Promise(() => {})); (global.fetch as jest.Mock).mockReturnValue(new Promise(() => {}));
const { container } = render(<CurrentlyReadingComp />); render(<CurrentlyReadingComp />);
expect(container.querySelector(".animate-pulse")).toBeInTheDocument(); expect(screen.getByTestId("boneyard-skeleton")).toBeInTheDocument();
}); });
it("renders a book when data is fetched", async () => { it("renders a book when data is fetched", async () => {

View File

@@ -23,7 +23,7 @@ jest.mock('next/navigation', () => ({
describe('Header', () => { describe('Header', () => {
it('renders the header with the dk logo', () => { it('renders the header with the dk logo', () => {
render(<Header />); render(<Header />);
expect(screen.getByText('dk')).toBeInTheDocument(); expect(screen.getByText('dk0')).toBeInTheDocument();
// Check for navigation links (appear in both desktop and mobile menus) // Check for navigation links (appear in both desktop and mobile menus)
expect(screen.getAllByText('Home').length).toBeGreaterThan(0); expect(screen.getAllByText('Home').length).toBeGreaterThan(0);

View File

@@ -7,6 +7,7 @@ import { ToastProvider } from "@/components/Toast";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import { ConsentProvider } from "./ConsentProvider"; import { ConsentProvider } from "./ConsentProvider";
import { ThemeProvider } from "./ThemeProvider"; import { ThemeProvider } from "./ThemeProvider";
import "../../bones/registry";
const BackgroundBlobs = dynamic( const BackgroundBlobs = dynamic(
() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), () => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })),

View File

@@ -5,7 +5,7 @@ import { BookOpen } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import { Skeleton } from "./ui/Skeleton"; import { Skeleton } from "boneyard-js/react";
interface CurrentlyReading { interface CurrentlyReading {
title: string; title: string;
@@ -55,31 +55,14 @@ const CurrentlyReading = () => {
fetchCurrentlyReading(); fetchCurrentlyReading();
}, []); // Leeres Array = nur einmal beim Mount }, []); // Leeres Array = nur einmal beim Mount
if (loading) { // Zeige nichts wenn kein Buch gelesen wird
return ( if (books.length === 0 && !loading) {
<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) {
return null; return null;
} }
return ( return (
<div className="space-y-4"> <Skeleton name="currently-reading" loading={loading} animate="shimmer" transition>
<div className="space-y-4">
{/* Header */} {/* Header */}
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<BookOpen size={18} className="text-stone-600 dark:text-stone-300 flex-shrink-0" /> <BookOpen size={18} className="text-stone-600 dark:text-stone-300 flex-shrink-0" />
@@ -170,8 +153,9 @@ const CurrentlyReading = () => {
</div> </div>
</div> </div>
</motion.div> </motion.div>
))} ))}
</div> </div>
</Skeleton>
); );
}; };

View File

@@ -31,7 +31,7 @@ const Header = () => {
href={`/${locale}`} 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" 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> </Link>
{/* Desktop Menu */} {/* Desktop Menu */}

View File

@@ -5,6 +5,7 @@ import { SiGithub, SiLinkedin } from "react-icons/si";
import Link from "next/link"; import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation"; import { usePathname, useSearchParams } from "next/navigation";
import type { NavTranslations } from "@/types/translations"; import type { NavTranslations } from "@/types/translations";
import { ThemeToggle } from "./ThemeToggle";
// Inline SVG icons to avoid loading the full lucide-react chunk (~116KB) // Inline SVG icons to avoid loading the full lucide-react chunk (~116KB)
const MenuIcon = ({ size = 24 }: { size?: number }) => ( const MenuIcon = ({ size = 24 }: { size?: number }) => (
@@ -128,6 +129,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
> >
DE DE
</Link> </Link>
<ThemeToggle />
</div> </div>
</nav> </nav>
@@ -188,7 +190,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
</nav> </nav>
{/* Language Switcher Mobile */} {/* 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 <Link
href={enHref} href={enHref}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
@@ -211,6 +213,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
> >
DE DE
</Link> </Link>
<ThemeToggle />
</div> </div>
<div className="mt-8 pt-6 border-t border-stone-200"> <div className="mt-8 pt-6 border-t border-stone-200">

View File

@@ -6,7 +6,7 @@ import { ArrowUpRight } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import { Skeleton } from "./ui/Skeleton"; import { Skeleton } from "boneyard-js/react";
interface Project { interface Project {
id: number; id: number;
@@ -63,18 +63,9 @@ const Projects = () => {
</Link> </Link>
</div> </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"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-12">
{loading ? ( {projects.length === 0 && !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 ? (
<div className="col-span-2 py-12 text-center text-stone-400 dark:text-stone-600 text-sm"> <div className="col-span-2 py-12 text-center text-stone-400 dark:text-stone-600 text-sm">
No projects yet. No projects yet.
</div> </div>
@@ -125,6 +116,7 @@ const Projects = () => {
</motion.div> </motion.div>
)))} )))}
</div> </div>
</Skeleton>
</div> </div>
</section> </section>
); );

View File

@@ -5,7 +5,7 @@ import { BookCheck, Star, ChevronDown, ChevronUp } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import { Skeleton } from "./ui/Skeleton"; import { Skeleton } from "boneyard-js/react";
interface BookReview { interface BookReview {
id: string; id: string;
@@ -48,6 +48,7 @@ const ReadBooks = () => {
const [reviews, setReviews] = useState<BookReview[]>([]); const [reviews, setReviews] = useState<BookReview[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [expandedReviews, setExpandedReviews] = useState<Set<string>>(new Set());
const INITIAL_SHOW = 3; const INITIAL_SHOW = 3;
@@ -82,25 +83,7 @@ const ReadBooks = () => {
fetchReviews(); fetchReviews();
}, [locale]); }, [locale]);
if (loading) { if (reviews.length === 0 && !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) {
return ( return (
<div className="flex items-center gap-3 py-4 text-sm text-stone-400 dark:text-stone-500"> <div className="flex items-center gap-3 py-4 text-sm text-stone-400 dark:text-stone-500">
<BookCheck size={16} className="shrink-0" /> <BookCheck size={16} className="shrink-0" />
@@ -113,7 +96,8 @@ const ReadBooks = () => {
const hasMore = reviews.length > INITIAL_SHOW; const hasMore = reviews.length > INITIAL_SHOW;
return ( return (
<div className="space-y-4"> <Skeleton name="read-books" loading={loading} animate="shimmer" transition>
<div className="space-y-4">
{/* Header */} {/* Header */}
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<BookCheck size={18} className="text-stone-600 dark:text-stone-300 flex-shrink-0" /> <BookCheck size={18} className="text-stone-600 dark:text-stone-300 flex-shrink-0" />
@@ -198,9 +182,27 @@ const ReadBooks = () => {
{/* Review Text (Optional) */} {/* Review Text (Optional) */}
{review.review && ( {review.review && (
<p className="text-sm text-stone-700 dark:text-stone-300 leading-relaxed italic"> <div className="relative">
&ldquo;{stripHtml(review.review)}&rdquo; <p className={`text-sm text-stone-700 dark:text-stone-300 leading-relaxed italic ${!expandedReviews.has(review.id) ? 'line-clamp-3' : ''}`}>
</p> &ldquo;{stripHtml(review.review)}&rdquo;
</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 */} {/* Finished Date */}
@@ -240,8 +242,8 @@ const ReadBooks = () => {
</motion.button> </motion.button>
)} )}
</div> </div>
</Skeleton>
); );
}; };

View File

@@ -61,11 +61,15 @@ export default function PrivacyPolicy() {
<div className="space-y-16"> <div className="space-y-16">
<section> <section>
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight flex items-center gap-3"> <h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight flex items-center gap-3">
<Shield className="text-liquid-mint" size={28} /> Allgemeiner Überblick <Shield className="text-liquid-mint" size={28} /> Verantwortlicher
</h2> </h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400"> <div className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400 space-y-2">
Der Schutz Ihrer persönlichen Daten ist mir ein besonderes Anliegen. Ich verarbeite Ihre Daten daher ausschließlich auf Grundlage der gesetzlichen Bestimmungen (DSGVO, TMG). <p className="font-bold text-stone-900 dark:text-stone-100">Dennis Konkol</p>
</p> <p>Auf dem Ziegenbrink 2B</p>
<p>49082 Osnabrück, Deutschland</p>
<p>E-Mail: <a href="mailto:contact@dk0.dev" className="text-liquid-mint hover:underline">contact@dk0.dev</a></p>
<p className="text-sm text-stone-500 dark:text-stone-400 mt-4">Diese Datenschutzerklärung gilt für die Verarbeitung personenbezogener Daten durch den oben genannten Verantwortlichen.</p>
</div>
</section> </section>
<section> <section>
@@ -73,8 +77,80 @@ export default function PrivacyPolicy() {
<Database className="text-liquid-sky" size={28} /> Datenerfassung <Database className="text-liquid-sky" size={28} /> Datenerfassung
</h2> </h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400"> <p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400">
Wenn Sie per Formular auf der Website oder per E-Mail Kontakt mit mir aufnehmen, werden Ihre angegebenen Daten zwecks Bearbeitung der Anfrage bei mir gespeichert. Beim Zugriff auf diese Website werden automatisch Informationen allgemeiner Natur erfasst. Diese beinhalten unter anderem:
</p> </p>
<ul className="mt-4 space-y-2 text-xl font-light text-stone-600 dark:text-stone-400">
<li className="flex items-start gap-3"><span className="text-liquid-sky mt-2"></span> IP-Adresse (in anonymisierter Form)</li>
<li className="flex items-start gap-3"><span className="text-liquid-sky mt-2"></span> Uhrzeit und Datum des Zugriffs</li>
<li className="flex items-start gap-3"><span className="text-liquid-sky mt-2"></span> Browsertyp und Betriebssystem</li>
<li className="flex items-start gap-3"><span className="text-liquid-sky mt-2"></span> Referrer-URL (die zuvor besuchte Seite)</li>
</ul>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400 mt-4">
Diese Informationen werden anonymisiert erfasst und dienen ausschließlich statistischen Auswertungen. Rückschlüsse auf Ihre Person sind nicht möglich.
</p>
</section>
<section>
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight">Analyse- und Tracking-Tools</h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400">
Zur Analyse der Nutzung dieser Website setze ich <strong className="text-stone-900 dark:text-stone-100">Umami</strong> ein. Umami speichert keine IP-Adressen oder Cookies. Alle erfassten Daten sind anonymisiert. Da ich Umami auf meinem eigenen Server betreibe, erfolgt keine Weitergabe an Dritte. Rechtsgrundlage: Art. 6 Abs. 1 S. 1 lit. f DSGVO (berechtigtes Interesse an der Analyse und Optimierung der Website).
</p>
</section>
<section>
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight">Kontaktformular</h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400">
Wenn Sie das Kontaktformular nutzen oder per E-Mail Kontakt aufnehmen, werden Ihre Angaben zur Bearbeitung Ihrer Anfrage gespeichert. Diese Daten werden nicht an Dritte weitergegeben und nach Erfüllung des Zwecks gelöscht. Rechtsgrundlage: Art. 6 Abs. 1 S. 1 lit. a DSGVO (Einwilligung).
</p>
</section>
<section>
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight">Social Media Links</h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400">
Diese Website enthält Links zu GitHub und LinkedIn. Durch das Anklicken dieser Links gelten die Datenschutzbestimmungen der jeweiligen Anbieter.
</p>
</section>
<section>
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight">Weitergabe von Daten</h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400 mb-4">Eine Weitergabe Ihrer personenbezogenen Daten erfolgt nur, wenn:</p>
<ul className="space-y-2 text-xl font-light text-stone-600 dark:text-stone-400">
<li className="flex items-start gap-3"><span className="text-liquid-mint mt-2"></span> Sie nach Art. 6 Abs. 1 S. 1 lit. a DSGVO ausdrücklich eingewilligt haben,</li>
<li className="flex items-start gap-3"><span className="text-liquid-mint mt-2"></span> dies zur Vertragserfüllung gemäß Art. 6 Abs. 1 S. 1 lit. b DSGVO erforderlich ist,</li>
<li className="flex items-start gap-3"><span className="text-liquid-mint mt-2"></span> eine gesetzliche Verpflichtung nach Art. 6 Abs. 1 S. 1 lit. c DSGVO besteht, oder</li>
<li className="flex items-start gap-3"><span className="text-liquid-mint mt-2"></span> die Verarbeitung nach Art. 6 Abs. 1 S. 1 lit. f DSGVO zur Wahrung berechtigter Interessen erforderlich ist.</li>
</ul>
</section>
<section>
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight">Ihre Rechte</h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400 mb-4">Sie haben gemäß DSGVO folgende Rechte:</p>
<ul className="space-y-2 text-xl font-light text-stone-600 dark:text-stone-400">
<li className="flex items-start gap-3"><span className="text-liquid-purple mt-2"></span> Art. 15 DSGVO: Auskunftsrecht über Ihre gespeicherten Daten</li>
<li className="flex items-start gap-3"><span className="text-liquid-purple mt-2"></span> Art. 16 DSGVO: Recht auf Berichtigung unrichtiger Daten</li>
<li className="flex items-start gap-3"><span className="text-liquid-purple mt-2"></span> Art. 17 DSGVO: Recht auf Löschung (soweit keine Aufbewahrungspflichten entgegenstehen)</li>
<li className="flex items-start gap-3"><span className="text-liquid-purple mt-2"></span> Art. 18 DSGVO: Recht auf Einschränkung der Verarbeitung</li>
<li className="flex items-start gap-3"><span className="text-liquid-purple mt-2"></span> Art. 20 DSGVO: Recht auf Datenübertragbarkeit</li>
<li className="flex items-start gap-3"><span className="text-liquid-purple mt-2"></span> Art. 21 DSGVO: Widerspruchsrecht gegen die Verarbeitung</li>
</ul>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400 mt-4">
Beschwerden können Sie an die zuständige Datenschutzaufsichtsbehörde richten: <a href="https://www.bfdi.bund.de/" className="text-liquid-mint hover:underline" target="_blank" rel="noopener noreferrer">bfdi.bund.de</a>
</p>
</section>
<section>
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight">Datensicherheit</h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400">
Ich setze technische und organisatorische Maßnahmen ein, um Ihre Daten zu schützen. Dazu gehören unter anderem die SSL-Verschlüsselung. Diese Verschlüsselung erkennen Sie an dem Schloss-Symbol in der Adresszeile Ihres Browsers und an der URL, die mit &ldquo;https://&rdquo; beginnt.
</p>
</section>
<section>
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-8 uppercase tracking-tight">Änderungen</h2>
<p className="text-xl font-light leading-relaxed text-stone-600 dark:text-stone-400">
Diese Datenschutzerklärung wird regelmäßig aktualisiert, um den gesetzlichen Anforderungen zu entsprechen. Die jeweils aktuelle Version finden Sie auf dieser Seite.
</p>
<p className="text-sm text-stone-400 dark:text-stone-500 mt-6">Letzte Aktualisierung: April 2025</p>
</section> </section>
</div> </div>
)} )}

6
bones/registry.ts Normal file
View File

@@ -0,0 +1,6 @@
// Boneyard skeleton registry - auto-generated by `npx boneyard-js build`
// This file is imported once at app initialization to register all bone layouts.
// Run `npx boneyard-js build` to generate bone files from your components.
const registry: Record<string, unknown> = {};
export default registry;

7
boneyard.config.json Normal file
View File

@@ -0,0 +1,7 @@
{
"breakpoints": [375, 768, 1280],
"out": "./bones",
"wait": 800,
"color": "rgba(0,0,0,0.08)",
"animate": "pulse"
}

View File

@@ -16,11 +16,12 @@ const config: Config = {
testPathIgnorePatterns: ["/node_modules/", "/__mocks__/", "/.next/", "/e2e/"], testPathIgnorePatterns: ["/node_modules/", "/__mocks__/", "/.next/", "/e2e/"],
// Transform react-markdown and other ESM modules // Transform react-markdown and other ESM modules
transformIgnorePatterns: [ transformIgnorePatterns: [
"node_modules/(?!(react-markdown|remark-.*|rehype-.*|unified|bail|is-plain-obj|trough|vfile|vfile-message|unist-.*|micromark|parse-entities|character-entities|mdast-.*|hast-.*|property-information|space-separated-tokens|comma-separated-tokens|web-namespaces|zwitch|longest-streak|ccount)/)", "node_modules/(?!(react-markdown|remark-.*|rehype-.*|unified|bail|is-plain-obj|trough|vfile|vfile-message|unist-.*|micromark|parse-entities|character-entities|mdast-.*|hast-.*|property-information|space-separated-tokens|comma-separated-tokens|web-namespaces|zwitch|longest-streak|ccount|boneyard-js)/)",
], ],
// Module name mapping to fix haste collision // Module name mapping to fix haste collision
moduleNameMapper: { moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1", "^@/(.*)$": "<rootDir>/$1",
"^boneyard-js/react$": "<rootDir>/__mocks__/boneyard-js/react.tsx",
}, },
// Exclude problematic directories from haste // Exclude problematic directories from haste
modulePathIgnorePatterns: ["<rootDir>/.next/", "<rootDir>/node_modules/", "<rootDir>/e2e/"], modulePathIgnorePatterns: ["<rootDir>/.next/", "<rootDir>/node_modules/", "<rootDir>/e2e/"],

View File

@@ -73,6 +73,8 @@
"finishedAt": "Beendet am", "finishedAt": "Beendet am",
"showMore": "{count} weitere anzeigen", "showMore": "{count} weitere anzeigen",
"showLess": "Weniger anzeigen", "showLess": "Weniger anzeigen",
"readMore": "Weiterlesen",
"collapseReview": "Weniger anzeigen",
"empty": "In Hardcover fertig gelesene Bücher erscheinen hier automatisch." "empty": "In Hardcover fertig gelesene Bücher erscheinen hier automatisch."
}, },
"activity": { "activity": {

View File

@@ -74,6 +74,8 @@
"finishedAt": "Finished", "finishedAt": "Finished",
"showMore": "{count} more", "showMore": "{count} more",
"showLess": "Show less", "showLess": "Show less",
"readMore": "Read more",
"collapseReview": "Show less",
"empty": "Books finished in Hardcover will appear here automatically." "empty": "Books finished in Hardcover will appear here automatically."
}, },
"activity": { "activity": {

57
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"@tiptap/react": "^3.15.3", "@tiptap/react": "^3.15.3",
"@tiptap/starter-kit": "^3.15.3", "@tiptap/starter-kit": "^3.15.3",
"@vercel/og": "^0.6.5", "@vercel/og": "^0.6.5",
"boneyard-js": "^1.7.6",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"framer-motion": "^12.24.10", "framer-motion": "^12.24.10",
@@ -595,6 +596,13 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@chenglou/pretext": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/@chenglou/pretext/-/pretext-0.0.5.tgz",
"integrity": "sha512-A8GZN10REdFGsyuiUgLV8jjPDDFMg5GmgxGWV0I3igxBOnzj+jgz2VMmVD7g+SFyoctfeqHFxbNatKSzVRWtRg==",
"license": "MIT",
"optional": true
},
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -5437,6 +5445,53 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/boneyard-js": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/boneyard-js/-/boneyard-js-1.7.6.tgz",
"integrity": "sha512-9K3+cD684J131itS2iUI1+dsWqE7K3hSZid0nyXeBxtSTWQTFSpnGM7xmup16QwtmeGR70MPvpzluhA2Nl5LuQ==",
"license": "MIT",
"dependencies": {
"playwright": "^1.58.2"
},
"bin": {
"boneyard-js": "bin/cli.js"
},
"optionalDependencies": {
"@chenglou/pretext": "^0.0.5"
},
"peerDependencies": {
"@angular/core": ">=14",
"preact": ">=10",
"react": ">=18",
"react-native": ">=0.71",
"svelte": ">=5.29",
"vite": ">=5",
"vue": ">=3"
},
"peerDependenciesMeta": {
"@angular/core": {
"optional": true
},
"preact": {
"optional": true
},
"react": {
"optional": true
},
"react-native": {
"optional": true
},
"svelte": {
"optional": true
},
"vite": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/boolbase": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -12015,7 +12070,6 @@
"version": "1.58.2", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"playwright-core": "1.58.2" "playwright-core": "1.58.2"
@@ -12034,7 +12088,6 @@
"version": "1.58.2", "version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"

View File

@@ -64,6 +64,7 @@
"@tiptap/react": "^3.15.3", "@tiptap/react": "^3.15.3",
"@tiptap/starter-kit": "^3.15.3", "@tiptap/starter-kit": "^3.15.3",
"@vercel/og": "^0.6.5", "@vercel/og": "^0.6.5",
"boneyard-js": "^1.7.6",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"framer-motion": "^12.24.10", "framer-motion": "^12.24.10",