From 12245eec8eeaeac384a3d6d6f86fd2339e7c02cc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 12 Jan 2026 14:36:10 +0000 Subject: [PATCH 01/12] Refactor for i18n, CMS integration, and project slugs; enhance admin & analytics Co-authored-by: dennis --- app/[locale]/layout.tsx | 21 + app/[locale]/legal-notice/page.tsx | 2 + app/[locale]/page.tsx | 2 + app/[locale]/privacy-policy/page.tsx | 2 + app/[locale]/projects/[slug]/page.tsx | 36 + app/[locale]/projects/page.tsx | 36 + app/__tests__/components/Header.test.tsx | 2 +- app/_ui/HomePage.tsx | 182 ++ app/_ui/ProjectDetailClient.tsx | 238 +++ app/_ui/ProjectsPageClient.tsx | 292 +++ app/api/analytics/track/route.ts | 17 +- app/api/contacts/[id]/route.tsx | 16 +- app/api/content/page/route.ts | 18 + app/api/content/pages/route.ts | 55 + app/api/email/route.tsx | 14 +- app/api/projects/[id]/route.ts | 28 +- app/api/projects/[id]/translation/route.ts | 71 + app/api/projects/route.ts | 19 +- app/api/projects/search/route.ts | 23 +- app/components/About.tsx | 69 +- app/components/ClientProviders.tsx | 48 +- app/components/ConsentBanner.tsx | 108 + app/components/ConsentProvider.tsx | 68 + app/components/Contact.tsx | 33 +- app/components/Footer.tsx | 8 +- app/components/Header.tsx | 56 +- app/components/Hero.tsx | 59 +- app/components/Projects.tsx | 7 +- app/components/RichText.tsx | 21 + app/components/RichTextClient.tsx | 24 + app/editor/page.tsx | 93 +- app/layout.tsx | 13 +- app/legal-notice/page.tsx | 127 +- app/page.tsx | 181 +- app/privacy-policy/page.tsx | 172 +- app/projects/[slug]/page.tsx | 8 +- app/projects/page.tsx | 10 +- components/ContentManager.tsx | 414 ++++ components/EmailManager.tsx | 18 + components/ModernAdminDashboard.tsx | 15 +- i18n/request.ts | 16 + jest.setup.ts | 28 + lib/content.ts | 71 + lib/prisma.ts | 18 + lib/richtext.ts | 71 + lib/slug.ts | 30 + lib/tiptap/fontFamily.ts | 67 + messages/de.json | 25 + messages/en.json | 25 + middleware.ts | 77 +- next.config.ts | 9 +- package-lock.json | 2161 ++++++++++++++++---- package.json | 17 +- prisma/schema.prisma | 73 + prisma/seed.ts | 12 + 55 files changed, 4573 insertions(+), 753 deletions(-) create mode 100644 app/[locale]/layout.tsx create mode 100644 app/[locale]/legal-notice/page.tsx create mode 100644 app/[locale]/page.tsx create mode 100644 app/[locale]/privacy-policy/page.tsx create mode 100644 app/[locale]/projects/[slug]/page.tsx create mode 100644 app/[locale]/projects/page.tsx create mode 100644 app/_ui/HomePage.tsx create mode 100644 app/_ui/ProjectDetailClient.tsx create mode 100644 app/_ui/ProjectsPageClient.tsx create mode 100644 app/api/content/page/route.ts create mode 100644 app/api/content/pages/route.ts create mode 100644 app/api/projects/[id]/translation/route.ts create mode 100644 app/components/ConsentBanner.tsx create mode 100644 app/components/ConsentProvider.tsx create mode 100644 app/components/RichText.tsx create mode 100644 app/components/RichTextClient.tsx create mode 100644 components/ContentManager.tsx create mode 100644 i18n/request.ts create mode 100644 lib/content.ts create mode 100644 lib/richtext.ts create mode 100644 lib/slug.ts create mode 100644 lib/tiptap/fontFamily.ts create mode 100644 messages/de.json create mode 100644 messages/en.json diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..f1bb807 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,21 @@ +import { NextIntlClientProvider } from "next-intl"; +import { getMessages } from "next-intl/server"; +import React from "react"; + +export default async function LocaleLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const messages = await getMessages({ locale }); + + return ( + + {children} + + ); +} + diff --git a/app/[locale]/legal-notice/page.tsx b/app/[locale]/legal-notice/page.tsx new file mode 100644 index 0000000..c4963d3 --- /dev/null +++ b/app/[locale]/legal-notice/page.tsx @@ -0,0 +1,2 @@ +export { default } from "../../legal-notice/page"; + diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx new file mode 100644 index 0000000..f93682d --- /dev/null +++ b/app/[locale]/page.tsx @@ -0,0 +1,2 @@ +export { default } from "../_ui/HomePage"; + diff --git a/app/[locale]/privacy-policy/page.tsx b/app/[locale]/privacy-policy/page.tsx new file mode 100644 index 0000000..67cb9e3 --- /dev/null +++ b/app/[locale]/privacy-policy/page.tsx @@ -0,0 +1,2 @@ +export { default } from "../../privacy-policy/page"; + diff --git a/app/[locale]/projects/[slug]/page.tsx b/app/[locale]/projects/[slug]/page.tsx new file mode 100644 index 0000000..06de5e9 --- /dev/null +++ b/app/[locale]/projects/[slug]/page.tsx @@ -0,0 +1,36 @@ +import { prisma } from "@/lib/prisma"; +import ProjectDetailClient from "@/app/_ui/ProjectDetailClient"; +import { notFound } from "next/navigation"; + +export const revalidate = 300; + +export default async function ProjectPage({ + params, +}: { + params: Promise<{ locale: string; slug: string }>; +}) { + const { locale, slug } = await params; + + const project = await prisma.project.findFirst({ + where: { slug, published: true }, + include: { + translations: { + where: { locale }, + select: { title: true, description: true }, + }, + }, + }); + + if (!project) return notFound(); + + const tr = project.translations?.[0]; + const { translations: _translations, ...rest } = project; + const localized = { + ...rest, + title: tr?.title ?? project.title, + description: tr?.description ?? project.description, + }; + + return ; +} + diff --git a/app/[locale]/projects/page.tsx b/app/[locale]/projects/page.tsx new file mode 100644 index 0000000..5e4a9cd --- /dev/null +++ b/app/[locale]/projects/page.tsx @@ -0,0 +1,36 @@ +import { prisma } from "@/lib/prisma"; +import ProjectsPageClient from "@/app/_ui/ProjectsPageClient"; + +export const revalidate = 300; + +export default async function ProjectsPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + + const projects = await prisma.project.findMany({ + where: { published: true }, + orderBy: { createdAt: "desc" }, + include: { + translations: { + where: { locale }, + select: { title: true, description: true }, + }, + }, + }); + + const localized = projects.map((p) => { + const tr = p.translations?.[0]; + const { translations: _translations, ...rest } = p; + return { + ...rest, + title: tr?.title ?? p.title, + description: tr?.description ?? p.description, + }; + }); + + return ; +} + diff --git a/app/__tests__/components/Header.test.tsx b/app/__tests__/components/Header.test.tsx index e9c1108..8c8edd9 100644 --- a/app/__tests__/components/Header.test.tsx +++ b/app/__tests__/components/Header.test.tsx @@ -21,7 +21,7 @@ describe('Header', () => { it('renders the mobile header', () => { render(
); // Check for mobile menu button (hamburger icon) - const menuButton = screen.getByRole('button'); + const menuButton = screen.getByLabelText('Open menu'); expect(menuButton).toBeInTheDocument(); }); }); \ No newline at end of file diff --git a/app/_ui/HomePage.tsx b/app/_ui/HomePage.tsx new file mode 100644 index 0000000..40ed916 --- /dev/null +++ b/app/_ui/HomePage.tsx @@ -0,0 +1,182 @@ +"use client"; + +import Header from "../components/Header"; +import Hero from "../components/Hero"; +import About from "../components/About"; +import Projects from "../components/Projects"; +import Contact from "../components/Contact"; +import Footer from "../components/Footer"; +import Script from "next/script"; +import dynamic from "next/dynamic"; +import ErrorBoundary from "@/components/ErrorBoundary"; +import { motion } from "framer-motion"; + +// Wrap ActivityFeed in error boundary to prevent crashes +const ActivityFeed = dynamic( + () => + import("../components/ActivityFeed").catch(() => ({ default: () => null })), + { + ssr: false, + loading: () => null, + }, +); + +export default function HomePage() { + return ( +
+ - Dennis Konkol's Portfolio {children} diff --git a/app/legal-notice/page.tsx b/app/legal-notice/page.tsx index 782b249..02c11ae 100644 --- a/app/legal-notice/page.tsx +++ b/app/legal-notice/page.tsx @@ -6,8 +6,34 @@ import { ArrowLeft } 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 type { JSONContent } from "@tiptap/react"; +import RichTextClient from "../components/RichTextClient"; export default function LegalNotice() { + const locale = useLocale(); + const t = useTranslations("common"); + const [cmsDoc, setCmsDoc] = useState(null); + const [cmsTitle, setCmsTitle] = 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?.content) { + setCmsDoc(data.content.content as JSONContent); + setCmsTitle((data.content.title as string | null) ?? null); + } + } catch { + // ignore; fallback to static content + } + })(); + }, [locale]); + return (
@@ -19,15 +45,15 @@ export default function LegalNotice() { className="mb-8" > - Back to Home + {t("backToHome")}

- Impressum + {cmsTitle || "Impressum"}

@@ -37,47 +63,68 @@ export default function LegalNotice() { transition={{ duration: 0.8, delay: 0.2 }} className="glass-card p-8 rounded-2xl space-y-6" > -
-

- Verantwortlicher für die Inhalte dieser Website -

-
-

Name: Dennis Konkol

-

Adresse: Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland

-

E-Mail: info@dk0.dev

-

Website: dk0.dev

-
-
+ {cmsDoc ? ( + + ) : ( + <> +
+

Verantwortlicher für die Inhalte dieser Website

+
+

+ Name: Dennis Konkol +

+

+ Adresse: Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland +

+

+ E-Mail:{" "} + + info@dk0.dev + +

+

+ Website:{" "} + + dk0.dev + +

+
+
-
-

Haftung für Links

-

- Meine Website enthält Links auf externe Websites. Ich habe keinen Einfluss auf die Inhalte dieser Websites - und kann daher keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der Betreiber oder - Anbieter der Seiten verantwortlich. Jedoch überprüfe ich die verlinkten Seiten zum Zeitpunkt der Verlinkung - auf mögliche Rechtsverstöße. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Links umgehend entfernen. -

-
+
+

Haftung für Links

+

+ Meine Website enthält Links auf externe Websites. Ich habe keinen Einfluss auf die Inhalte dieser + Websites und kann daher keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der + Betreiber oder Anbieter der Seiten verantwortlich. Jedoch überprüfe ich die verlinkten Seiten zum + Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße. Bei Bekanntwerden von Rechtsverletzungen werde + ich derartige Links umgehend entfernen. +

+
-
-

Urheberrecht

-

- Alle Inhalte dieser Website, einschließlich Texte, Fotos und Designs, stehen unter Urheberrechtsschutz. - Jegliche Nutzung ohne vorherige schriftliche Zustimmung des Urhebers ist verboten. -

-
+
+

Urheberrecht

+

+ Alle Inhalte dieser Website, einschließlich Texte, Fotos und Designs, stehen unter + Urheberrechtsschutz. Jegliche Nutzung ohne vorherige schriftliche Zustimmung des Urhebers ist + verboten. +

+
-
-

Gewährleistung

-

- Die Nutzung der Inhalte dieser Website erfolgt auf eigene Gefahr. Als Diensteanbieter kann ich keine - Gewähr übernehmen für Schäden, die entstehen können, durch den Zugriff oder die Nutzung dieser Website. -

-
+
+

Gewährleistung

+

+ Die Nutzung der Inhalte dieser Website erfolgt auf eigene Gefahr. Als Diensteanbieter kann ich keine + Gewähr übernehmen für Schäden, die entstehen können, durch den Zugriff oder die Nutzung dieser + Website. +

+
-
-

Letzte Aktualisierung: 12.02.2025

-
+
+

Letzte Aktualisierung: 12.02.2025

+
+ + )}