Compare commits
3 Commits
8397e5acf2
...
4d5dc1f8f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d5dc1f8f9 | ||
|
|
32abc7f3ef | ||
|
|
87e337a3a0 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
6
__mocks__/boneyard-js/react.tsx
Normal file
6
__mocks__/boneyard-js/react.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 })),
|
||||||
|
|||||||
@@ -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,30 +55,13 @@ 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 (
|
||||||
|
<Skeleton name="currently-reading" loading={loading} animate="shimmer" transition>
|
||||||
<div className="space-y-4">
|
<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">
|
||||||
@@ -172,6 +155,7 @@ const CurrentlyReading = () => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</Skeleton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,6 +96,7 @@ const ReadBooks = () => {
|
|||||||
const hasMore = reviews.length > INITIAL_SHOW;
|
const hasMore = reviews.length > INITIAL_SHOW;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Skeleton name="read-books" loading={loading} animate="shimmer" transition>
|
||||||
<div className="space-y-4">
|
<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">
|
||||||
@@ -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">
|
||||||
|
<p className={`text-sm text-stone-700 dark:text-stone-300 leading-relaxed italic ${!expandedReviews.has(review.id) ? 'line-clamp-3' : ''}`}>
|
||||||
“{stripHtml(review.review)}”
|
“{stripHtml(review.review)}”
|
||||||
</p>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 “https://” 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
6
bones/registry.ts
Normal 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
7
boneyard.config.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"breakpoints": [375, 768, 1280],
|
||||||
|
"out": "./bones",
|
||||||
|
"wait": 800,
|
||||||
|
"color": "rgba(0,0,0,0.08)",
|
||||||
|
"animate": "pulse"
|
||||||
|
}
|
||||||
@@ -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/"],
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
57
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user