"use client"; import { motion, Variants } from "framer-motion"; import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb } from "lucide-react"; import { useEffect, useState } from "react"; import { useLocale, useTranslations } from "next-intl"; import type { JSONContent } from "@tiptap/react"; import RichTextClient from "./RichTextClient"; import CurrentlyReading from "./CurrentlyReading"; // Type definitions for CMS data interface TechStackItem { id: string; name: string | number | null | undefined; url?: string; icon_url?: string; sort: number; } interface TechStackCategory { id: string; key: string; icon: string; sort: number; name: string; items: TechStackItem[]; } interface Hobby { id: string; key: string; icon: string; title: string | number | null | undefined; description?: string; } const staggerContainer: Variants = { hidden: { opacity: 0 }, visible: { opacity: 1, transition: { staggerChildren: 0.15, delayChildren: 0.2, }, }, }; const fadeInUp: Variants = { hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { duration: 0.5, ease: [0.25, 0.1, 0.25, 1], }, }, }; const About = () => { const locale = useLocale(); const t = useTranslations("home.about"); const [cmsDoc, setCmsDoc] = useState(null); const [techStackFromCMS, setTechStackFromCMS] = useState(null); const [hobbiesFromCMS, setHobbiesFromCMS] = useState(null); useEffect(() => { (async () => { try { const res = await fetch( `/api/content/page?key=${encodeURIComponent("home-about")}&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); } else { setCmsDoc(null); } } catch { // ignore; fallback to static setCmsDoc(null); } })(); }, [locale]); // Load Tech Stack from Directus useEffect(() => { (async () => { try { const res = await fetch(`/api/tech-stack?locale=${encodeURIComponent(locale)}`); if (res.ok) { const data = await res.json(); if (data?.techStack && data.techStack.length > 0) { setTechStackFromCMS(data.techStack); } } } catch (error) { if (process.env.NODE_ENV === 'development') { console.log('Tech Stack from Directus not available, using fallback'); } } })(); }, [locale]); // Load Hobbies from Directus useEffect(() => { (async () => { try { const res = await fetch(`/api/hobbies?locale=${encodeURIComponent(locale)}`); if (res.ok) { const data = await res.json(); if (data?.hobbies && data.hobbies.length > 0) { setHobbiesFromCMS(data.hobbies); } } } catch (error) { if (process.env.NODE_ENV === 'development') { console.log('Hobbies from Directus not available, using fallback'); } } })(); }, [locale]); // Fallback Tech Stack (from messages/en.json, messages/de.json) const techStackFallback = [ { key: 'frontend', category: t("techStack.categories.frontendMobile"), icon: Globe, items: ["Next.js", "Tailwind CSS", "Flutter"], }, { key: 'backend', category: t("techStack.categories.backendDevops"), icon: Server, items: ["Docker Swarm", "Traefik", "Nginx Proxy Manager", "Redis"], }, { key: 'tools', category: t("techStack.categories.toolsAutomation"), icon: Wrench, items: ["Git", "CI/CD", "n8n", t("techStack.items.selfHostedServices")], }, { key: 'security', category: t("techStack.categories.securityAdmin"), icon: Shield, items: ["CrowdSec", "Suricata", "Mailcow"], }, ]; // Map icon names from Directus to Lucide components const iconMap: Record = { Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2 }; // Fallback Hobbies const hobbiesFallback: Array<{ icon: typeof Code; text: string }> = [ { icon: Code, text: t("hobbies.selfHosting") }, { icon: Gamepad2, text: t("hobbies.gaming") }, { icon: Server, text: t("hobbies.gameServers") }, { icon: Activity, text: t("hobbies.jogging") }, ]; // Use CMS Hobbies if available, otherwise fallback const hobbies = hobbiesFromCMS ? hobbiesFromCMS .map((hobby: Hobby) => { // Convert to string, handling NaN/null/undefined const text = hobby.title == null || (typeof hobby.title === 'number' && isNaN(hobby.title)) ? '' : String(hobby.title); return { icon: iconMap[hobby.icon] || Code, text }; }) .filter(h => { const isValid = h.text.trim().length > 0; if (!isValid && process.env.NODE_ENV === 'development') { console.log('[About] Filtered out invalid hobby:', h); } return isValid; }) : hobbiesFallback; // Use CMS Tech Stack if available, otherwise fallback const techStack = techStackFromCMS ? techStackFromCMS.map((cat: TechStackCategory) => { const items = cat.items .map((item: TechStackItem) => { // Convert to string, handling NaN/null/undefined if (item.name == null || (typeof item.name === 'number' && isNaN(item.name))) { if (process.env.NODE_ENV === 'development') { console.log('[About] Invalid item.name in category', cat.key, ':', item); } return ''; } return String(item.name); }) .filter(name => { const isValid = name.trim().length > 0; if (!isValid && process.env.NODE_ENV === 'development') { console.log('[About] Filtered out empty item name in category', cat.key); } return isValid; }); if (items.length === 0 && process.env.NODE_ENV === 'development') { console.warn('[About] Category has no valid items after filtering:', cat.key); } return { key: cat.key, category: cat.name, icon: iconMap[cat.icon] || Code, items }; }) : techStackFallback; return (
{/* Text Content */} {t("title")} {cmsDoc ? ( ) : ( <>

{t("p1")}

{t("p2")}

{t("p3")}

)}

{t("funFactTitle")}

{t("funFactBody")}

{/* Tech Stack & Hobbies */}
{t("techStackTitle")}
{techStack.map((stack, idx) => (

{stack.category}

{stack.items.map((item, itemIdx) => ( {String(item)} ))}
))}
{/* Hobbies */}
{t("hobbiesTitle")}
{hobbies.map((hobby, idx) => ( {String(hobby.text)} ))}
{/* Currently Reading */}
); }; export default About;