feat: complete design overhaul with bento grid and island nav
Refactored About section to use a responsive Bento Grid layout. Redesigned Hero for stronger visual impact. Implemented floating Island navigation. Updated Project cards for cleaner aesthetic.
This commit is contained in:
@@ -1,421 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import { Globe, Server, Wrench, Shield, Gamepad2, Gamepad, Code, Activity, Lightbulb } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { BentoGrid, BentoGridItem } from "./ui/BentoGrid";
|
||||
import {
|
||||
IconClipboardCopy,
|
||||
IconFileBroken,
|
||||
IconSignature,
|
||||
IconTableColumn,
|
||||
} from "@tabler/icons-react"; // Wir nutzen Lucide, ich tausche die gleich aus
|
||||
import { Globe, Server, Wrench, Shield, Gamepad2, Code, Activity, Lightbulb, MapPin, User } 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";
|
||||
|
||||
// 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],
|
||||
},
|
||||
},
|
||||
// Helper for Tech Stack Icons
|
||||
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 [techStackFromCMS, setTechStackFromCMS] = useState<TechStackCategory[] | null>(null);
|
||||
const [hobbiesFromCMS, setHobbiesFromCMS] = useState<Hobby[] | null>(null);
|
||||
|
||||
// Data State
|
||||
const [techStack, setTechStack] = useState<any[]>([]);
|
||||
const [hobbies, setHobbies] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Load Content Page
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/content/page?key=${encodeURIComponent("home-about")}&locale=${encodeURIComponent(locale)}`,
|
||||
);
|
||||
const res = await fetch(`/api/content/page?key=home-about&locale=${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);
|
||||
}
|
||||
if (data?.content?.content) setCmsDoc(data.content.content as JSONContent);
|
||||
} catch {}
|
||||
})();
|
||||
}, [locale]);
|
||||
|
||||
// Load Tech Stack from Directus
|
||||
useEffect(() => {
|
||||
// Load Tech Stack
|
||||
(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');
|
||||
}
|
||||
}
|
||||
const res = await fetch(`/api/tech-stack?locale=${locale}`);
|
||||
const data = await res.json();
|
||||
if (data?.techStack) setTechStack(data.techStack);
|
||||
} catch {}
|
||||
})();
|
||||
}, [locale]);
|
||||
|
||||
// Load Hobbies from Directus
|
||||
useEffect(() => {
|
||||
// Load Hobbies
|
||||
(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');
|
||||
}
|
||||
}
|
||||
const res = await fetch(`/api/hobbies?locale=${locale}`);
|
||||
const data = await res.json();
|
||||
if (data?.hobbies) setHobbies(data.hobbies);
|
||||
} catch {}
|
||||
})();
|
||||
}, [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<string, any> = {
|
||||
Globe,
|
||||
Server,
|
||||
Code,
|
||||
Wrench,
|
||||
Shield,
|
||||
Activity,
|
||||
Lightbulb,
|
||||
Gamepad2,
|
||||
Gamepad
|
||||
};
|
||||
|
||||
// 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 (
|
||||
<section
|
||||
id="about"
|
||||
className="py-24 px-4 bg-gradient-to-br from-liquid-sky/15 via-liquid-lavender/10 to-liquid-pink/15 relative overflow-hidden"
|
||||
>
|
||||
<div className="max-w-6xl mx-auto relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-start">
|
||||
{/* Left Column: Bio & Hobbies */}
|
||||
<div className="space-y-16">
|
||||
{/* Biography */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
variants={staggerContainer}
|
||||
className="space-y-8"
|
||||
>
|
||||
<motion.h2
|
||||
variants={fadeInUp}
|
||||
className="text-4xl md:text-5xl font-bold text-stone-900"
|
||||
>
|
||||
{t("title")}
|
||||
</motion.h2>
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="prose prose-stone prose-lg text-stone-700 space-y-4"
|
||||
>
|
||||
{cmsDoc ? (
|
||||
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-none" />
|
||||
) : (
|
||||
<>
|
||||
<p>{t("p1")}</p>
|
||||
<p>{t("p2")}</p>
|
||||
<p>{t("p3")}</p>
|
||||
</>
|
||||
)}
|
||||
<motion.div
|
||||
variants={fadeInUp}
|
||||
className="relative overflow-hidden bg-gradient-to-br from-liquid-mint/15 via-liquid-sky/10 to-liquid-lavender/15 border-2 border-liquid-mint/30 rounded-xl p-5 backdrop-blur-sm"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Lightbulb size={20} className="text-stone-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-stone-800 mb-1">
|
||||
{t("funFactTitle")}
|
||||
</p>
|
||||
<p className="text-sm text-stone-700 leading-relaxed">
|
||||
{t("funFactBody")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<section id="about" className="py-32 px-4 relative bg-stone-50 dark:bg-stone-950">
|
||||
{/* Background Noise/Gradient */}
|
||||
<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 via-transparent to-transparent blur-3xl pointer-events-none"></div>
|
||||
|
||||
{/* Hobbies Section */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
variants={staggerContainer}
|
||||
className="space-y-6"
|
||||
>
|
||||
<motion.h3
|
||||
variants={fadeInUp}
|
||||
className="text-2xl font-bold text-stone-900"
|
||||
>
|
||||
{t("hobbiesTitle")}
|
||||
</motion.h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{hobbies.map((hobby, idx) => (
|
||||
<motion.div
|
||||
key={`hobby-${hobby.text}-${idx}`}
|
||||
variants={fadeInUp}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
transition: { duration: 0.4, ease: "easeOut" },
|
||||
}}
|
||||
className={`flex items-center gap-3 p-4 rounded-xl border-2 transition-[background-color,border-color,box-shadow] duration-500 ease-out backdrop-blur-md ${
|
||||
idx % 4 === 0
|
||||
? "bg-gradient-to-r from-liquid-mint/25 to-liquid-sky/25 border-liquid-mint/50 hover:border-liquid-mint/70"
|
||||
: idx % 4 === 1
|
||||
? "bg-gradient-to-r from-liquid-coral/25 to-liquid-peach/25 border-liquid-coral/50 hover:border-liquid-coral/70"
|
||||
: idx % 4 === 2
|
||||
? "bg-gradient-to-r from-liquid-lavender/25 to-liquid-pink/25 border-liquid-lavender/50 hover:border-liquid-lavender/70"
|
||||
: "bg-gradient-to-r from-liquid-lime/25 to-liquid-teal/25 border-liquid-lime/50 hover:border-liquid-lime/70"
|
||||
}`}
|
||||
>
|
||||
<hobby.icon size={20} className="text-stone-700" />
|
||||
<span className="text-stone-800 font-semibold text-sm">
|
||||
{String(hobby.text)}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Tech Stack & Reading */}
|
||||
<div className="space-y-16">
|
||||
{/* Tech Stack */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
variants={staggerContainer}
|
||||
className="space-y-8"
|
||||
>
|
||||
<motion.h3
|
||||
variants={fadeInUp}
|
||||
className="text-2xl font-bold text-stone-900"
|
||||
>
|
||||
{t("techStackTitle")}
|
||||
</motion.h3>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{techStack.map((stack, idx) => (
|
||||
<motion.div
|
||||
key={`${stack.category}-${idx}`}
|
||||
variants={fadeInUp}
|
||||
whileHover={{
|
||||
scale: 1.02,
|
||||
transition: { duration: 0.4, ease: "easeOut" },
|
||||
}}
|
||||
className={`p-5 rounded-xl border-2 transition-[background-color,border-color,box-shadow] duration-500 ease-out backdrop-blur-md ${
|
||||
idx === 0
|
||||
? "bg-gradient-to-br from-liquid-sky/20 to-liquid-mint/20 border-liquid-sky/40 hover:border-liquid-sky/60"
|
||||
: idx === 1
|
||||
? "bg-gradient-to-br from-liquid-peach/20 to-liquid-coral/20 border-liquid-peach/40 hover:border-liquid-peach/60"
|
||||
: idx === 2
|
||||
? "bg-gradient-to-br from-liquid-lavender/20 to-liquid-pink/20 border-liquid-lavender/40 hover:border-liquid-lavender/60"
|
||||
: "bg-gradient-to-br from-liquid-teal/20 to-liquid-lime/20 border-liquid-teal/40 hover:border-liquid-teal/60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="p-2 bg-white rounded-lg shadow-sm text-stone-700">
|
||||
<stack.icon size={18} />
|
||||
</div>
|
||||
<h4 className="font-semibold text-stone-800">
|
||||
{stack.category}
|
||||
</h4>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{stack.items.map((item, itemIdx) => (
|
||||
<span
|
||||
key={`${stack.category}-${item}-${itemIdx}`}
|
||||
className={`px-3 py-1.5 rounded-lg border-2 text-sm text-stone-800 font-semibold transition-all duration-400 ease-out backdrop-blur-sm ${
|
||||
itemIdx % 4 === 0
|
||||
? "bg-liquid-mint/20 border-liquid-mint/40 hover:bg-liquid-mint/30"
|
||||
: itemIdx % 4 === 1
|
||||
? "bg-liquid-lavender/20 border-liquid-lavender/40 hover:bg-liquid-lavender/30"
|
||||
: itemIdx % 4 === 2
|
||||
? "bg-liquid-rose/20 border-liquid-rose/40 hover:bg-liquid-rose/30"
|
||||
: "bg-liquid-sky/20 border-liquid-sky/40 hover:bg-liquid-sky/30"
|
||||
}`}
|
||||
>
|
||||
{String(item)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Reading Section */}
|
||||
<div className="space-y-10">
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<CurrentlyReading />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true }}
|
||||
variants={fadeInUp}
|
||||
>
|
||||
<ReadBooks />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-7xl mx-auto mb-16 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-6xl font-black text-stone-900 dark:text-stone-50 tracking-tight"
|
||||
>
|
||||
{t("title")}
|
||||
</motion.h2>
|
||||
</div>
|
||||
|
||||
<BentoGrid className="max-w-6xl mx-auto relative z-10">
|
||||
|
||||
{/* 1. The Bio (Large Item) */}
|
||||
<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"
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<User size={20} className="text-liquid-mint" />
|
||||
<span>Dennis Konkol</span>
|
||||
</div>
|
||||
}
|
||||
description={
|
||||
<div className="mt-4 prose prose-stone dark:prose-invert max-w-none text-base leading-relaxed">
|
||||
{cmsDoc ? (
|
||||
<RichTextClient doc={cmsDoc} />
|
||||
) : (
|
||||
<p className="text-stone-600 dark:text-stone-400">
|
||||
{t("p1")} {t("p2")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
header={
|
||||
<div className="w-full h-40 bg-gradient-to-r from-liquid-mint/20 to-liquid-sky/20 rounded-xl mb-4 flex items-center justify-center overflow-hidden relative group">
|
||||
<div className="absolute inset-0 bg-[url('/images/me.jpg')] bg-cover bg-center opacity-40 group-hover:scale-105 transition-transform duration-700"></div>
|
||||
<div className="relative z-10 bg-white/30 dark:bg-black/30 backdrop-blur-md px-6 py-2 rounded-full border border-white/20">
|
||||
<span className="font-mono text-sm font-bold text-stone-800 dark:text-white">Software Engineer</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 2. Location & Status */}
|
||||
<BentoGridItem
|
||||
className="md:col-span-1"
|
||||
title="Osnabrück, Germany"
|
||||
description="Available for new opportunities"
|
||||
icon={<MapPin className="h-4 w-4 text-neutral-500" />}
|
||||
header={
|
||||
<div className="flex flex-1 w-full h-full min-h-[6rem] rounded-xl bg-gradient-to-br from-neutral-200 dark:from-neutral-900 to-neutral-100 dark:to-neutral-800 items-center justify-center">
|
||||
<div className="relative">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full animate-ping absolute top-0 right-0"></div>
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full relative z-10 border-2 border-white dark:border-stone-900"></div>
|
||||
</div>
|
||||
<span className="ml-3 text-sm font-bold text-stone-600 dark:text-stone-300">Online & Active</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 3. Tech Stack (Marquee Style or Grid) */}
|
||||
<BentoGridItem
|
||||
className="md:col-span-1"
|
||||
title={t("techStackTitle")}
|
||||
description="Tools I work with daily"
|
||||
header={
|
||||
<div className="flex flex-wrap gap-2 min-h-[6rem] p-2">
|
||||
{techStack.length > 0 ? (
|
||||
techStack.slice(0, 8).flatMap(cat => cat.items.map((item: any) => (
|
||||
<span key={item.name} className="px-2 py-1 bg-stone-100 dark:bg-stone-800 rounded text-xs font-mono border border-stone-200 dark:border-stone-700">
|
||||
{item.name}
|
||||
</span>
|
||||
))).slice(0, 12)
|
||||
) : (
|
||||
<div className="text-xs text-stone-400">Loading Stack...</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
icon={<Code className="h-4 w-4 text-neutral-500" />}
|
||||
/>
|
||||
|
||||
{/* 4. Currently Reading */}
|
||||
<BentoGridItem
|
||||
className="md:col-span-1 md:row-span-2"
|
||||
title={null}
|
||||
description={null}
|
||||
header={
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="font-bold text-stone-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Activity size={16} /> Reading
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<CurrentlyReading />
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-stone-100 dark:border-stone-800">
|
||||
<ReadBooks />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 5. Hobbies */}
|
||||
<BentoGridItem
|
||||
className="md:col-span-2"
|
||||
title={t("hobbiesTitle")}
|
||||
description="What keeps me busy"
|
||||
header={
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-2">
|
||||
{hobbies.map((hobby, i) => {
|
||||
const Icon = iconMap[hobby.icon] || Lightbulb;
|
||||
return (
|
||||
<div key={i} className="flex flex-col items-center justify-center p-4 bg-stone-50 dark:bg-stone-800/50 rounded-xl border border-stone-100 dark:border-stone-700/50 hover:bg-white dark:hover:bg-stone-800 transition-colors">
|
||||
<Icon size={24} className="mb-2 text-liquid-purple" />
|
||||
<span className="text-xs font-medium text-center">{hobby.title}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
icon={<Gamepad2 className="h-4 w-4 text-neutral-500" />}
|
||||
/>
|
||||
|
||||
</BentoGrid>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user