Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Failing after 9m19s
Fixed missing types, import errors, and updated test suites to match the new editorial design. Verified Docker container build.
229 lines
11 KiB
TypeScript
229 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, BookOpen, MessageSquare, ArrowRight } 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";
|
|
import Link from "next/link";
|
|
import ActivityFeed from "./ActivityFeed";
|
|
import BentoChat from "./BentoChat";
|
|
import { Skeleton } from "./ui/Skeleton";
|
|
|
|
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);
|
|
const [techStack, setTechStack] = useState<TechStackCategory[]>([]);
|
|
const [hobbies, setHobbies] = useState<Hobby[]>([]);
|
|
const [reviewsCount, setReviewsCount] = useState(0);
|
|
const [cmsMessages, setCmsMessages] = useState<Record<string, string>>({});
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const [cmsRes, techRes, hobbiesRes, msgRes, booksRes] = 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/messages?locale=${locale}`),
|
|
fetch(`/api/book-reviews?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);
|
|
|
|
const booksData = await booksRes.json();
|
|
if (booksData?.bookReviews) setReviewsCount(booksData.bookReviews.length);
|
|
} catch (error) {
|
|
console.error("About data fetch failed:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, [locale]);
|
|
|
|
return (
|
|
<section id="about" className="py-32 px-6 bg-[#fdfcf8] dark:bg-stone-950 transition-colors duration-500">
|
|
<div className="max-w-7xl mx-auto">
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-8">
|
|
|
|
{/* 1. Large Bio Text */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
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 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">
|
|
{isLoading ? (
|
|
<div className="space-y-3">
|
|
<Skeleton className="h-6 w-full" />
|
|
<Skeleton className="h-6 w-[95%]" />
|
|
<Skeleton className="h-6 w-[90%]" />
|
|
</div>
|
|
) : cmsDoc ? (
|
|
<RichTextClient doc={cmsDoc} />
|
|
) : (
|
|
<p>{t("p1")} {t("p2")}</p>
|
|
)}
|
|
</div>
|
|
<div className="pt-8">
|
|
<div className="inline-block bg-stone-50 dark:bg-stone-800 px-8 py-4 rounded-3xl border border-stone-100 dark:border-stone-700">
|
|
<p className="text-[10px] font-black uppercase tracking-[0.2em] text-liquid-mint mb-2">{t("funFactTitle")}</p>
|
|
{isLoading ? <Skeleton className="h-5 w-48" /> : <p className="text-base font-bold opacity-90">{t("funFactBody")}</p>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* 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 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">
|
|
<h3 className="text-xl font-black mb-10 flex items-center gap-2 uppercase tracking-widest text-liquid-mint">
|
|
<Activity size={20} /> Status
|
|
</h3>
|
|
<ActivityFeed idleQuote={cmsMessages["about.quote.idle"]} />
|
|
</div>
|
|
<div className="absolute top-0 right-0 w-40 h-40 bg-liquid-mint/10 blur-[100px] rounded-full" />
|
|
</motion.div>
|
|
|
|
{/* 3. AI Chat Box */}
|
|
<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-4 bg-stone-50 dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 flex flex-col shadow-sm"
|
|
>
|
|
<div className="flex items-center gap-2 mb-8">
|
|
<MessageSquare className="text-liquid-purple" size={24} />
|
|
<h3 className="text-2xl font-black text-stone-900 dark:text-stone-50 uppercase tracking-tighter text-liquid-purple">AI Assistant</h3>
|
|
</div>
|
|
<div className="flex-1">
|
|
<BentoChat />
|
|
</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 sm:grid-cols-2 md:grid-cols-4 gap-12">
|
|
{isLoading ? (
|
|
Array.from({ length: 4 }).map((_, i) => (
|
|
<div key={i} className="space-y-6">
|
|
<Skeleton className="h-3 w-20" />
|
|
<div className="flex flex-wrap gap-2">
|
|
<Skeleton className="h-8 w-24 rounded-xl" />
|
|
<Skeleton className="h-8 w-16 rounded-xl" />
|
|
<Skeleton className="h-8 w-20 rounded-xl" />
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
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>
|
|
<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 rounded-xl text-xs font-bold border border-stone-100 dark:border-stone-700/50 hover:border-liquid-mint transition-colors">
|
|
{item.name}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
{/* 5. Library & Hobbies */}
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ delay: 0.4 }}
|
|
className="md:col-span-12 grid grid-cols-1 md:grid-cols-2 gap-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 className="mt-6">
|
|
<ReadBooks />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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-10">
|
|
{isLoading ? (
|
|
Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="w-12 h-12 rounded-2xl" />)
|
|
) : (
|
|
hobbies.map((hobby) => {
|
|
const Icon = iconMap[hobby.icon] || Lightbulb;
|
|
return (
|
|
<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 text-lg">Curiosity beyond software engineering.</p>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export default About;
|