From 87e337a3a053e8a16b09e1d4e13c6a0d7518d741 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 15 Apr 2026 14:26:08 +0200 Subject: [PATCH 1/2] 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 --- .gitignore | 3 + app/components/ClientProviders.tsx | 1 + app/components/CurrentlyReading.tsx | 30 +++------- app/components/Header.tsx | 2 +- app/components/HeaderClient.tsx | 5 +- app/components/Projects.tsx | 16 ++---- app/components/ReadBooks.tsx | 52 ++++++++--------- app/privacy-policy/page.tsx | 86 +++++++++++++++++++++++++++-- bones/registry.ts | 6 ++ boneyard.config.json | 7 +++ messages/de.json | 2 + messages/en.json | 2 + package-lock.json | 57 ++++++++++++++++++- package.json | 1 + 14 files changed, 201 insertions(+), 69 deletions(-) create mode 100644 bones/registry.ts create mode 100644 boneyard.config.json diff --git a/.gitignore b/.gitignore index 9626976..94f151a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,9 @@ coverage/ .idea/ .vscode/ +# boneyard generated bones +bones/*.bones.json + # OS .DS_Store Thumbs.db diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index 7aaa57a..545e874 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -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 })), diff --git a/app/components/CurrentlyReading.tsx b/app/components/CurrentlyReading.tsx index 704416c..5ca34ea 100644 --- a/app/components/CurrentlyReading.tsx +++ b/app/components/CurrentlyReading.tsx @@ -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 ( -
-
- -
- - -
- - -
-
-
-
- ); - } - - // 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 ( -
+ +
{/* Header */}
@@ -170,8 +153,9 @@ const CurrentlyReading = () => {
- ))} + ))}
+ ); }; diff --git a/app/components/Header.tsx b/app/components/Header.tsx index 607239c..20df2c4 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -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" > - dk + dk0 {/* Desktop Menu */} diff --git a/app/components/HeaderClient.tsx b/app/components/HeaderClient.tsx index e253c38..6dcf8f2 100644 --- a/app/components/HeaderClient.tsx +++ b/app/components/HeaderClient.tsx @@ -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 + @@ -188,7 +190,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps {/* Language Switcher Mobile */} -
+
setIsOpen(false)} @@ -211,6 +213,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps > DE +
diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index 80cecc5..d6de5bc 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -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 = () => {
+
- {loading ? ( - Array.from({ length: 2 }).map((_, i) => ( -
- -
- - -
-
- )) - ) : projects.length === 0 ? ( + {projects.length === 0 && !loading ? (
No projects yet.
@@ -125,6 +116,7 @@ const Projects = () => { )))}
+
); diff --git a/app/components/ReadBooks.tsx b/app/components/ReadBooks.tsx index 171c222..9bdeaf4 100644 --- a/app/components/ReadBooks.tsx +++ b/app/components/ReadBooks.tsx @@ -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([]); const [loading, setLoading] = useState(true); const [expanded, setExpanded] = useState(false); + const [expandedReviews, setExpandedReviews] = useState>(new Set()); const INITIAL_SHOW = 3; @@ -82,25 +83,7 @@ const ReadBooks = () => { fetchReviews(); }, [locale]); - if (loading) { - return ( -
- {[1, 2].map((i) => ( -
- -
- - - - -
-
- ))} -
- ); - } - - if (reviews.length === 0) { + if (reviews.length === 0 && !loading) { return (
@@ -113,7 +96,8 @@ const ReadBooks = () => { const hasMore = reviews.length > INITIAL_SHOW; return ( -
+ +
{/* Header */}
@@ -198,9 +182,27 @@ const ReadBooks = () => { {/* Review Text (Optional) */} {review.review && ( -

- “{stripHtml(review.review)}” -

+
+

+ “{stripHtml(review.review)}” +

+ {stripHtml(review.review).length > 100 && ( + + )} +
)} {/* Finished Date */} @@ -240,8 +242,8 @@ const ReadBooks = () => { )} -
+ ); }; diff --git a/app/privacy-policy/page.tsx b/app/privacy-policy/page.tsx index 93bbd5f..1f12ab3 100644 --- a/app/privacy-policy/page.tsx +++ b/app/privacy-policy/page.tsx @@ -61,11 +61,15 @@ export default function PrivacyPolicy() {

- Allgemeiner Überblick + Verantwortlicher

-

- 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). -

+
+

Dennis Konkol

+

Auf dem Ziegenbrink 2B

+

49082 Osnabrück, Deutschland

+

E-Mail: contact@dk0.dev

+

Diese Datenschutzerklärung gilt für die Verarbeitung personenbezogener Daten durch den oben genannten Verantwortlichen.

+
@@ -73,8 +77,80 @@ export default function PrivacyPolicy() { Datenerfassung

- 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:

+
    +
  • IP-Adresse (in anonymisierter Form)
  • +
  • Uhrzeit und Datum des Zugriffs
  • +
  • Browsertyp und Betriebssystem
  • +
  • Referrer-URL (die zuvor besuchte Seite)
  • +
+

+ Diese Informationen werden anonymisiert erfasst und dienen ausschließlich statistischen Auswertungen. Rückschlüsse auf Ihre Person sind nicht möglich. +

+
+ +
+

Analyse- und Tracking-Tools

+

+ Zur Analyse der Nutzung dieser Website setze ich Umami 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). +

+
+ +
+

Kontaktformular

+

+ 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). +

+
+ +
+

Social Media Links

+

+ Diese Website enthält Links zu GitHub und LinkedIn. Durch das Anklicken dieser Links gelten die Datenschutzbestimmungen der jeweiligen Anbieter. +

+
+ +
+

Weitergabe von Daten

+

Eine Weitergabe Ihrer personenbezogenen Daten erfolgt nur, wenn:

+
    +
  • Sie nach Art. 6 Abs. 1 S. 1 lit. a DSGVO ausdrücklich eingewilligt haben,
  • +
  • dies zur Vertragserfüllung gemäß Art. 6 Abs. 1 S. 1 lit. b DSGVO erforderlich ist,
  • +
  • eine gesetzliche Verpflichtung nach Art. 6 Abs. 1 S. 1 lit. c DSGVO besteht, oder
  • +
  • die Verarbeitung nach Art. 6 Abs. 1 S. 1 lit. f DSGVO zur Wahrung berechtigter Interessen erforderlich ist.
  • +
+
+ +
+

Ihre Rechte

+

Sie haben gemäß DSGVO folgende Rechte:

+
    +
  • Art. 15 DSGVO: Auskunftsrecht über Ihre gespeicherten Daten
  • +
  • Art. 16 DSGVO: Recht auf Berichtigung unrichtiger Daten
  • +
  • Art. 17 DSGVO: Recht auf Löschung (soweit keine Aufbewahrungspflichten entgegenstehen)
  • +
  • Art. 18 DSGVO: Recht auf Einschränkung der Verarbeitung
  • +
  • Art. 20 DSGVO: Recht auf Datenübertragbarkeit
  • +
  • Art. 21 DSGVO: Widerspruchsrecht gegen die Verarbeitung
  • +
+

+ Beschwerden können Sie an die zuständige Datenschutzaufsichtsbehörde richten: bfdi.bund.de +

+
+ +
+

Datensicherheit

+

+ 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 “https://” beginnt. +

+
+ +
+

Änderungen

+

+ Diese Datenschutzerklärung wird regelmäßig aktualisiert, um den gesetzlichen Anforderungen zu entsprechen. Die jeweils aktuelle Version finden Sie auf dieser Seite. +

+

Letzte Aktualisierung: April 2025

)} diff --git a/bones/registry.ts b/bones/registry.ts new file mode 100644 index 0000000..40ad292 --- /dev/null +++ b/bones/registry.ts @@ -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 = {}; + +export default registry; \ No newline at end of file diff --git a/boneyard.config.json b/boneyard.config.json new file mode 100644 index 0000000..829d4f1 --- /dev/null +++ b/boneyard.config.json @@ -0,0 +1,7 @@ +{ + "breakpoints": [375, 768, 1280], + "out": "./bones", + "wait": 800, + "color": "rgba(0,0,0,0.08)", + "animate": "pulse" +} \ No newline at end of file diff --git a/messages/de.json b/messages/de.json index 7cbe25c..804867c 100644 --- a/messages/de.json +++ b/messages/de.json @@ -73,6 +73,8 @@ "finishedAt": "Beendet am", "showMore": "{count} weitere anzeigen", "showLess": "Weniger anzeigen", + "readMore": "Weiterlesen", + "collapseReview": "Weniger anzeigen", "empty": "In Hardcover fertig gelesene Bücher erscheinen hier automatisch." }, "activity": { diff --git a/messages/en.json b/messages/en.json index a199634..189c247 100644 --- a/messages/en.json +++ b/messages/en.json @@ -74,6 +74,8 @@ "finishedAt": "Finished", "showMore": "{count} more", "showLess": "Show less", + "readMore": "Read more", + "collapseReview": "Show less", "empty": "Books finished in Hardcover will appear here automatically." }, "activity": { diff --git a/package-lock.json b/package-lock.json index e44a13c..e34722a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@tiptap/react": "^3.15.3", "@tiptap/starter-kit": "^3.15.3", "@vercel/og": "^0.6.5", + "boneyard-js": "^1.7.6", "clsx": "^2.1.1", "dotenv": "^16.6.1", "framer-motion": "^12.24.10", @@ -595,6 +596,13 @@ "dev": true, "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": { "version": "0.8.1", "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" } }, + "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -12015,7 +12070,6 @@ "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", - "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.58.2" @@ -12034,7 +12088,6 @@ "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" diff --git a/package.json b/package.json index 5d073c0..b4c527d 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@tiptap/react": "^3.15.3", "@tiptap/starter-kit": "^3.15.3", "@vercel/og": "^0.6.5", + "boneyard-js": "^1.7.6", "clsx": "^2.1.1", "dotenv": "^16.6.1", "framer-motion": "^12.24.10", From 32abc7f3ef908f4d104e21bdb88bf4534b2544f8 Mon Sep 17 00:00:00 2001 From: denshooter Date: Wed, 15 Apr 2026 14:37:50 +0200 Subject: [PATCH 2/2] fix: update tests for dk0 logo and boneyard-js mock, add jest moduleNameMapper --- __mocks__/boneyard-js/react.tsx | 6 ++++++ app/__tests__/components/CurrentlyReading.test.tsx | 7 ++----- app/__tests__/components/Header.test.tsx | 2 +- jest.config.ts | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 __mocks__/boneyard-js/react.tsx diff --git a/__mocks__/boneyard-js/react.tsx b/__mocks__/boneyard-js/react.tsx new file mode 100644 index 0000000..e13fd62 --- /dev/null +++ b/__mocks__/boneyard-js/react.tsx @@ -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
Loading...
; + return <>{children}; +} \ No newline at end of file diff --git a/app/__tests__/components/CurrentlyReading.test.tsx b/app/__tests__/components/CurrentlyReading.test.tsx index aac06bb..9f2ec76 100644 --- a/app/__tests__/components/CurrentlyReading.test.tsx +++ b/app/__tests__/components/CurrentlyReading.test.tsx @@ -2,16 +2,13 @@ import { render, screen, waitFor } from "@testing-library/react"; import CurrentlyReadingComp from "@/app/components/CurrentlyReading"; import React from "react"; -// Mock next-intl completely to avoid ESM issues jest.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en", })); -// Mock next/image jest.mock("next/image", () => ({ __esModule: true, - // eslint-disable-next-line @next/next/no-img-element default: (props: React.ImgHTMLAttributes) => {props.alt, })); @@ -22,8 +19,8 @@ describe("CurrentlyReading Component", () => { it("renders skeleton when loading", () => { (global.fetch as jest.Mock).mockReturnValue(new Promise(() => {})); - const { container } = render(); - expect(container.querySelector(".animate-pulse")).toBeInTheDocument(); + render(); + expect(screen.getByTestId("boneyard-skeleton")).toBeInTheDocument(); }); it("renders a book when data is fetched", async () => { diff --git a/app/__tests__/components/Header.test.tsx b/app/__tests__/components/Header.test.tsx index 1855e36..a66c4e0 100644 --- a/app/__tests__/components/Header.test.tsx +++ b/app/__tests__/components/Header.test.tsx @@ -23,7 +23,7 @@ jest.mock('next/navigation', () => ({ describe('Header', () => { it('renders the header with the dk logo', () => { render(
); - expect(screen.getByText('dk')).toBeInTheDocument(); + expect(screen.getByText('dk0')).toBeInTheDocument(); // Check for navigation links (appear in both desktop and mobile menus) expect(screen.getAllByText('Home').length).toBeGreaterThan(0); diff --git a/jest.config.ts b/jest.config.ts index 60d8666..f5e25e4 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -16,11 +16,12 @@ const config: Config = { testPathIgnorePatterns: ["/node_modules/", "/__mocks__/", "/.next/", "/e2e/"], // Transform react-markdown and other ESM modules 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 moduleNameMapper: { "^@/(.*)$": "/$1", + "^boneyard-js/react$": "/__mocks__/boneyard-js/react.tsx", }, // Exclude problematic directories from haste modulePathIgnorePatterns: ["/.next/", "/node_modules/", "/e2e/"],