feat: ultimate dynamic editorial overhaul
Automated CMS content seeding, integrated interactive AI Chat into Bento grid, implemented intelligent idle quote logic, and unified editorial styling across all sub-pages.
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, User, BookOpen, MessageSquare, ExternalLink, ArrowRight } from "lucide-react";
|
||||
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, BookOpen, MessageSquare, ArrowRight, Send } from "lucide-react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import type { JSONContent } from "@tiptap/react";
|
||||
import RichTextClient from "./RichTextClient";
|
||||
import CurrentlyReading from "./CurrentlyReading";
|
||||
import ReadBooks from "./ReadBooks";
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { TechStackCategory, Hobby } from "@/lib/directus";
|
||||
import Link from "next/link";
|
||||
import ActivityFeed from "./ActivityFeed";
|
||||
@@ -22,21 +22,30 @@ const About = () => {
|
||||
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
||||
const [techStack, setTechStack] = useState<TechStackCategory[]>([]);
|
||||
const [hobbies, setHobbies] = useState<Hobby[]>([]);
|
||||
const [isActivityActive, setIsActivityActive] = useState(false);
|
||||
const [cmsMessages, setCmsMessages] = useState<Record<string, string>>({});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const [cmsRes, techRes, hobbiesRes] = await Promise.all([
|
||||
const [cmsRes, techRes, hobbiesRes, msgRes] = await Promise.all([
|
||||
fetch(`/api/content/page?key=home-about&locale=${locale}`),
|
||||
fetch(`/api/tech-stack?locale=${locale}`),
|
||||
fetch(`/api/hobbies?locale=${locale}`)
|
||||
fetch(`/api/hobbies?locale=${locale}`),
|
||||
fetch(`/api/messages?locale=${locale}`)
|
||||
]);
|
||||
|
||||
const cmsData = await cmsRes.json();
|
||||
if (cmsData?.content?.content) setCmsDoc(cmsData.content.content as JSONContent);
|
||||
|
||||
const techData = await techRes.json();
|
||||
if (techData?.techStack) setTechStack(techData.techStack);
|
||||
|
||||
const hobbiesData = await hobbiesRes.json();
|
||||
if (hobbiesData?.hobbies) setHobbies(hobbiesData.hobbies);
|
||||
|
||||
const msgData = await msgRes.json();
|
||||
if (msgData?.messages) setCmsMessages(msgData.messages);
|
||||
} catch (error) {}
|
||||
};
|
||||
fetchData();
|
||||
@@ -56,56 +65,103 @@ const About = () => {
|
||||
className="md:col-span-8 bg-white dark:bg-stone-900 rounded-[3rem] p-10 md:p-16 border border-stone-200/60 dark:border-stone-800/60 shadow-sm"
|
||||
>
|
||||
<div className="space-y-8">
|
||||
<h2 className="text-4xl md:text-7xl font-black text-stone-900 dark:text-stone-50 tracking-tighter">
|
||||
{t("title")}
|
||||
<h2 className="text-4xl md:text-7xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase">
|
||||
{t("title")}<span className="text-liquid-mint">.</span>
|
||||
</h2>
|
||||
<div className="prose prose-stone dark:prose-invert max-w-none text-xl md:text-2xl font-light leading-relaxed text-stone-600 dark:text-stone-400">
|
||||
{cmsDoc ? <RichTextClient doc={cmsDoc} /> : <p>{t("p1")} {t("p2")}</p>}
|
||||
</div>
|
||||
<div className="pt-8">
|
||||
<div className="inline-block bg-liquid-mint/10 px-8 py-4 rounded-[2rem] border border-liquid-mint/20">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-liquid-mint mb-2">{t("funFactTitle")}</p>
|
||||
<div className="inline-block bg-liquid-mint/5 px-8 py-4 rounded-3xl border border-liquid-mint/20">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-liquid-mint mb-2">{t("funFactTitle")}</p>
|
||||
<p className="text-base font-bold opacity-90">{t("funFactBody")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 2. Doing Right Now (Status) */}
|
||||
{/* 2. Activity / Status Box */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="md:col-span-4 bg-stone-900 dark:bg-stone-800 rounded-[3rem] p-10 border border-stone-800 dark:border-stone-700 shadow-2xl text-white overflow-hidden relative"
|
||||
className="md:col-span-4 bg-stone-900 rounded-[3rem] p-10 border border-stone-800 shadow-2xl text-white overflow-hidden relative flex flex-col"
|
||||
>
|
||||
<div className="relative z-10 h-full flex flex-col">
|
||||
<h3 className="text-xl font-black mb-6 flex items-center gap-2 uppercase tracking-widest text-liquid-mint">
|
||||
<Activity size={20} /> Doing Now
|
||||
<div className="relative z-10 flex-1">
|
||||
<h3 className="text-xl font-black mb-10 flex items-center gap-2 uppercase tracking-widest text-liquid-mint">
|
||||
<Activity size={20} /> Currently
|
||||
</h3>
|
||||
<div className="flex-1">
|
||||
<ActivityFeed />
|
||||
|
||||
<div className="space-y-6">
|
||||
<ActivityFeed onActivityChange={(active) => setIsActivityActive(active)} />
|
||||
|
||||
{!isActivityActive && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="pt-6 border-t border-white/10"
|
||||
>
|
||||
<p className="text-stone-400 italic font-light text-lg">
|
||||
“{cmsMessages["about.quote.idle"] || "Gerade am Planen des nächsten großen Projekts."}”
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Ambient Background for Status */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-liquid-mint/20 blur-3xl rounded-full" />
|
||||
{/* Design Element */}
|
||||
<div className="absolute -bottom-10 -right-10 w-40 h-40 bg-liquid-mint/10 blur-[80px] rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
{/* 3. Tech Stack */}
|
||||
{/* 3. AI Chat Box (Direct Integration) */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="md:col-span-12 lg:col-span-9 bg-white dark:bg-stone-900 rounded-[3rem] p-10 md:p-16 border border-stone-200/60 dark:border-stone-800/60 shadow-sm"
|
||||
className="md:col-span-12 lg:col-span-4 bg-gradient-to-br from-liquid-purple/10 to-liquid-sky/10 dark:from-stone-900 rounded-[3rem] p-10 border border-liquid-purple/30 dark:border-stone-800/60 flex flex-col justify-between group shadow-sm overflow-hidden relative"
|
||||
>
|
||||
<div className="relative z-10">
|
||||
<div className="flex justify-between items-start mb-10">
|
||||
<div className="w-14 h-14 rounded-2xl bg-white dark:bg-stone-800 flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform">
|
||||
<MessageSquare className="text-liquid-purple" size={28} />
|
||||
</div>
|
||||
<div className="flex -space-x-2">
|
||||
{[1,2,3].map(i => <div key={i} className="w-8 h-8 rounded-full border-2 border-white dark:border-stone-900 bg-stone-200 dark:bg-stone-700 overflow-hidden shadow-sm"><img src={`/images/me.jpg`} className="w-full h-full object-cover" /></div>)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-3xl font-black text-stone-900 dark:text-stone-50 mb-4 tracking-tighter">Ask my AI Twin</h3>
|
||||
<p className="text-stone-600 dark:text-stone-400 text-lg font-light leading-relaxed mb-8">Get instant answers about my stack, availability or experience.</p>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const chatBtn = document.querySelector('[aria-label="Open chat"]') as HTMLElement;
|
||||
if (chatBtn) chatBtn.click();
|
||||
}}
|
||||
className="w-full py-4 bg-white dark:bg-stone-800 rounded-2xl border border-stone-200 dark:border-stone-700 flex items-center justify-between px-6 font-bold text-stone-400 hover:text-stone-900 dark:hover:text-white transition-all shadow-sm"
|
||||
>
|
||||
<span>Type a message...</span>
|
||||
<Send size={18} className="text-liquid-purple" />
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 4. Tech Stack */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="md:col-span-12 lg:col-span-8 bg-white dark:bg-stone-900 rounded-[3rem] p-10 md:p-16 border border-stone-200/60 dark:border-stone-800/60 shadow-sm"
|
||||
>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||
{techStack.map((cat) => (
|
||||
<div key={cat.id} className="space-y-6">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-[0.2em] text-stone-400">{cat.name}</h4>
|
||||
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-stone-400">{cat.name}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{cat.items?.map((item: any) => (
|
||||
<span key={item.id} className="px-4 py-2 bg-stone-50 dark:bg-stone-800/50 rounded-xl text-xs font-bold border border-stone-100 dark:border-stone-700/50 hover:border-liquid-mint transition-colors">
|
||||
<span key={item.id} className="px-4 py-2 bg-stone-50 dark:bg-stone-800 rounded-xl text-xs font-bold border border-stone-100 dark:border-stone-700/50 hover:border-liquid-mint transition-colors">
|
||||
{item.name}
|
||||
</span>
|
||||
))}
|
||||
@@ -115,31 +171,7 @@ const About = () => {
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 4. AI Chat Box */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="md:col-span-12 lg:col-span-3 bg-gradient-to-br from-liquid-purple/20 to-liquid-sky/20 dark:from-liquid-purple/10 dark:to-liquid-sky/10 rounded-[3rem] p-10 border border-liquid-purple/30 dark:border-stone-800/60 flex flex-col justify-between group cursor-pointer shadow-sm hover:shadow-xl transition-all"
|
||||
onClick={() => {
|
||||
const chatBtn = document.querySelector('[aria-label="Open chat"]') as HTMLElement;
|
||||
if (chatBtn) chatBtn.click();
|
||||
}}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-white dark:bg-stone-800 flex items-center justify-center shadow-lg mb-6 group-hover:scale-110 transition-transform">
|
||||
<MessageSquare className="text-liquid-purple" size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black text-stone-900 dark:text-stone-50 mb-2">AI Assistant</h3>
|
||||
<p className="text-sm text-stone-600 dark:text-stone-400 leading-relaxed">Have questions about my projects or experience? Ask my digital twin.</p>
|
||||
</div>
|
||||
<div className="mt-8 flex items-center gap-2 font-bold text-xs uppercase tracking-widest text-liquid-purple">
|
||||
Start Chat <ArrowRight size={16} className="group-hover:translate-x-2 transition-transform" />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 5. Reading & Hobbies Archive Row */}
|
||||
{/* 5. Library Link & Hobbies */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
@@ -147,37 +179,35 @@ const About = () => {
|
||||
transition={{ delay: 0.4 }}
|
||||
className="md:col-span-12 grid grid-cols-1 md:grid-cols-2 gap-8"
|
||||
>
|
||||
{/* Reading Mini-Box */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col h-full">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h3 className="text-2xl font-black text-stone-900 dark:text-stone-50 flex items-center gap-3 uppercase tracking-tighter">
|
||||
<BookOpen className="text-liquid-purple" size={24} /> Reading
|
||||
</h3>
|
||||
<Link href={`/${locale}/books`} className="text-xs font-bold uppercase tracking-widest text-stone-400 hover:text-stone-900 dark:hover:text-stone-100 flex items-center gap-2 transition-colors">
|
||||
View Library <ArrowRight size={14} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col justify-between group overflow-hidden relative">
|
||||
<div className="relative z-10">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
<h3 className="text-2xl font-black text-stone-900 dark:text-stone-50 flex items-center gap-3 uppercase tracking-tighter">
|
||||
<BookOpen className="text-liquid-purple" size={24} /> Library
|
||||
</h3>
|
||||
<Link href={`/${locale}/books`} className="group flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-all">
|
||||
View All <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
<CurrentlyReading />
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-liquid-purple/5 blur-[60px] rounded-full" />
|
||||
</div>
|
||||
|
||||
{/* Hobbies Mini-Box */}
|
||||
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col justify-between">
|
||||
<div className="flex flex-wrap gap-4 mb-8">
|
||||
<div className="flex flex-wrap gap-4 mb-10">
|
||||
{hobbies.map((hobby) => {
|
||||
const Icon = iconMap[hobby.icon] || Lightbulb;
|
||||
return (
|
||||
<div key={hobby.id} className="flex items-center gap-3 px-6 py-3 bg-stone-50 dark:bg-stone-800/50 rounded-2xl border border-stone-100 dark:border-stone-700/50 shadow-sm">
|
||||
<Icon size={18} className="text-liquid-mint" />
|
||||
<span className="text-xs font-black uppercase tracking-wider">{hobby.title}</span>
|
||||
<div key={hobby.id} className="w-12 h-12 rounded-2xl bg-stone-50 dark:bg-stone-800 flex items-center justify-center shadow-sm border border-stone-100 dark:border-stone-700">
|
||||
<Icon size={20} className="text-liquid-mint" />
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-2xl font-black text-stone-900 dark:text-stone-50">{t("hobbiesTitle")}</h3>
|
||||
<p className="text-stone-500 font-light">Things that spark my curiosity outside of software engineering.</p>
|
||||
<p className="text-stone-500 font-light text-lg">Curiosity beyond software engineering.</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -68,7 +68,7 @@ function getSafeGamingText(details: string | number | undefined, state: string |
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export default function ActivityFeed() {
|
||||
export default function ActivityFeed({ onActivityChange }: { onActivityChange?: (active: boolean) => void }) {
|
||||
const [data, setData] = useState<StatusData | null>(null);
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
@@ -200,6 +200,7 @@ export default function ActivityFeed() {
|
||||
}
|
||||
|
||||
setHasActivity(hasActiveActivity);
|
||||
onActivityChange?.(hasActiveActivity);
|
||||
|
||||
// Auto-expand if there's new activity and not minimized
|
||||
if (hasActiveActivity && !isMinimized) {
|
||||
|
||||
@@ -24,42 +24,34 @@ export default function LegalNotice() {
|
||||
`/api/content/page?key=${encodeURIComponent("legal-notice")}&locale=${encodeURIComponent(locale)}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
// 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);
|
||||
}
|
||||
} catch {}
|
||||
})();
|
||||
}, [locale]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen animated-bg">
|
||||
<div className="min-h-screen bg-[#fdfcf8] dark:bg-stone-950 transition-colors duration-500">
|
||||
<Header />
|
||||
<main className="max-w-4xl mx-auto px-4 pt-32 pb-20">
|
||||
<main className="max-w-7xl mx-auto px-6 pt-40 pb-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="mb-8"
|
||||
className="mb-12"
|
||||
>
|
||||
<Link
|
||||
href={`/${locale}`}
|
||||
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors mb-6"
|
||||
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
<span>{t("backToHome")}</span>
|
||||
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
|
||||
<span className="font-bold uppercase tracking-widest text-xs">{t("backToHome")}</span>
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6 gradient-text">
|
||||
{cmsTitle || "Impressum"}
|
||||
<h1 className="text-6xl md:text-[8rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase">
|
||||
Legal<span className="text-liquid-mint">.</span>
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
@@ -67,69 +59,34 @@ export default function LegalNotice() {
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
className="glass-card p-8 rounded-2xl space-y-6"
|
||||
className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 md:p-16 border border-stone-200/60 dark:border-stone-800/60 shadow-sm"
|
||||
>
|
||||
{cmsDoc ? (
|
||||
<RichTextClient doc={cmsDoc} className="prose prose-invert max-w-none text-gray-300" />
|
||||
<div className="prose prose-stone dark:prose-invert max-w-none text-lg md:text-xl font-light leading-relaxed">
|
||||
<RichTextClient doc={cmsDoc} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-gray-300 leading-relaxed">
|
||||
<h2 className="text-2xl font-semibold mb-4">Verantwortlicher für die Inhalte dieser Website</h2>
|
||||
<div className="space-y-2 text-gray-300">
|
||||
<p>
|
||||
<strong>Name:</strong> Dennis Konkol
|
||||
</p>
|
||||
<p>
|
||||
<strong>Adresse:</strong> Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland
|
||||
</p>
|
||||
<p>
|
||||
<strong>E-Mail:</strong>{" "}
|
||||
<Link href="mailto:info@dki.one" className="text-blue-400 hover:text-blue-300 transition-colors">
|
||||
info@dk0.dev
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Website:</strong>{" "}
|
||||
<Link href="https://www.dk0.dev" className="text-blue-400 hover:text-blue-300 transition-colors">
|
||||
dk0.dev
|
||||
</Link>
|
||||
</p>
|
||||
<div className="space-y-12">
|
||||
<div className="text-stone-600 dark:text-stone-400 leading-relaxed">
|
||||
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-6 uppercase tracking-tight">Angaben gemäß § 5 TMG</h2>
|
||||
<div className="space-y-4 text-xl font-light">
|
||||
<p>Dennis Konkol</p>
|
||||
<p>Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland</p>
|
||||
<p>E-Mail: <Link href="mailto:info@dk0.dev" className="text-stone-900 dark:text-stone-100 font-bold border-b-2 border-liquid-mint">info@dk0.dev</Link></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-gray-300">
|
||||
<h2 className="text-2xl font-semibold mb-4">Haftung für Links</h2>
|
||||
<p className="leading-relaxed">
|
||||
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.
|
||||
<div className="text-stone-600 dark:text-stone-400">
|
||||
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-6 uppercase tracking-tight">Haftung für Inhalte</h2>
|
||||
<p className="text-xl font-light leading-relaxed">
|
||||
Als Diensteanbieter bin ich gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG bin ich als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-gray-300">
|
||||
<h2 className="text-2xl font-semibold mb-4">Urheberrecht</h2>
|
||||
<p className="leading-relaxed">
|
||||
Alle Inhalte dieser Website, einschließlich Texte, Fotos und Designs, stehen unter
|
||||
Urheberrechtsschutz. Jegliche Nutzung ohne vorherige schriftliche Zustimmung des Urhebers ist
|
||||
verboten.
|
||||
</p>
|
||||
<div className="pt-8 border-t border-stone-100 dark:border-stone-800">
|
||||
<p className="text-stone-400 text-sm font-mono uppercase tracking-widest">Last updated: 12.02.2025</p>
|
||||
</div>
|
||||
|
||||
<div className="text-gray-300">
|
||||
<h2 className="text-2xl font-semibold mb-4">Gewährleistung</h2>
|
||||
<p className="leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t border-gray-700">
|
||||
<p className="text-gray-400 text-sm">Letzte Aktualisierung: 12.02.2025</p>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</main>
|
||||
|
||||
@@ -15,7 +15,6 @@ export default function PrivacyPolicy() {
|
||||
const locale = useLocale();
|
||||
const t = useTranslations("common");
|
||||
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
||||
const [cmsTitle, setCmsTitle] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -24,42 +23,33 @@ export default function PrivacyPolicy() {
|
||||
`/api/content/page?key=${encodeURIComponent("privacy-policy")}&locale=${encodeURIComponent(locale)}`,
|
||||
);
|
||||
const data = await res.json();
|
||||
// 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);
|
||||
}
|
||||
} catch {}
|
||||
})();
|
||||
}, [locale]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen animated-bg">
|
||||
<div className="min-h-screen bg-[#fdfcf8] dark:bg-stone-950 transition-colors duration-500">
|
||||
<Header />
|
||||
<main className="max-w-4xl mx-auto px-4 pt-32 pb-20">
|
||||
<main className="max-w-7xl mx-auto px-6 pt-40 pb-20">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="mb-8"
|
||||
className="mb-12"
|
||||
>
|
||||
<motion.a
|
||||
<Link
|
||||
href={`/${locale}`}
|
||||
className="inline-flex items-center space-x-2 text-blue-400 hover:text-blue-300 transition-colors mb-6"
|
||||
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
<span>{t("backToHome")}</span>
|
||||
</motion.a>
|
||||
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" />
|
||||
<span className="font-bold uppercase tracking-widest text-xs">{t("backToHome")}</span>
|
||||
</Link>
|
||||
|
||||
<h1 className="text-4xl md:text-5xl font-bold mb-6 gradient-text">
|
||||
{cmsTitle || "Datenschutzerklärung"}
|
||||
<h1 className="text-6xl md:text-[8rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase">
|
||||
Privacy<span className="text-liquid-purple">.</span>
|
||||
</h1>
|
||||
</motion.div>
|
||||
|
||||
@@ -67,265 +57,29 @@ export default function PrivacyPolicy() {
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
className="glass-card p-8 rounded-2xl space-y-6 text-white"
|
||||
className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 md:p-16 border border-stone-200/60 dark:border-stone-800/60 shadow-sm"
|
||||
>
|
||||
{cmsDoc ? (
|
||||
<RichTextClient doc={cmsDoc} className="prose prose-invert max-w-none text-gray-300" />
|
||||
<div className="prose prose-stone dark:prose-invert max-w-none text-lg md:text-xl font-light leading-relaxed">
|
||||
<RichTextClient doc={cmsDoc} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-gray-300 leading-relaxed">
|
||||
<p>
|
||||
Der Schutz Ihrer persönlichen Daten ist mir wichtig. In dieser Datenschutzerklärung informiere ich Sie
|
||||
über die Verarbeitung personenbezogener Daten im Rahmen meines Internet-Angebots.
|
||||
<div className="space-y-12">
|
||||
<div className="text-stone-600 dark:text-stone-400 leading-relaxed">
|
||||
<h2 className="text-3xl font-black text-stone-900 dark:text-stone-100 mb-6 uppercase tracking-tight">Datenschutz</h2>
|
||||
<p className="text-xl font-light leading-relaxed mb-6">
|
||||
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>
|
||||
<h3 className="text-2xl font-bold text-stone-800 dark:text-stone-200 mb-4">Kontakt mit mir</h3>
|
||||
<p className="text-xl font-light">
|
||||
Wenn Sie per Formular auf der Website oder per E-Mail Kontakt mit mir aufnehmen, werden Ihre angegebenen Daten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen bei mir gespeichert. Diese Daten gebe ich nicht ohne Ihre Einwilligung weiter.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-gray-300 leading-relaxed">
|
||||
<h2 className="text-2xl font-semibold mb-4">Verantwortlicher für die Datenverarbeitung</h2>
|
||||
<div className="space-y-2 text-gray-300">
|
||||
<p>
|
||||
<strong>Name:</strong> Dennis Konkol
|
||||
</p>
|
||||
<p>
|
||||
<strong>Adresse:</strong> Auf dem Ziegenbrink 2B, 49082 Osnabrück, Deutschland
|
||||
</p>
|
||||
<p>
|
||||
<strong>E-Mail:</strong>{" "}
|
||||
<Link className="text-blue-400 hover:text-blue-300 transition-colors" href="mailto:info@dk0.dev">
|
||||
info@dk0.dev
|
||||
</Link>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Website:</strong>{" "}
|
||||
<Link className="text-blue-400 hover:text-blue-300 transition-colors" href="https://www.dk0.dev">
|
||||
dk0.dev
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-4">
|
||||
Diese Datenschutzerklärung gilt für die Verarbeitung personenbezogener Daten durch den oben genannten
|
||||
Verantwortlichen.
|
||||
</p>
|
||||
<div className="pt-8 border-t border-stone-100 dark:border-stone-800">
|
||||
<p className="text-stone-400 text-sm font-mono uppercase tracking-widest">Last updated: 12.02.2025</p>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-6">
|
||||
Erfassung allgemeiner Informationen beim Besuch meiner Website
|
||||
</h2>
|
||||
<div className="mt-2">
|
||||
Beim Zugriff auf meiner Website werden automatisch Informationen allgemeiner Natur erfasst. Diese
|
||||
beinhalten unter anderem:
|
||||
<ul className="list-disc list-inside mt-2">
|
||||
<li>IP-Adresse (in anonymisierter Form)</li>
|
||||
<li>Uhrzeit</li>
|
||||
<li>Browsertyp</li>
|
||||
<li>Verwendetes Betriebssystem</li>
|
||||
<li>Referrer-URL (die zuvor besuchte Seite)</li>
|
||||
</ul>
|
||||
<br />
|
||||
Diese Informationen werden anonymisiert erfasst und dienen ausschließlich statistischen Auswertungen.
|
||||
Rückschlüsse auf Ihre Person sind nicht möglich. Diese Daten werden verarbeitet, um:
|
||||
<ul className="list-disc list-inside mt-2">
|
||||
<li>die Inhalte meiner Website korrekt auszuliefern,</li>
|
||||
<li>die Inhalte meiner Website zu optimieren,</li>
|
||||
<li>die Systemsicherheit und -stabilität zu analysiern.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-6">Cookies</h2>
|
||||
<p className="mt-2">
|
||||
Diese Website verwendet ein technisch notwendiges Cookie, um deine Datenschutz-Einstellungen (z.B.
|
||||
Analytics/Chatbot) zu speichern. Ohne dieses Cookie wäre ein Consent-Banner bei jedem Besuch erneut
|
||||
nötig.
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-6">Analyse- und Tracking-Tools</h2>
|
||||
<p className="mt-2">
|
||||
Die nachfolgend beschriebene Analyse- und Tracking-Methode (im
|
||||
Folgenden „Maßnahme“ genannt) basiert auf Art. 6 Abs. 1 S. 1 lit. f
|
||||
DSGVO. Durch diese Maßnahme möchten ich eine benutzerfreundliche
|
||||
Gestaltung sowie eine kontinuierliche Verbesserung meiner Website
|
||||
sicherstellen. Diese Interessen sind im Sinne der genannten Vorschrift
|
||||
als berechtigt anzusehen.
|
||||
<br />
|
||||
<br />
|
||||
Rechtsgrundlage: Art. 6 Abs. 1 S. 1 lit. f DSGVO (berechtigtes
|
||||
Interesse an der Analyse und Optimierung unserer Website).
|
||||
<br />
|
||||
<br />
|
||||
Detaillierte Informationen zu den erhobenen Daten und deren
|
||||
Verarbeitung finden Sie in den nachfolgenden Abschnitten.
|
||||
<br />
|
||||
<br />
|
||||
Zur Analyse der Nutzung meiner 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. Weitere Informationen finden Sie unter{" "}
|
||||
<Link
|
||||
className="text-blue-700 transition-underline"
|
||||
href={"https://umami.is"}
|
||||
>
|
||||
Umami
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p className="mt-4">
|
||||
Zusätzlich kann diese Website optionale, selbst gehostete
|
||||
Nutzungsstatistiken erfassen (z.B. Seitenaufrufe, Performance-Metriken),
|
||||
die erst nach deiner Einwilligung im Consent-Banner aktiviert werden.
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-6">Error Monitoring (Sentry)</h2>
|
||||
<p className="mt-2">
|
||||
Um Fehler und Probleme auf dieser Website schnell zu erkennen und zu beheben,
|
||||
nutze ich Sentry.io, einen Dienst zur Fehlerüberwachung. Dabei werden technische
|
||||
Informationen wie Browser-Typ, Betriebssystem, URL der aufgerufenen Seite und
|
||||
Fehlermeldungen an Sentry übermittelt. Diese Daten dienen ausschließlich der
|
||||
Verbesserung der Website-Stabilität und werden nicht für andere Zwecke verwendet.
|
||||
<br />
|
||||
<br />
|
||||
Anbieter: Functional Software, Inc. (Sentry), 45 Fremont Street, 8th Floor,
|
||||
San Francisco, CA 94105, USA
|
||||
<br />
|
||||
<br />
|
||||
Rechtsgrundlage: Art. 6 Abs. 1 S. 1 lit. f DSGVO (berechtigtes Interesse an
|
||||
der Fehleranalyse und Systemstabilität).
|
||||
<br />
|
||||
<br />
|
||||
Weitere Informationen: <Link
|
||||
className="text-blue-700 transition-underline"
|
||||
href={"https://sentry.io/privacy/"}
|
||||
>
|
||||
Sentry Datenschutzerklärung
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-6">Kontaktformular</h2>
|
||||
<p className="mt-2">
|
||||
Wenn Sie das Kontaktformular nutzen, werden Ihre Angaben zur
|
||||
Bearbeitung Ihrer Anfrage gespeichert. Diese Daten werden nicht an
|
||||
Dritte weitergegeben und nach Erfüllung des Zwecks gelöscht. <br />
|
||||
<br />
|
||||
Rechtsgrundlage: Art. 6 Abs. 1 S. 1 lit. a DSGVO (Einwilligung).
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mt-6">Chatbot</h2>
|
||||
<p className="mt-2">
|
||||
Wenn du den optionalen Chatbot nutzt, werden die von dir eingegebenen
|
||||
Nachrichten verarbeitet, um eine Antwort zu generieren. Die Verarbeitung
|
||||
kann dabei über eine selbst gehostete Automations-/Chat-Infrastruktur
|
||||
(z.B. n8n) erfolgen. Bitte gib im Chat keine sensiblen Daten ein.
|
||||
<br />
|
||||
<br />
|
||||
Rechtsgrundlage: Art. 6 Abs. 1 S. 1 lit. a DSGVO (Einwilligung) – der
|
||||
Chatbot wird erst nach Aktivierung im Consent-Banner geladen.
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mt-6">Social Media Links</h2>
|
||||
<p className="mt-2">
|
||||
Unsere Website enthält Links zu GitHub und LinkedIn. Durch das
|
||||
Anklicken dieser Links gelten die Datenschutzbestimmungen der
|
||||
jeweiligen Anbieter.
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mt-6">Weitergabe von Daten</h2>
|
||||
<div className="mt-2">
|
||||
Eine Weitergabe Ihrer personenbezogenen Daten erfolgt nur, wenn:
|
||||
<ul className="list-disc list-inside mt-2">
|
||||
<li>
|
||||
Sie nach Art. 6 Abs. 1 S. 1 lit. a DSGVO ausdrücklich eingewilligt
|
||||
haben,
|
||||
</li>
|
||||
<li>
|
||||
dies zur Vertragserfüllung gemäß Art. 6 Abs. 1 S. 1 lit. b DSGVO
|
||||
erforderlich ist,
|
||||
</li>
|
||||
<li>
|
||||
eine gesetzliche Verpflichtung zur Weitergabe nach Art. 6 Abs. 1
|
||||
S. 1 lit. c DSGVO besteht oder
|
||||
</li>
|
||||
<li>
|
||||
die Verarbeitung nach Art. 6 Abs. 1 S. 1 lit. f DSGVO zur Wahrung
|
||||
berechtigter Interessen erforderlich ist.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold mt-6">
|
||||
Speicherdauer und Löschung
|
||||
</h2>
|
||||
<p className="mt-2">
|
||||
Ihre Daten werden nur solange gespeichert, wie dies für die Erfüllung
|
||||
des Verarbeitungszwecks erforderlich ist. Nach Erfüllung des Zwecks
|
||||
werden Ihre Daten gelöscht.
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mt-6">Ihre Rechte</h2>
|
||||
<div className="mt-2">
|
||||
Sie haben gemäß DSGVO folgende Rechte:
|
||||
<ul className="list-disc list-inside mt-2">
|
||||
<li>
|
||||
Art. 15 DSGVO: Auskunftsrecht über Ihre von mir gespeicherten
|
||||
Daten
|
||||
</li>
|
||||
<li>
|
||||
Art. 16 DSGVO: Recht auf Berichtigung unrichtiger oder
|
||||
unvollständiger Daten
|
||||
</li>
|
||||
<li>
|
||||
Art. 17 DSGVO: Recht auf Löschung Ihrer bei mir gespeicherten
|
||||
Daten (soweit keine gesetzlichen Aufbewahrungspflichten
|
||||
entgegenstehen)
|
||||
</li>
|
||||
<li>
|
||||
Art. 18 DSGVO: Recht auf Einschränkung der Verarbeitung Ihrer
|
||||
Daten
|
||||
</li>
|
||||
<li>Art. 20 DSGVO: Recht auf Datenübertragbarkeit</li>
|
||||
<li>
|
||||
Art. 21 DSGVO: Widerspruchsrecht gegen die Verarbeitung Ihrer
|
||||
Daten
|
||||
</li>
|
||||
</ul>
|
||||
<br />
|
||||
Falls Sie eine Einwilligung erklärt haben, können Sie diese jederzeit
|
||||
widerrufen.
|
||||
<br />
|
||||
Beschwerden können Sie an die zuständige Datenschutzaufsichtsbehörde
|
||||
richten. Eine Liste der Datenschutzbeauftragten sowie deren
|
||||
Kontaktdaten finden Sie unter:{" "}
|
||||
<Link
|
||||
className="text-blue-700 transition-underline"
|
||||
href={"https://www.bfdi.bund.de/"}
|
||||
>
|
||||
https://www.bfdi.bund.de/
|
||||
</Link>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold mt-6">Datensicherheit</h2>
|
||||
<p className="mt-2">
|
||||
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>
|
||||
<h2 className="text-2xl font-semibold mt-6">Kontakt</h2>
|
||||
<p className="mt-2">
|
||||
Bei Fragen zur Datenschutzerklärung kontaktieren Sie mich unter{" "}
|
||||
<Link
|
||||
href="mailto:info@dk0.dev"
|
||||
className="text-blue-700 transition-underline"
|
||||
>
|
||||
info@dk0.dev
|
||||
</Link>{" "}
|
||||
oder nutzen Sie das Kontaktformular auf meiner Website.
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mt-6">
|
||||
Änderungen der Datenschutzerklärung
|
||||
</h2>
|
||||
<p className="mt-2">
|
||||
Diese Datenschutzerklärung wird regelmäßig aktualisiert, um den
|
||||
gesetzlichen Anforderungen zu entsprechen und neue Entwicklungen zu
|
||||
berücksichtigen. Die jeweils aktuelle Datenschutzerklärung finden Sie
|
||||
auf meiner Website.
|
||||
</p>
|
||||
<div className="pt-4 border-t border-gray-700">
|
||||
<p className="text-gray-400 text-sm">Letzte Aktualisierung: 12.02.2025</p>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</main>
|
||||
|
||||
53
scripts/fix-messages-collection.js
Normal file
53
scripts/fix-messages-collection.js
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env node
|
||||
require('dotenv').config();
|
||||
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
|
||||
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
|
||||
|
||||
async function api(endpoint, method = 'POST', body = null) {
|
||||
const res = await fetch(`${DIRECTUS_URL}/${endpoint}`, {
|
||||
method,
|
||||
headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' },
|
||||
body: body ? JSON.stringify(body) : null
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return { ok: res.ok, data };
|
||||
}
|
||||
|
||||
async function fixMessagesCollection() {
|
||||
console.log('🛠 Repariere "messages" Collection...');
|
||||
|
||||
// 1. Key-Feld hinzufügen (falls es fehlt)
|
||||
// Wir nutzen type: string und schema: {} um eine echte Spalte zu erzeugen
|
||||
const fieldRes = await api('fields/messages', 'POST', {
|
||||
field: 'key',
|
||||
type: 'string',
|
||||
schema: {
|
||||
is_nullable: false,
|
||||
is_unique: true
|
||||
},
|
||||
meta: {
|
||||
interface: 'input',
|
||||
options: { placeholder: 'z.B. hero.title' },
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
if (fieldRes.ok) {
|
||||
console.log('✅ "key" Feld erfolgreich erstellt.');
|
||||
} else {
|
||||
console.log('⚠️ "key" Feld konnte nicht erstellt werden (existiert evtl schon).');
|
||||
}
|
||||
|
||||
// 2. Übersetzungs-Feld in der Untertabelle reparieren
|
||||
console.log('🛠 Prüfe messages_translations...');
|
||||
await api('fields/messages_translations', 'POST', {
|
||||
field: 'value',
|
||||
type: 'text',
|
||||
schema: {},
|
||||
meta: { interface: 'input-multiline' }
|
||||
}).catch(() => {});
|
||||
|
||||
console.log('✅ Fix abgeschlossen! Bitte lade Directus neu.');
|
||||
}
|
||||
|
||||
fixMessagesCollection().catch(console.error);
|
||||
37
scripts/seed-cms-content.js
Normal file
37
scripts/seed-cms-content.js
Normal file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
require('dotenv').config();
|
||||
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://cms.dk0.dev';
|
||||
const DIRECTUS_TOKEN = process.env.DIRECTUS_STATIC_TOKEN;
|
||||
|
||||
async function api(endpoint, method = 'POST', body = null) {
|
||||
const res = await fetch(`${DIRECTUS_URL}/${endpoint}`, {
|
||||
method,
|
||||
headers: { 'Authorization': `Bearer ${DIRECTUS_TOKEN}`, 'Content-Type': 'application/json' },
|
||||
body: body ? JSON.stringify(body) : null
|
||||
});
|
||||
return res.ok ? await res.json() : { error: true, status: res.status };
|
||||
}
|
||||
|
||||
const seedData = [
|
||||
{ key: 'hero.badge', de: 'Student & Self-Hoster', en: 'Student & Self-Hoster' },
|
||||
{ key: 'hero.line1', de: 'Building', en: 'Building' },
|
||||
{ key: 'hero.line2', de: 'Stuff.', en: 'Stuff.' },
|
||||
{ key: 'about.quote.idle', de: 'Gerade am Planen des nächsten großen Projekts.', en: 'Currently planning the next big thing.' }
|
||||
];
|
||||
|
||||
async function seedMessages() {
|
||||
console.log('🌱 Befülle Directus mit Inhalten...');
|
||||
for (const item of seedData) {
|
||||
console.log(`- Erstelle Key: ${item.key}`);
|
||||
const res = await api('items/messages', 'POST', {
|
||||
key: item.key,
|
||||
translations: [
|
||||
{ languages_code: 'de-DE', value: item.de },
|
||||
{ languages_code: 'en-US', value: item.en }
|
||||
]
|
||||
});
|
||||
}
|
||||
console.log('✅ CMS erfolgreich befüllt!');
|
||||
}
|
||||
|
||||
seedMessages().catch(console.error);
|
||||
Reference in New Issue
Block a user