-
- {/* Header */}
-
-
-
-
{tCommon("backToHome")}
+ <>
+
+ {tCommon("backToHome")}
-
-
- {tList("title")}.
-
-
- {tList("intro")}
-
-
+ }
+ kicker={`${String(projects.length).padStart(2, "0")} ${locale === "de" ? "Projekte" : "projects"}`}
+ title={tList("title")}
+ >
+
{tList("intro")}
+
- {/* Filters */}
-
-
- {categories.map((cat) => (
- setSelectedCategory(cat)}
- className={`px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-widest transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.97] ${
- selectedCategory === cat
- ? "bg-stone-900 dark:bg-stone-100 text-white dark:text-stone-900"
- : "bg-white dark:bg-stone-900 text-stone-500 border border-stone-200 dark:border-stone-800"
- }`}
- >
- {cat === 'all' ? tList('all') : cat}
-
- ))}
-
-
-
- setSearchQuery(e.target.value)}
- className="w-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 rounded-2xl py-4 pl-12 pr-6 focus:outline-none focus:ring-2 focus:ring-liquid-mint/30 transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-sm"
- />
-
-
-
- {/* Grid */}
-
- {loading ? (
- Array.from({ length: 4 }).map((_, i) => (
-
- ))
- ) : (
- filteredProjects.map((project) => (
-
-
-
- {project.imageUrl ? (
-
-
-
- ) : (
-
- )}
-
-
-
{project.description}
-
- {project.tags.slice(0, 3).map(tag => (
- {tag}
- ))}
-
-
-
-
-
- )))}
+ {/* Filter: Kategorien als Pillen, Suche als Tinten-Feld */}
+
+
+ {categories.map((cat) => (
+ setSelectedCategory(cat)}
+ aria-pressed={selectedCategory === cat}
+ className={filterPill(selectedCategory === cat)}
+ >
+ {cat === "all" ? tList("all") : cat}
+
+ ))}
+
+ {tList("searchPlaceholder")}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ className="w-full rounded-full border-2 border-ink bg-paper py-3 pl-11 pr-5 text-base text-ink placeholder:text-ink/60 transition-shadow duration-150 ease-out focus:shadow-[4px_4px_0_rgb(var(--plakat-ink))] focus:outline-none"
+ />
+
-
+
+ {filteredProjects.length === 0 ? (
+
+
+ {tList("noResults")}
+
+
+ {tList("clearFilters")}
+
+
+ ) : (
+
+ {filteredProjects.map((project, i) => {
+ const year = yearOf(project);
+ return (
+
+
+
+
+ {project.imageUrl ? (
+
+ ) : (
+
+ )}
+
+
+
{[project.category, year].filter(Boolean).join(" · ")}
+
+
+ {project.title}
+
+
+ ↗
+
+
+
{project.description}
+ {project.tags.length > 0 && (
+
+ {project.tags.slice(0, 4).map((tag) => (
+
+ {tag}
+
+ ))}
+
+ )}
+
+
+
+
+ );
+ })}
+
+ )}
+ >
);
}
diff --git a/app/_ui/SubpageShell.tsx b/app/_ui/SubpageShell.tsx
new file mode 100644
index 0000000..47ea09d
--- /dev/null
+++ b/app/_ui/SubpageShell.tsx
@@ -0,0 +1,27 @@
+import type { ReactNode } from "react";
+import Header from "../components/Header.server";
+import { FooterClient } from "../components/ClientWrappers";
+import { getFooterTranslations } from "@/lib/translations-loader";
+
+interface SubpageShellProps {
+ locale: string;
+ children: ReactNode;
+}
+
+/**
+ * Rahmen für alle Unterseiten: derselbe Kopf wie auf der Startseite
+ * (dort als Papierstreifen), Papier als Grund, Tinten-Footer.
+ */
+export default async function SubpageShell({ locale, children }: SubpageShellProps) {
+ const footerT = await getFooterTranslations(locale);
+
+ return (
+
+ );
+}
diff --git a/app/_ui/subpage.tsx b/app/_ui/subpage.tsx
new file mode 100644
index 0000000..32aaa59
--- /dev/null
+++ b/app/_ui/subpage.tsx
@@ -0,0 +1,48 @@
+import type { ReactNode } from "react";
+
+// Gemeinsame Bausteine der Unterseiten im Plakat-Stil. Bewusst ohne "use client",
+// damit Server- und Client-Seiten sie gleichermaßen nutzen können.
+
+export const btn =
+ "inline-flex items-center gap-2 rounded-full bg-ink px-5 py-3 text-[15px] font-semibold text-paper transition-transform duration-150 ease-out active:scale-[0.97]";
+
+export const ghost =
+ "inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-medium shadow-[inset_0_0_0_1.5px_rgb(var(--plakat-ink))] transition-[background-color,color,transform] duration-150 ease-out hover:bg-ink hover:text-paper active:scale-[0.97]";
+
+/** Rahmen mit hartem Versatzschatten, ohne Hintergrund (für farbige Karten) */
+export const frame = "rounded-[18px] border-2 border-ink shadow-[6px_6px_0_rgb(var(--plakat-ink))]";
+
+export const card = `${frame} bg-paper`;
+
+export const monoLabel = "font-mono text-[13px] leading-snug";
+
+interface SubpageIntroProps {
+ /** Zurück-Link oben links */
+ back: ReactNode;
+ /** Kleine Zeile oben rechts, z. B. Kategorie oder Rechtsgrundlage */
+ kicker?: ReactNode;
+ title: ReactNode;
+ children?: ReactNode;
+}
+
+/** Kopf einer Unterseite: Zurück-Pille, große Plakat-Zeile, Einleitung. */
+export function SubpageIntro({ back, kicker, title, children }: SubpageIntroProps) {
+ return (
+
+
+ {back}
+ {kicker &&
{kicker}
}
+
+
+ {title}
+ .
+
+ {children && {children}
}
+
+ );
+}
+
+/** Pfeil nach links für Zurück-Links, rein dekorativ */
+export function BackArrow() {
+ return
← ;
+}
diff --git a/app/_ui/useCmsPage.ts b/app/_ui/useCmsPage.ts
new file mode 100644
index 0000000..40ce7c8
--- /dev/null
+++ b/app/_ui/useCmsPage.ts
@@ -0,0 +1,29 @@
+"use client";
+
+import { useEffect, useState } from "react";
+
+/**
+ * Lädt eine Inhaltsseite (Impressum, Datenschutz) aus dem CMS nach.
+ * Bis dahin und ohne JavaScript bleibt der eingebaute Text stehen.
+ */
+export function useCmsPage(key: string, locale: string): string | null {
+ const [html, setHtml] = useState
(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const res = await fetch(`/api/content/page?key=${encodeURIComponent(key)}&locale=${encodeURIComponent(locale)}`);
+ const data = await res.json();
+ if (!cancelled && data?.content?.html && data?.content?.locale === locale) {
+ setHtml(data.content.html as string);
+ }
+ } catch {}
+ })();
+ return () => {
+ cancelled = true;
+ };
+ }, [key, locale]);
+
+ return html;
+}
diff --git a/app/components/Header.tsx b/app/components/Header.tsx
deleted file mode 100644
index 0eb5d9d..0000000
--- a/app/components/Header.tsx
+++ /dev/null
@@ -1,98 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import { Menu, X } from "lucide-react";
-import Link from "next/link";
-import { useLocale, useTranslations } from "next-intl";
-import { usePathname } from "next/navigation";
-import { ThemeToggle } from "./ThemeToggle";
-
-const Header = () => {
- const [isOpen, setIsOpen] = useState(false);
- const locale = useLocale();
- const pathname = usePathname();
- const t = useTranslations("nav");
-
- const isHome = pathname === `/${locale}` || pathname === `/${locale}/`;
-
- const navItems = [
- { name: t("home"), href: `/${locale}` },
- { name: t("about"), href: isHome ? "#projects" : `/${locale}#projects` },
- { name: t("projects"), href: isHome ? "#projects" : `/${locale}/projects` },
- { name: t("contact"), href: isHome ? "#contact" : `/${locale}#contact` },
- ];
-
- return (
- <>
-
-
- {/* Logo Pill */}
-
- dk0
-
-
- {/* Desktop Menu */}
-
- {navItems.map((item) => (
-
- {item.name}
-
- ))}
-
-
-
-
- {/* Actions Pill */}
-
-
- {locale === "en" ? "DE" : "EN"}
-
-
-
- {/* Mobile Menu Toggle */}
- setIsOpen(!isOpen)}
- className="w-8 h-8 flex md:hidden items-center justify-center text-stone-600 dark:text-stone-400 hover:bg-white dark:hover:bg-stone-800 rounded-full transition-[background-color,transform] duration-150 ease-out active:scale-[0.97] shadow-sm"
- >
- {isOpen ? : }
-
-
-
-
-
- {/* Mobile Menu Overlay */}
-
-
- {navItems.map((item) => (
- setIsOpen(false)}
- className="px-6 py-4 text-sm font-black uppercase tracking-[0.2em] text-stone-900 dark:text-stone-100 bg-stone-50 dark:bg-white/5 rounded-2xl transition-colors hover:bg-liquid-mint/10"
- >
- {item.name}
-
- ))}
-
-
- >
- );
-};
-
-export default Header;
diff --git a/app/components/ProjectThumbnail.tsx b/app/components/ProjectThumbnail.tsx
index 44eec1f..0ee674d 100644
--- a/app/components/ProjectThumbnail.tsx
+++ b/app/components/ProjectThumbnail.tsx
@@ -24,73 +24,30 @@ interface ProjectThumbnailProps {
size?: "card" | "hero";
}
+// Plakatflächen statt Pastellverläufe: jede Kategorie bekommt eine feste Ton-Kombination
+// aus den Plakat-Tokens. Tinte/Papier tauschen im Dark Mode von selbst die Rollen.
+const tones = {
+ signal: "bg-signal text-ink",
+ ink: "bg-ink text-paper",
+ paper: "bg-paper-2 text-ink",
+ ok: "bg-ok text-ink",
+} as const;
+
const categoryThemes: Record<
string,
{
icon: LucideIcon;
- gradient: string;
- darkGradient: string;
- iconColor: string;
- darkIconColor: string;
+ tone: keyof typeof tones;
pattern: "dots" | "grid" | "diagonal" | "circuit" | "waves" | "terminal";
}
> = {
- "Web Development": {
- icon: Code,
- gradient: "from-liquid-sky/20 via-liquid-blue/10 to-liquid-lavender/20",
- darkGradient: "dark:from-liquid-sky/10 dark:via-liquid-blue/5 dark:to-liquid-lavender/10",
- iconColor: "text-blue-500",
- darkIconColor: "dark:text-blue-400",
- pattern: "circuit",
- },
- "Mobile Development": {
- icon: Smartphone,
- gradient: "from-liquid-mint/20 via-liquid-teal/10 to-liquid-sky/20",
- darkGradient: "dark:from-liquid-mint/10 dark:via-liquid-teal/5 dark:to-liquid-sky/10",
- iconColor: "text-emerald-500",
- darkIconColor: "dark:text-emerald-400",
- pattern: "waves",
- },
- "Web Application": {
- icon: Globe,
- gradient: "from-liquid-lavender/20 via-liquid-purple/10 to-liquid-pink/20",
- darkGradient: "dark:from-liquid-lavender/10 dark:via-liquid-purple/5 dark:to-liquid-pink/10",
- iconColor: "text-violet-500",
- darkIconColor: "dark:text-violet-400",
- pattern: "dots",
- },
- "Backend Development": {
- icon: Cpu,
- gradient: "from-liquid-amber/20 via-liquid-yellow/10 to-liquid-peach/20",
- darkGradient: "dark:from-liquid-amber/10 dark:via-liquid-yellow/5 dark:to-liquid-peach/10",
- iconColor: "text-amber-500",
- darkIconColor: "dark:text-amber-400",
- pattern: "grid",
- },
- "Full-Stack Development": {
- icon: Boxes,
- gradient: "from-liquid-teal/20 via-liquid-mint/10 to-liquid-lavender/20",
- darkGradient: "dark:from-liquid-teal/10 dark:via-liquid-mint/5 dark:to-liquid-lavender/10",
- iconColor: "text-teal-500",
- darkIconColor: "dark:text-teal-400",
- pattern: "grid",
- },
- DevOps: {
- icon: Shield,
- gradient: "from-liquid-coral/20 via-liquid-rose/10 to-liquid-peach/20",
- darkGradient: "dark:from-liquid-coral/10 dark:via-liquid-rose/5 dark:to-liquid-peach/10",
- iconColor: "text-red-500",
- darkIconColor: "dark:text-red-400",
- pattern: "diagonal",
- },
- default: {
- icon: Wrench,
- gradient: "from-liquid-peach/20 via-liquid-rose/10 to-liquid-lavender/20",
- darkGradient: "dark:from-liquid-peach/10 dark:via-liquid-rose/5 dark:to-liquid-lavender/10",
- iconColor: "text-stone-400",
- darkIconColor: "dark:text-stone-500",
- pattern: "dots",
- },
+ "Web Development": { icon: Code, tone: "signal", pattern: "circuit" },
+ "Mobile Development": { icon: Smartphone, tone: "ink", pattern: "waves" },
+ "Web Application": { icon: Globe, tone: "paper", pattern: "dots" },
+ "Backend Development": { icon: Cpu, tone: "ink", pattern: "grid" },
+ "Full-Stack Development": { icon: Boxes, tone: "signal", pattern: "grid" },
+ DevOps: { icon: Shield, tone: "ok", pattern: "diagonal" },
+ default: { icon: Wrench, tone: "paper", pattern: "dots" },
};
const slugIcons: Record = {
@@ -106,7 +63,7 @@ const slugIcons: Record = {
function PatternOverlay({ pattern, id }: { pattern: string; id: string }) {
const patterns: Record = {
dots: (
-
+
@@ -116,7 +73,7 @@ function PatternOverlay({ pattern, id }: { pattern: string; id: string }) {
),
grid: (
-
+
@@ -126,7 +83,7 @@ function PatternOverlay({ pattern, id }: { pattern: string; id: string }) {
),
diagonal: (
-
+
@@ -136,7 +93,7 @@ function PatternOverlay({ pattern, id }: { pattern: string; id: string }) {
),
circuit: (
-
+
@@ -151,7 +108,7 @@ function PatternOverlay({ pattern, id }: { pattern: string; id: string }) {
),
waves: (
-
+
@@ -161,7 +118,7 @@ function PatternOverlay({ pattern, id }: { pattern: string; id: string }) {
),
terminal: (
-
+
$_
@@ -197,40 +154,28 @@ export default function ProjectThumbnail({
const Icon = theme.icon;
const isHero = size === "hero";
- const displayTags = tags?.slice(0, 3) ?? [];
+ const displayTags = tags?.slice(0, isHero ? 5 : 3) ?? [];
return (
-
+
-
-
-
-
-
+
+
{displayTags.join(" · ")}
- {title}
+
-
- {displayTags.length > 0 && (
-
- {displayTags.map((tag) => (
-
- {tag}
-
- ))}
-
- )}
+
+
+ {title}
+
);
-}
\ No newline at end of file
+}
diff --git a/app/globals.css b/app/globals.css
index f10c47e..955400e 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -106,16 +106,7 @@ html {
}
}
-/* Liquid Glass Effects */
-.glass-panel {
- background: var(--card);
- backdrop-filter: blur(20px) saturate(130%);
- -webkit-backdrop-filter: blur(20px) saturate(130%);
- border: 1px solid var(--border);
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
- will-change: backdrop-filter;
-}
-
+/* Glass-Karten, nur noch im Editor */
.glass-card {
background: var(--card);
backdrop-filter: blur(30px) saturate(200%);
@@ -177,31 +168,6 @@ h6 {
scrollbar-width: none;
}
-/* Animations */
-@keyframes float {
- 0%,
- 100% {
- transform: translateY(0px);
- }
- 50% {
- transform: translateY(-10px);
- }
-}
-
-.animate-float {
- animation: float 8s ease-in-out infinite;
- will-change: transform;
-}
-
-/* Liquid Blobs Background */
-.liquid-bg-blob {
- position: absolute;
- filter: blur(80px);
- opacity: 0.6;
- z-index: -1;
- animation: float 10s ease-in-out infinite;
-}
-
/* Markdown Specifics for Blog/Projects */
.markdown h1 {
@apply text-4xl font-bold mb-6 tracking-tight;
@@ -246,31 +212,130 @@ h6 {
z-index: -1;
}
-.admin-glass {
- background: rgba(250, 248, 243, 0.95);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border-bottom: 1px solid #d7ccc8;
- color: #3e2723;
-}
-
-.admin-glass-light {
- background: #fffcf5;
- border: 1px solid #d7ccc8;
- color: #3e2723;
- transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
- box-shadow: 0 1px 2px rgba(62, 39, 35, 0.05);
-}
-
-.admin-glass-light:hover {
- background: #faf8f3;
- border-color: #bcaaa4;
- box-shadow: 0 4px 6px rgba(62, 39, 35, 0.08);
-}
-
.admin-glass-card {
background: #fffcf5;
border: 1px solid #d7ccc8;
box-shadow: 0 4px 6px -1px rgba(62, 39, 35, 0.06);
color: #3e2723;
}
+
+/* Fließtext der Unterseiten (Markdown, CMS-HTML) im Plakat-Stil.
+ Ersetzt das nicht installierte Typography-Plugin. */
+.plakat-prose {
+ font-size: 17px;
+ line-height: 1.7;
+ overflow-wrap: anywhere;
+}
+.plakat-prose > * + * {
+ margin-top: 1em;
+}
+.plakat-prose h1,
+.plakat-prose h2,
+.plakat-prose h3,
+.plakat-prose h4 {
+ font-family: var(--font-bricolage), "Avenir Next", "Segoe UI", sans-serif;
+ font-weight: 800;
+ letter-spacing: -0.035em;
+ line-height: 1;
+ margin-top: 1.8em;
+}
+.plakat-prose > :first-child {
+ margin-top: 0;
+}
+/* Markdown aus dem CMS beginnt oft mit dem Projekttitel als h1; die Seite hat schon einen */
+.plakat-prose > h1:first-child {
+ display: none;
+}
+.plakat-prose > h1:first-child + * {
+ margin-top: 0;
+}
+.plakat-prose h1 {
+ font-size: clamp(2rem, 4vw, 2.75rem);
+}
+.plakat-prose h2 {
+ font-size: clamp(1.7rem, 3.2vw, 2.25rem);
+}
+.plakat-prose h3 {
+ font-size: 1.4rem;
+}
+.plakat-prose h4 {
+ font-size: 1.15rem;
+}
+.plakat-prose strong {
+ font-weight: 700;
+}
+.plakat-prose a {
+ text-decoration: underline;
+ text-decoration-thickness: 2px;
+ text-underline-offset: 3px;
+ text-decoration-color: rgb(var(--plakat-red));
+ transition: color 150ms var(--ease-out);
+}
+@media (hover: hover) and (pointer: fine) {
+ .plakat-prose a:hover {
+ color: rgb(var(--plakat-red));
+ }
+}
+.plakat-prose ul,
+.plakat-prose ol {
+ padding-left: 1.25em;
+}
+.plakat-prose ul {
+ list-style: square;
+}
+.plakat-prose ol {
+ list-style: decimal;
+}
+.plakat-prose li + li {
+ margin-top: 0.35em;
+}
+.plakat-prose li::marker {
+ color: rgb(var(--plakat-red));
+}
+.plakat-prose blockquote {
+ border-left: 4px solid rgb(var(--plakat-red));
+ padding-left: 1em;
+}
+.plakat-prose code {
+ font-family: var(--font-geist-mono), "SF Mono", Menlo, monospace;
+ font-size: 0.9em;
+ background: rgb(var(--plakat-paper-2));
+ border-radius: 4px;
+ padding: 0.1em 0.35em;
+}
+.plakat-prose pre {
+ background: rgb(var(--plakat-ink));
+ color: rgb(var(--plakat-paper));
+ border-radius: 12px;
+ padding: 1rem 1.25rem;
+ overflow-x: auto;
+ overflow-wrap: normal;
+}
+.plakat-prose pre code {
+ background: transparent;
+ padding: 0;
+ color: inherit;
+}
+.plakat-prose img {
+ max-width: 100%;
+ height: auto;
+ border: 2px solid rgb(var(--plakat-ink));
+ border-radius: 12px;
+}
+.plakat-prose hr {
+ border: 0;
+ border-top: 2px solid rgb(var(--plakat-ink));
+}
+.plakat-prose table {
+ display: block;
+ max-width: 100%;
+ overflow-x: auto;
+ border-collapse: collapse;
+}
+.plakat-prose th,
+.plakat-prose td {
+ border-bottom: 1px solid rgb(var(--plakat-ink) / 0.2);
+ padding: 0.5em 0.75em;
+ text-align: left;
+}
+
diff --git a/app/legal-notice/page.tsx b/app/legal-notice/page.tsx
deleted file mode 100644
index 59f210c..0000000
--- a/app/legal-notice/page.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-"use client";
-
-import React from "react";
-import { ArrowLeft, Mail, MapPin, Scale, ShieldCheck, Clock } from 'lucide-react';
-import Header from "../components/Header";
-import Footer from "../components/Footer";
-import Link from "next/link";
-import { useLocale, useTranslations } from "next-intl";
-import { useEffect, useState } from "react";
-import RichTextClient from "../components/RichTextClient";
-
-export default function LegalNotice() {
- const locale = useLocale();
- const t = useTranslations("common");
- const [cmsHtml, setCmsHtml] = useState
(null);
-
- useEffect(() => {
- (async () => {
- try {
- const res = await fetch(
- `/api/content/page?key=${encodeURIComponent("legal-notice")}&locale=${encodeURIComponent(locale)}`,
- );
- const data = await res.json();
- if (data?.content?.html && data?.content?.locale === locale) {
- setCmsHtml(data.content.html as string);
- }
- } catch {}
- })();
- }, [locale]);
-
- return (
- <>
-
-
-
-
- {/* Editorial Header */}
-
-
-
-
{t("backToHome")}
-
-
-
- Legal.
-
-
-
- {/* Bento Content Grid */}
-
-
- {/* Main Legal Content (Large Box) */}
-
- {cmsHtml ? (
-
-
-
- ) : (
-
-
-
- Angaben gemäß § 5 TMG
-
-
-
Dennis Konkol
-
Auf dem Ziegenbrink 2B
-
49082 Osnabrück, Deutschland
-
-
-
-
-
- Haftungsausschluss
-
-
- Die Inhalte dieser Seiten wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann ich jedoch keine Gewähr übernehmen.
-
-
-
- )}
-
-
- {/* Sidebar Widgets */}
-
-
- {/* Quick Contact Box */}
-
-
Direct Contact
-
-
-
-
-
-
-
-
Location
-
Osnabrück, GER
-
-
-
-
-
- {/* Meta Info Box */}
-
-
-
-
-
Last Review
-
May 14, 2025
-
-
-
- This legal notice applies to all contents on dk0.dev and related social media profiles.
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/app/privacy-policy/page.tsx b/app/privacy-policy/page.tsx
deleted file mode 100644
index ddcf492..0000000
--- a/app/privacy-policy/page.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-"use client";
-
-import React from "react";
-import { ArrowLeft, Shield, Lock, Eye, Database, Globe } from 'lucide-react';
-import Header from "../components/Header";
-import Footer from "../components/Footer";
-import Link from "next/link";
-import { useLocale, useTranslations } from "next-intl";
-import { useEffect, useState } from "react";
-import RichTextClient from "../components/RichTextClient";
-
-export default function PrivacyPolicy() {
- const locale = useLocale();
- const t = useTranslations("common");
- const [cmsHtml, setCmsHtml] = useState(null);
-
- useEffect(() => {
- (async () => {
- try {
- const res = await fetch(
- `/api/content/page?key=${encodeURIComponent("privacy-policy")}&locale=${encodeURIComponent(locale)}`,
- );
- const data = await res.json();
- if (data?.content?.html && data?.content?.locale === locale) {
- setCmsHtml(data.content.html as string);
- }
- } catch {}
- })();
- }, [locale]);
-
- return (
- <>
-
-
-
-
- {/* Editorial Header */}
-
-
-
-
{t("backToHome")}
-
-
-
- Privacy.
-
-
-
- {/* Bento Content Grid */}
-
-
- {/* Main Privacy Text (Large) */}
-
- {cmsHtml ? (
-
-
-
- ) : (
-
-
-
- Verantwortlicher
-
-
-
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.
-
-
-
-
-
- Datenerfassung
-
-
- 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: Mai 2025
-
-
- )}
-
-
- {/* Quick Info Cards */}
-
-
- {/* Core Values Box */}
-
-
Principles
-
-
-
-
-
Encryption
-
SSL/TLS secured data transfer.
-
-
-
-
-
-
Transparency
-
No hidden tracking algorithms.
-
-
-
-
-
-
Compliance
-
GDPR / DSGVO optimized.
-
-
-
-
-
- {/* Cookie Status Indicator */}
-
-
-
Your connection is private and secure.
-
{
- document.cookie = 'dk0_consent_v1=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; localStorage.removeItem('cookie-consent');
- window.location.reload();
- }}
- className="text-[10px] font-black uppercase tracking-widest border-b border-stone-300 dark:border-stone-700 pb-1 hover:text-liquid-mint transition-colors"
- >
- Reset Privacy Settings
-
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/docs/CHANGING_TEXTS.md b/docs/CHANGING_TEXTS.md
index 4a9c1b5..f1557ae 100644
--- a/docs/CHANGING_TEXTS.md
+++ b/docs/CHANGING_TEXTS.md
@@ -105,8 +105,8 @@ The privacy policy and legal notice use a **dynamic CMS system**:
### Option 2: Edit Static Fallback
If you haven't set up CMS content, the fallback static content is in:
-- Privacy Policy: `/app/privacy-policy/page.tsx` (lines 76-302)
-- Legal Notice: `/app/legal-notice/page.tsx`
+- Privacy Policy: `/app/_ui/PrivacyPolicyClient.tsx` (Fallback-Text, sonst CMS-Seite `privacy-policy`)
+- Legal Notice: `/app/_ui/LegalNoticeClient.tsx` (Fallback-Text, sonst CMS-Seite `legal-notice`)
⚠️ **Note**: Static content is hardcoded in German. For CMS-based content, you can manage both languages separately.
diff --git a/tailwind.config.ts b/tailwind.config.ts
index caf0b33..3597888 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -99,21 +99,6 @@ export default {
// Feste Objektfarben auf dem Tisch, unabhängig vom Theme
polaroid: { DEFAULT: "#fbfaf8", ink: "#140c0b" },
bookcover: { DEFAULT: "#2b1a14", text: "#f3e7dc" },
- // Warm brown palette
- cream: "#FAF8F3",
- sand: "#EFEBE9",
- brown: {
- 50: "#EFEBE9",
- 100: "#D7CCC8",
- 200: "#BCAAA4",
- 300: "#A1887F",
- 400: "#8D6E63",
- 500: "#795548",
- 600: "#6D4C41",
- 700: "#5D4037",
- 800: "#4E342E",
- 900: "#3E2723",
- },
stone: {
50: "#FAFAF9",
100: "#F5F5F4",
@@ -126,6 +111,7 @@ export default {
800: "#292524",
900: "#1C1917",
},
+ // Alte Pastellpalette, nur noch im Admin-Bereich und auf der 404-Seite
liquid: {
mint: "#A7F3D0",
lavender: "#DDD6FE",