From 6f1ad8eb4db5297146ffd53a65d6c9825481dec2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 12 Jan 2026 16:10:22 +0000 Subject: [PATCH] Refine CMS i18n fallback, refresh UI, add consent minimize, seed i18n content Co-authored-by: dennis --- app/components/About.tsx | 6 +- app/components/ActivityFeed.tsx | 28 +++---- app/components/ChatWidget.tsx | 20 ++--- app/components/ConsentBanner.tsx | 127 ++++++++++++++++++------------- app/components/Contact.tsx | 6 +- app/components/Hero.tsx | 8 +- app/legal-notice/page.tsx | 8 +- app/privacy-policy/page.tsx | 8 +- next.config.ts | 4 +- prisma/seed.ts | 110 ++++++++++++++++++++++++++ 10 files changed, 244 insertions(+), 81 deletions(-) diff --git a/app/components/About.tsx b/app/components/About.tsx index faf2279..000af7a 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -42,11 +42,15 @@ const About = () => { `/api/content/page?key=${encodeURIComponent("home-about")}&locale=${encodeURIComponent(locale)}`, ); const data = await res.json(); - if (data?.content?.content) { + // Only use CMS content if it exists for the active locale. + if (data?.content?.content && data?.content?.locale === locale) { setCmsDoc(data.content.content as JSONContent); + } else { + setCmsDoc(null); } } catch { // ignore; fallback to static + setCmsDoc(null); } })(); }, [locale]); diff --git a/app/components/ActivityFeed.tsx b/app/components/ActivityFeed.tsx index 335e6ab..8405857 100644 --- a/app/components/ActivityFeed.tsx +++ b/app/components/ActivityFeed.tsx @@ -1463,15 +1463,15 @@ export default function ActivityFeed() { @@ -1485,15 +1485,15 @@ export default function ActivityFeed() { @@ -1508,16 +1508,16 @@ export default function ActivityFeed() {
- +
-

Live Activity

-

Loading...

+

Live Activity

+

Loading...

@@ -1542,11 +1542,11 @@ export default function ActivityFeed() { initial={{ scale: 0 }} animate={{ scale: 1 }} onClick={() => setIsMinimized(false)} - className="fixed bottom-4 right-4 md:bottom-6 md:right-6 z-40 pointer-events-auto bg-black/80 backdrop-blur-xl border border-white/10 p-3 rounded-full shadow-2xl hover:scale-110 transition-transform" + className="fixed bottom-4 right-4 md:bottom-6 md:right-6 z-40 pointer-events-auto bg-white/80 backdrop-blur-xl border border-white/60 p-3 rounded-full shadow-xl hover:scale-110 transition-transform" > - + {activeCount > 0 && ( - + {activeCount} )} @@ -1559,7 +1559,7 @@ export default function ActivityFeed() { {/* Main Container */} {/* Header - Always Visible - Changed from button to div to fix nesting error */}
- + {/* Tooltip */} @@ -315,16 +315,16 @@ export default function ChatWidget() { animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }} exit={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }} transition={{ type: "spring", damping: 30, stiffness: 400 }} - className="fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-[#fdfcf8]/95 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.2)] flex flex-col overflow-hidden border border-[#e7e5e4] ring-1 ring-[#f3f1e7]" + className="fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-white/80 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.16)] flex flex-col overflow-hidden border border-white/60 ring-1 ring-white/30" > {/* Header */} -
+
-
- +
+
- +

@@ -366,12 +366,12 @@ export default function ChatWidget() {

{message.text}

diff --git a/app/components/ConsentBanner.tsx b/app/components/ConsentBanner.tsx index d23b575..9e04284 100644 --- a/app/components/ConsentBanner.tsx +++ b/app/components/ConsentBanner.tsx @@ -6,6 +6,7 @@ import { useConsent, type ConsentState } from "./ConsentProvider"; export default function ConsentBanner() { const { consent, setConsent } = useConsent(); const [draft, setDraft] = useState({ analytics: false, chat: false }); + const [minimized, setMinimized] = useState(false); const locale = useMemo(() => { if (typeof document === "undefined") return "en"; @@ -44,62 +45,86 @@ export default function ConsentBanner() { rejectAll: "Reject all", }; + if (minimized) { + return ( +
+ +
+ ); + } + return ( -
-
-
+
+
+
-
{s.title}
-

{s.description}

+
{s.title}
+

{s.description}

+
+ +
-
-
-
{s.essential}
-
Always on
-
- - - - -
+
+
+
{s.essential}
+
Always on
-
- - - -
+ + + +
+ +
+ + +
diff --git a/app/components/Contact.tsx b/app/components/Contact.tsx index fb26a7e..df2612b 100644 --- a/app/components/Contact.tsx +++ b/app/components/Contact.tsx @@ -21,11 +21,15 @@ const Contact = () => { `/api/content/page?key=${encodeURIComponent("home-contact")}&locale=${encodeURIComponent(locale)}`, ); const data = await res.json(); - if (data?.content?.content) { + // Only use CMS content if it exists for the active locale. + if (data?.content?.content && data?.content?.locale === locale) { setCmsDoc(data.content.content as JSONContent); + } else { + setCmsDoc(null); } } catch { // ignore; fallback to static + setCmsDoc(null); } })(); }, [locale]); diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 9284645..89d12f6 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -20,11 +20,17 @@ const Hero = () => { `/api/content/page?key=${encodeURIComponent("home-hero")}&locale=${encodeURIComponent(locale)}`, ); const data = await res.json(); - if (data?.content?.content) { + // Only use CMS content if it exists for the active locale. + // If the API falls back to another locale, keep showing next-intl strings + // so the locale switch visibly changes the page. + if (data?.content?.content && data?.content?.locale === locale) { setCmsDoc(data.content.content as JSONContent); + } else { + setCmsDoc(null); } } catch { // ignore; fallback to static + setCmsDoc(null); } })(); }, [locale]); diff --git a/app/legal-notice/page.tsx b/app/legal-notice/page.tsx index 02c11ae..877dce4 100644 --- a/app/legal-notice/page.tsx +++ b/app/legal-notice/page.tsx @@ -24,12 +24,18 @@ export default function LegalNotice() { `/api/content/page?key=${encodeURIComponent("legal-notice")}&locale=${encodeURIComponent(locale)}`, ); const data = await res.json(); - if (data?.content?.content) { + // Only use CMS content if it exists for the active locale. + if (data?.content?.content && data?.content?.locale === locale) { setCmsDoc(data.content.content as JSONContent); setCmsTitle((data.content.title as string | null) ?? null); + } else { + setCmsDoc(null); + setCmsTitle(null); } } catch { // ignore; fallback to static content + setCmsDoc(null); + setCmsTitle(null); } })(); }, [locale]); diff --git a/app/privacy-policy/page.tsx b/app/privacy-policy/page.tsx index bc36637..e843b4f 100644 --- a/app/privacy-policy/page.tsx +++ b/app/privacy-policy/page.tsx @@ -24,12 +24,18 @@ export default function PrivacyPolicy() { `/api/content/page?key=${encodeURIComponent("privacy-policy")}&locale=${encodeURIComponent(locale)}`, ); const data = await res.json(); - if (data?.content?.content) { + // Only use CMS content if it exists for the active locale. + if (data?.content?.content && data?.content?.locale === locale) { setCmsDoc(data.content.content as JSONContent); setCmsTitle((data.content.title as string | null) ?? null); + } else { + setCmsDoc(null); + setCmsTitle(null); } } catch { // ignore; fallback to static content + setCmsDoc(null); + setCmsTitle(null); } })(); }, [locale]); diff --git a/next.config.ts b/next.config.ts index a7bfbf2..4a20d7d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -17,7 +17,9 @@ const nextConfig: NextConfig = { poweredByHeader: false, // React Strict Mode - reactStrictMode: true, + // In dev, React StrictMode double-mount can cause visible animation flicker + // (Framer Motion "fade starts, disappears, then pops"). + reactStrictMode: process.env.NODE_ENV === "production", // Disable ESLint during build for Docker eslint: { diff --git a/prisma/seed.ts b/prisma/seed.ts index fa8a4f2..7087fee 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -3,6 +3,18 @@ import { slugify } from "../lib/slug"; const prisma = new PrismaClient(); +function tiptapParagraph(text: string) { + return { + type: "doc", + content: [ + { + type: "paragraph", + content: [{ type: "text", text }], + }, + ], + }; +} + async function main() { console.log("🌱 Seeding database..."); @@ -11,6 +23,104 @@ async function main() { await prisma.pageView.deleteMany(); await prisma.project.deleteMany(); + // Ensure base site settings & minimal localized CMS defaults (do NOT overwrite existing content). + await prisma.siteSettings.upsert({ + where: { id: 1 }, + update: {}, + create: { id: 1, defaultLocale: "en", locales: ["en", "de"] }, + }); + + async function ensureContentPage( + key: string, + translations: Array<{ locale: "en" | "de"; title: string; contentText: string }>, + ) { + const page = await prisma.contentPage.upsert({ + where: { key }, + update: {}, + create: { key, status: "PUBLISHED" }, + }); + + for (const tr of translations) { + await prisma.contentPageTranslation.upsert({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + where: { pageId_locale: { pageId: page.id, locale: tr.locale } } as any, + update: {}, + create: { + pageId: page.id, + locale: tr.locale, + title: tr.title, + content: tiptapParagraph(tr.contentText), + }, + }); + } + } + + await ensureContentPage("home-hero", [ + { + locale: "en", + title: "Hero", + contentText: "I build fast, secure, self-hosted platforms — and I love clean UX.", + }, + { + locale: "de", + title: "Hero", + contentText: "Ich baue schnelle, sichere, selbst gehostete Plattformen — mit sauberem UX.", + }, + ]); + + await ensureContentPage("home-about", [ + { + locale: "en", + title: "About", + contentText: "I’m a software engineer focused on performance, security, and maintainable systems.", + }, + { + locale: "de", + title: "Über mich", + contentText: "Ich bin Software Engineer mit Fokus auf Performance, Security und wartbare Systeme.", + }, + ]); + + await ensureContentPage("home-contact", [ + { + locale: "en", + title: "Contact", + contentText: "Want to work together? Send me a message and I’ll get back to you.", + }, + { + locale: "de", + title: "Kontakt", + contentText: "Lust auf Zusammenarbeit? Schreib mir und ich melde mich zurück.", + }, + ]); + + // These are used by /[locale]/legal-notice and /[locale]/privacy-policy (re-exported pages) + await ensureContentPage("legal-notice", [ + { + locale: "en", + title: "Legal notice", + contentText: "Legal notice content can be edited in the CMS per language.", + }, + { + locale: "de", + title: "Impressum", + contentText: "Impressum-Inhalt kann im CMS pro Sprache bearbeitet werden.", + }, + ]); + + await ensureContentPage("privacy-policy", [ + { + locale: "en", + title: "Privacy policy", + contentText: "Privacy policy content can be edited in the CMS per language.", + }, + { + locale: "de", + title: "Datenschutzerklärung", + contentText: "Datenschutzerklärung kann im CMS pro Sprache bearbeitet werden.", + }, + ]); + // Create real projects const projects = [ {