Hardened rich text conversion logic to handle malformed Tiptap documents and added null checks for CMS data in About section.
213 lines
9.5 KiB
TypeScript
213 lines
9.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { BentoGrid, BentoGridItem } from "./ui/BentoGrid";
|
|
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, MapPin, User, BookOpen } 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 { TechStackCategory, Hobby } from "@/lib/directus";
|
|
|
|
// Map icon names from Directus to Lucide components
|
|
const iconMap: Record<string, any> = {
|
|
Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2
|
|
};
|
|
|
|
const About = () => {
|
|
const locale = useLocale();
|
|
const t = useTranslations("home.about");
|
|
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
|
|
|
// Data State
|
|
const [techStack, setTechStack] = useState<TechStackCategory[]>([]);
|
|
const [hobbies, setHobbies] = useState<Hobby[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [cmsRes, techRes, hobbiesRes] = await Promise.all([
|
|
fetch(`/api/content/page?key=home-about&locale=${locale}`),
|
|
fetch(`/api/tech-stack?locale=${locale}`),
|
|
fetch(`/api/hobbies?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);
|
|
} catch (error) {
|
|
console.error("Error fetching about data:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, [locale]);
|
|
|
|
return (
|
|
<section id="about" className="py-24 md:py-32 px-4 relative bg-stone-50 dark:bg-stone-950 transition-colors duration-500">
|
|
{/* Background Decor */}
|
|
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20 pointer-events-none mix-blend-soft-light"></div>
|
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full h-[500px] bg-gradient-to-b from-liquid-mint/10 dark:from-liquid-mint/5 via-transparent to-transparent blur-3xl pointer-events-none"></div>
|
|
|
|
<div className="max-w-7xl mx-auto mb-12 md:mb-20 text-center relative z-10">
|
|
<motion.h2
|
|
initial={{ opacity: 0, y: 20 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
className="text-4xl md:text-7xl font-black text-stone-900 dark:text-stone-50 tracking-tighter"
|
|
>
|
|
{t("title")}
|
|
</motion.h2>
|
|
</div>
|
|
|
|
<BentoGrid className="max-w-6xl mx-auto relative z-10">
|
|
|
|
{/* 1. Bio (2x2) */}
|
|
<BentoGridItem
|
|
className="md:col-span-2 md:row-span-2 bg-gradient-to-br from-white to-stone-50 dark:from-stone-900 dark:to-stone-950 border-stone-200/60 dark:border-stone-800/60"
|
|
title={
|
|
<div className="flex items-center gap-2 text-lg">
|
|
<User size={20} className="text-liquid-mint" />
|
|
<span className="font-black">Dennis Konkol</span>
|
|
</div>
|
|
}
|
|
description={
|
|
<div className="mt-4 prose prose-stone dark:prose-invert max-w-none text-stone-600 dark:text-stone-400 leading-relaxed">
|
|
{cmsDoc ? (
|
|
<RichTextClient doc={cmsDoc} />
|
|
) : (
|
|
<p className="text-lg font-light">
|
|
{t("p1")} {t("p2")} {t("p3")}
|
|
</p>
|
|
)}
|
|
</div>
|
|
}
|
|
header={
|
|
<div className="w-full h-48 md:h-64 bg-stone-200 dark:bg-stone-800 rounded-2xl mb-4 overflow-hidden relative group shadow-inner">
|
|
<motion.div
|
|
initial={{ scale: 1.1 }}
|
|
whileInView={{ scale: 1 }}
|
|
transition={{ duration: 1.5 }}
|
|
className="absolute inset-0 bg-[url('/images/me.jpg')] bg-cover bg-center opacity-60 dark:opacity-40 group-hover:scale-105 transition-transform duration-1000"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-t from-stone-50 dark:from-stone-950 via-transparent to-transparent" />
|
|
<div className="absolute bottom-6 left-6 z-10">
|
|
<div className="bg-white/80 dark:bg-black/40 backdrop-blur-md px-4 py-1.5 rounded-full border border-white/20 shadow-sm">
|
|
<span className="font-mono text-xs font-bold text-stone-800 dark:text-white uppercase tracking-widest">Available for hire</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{/* 2. Tech Stack */}
|
|
<BentoGridItem
|
|
className="md:col-span-1 bg-white dark:bg-stone-900 border-stone-200/60 dark:border-stone-800/60"
|
|
title={t("techStackTitle")}
|
|
description="Tools & Technologies"
|
|
header={
|
|
<div className="flex flex-wrap gap-2 p-2 overflow-y-auto max-h-40 scrollbar-hide">
|
|
{techStack && techStack.length > 0 ? (
|
|
techStack.flatMap(cat => cat.items?.map((item: any) => (
|
|
<motion.span
|
|
key={item.id}
|
|
whileHover={{ scale: 1.05 }}
|
|
className="px-3 py-1 bg-stone-100 dark:bg-stone-800 rounded-lg text-xs font-bold border border-stone-200/50 dark:border-stone-700/50 text-stone-700 dark:text-stone-300"
|
|
>
|
|
{item.name}
|
|
</motion.span>
|
|
)))
|
|
) : (
|
|
<div className="flex gap-2 flex-wrap">
|
|
{[1,2,3,4,5,6].map(i => <div key={i} className="h-6 w-16 bg-stone-100 dark:bg-stone-800 rounded animate-pulse" />)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
}
|
|
icon={<Code className="h-4 w-4 text-liquid-mint" />}
|
|
/>
|
|
|
|
{/* 3. Location */}
|
|
<BentoGridItem
|
|
className="md:col-span-1 bg-white dark:bg-stone-900 border-stone-200/60 dark:border-stone-800/60"
|
|
title="Osnabrück, Germany"
|
|
description="UTC+1 (CET)"
|
|
icon={<MapPin className="h-4 w-4 text-liquid-rose" />}
|
|
header={
|
|
<div className="relative w-full h-full min-h-[6rem] rounded-xl overflow-hidden bg-stone-100 dark:bg-stone-800 flex items-center justify-center">
|
|
<div className="absolute inset-0 opacity-20 dark:opacity-10 bg-[url('https://www.transparenttextures.com/patterns/carbon-fibre.png')]" />
|
|
<div className="relative z-10 flex flex-col items-center">
|
|
<div className="flex items-center gap-2 bg-white dark:bg-stone-950 px-4 py-2 rounded-xl shadow-sm border border-stone-200/50 dark:border-stone-700/50">
|
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
|
<span className="text-sm font-bold font-mono">52.27° N, 8.04° E</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{/* 4. Currently Reading & Reviews (Long) */}
|
|
<BentoGridItem
|
|
className="md:col-span-1 md:row-span-2 bg-white dark:bg-stone-900 border-stone-200/60 dark:border-stone-800/60"
|
|
title={null}
|
|
description={null}
|
|
header={
|
|
<div className="h-full flex flex-col">
|
|
<div className="font-black text-stone-900 dark:text-white mb-6 flex items-center gap-2 uppercase tracking-tighter">
|
|
<BookOpen size={18} className="text-liquid-purple" /> Reading Log
|
|
</div>
|
|
<div className="flex-1 space-y-8 overflow-y-auto pr-2 scrollbar-hide">
|
|
<CurrentlyReading />
|
|
<div className="pt-6 border-t border-stone-100 dark:border-stone-800">
|
|
<ReadBooks />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
|
|
{/* 5. Hobbies (Wide) */}
|
|
<BentoGridItem
|
|
className="md:col-span-2 bg-white dark:bg-stone-900 border-stone-200/60 dark:border-stone-800/60"
|
|
title={t("hobbiesTitle")}
|
|
description="Beyond the screen"
|
|
header={
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mt-2">
|
|
{hobbies && hobbies.length > 0 ? hobbies.map((hobby, i) => {
|
|
const Icon = iconMap[hobby.icon] || Lightbulb;
|
|
return (
|
|
<motion.div
|
|
key={hobby.id}
|
|
whileHover={{ y: -3 }}
|
|
className="flex flex-col items-center justify-center p-4 bg-stone-50 dark:bg-stone-800/40 rounded-2xl border border-stone-100 dark:border-stone-700/30 transition-colors"
|
|
>
|
|
<Icon size={24} className="mb-2 text-stone-700 dark:text-stone-300" />
|
|
<span className="text-[10px] font-bold uppercase tracking-widest text-center text-stone-500">{hobby.title}</span>
|
|
</motion.div>
|
|
)
|
|
}) : (
|
|
[1,2,3,4].map(i => <div key={i} className="h-20 bg-stone-50 dark:bg-stone-800 animate-pulse rounded-2xl" />)
|
|
)}
|
|
</div>
|
|
}
|
|
icon={<Gamepad2 className="h-4 w-4 text-liquid-purple" />}
|
|
/>
|
|
|
|
</BentoGrid>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default About;
|