Refine CMS i18n fallback, refresh UI, add consent minimize, seed i18n content
Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -1463,15 +1463,15 @@ export default function ActivityFeed() {
|
||||
<motion.div
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="bg-black/80 backdrop-blur-xl border border-white/10 rounded-xl p-3 shadow-2xl"
|
||||
className="bg-white/80 backdrop-blur-xl border border-white/60 rounded-2xl px-4 py-3 shadow-xl"
|
||||
>
|
||||
<button
|
||||
onClick={toggleTracking}
|
||||
className="flex items-center gap-2 text-white/60 hover:text-white transition-colors"
|
||||
className="flex items-center gap-2 text-stone-600 hover:text-stone-900 transition-colors"
|
||||
title="Activity tracking is disabled. Click to enable."
|
||||
>
|
||||
<Activity size={16} />
|
||||
<span className="text-xs">Tracking disabled</span>
|
||||
<span className="text-xs font-semibold">Tracking disabled</span>
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -1485,15 +1485,15 @@ export default function ActivityFeed() {
|
||||
<motion.div
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="bg-black/80 backdrop-blur-xl border border-white/10 rounded-xl p-3 shadow-2xl"
|
||||
className="bg-white/80 backdrop-blur-xl border border-white/60 rounded-2xl px-4 py-3 shadow-xl"
|
||||
>
|
||||
<button
|
||||
onClick={toggleTracking}
|
||||
className="flex items-center gap-2 text-white/60 hover:text-white transition-colors"
|
||||
className="flex items-center gap-2 text-stone-600 hover:text-stone-900 transition-colors"
|
||||
title="Activity tracking is disabled. Click to enable."
|
||||
>
|
||||
<Activity size={16} />
|
||||
<span className="text-xs">Tracking disabled</span>
|
||||
<span className="text-xs font-semibold">Tracking disabled</span>
|
||||
</button>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -1508,16 +1508,16 @@ export default function ActivityFeed() {
|
||||
<motion.div
|
||||
initial={{ scale: 0, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
className="pointer-events-auto bg-black/90 backdrop-blur-2xl border border-white/10 rounded-2xl shadow-2xl overflow-hidden w-full"
|
||||
className="pointer-events-auto bg-white/80 backdrop-blur-2xl border border-white/60 rounded-2xl shadow-xl overflow-hidden w-full"
|
||||
>
|
||||
<div className="w-full px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Activity size={18} className="text-white" />
|
||||
<Activity size={18} className="text-stone-900" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h3 className="text-sm font-bold text-white">Live Activity</h3>
|
||||
<p className="text-[10px] text-white/50">Loading...</p>
|
||||
<h3 className="text-sm font-bold text-stone-900">Live Activity</h3>
|
||||
<p className="text-[10px] text-stone-500">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2"></div>
|
||||
@@ -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"
|
||||
>
|
||||
<Activity size={20} className="text-white" />
|
||||
<Activity size={20} className="text-stone-900" />
|
||||
{activeCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-green-500 text-white text-[10px] font-bold rounded-full w-5 h-5 flex items-center justify-center">
|
||||
<span className="absolute -top-1 -right-1 bg-stone-900 text-white text-[10px] font-bold rounded-full w-5 h-5 flex items-center justify-center">
|
||||
{activeCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -1559,7 +1559,7 @@ export default function ActivityFeed() {
|
||||
{/* Main Container */}
|
||||
<motion.div
|
||||
layout
|
||||
className="pointer-events-auto bg-black/90 backdrop-blur-2xl border border-white/10 rounded-2xl shadow-2xl w-full overflow-hidden [&_a]:text-inherit [&_a]:no-underline"
|
||||
className="pointer-events-auto bg-black/95 backdrop-blur-2xl border border-white/10 rounded-2xl shadow-2xl w-full overflow-hidden [&_a]:text-inherit [&_a]:no-underline"
|
||||
>
|
||||
{/* Header - Always Visible - Changed from button to div to fix nesting error */}
|
||||
<div
|
||||
|
||||
@@ -292,11 +292,11 @@ export default function ChatWidget() {
|
||||
setIsOpen(true);
|
||||
}
|
||||
}}
|
||||
className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-[#292524] text-[#fdfcf8] p-3.5 rounded-full shadow-[0_8px_20px_rgba(41,37,36,0.25)] hover:bg-[#44403c] hover:scale-105 transition-all duration-300 group cursor-pointer border border-[#f3f1e7]/20 ring-1 ring-[#f3f1e7]/10"
|
||||
className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-white/80 backdrop-blur-xl text-stone-900 p-3.5 rounded-full shadow-[0_10px_26px_rgba(41,37,36,0.16)] hover:bg-white hover:scale-105 transition-all duration-300 group cursor-pointer border border-white/60 ring-1 ring-white/30"
|
||||
aria-label="Open chat"
|
||||
>
|
||||
<MessageCircle size={24} />
|
||||
<span className="absolute top-0 right-0 w-3 h-3 bg-green-500 rounded-full animate-pulse shadow-sm border-2 border-[#292524]" />
|
||||
<span className="absolute top-0 right-0 w-3 h-3 bg-green-500 rounded-full animate-pulse shadow-sm border-2 border-white" />
|
||||
|
||||
{/* Tooltip */}
|
||||
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-3 py-1.5 bg-stone-900/90 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-[100] shadow-xl backdrop-blur-sm">
|
||||
@@ -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 */}
|
||||
<div className="bg-[#fdfcf8] text-[#292524] p-4 flex items-center justify-between border-b border-[#e7e5e4]">
|
||||
<div className="bg-white/70 text-stone-900 p-4 flex items-center justify-between border-b border-white/50">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-[#f3f1e7] to-[#fdfcf8] flex items-center justify-center ring-1 ring-[#e7e5e4] shadow-sm">
|
||||
<Sparkles size={18} className="text-[#57534e]" />
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-liquid-mint/50 via-liquid-lavender/40 to-liquid-rose/40 flex items-center justify-center ring-1 ring-white/50 shadow-sm">
|
||||
<Sparkles size={18} className="text-stone-800" />
|
||||
</div>
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 rounded-full border-2 border-[#fdfcf8] shadow-sm" />
|
||||
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 bg-green-500 rounded-full border-2 border-white shadow-sm" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="font-bold text-sm truncate text-stone-900 tracking-tight">
|
||||
@@ -366,12 +366,12 @@ export default function ChatWidget() {
|
||||
<div
|
||||
className={`max-w-[85%] rounded-2xl px-4 py-3 shadow-sm ${
|
||||
message.sender === "user"
|
||||
? "bg-[#292524] text-[#fdfcf8]"
|
||||
: "bg-[#f3f1e7] text-[#292524] border border-[#e7e5e4]"
|
||||
? "bg-stone-900 text-white"
|
||||
: "bg-white/70 text-stone-900 border border-white/60"
|
||||
}`}
|
||||
>
|
||||
<p className={`text-sm whitespace-pre-wrap break-words leading-relaxed ${
|
||||
message.sender === "user" ? "text-[#fdfcf8]/90 font-light" : "text-[#292524] font-medium"
|
||||
message.sender === "user" ? "text-white/90 font-normal" : "text-stone-900 font-medium"
|
||||
}`}>
|
||||
{message.text}
|
||||
</p>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useConsent, type ConsentState } from "./ConsentProvider";
|
||||
export default function ConsentBanner() {
|
||||
const { consent, setConsent } = useConsent();
|
||||
const [draft, setDraft] = useState<ConsentState>({ 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 (
|
||||
<div className="fixed bottom-4 right-4 z-[60]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMinimized(false)}
|
||||
className="px-4 py-2 rounded-full bg-white/80 backdrop-blur-xl border border-white/60 shadow-lg text-stone-800 font-semibold hover:bg-white transition-colors"
|
||||
aria-label="Open privacy settings"
|
||||
>
|
||||
{s.title}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-4 right-4 z-[60]">
|
||||
<div className="max-w-3xl mx-auto bg-white/95 backdrop-blur-xl border border-stone-200 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.18)] p-5">
|
||||
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
|
||||
<div className="fixed bottom-4 right-4 z-[60] max-w-[calc(100vw-2rem)]">
|
||||
<div className="w-[360px] max-w-full bg-white/85 backdrop-blur-xl border border-white/60 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.14)] p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-lg font-bold text-stone-900">{s.title}</div>
|
||||
<p className="text-sm text-stone-600 mt-1">{s.description}</p>
|
||||
<div className="text-base font-bold text-stone-900">{s.title}</div>
|
||||
<p className="text-xs text-stone-600 mt-1 leading-snug">{s.description}</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMinimized(true)}
|
||||
className="shrink-0 text-xs text-stone-500 hover:text-stone-900 transition-colors"
|
||||
aria-label="Minimize privacy banner"
|
||||
title="Minimize"
|
||||
>
|
||||
Hide
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-stone-800">{s.essential}</div>
|
||||
<div className="text-xs text-stone-500">Always on</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center justify-between gap-3 py-1">
|
||||
<span className="text-sm font-medium text-stone-800">{s.analytics}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={draft.analytics}
|
||||
onChange={(e) => setDraft((p) => ({ ...p, analytics: e.target.checked }))}
|
||||
className="w-4 h-4 accent-stone-900"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between gap-3 py-1">
|
||||
<span className="text-sm font-medium text-stone-800">{s.chat}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={draft.chat}
|
||||
onChange={(e) => setDraft((p) => ({ ...p, chat: e.target.checked }))}
|
||||
className="w-4 h-4 accent-stone-900"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-xs font-semibold text-stone-800">{s.essential}</div>
|
||||
<div className="text-[11px] text-stone-500">Always on</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => setConsent({ analytics: true, chat: true })}
|
||||
className="px-4 py-2 rounded-xl bg-stone-900 text-stone-50 font-semibold hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
{s.acceptAll}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConsent(draft)}
|
||||
className="px-4 py-2 rounded-xl bg-white border border-stone-200 text-stone-800 font-semibold hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
{s.acceptSelected}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConsent({ analytics: false, chat: false })}
|
||||
className="px-4 py-2 rounded-xl bg-transparent text-stone-600 font-semibold hover:text-stone-900 transition-colors"
|
||||
>
|
||||
{s.rejectAll}
|
||||
</button>
|
||||
</div>
|
||||
<label className="flex items-center justify-between gap-3 py-1">
|
||||
<span className="text-sm font-semibold text-stone-800">{s.analytics}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={draft.analytics}
|
||||
onChange={(e) => setDraft((p) => ({ ...p, analytics: e.target.checked }))}
|
||||
className="w-4 h-4 accent-stone-900"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between gap-3 py-1">
|
||||
<span className="text-sm font-semibold text-stone-800">{s.chat}</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={draft.chat}
|
||||
onChange={(e) => setDraft((p) => ({ ...p, chat: e.target.checked }))}
|
||||
className="w-4 h-4 accent-stone-900"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => setConsent({ analytics: true, chat: true })}
|
||||
className="px-4 py-2 rounded-xl bg-stone-900 text-stone-50 font-semibold hover:bg-stone-800 transition-colors"
|
||||
>
|
||||
{s.acceptAll}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConsent(draft)}
|
||||
className="px-4 py-2 rounded-xl bg-white border border-stone-200 text-stone-800 font-semibold hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
{s.acceptSelected}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConsent({ analytics: false, chat: false })}
|
||||
className="px-4 py-2 rounded-xl bg-transparent text-stone-600 font-semibold hover:text-stone-900 transition-colors"
|
||||
>
|
||||
{s.rejectAll}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
110
prisma/seed.ts
110
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 = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user