style: refined bento layout and bio structure
Improved About section with side-by-side bio and photo, removed row constraints to prevent text overlap, and added consistent spacing.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { BentoGrid, BentoGridItem } from "./ui/BentoGrid";
|
import { BentoGrid, BentoGridItem } from "./ui/BentoGrid";
|
||||||
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, MapPin, User, BookOpen } from "lucide-react";
|
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, MapPin, User, BookOpen, ExternalLink } from "lucide-react";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import type { JSONContent } from "@tiptap/react";
|
import type { JSONContent } from "@tiptap/react";
|
||||||
import RichTextClient from "./RichTextClient";
|
import RichTextClient from "./RichTextClient";
|
||||||
@@ -10,8 +10,8 @@ import CurrentlyReading from "./CurrentlyReading";
|
|||||||
import ReadBooks from "./ReadBooks";
|
import ReadBooks from "./ReadBooks";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { TechStackCategory, Hobby } from "@/lib/directus";
|
import { TechStackCategory, Hobby } from "@/lib/directus";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
// Map icon names from Directus to Lucide components
|
|
||||||
const iconMap: Record<string, any> = {
|
const iconMap: Record<string, any> = {
|
||||||
Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2
|
Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2
|
||||||
};
|
};
|
||||||
@@ -20,191 +20,132 @@ const About = () => {
|
|||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const t = useTranslations("home.about");
|
const t = useTranslations("home.about");
|
||||||
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
const [cmsDoc, setCmsDoc] = useState<JSONContent | null>(null);
|
||||||
|
|
||||||
// Data State
|
|
||||||
const [techStack, setTechStack] = useState<TechStackCategory[]>([]);
|
const [techStack, setTechStack] = useState<TechStackCategory[]>([]);
|
||||||
const [hobbies, setHobbies] = useState<Hobby[]>([]);
|
const [hobbies, setHobbies] = useState<Hobby[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
const [cmsRes, techRes, hobbiesRes] = await Promise.all([
|
const [cmsRes, techRes, hobbiesRes] = await Promise.all([
|
||||||
fetch(`/api/content/page?key=home-about&locale=${locale}`),
|
fetch(`/api/content/page?key=home-about&locale=${locale}`),
|
||||||
fetch(`/api/tech-stack?locale=${locale}`),
|
fetch(`/api/tech-stack?locale=${locale}`),
|
||||||
fetch(`/api/hobbies?locale=${locale}`)
|
fetch(`/api/hobbies?locale=${locale}`)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const cmsData = await cmsRes.json();
|
const cmsData = await cmsRes.json();
|
||||||
if (cmsData?.content?.content) setCmsDoc(cmsData.content.content as JSONContent);
|
if (cmsData?.content?.content) setCmsDoc(cmsData.content.content as JSONContent);
|
||||||
|
|
||||||
const techData = await techRes.json();
|
const techData = await techRes.json();
|
||||||
if (techData?.techStack) setTechStack(techData.techStack);
|
if (techData?.techStack) setTechStack(techData.techStack);
|
||||||
|
|
||||||
const hobbiesData = await hobbiesRes.json();
|
const hobbiesData = await hobbiesRes.json();
|
||||||
if (hobbiesData?.hobbies) setHobbies(hobbiesData.hobbies);
|
if (hobbiesData?.hobbies) setHobbies(hobbiesData.hobbies);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching about data:", error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="about" className="py-24 md:py-32 px-4 relative bg-stone-50 dark:bg-stone-950 transition-colors duration-500">
|
<section id="about" className="py-24 px-4 bg-[#fdfcf8] dark:bg-stone-950 transition-colors duration-500">
|
||||||
{/* Background Decor */}
|
<div className="max-w-6xl mx-auto">
|
||||||
<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="grid grid-cols-1 md:grid-cols-4 gap-4 md:grid-rows-2">
|
||||||
<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>
|
|
||||||
|
{/* 1. Main Bio & Photo - The big anchor */}
|
||||||
<div className="max-w-7xl mx-auto mb-12 md:mb-20 text-center relative z-10">
|
<div className="md:col-span-3 md:row-span-1 bg-white dark:bg-stone-900 rounded-[2.5rem] p-8 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col md:flex-row gap-8 items-center">
|
||||||
<motion.h2
|
<div className="relative w-48 h-48 md:w-64 md:h-64 shrink-0">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<div className="absolute inset-0 bg-liquid-mint rounded-3xl rotate-3 scale-95 opacity-20 animate-pulse"></div>
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
<div className="relative w-full h-full rounded-3xl overflow-hidden border-4 border-white dark:border-stone-800 shadow-xl">
|
||||||
viewport={{ once: true }}
|
<Image
|
||||||
className="text-4xl md:text-7xl font-black text-stone-900 dark:text-stone-50 tracking-tighter"
|
src="/images/me.jpg"
|
||||||
>
|
alt="Dennis Konkol"
|
||||||
{t("title")}
|
fill
|
||||||
</motion.h2>
|
className="object-cover"
|
||||||
</div>
|
priority
|
||||||
|
/>
|
||||||
<BentoGrid className="max-w-6xl mx-auto relative z-10">
|
</div>
|
||||||
|
|
||||||
{/* 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>
|
</div>
|
||||||
}
|
<div className="flex-1 space-y-4">
|
||||||
description={
|
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-liquid-mint/10 text-liquid-mint text-[10px] font-black uppercase tracking-widest">
|
||||||
<div className="mt-4 prose prose-stone dark:prose-invert max-w-none text-stone-600 dark:text-stone-400 leading-relaxed">
|
<User size={12} /> Who am I
|
||||||
{cmsDoc ? (
|
</div>
|
||||||
<RichTextClient doc={cmsDoc} />
|
<h2 className="text-3xl md:text-5xl font-black text-stone-900 dark:text-stone-50 tracking-tighter">
|
||||||
) : (
|
Hi, I'm <span className="text-liquid-mint">Dennis</span>.
|
||||||
<p className="text-lg font-light">
|
</h2>
|
||||||
{t("p1")} {t("p2")} {t("p3")}
|
<div className="prose prose-stone dark:prose-invert max-w-none text-stone-600 dark:text-stone-400 text-base leading-relaxed line-clamp-4 md:line-clamp-none">
|
||||||
</p>
|
{cmsDoc ? <RichTextClient doc={cmsDoc} /> : <p>{t("p1")} {t("p2")}</p>}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
</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 */}
|
{/* 2. Status & Location */}
|
||||||
<BentoGridItem
|
<div className="md:col-span-1 md:row-span-1 bg-liquid-sky/5 dark:bg-stone-900 rounded-[2.5rem] p-8 border border-liquid-sky/20 dark:border-stone-800/60 flex flex-col justify-between group">
|
||||||
className="md:col-span-1 bg-white dark:bg-stone-900 border-stone-200/60 dark:border-stone-800/60"
|
<div className="w-12 h-12 rounded-2xl bg-white dark:bg-stone-800 flex items-center justify-center shadow-sm mb-4">
|
||||||
title={t("techStackTitle")}
|
<MapPin className="text-liquid-sky" size={24} />
|
||||||
description="Tools & Technologies"
|
</div>
|
||||||
header={
|
<div>
|
||||||
<div className="flex flex-wrap gap-2 p-2 overflow-y-auto max-h-40 scrollbar-hide">
|
<h3 className="font-black text-xl text-stone-900 dark:text-stone-50 mb-1">Osnabrück</h3>
|
||||||
{techStack && techStack.length > 0 ? (
|
<p className="text-sm text-stone-500 dark:text-stone-400">Germany, UTC+1</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex items-center gap-2 bg-white/50 dark:bg-black/20 p-3 rounded-2xl border border-white dark:border-stone-800">
|
||||||
|
<div className="w-2 h-2 bg-green-500 rounded-full animate-ping" />
|
||||||
|
<span className="text-xs font-bold uppercase tracking-widest opacity-70">Available Now</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3. Tech Stack - Bento wide */}
|
||||||
|
<div className="md:col-span-2 md:row-span-1 bg-white dark:bg-stone-900 rounded-[2.5rem] p-8 border border-stone-200/60 dark:border-stone-800/60 shadow-sm overflow-hidden flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="font-black text-xl text-stone-900 dark:text-stone-50 flex items-center gap-2">
|
||||||
|
<Code className="text-liquid-mint" size={20} /> {t("techStackTitle")}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 overflow-y-auto pr-2 scrollbar-hide">
|
||||||
|
{techStack.length > 0 ? (
|
||||||
techStack.flatMap(cat => cat.items?.map((item: any) => (
|
techStack.flatMap(cat => cat.items?.map((item: any) => (
|
||||||
<motion.span
|
<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 text-stone-700 dark:text-stone-300">
|
||||||
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}
|
{item.name}
|
||||||
</motion.span>
|
</span>
|
||||||
)))
|
)))
|
||||||
) : (
|
) : (
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="h-20 w-full bg-stone-50 dark:bg-stone-800 animate-pulse rounded-xl" />
|
||||||
{[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>
|
</div>
|
||||||
}
|
</div>
|
||||||
icon={<Code className="h-4 w-4 text-liquid-mint" />}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 3. Location */}
|
{/* 4. Reading Log - Compact but visual */}
|
||||||
<BentoGridItem
|
<div className="md:col-span-1 md:row-span-1 bg-white dark:bg-stone-900 rounded-[2.5rem] p-8 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col group overflow-hidden">
|
||||||
className="md:col-span-1 bg-white dark:bg-stone-900 border-stone-200/60 dark:border-stone-800/60"
|
<div className="flex items-center gap-2 mb-6">
|
||||||
title="Osnabrück, Germany"
|
<BookOpen className="text-liquid-purple" size={20} />
|
||||||
description="UTC+1 (CET)"
|
<h3 className="font-black text-xl text-stone-900 dark:text-stone-50 uppercase tracking-tighter">Reading</h3>
|
||||||
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>
|
</div>
|
||||||
}
|
<div className="flex-1 overflow-y-auto scrollbar-hide space-y-4">
|
||||||
/>
|
<CurrentlyReading />
|
||||||
|
<ReadBooks />
|
||||||
{/* 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>
|
</div>
|
||||||
}
|
</div>
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 5. Hobbies (Wide) */}
|
{/* 5. Hobbies - Small box */}
|
||||||
<BentoGridItem
|
<div className="md:col-span-1 md:row-span-1 bg-liquid-rose/5 dark:bg-stone-900 rounded-[2.5rem] p-8 border border-liquid-rose/20 dark:border-stone-800/60 flex flex-col justify-between">
|
||||||
className="md:col-span-2 bg-white dark:bg-stone-900 border-stone-200/60 dark:border-stone-800/60"
|
<div className="flex flex-wrap gap-3">
|
||||||
title={t("hobbiesTitle")}
|
{hobbies.slice(0, 4).map((hobby) => {
|
||||||
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;
|
const Icon = iconMap[hobby.icon] || Lightbulb;
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div key={hobby.id} className="p-2 rounded-lg bg-white dark:bg-stone-800 shadow-sm">
|
||||||
key={hobby.id}
|
<Icon size={18} className="text-liquid-rose" />
|
||||||
whileHover={{ y: -3 }}
|
</div>
|
||||||
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>
|
</div>
|
||||||
}
|
<div className="mt-4">
|
||||||
icon={<Gamepad2 className="h-4 w-4 text-liquid-purple" />}
|
<h3 className="font-black text-lg text-stone-900 dark:text-stone-50 mb-1">{t("hobbiesTitle")}</h3>
|
||||||
/>
|
<p className="text-xs text-stone-500 line-clamp-2">Exploration & Fun</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</BentoGrid>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user