diff --git a/app/components/About.tsx b/app/components/About.tsx index 955f21a..6bec1da 100644 --- a/app/components/About.tsx +++ b/app/components/About.tsx @@ -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 = { + Globe, Server, Code, Wrench, Shield, Activity, Lightbulb, Gamepad2 }; 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); + + // Data State + const [techStack, setTechStack] = useState([]); + const [hobbies, setHobbies] = useState([]); 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 = { - 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 ( -
-
-
- {/* Left Column: Bio & Hobbies */} -
- {/* Biography */} - - - {t("title")} - - - {cmsDoc ? ( - - ) : ( - <> -

{t("p1")}

-

{t("p2")}

-

{t("p3")}

- - )} - -
- -
-

- {t("funFactTitle")} -

-

- {t("funFactBody")} -

-
-
-
-
-
+
+ {/* Background Noise/Gradient */} +
+
- {/* Hobbies Section */} - - - {t("hobbiesTitle")} - -
- {hobbies.map((hobby, idx) => ( - - - - {String(hobby.text)} - - - ))} -
-
-
- - {/* Right Column: Tech Stack & Reading */} -
- {/* Tech Stack */} - - - {t("techStackTitle")} - -
- {techStack.map((stack, idx) => ( - -
-
- -
-

- {stack.category} -

-
-
- {stack.items.map((item, itemIdx) => ( - - {String(item)} - - ))} -
-
- ))} -
-
- - {/* Reading Section */} -
- - - - - - - -
-
-
+
+ + {t("title")} +
+ + + + {/* 1. The Bio (Large Item) */} + + + Dennis Konkol +
+ } + description={ +
+ {cmsDoc ? ( + + ) : ( +

+ {t("p1")} {t("p2")} +

+ )} +
+ } + header={ +
+
+
+ Software Engineer +
+
+ } + /> + + {/* 2. Location & Status */} + } + header={ +
+
+
+
+
+ Online & Active +
+ } + /> + + {/* 3. Tech Stack (Marquee Style or Grid) */} + + {techStack.length > 0 ? ( + techStack.slice(0, 8).flatMap(cat => cat.items.map((item: any) => ( + + {item.name} + + ))).slice(0, 12) + ) : ( +
Loading Stack...
+ )} + + } + icon={} + /> + + {/* 4. Currently Reading */} + +
+ Reading +
+
+ +
+
+ +
+ + } + /> + + {/* 5. Hobbies */} + + {hobbies.map((hobby, i) => { + const Icon = iconMap[hobby.icon] || Lightbulb; + return ( +
+ + {hobby.title} +
+ ) + })} + + } + icon={} + /> + +
); }; diff --git a/app/components/Header.tsx b/app/components/Header.tsx index d452abb..0c94e7f 100644 --- a/app/components/Header.tsx +++ b/app/components/Header.tsx @@ -2,32 +2,20 @@ import { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { Menu, X, Mail } from "lucide-react"; -import { SiGithub, SiLinkedin } from "react-icons/si"; +import { Menu, X } from "lucide-react"; import Link from "next/link"; import { useLocale, useTranslations } from "next-intl"; -import { usePathname, useSearchParams } from "next/navigation"; +import { usePathname } from "next/navigation"; import { ThemeToggle } from "./ThemeToggle"; const Header = () => { const [isOpen, setIsOpen] = useState(false); - const [scrolled, setScrolled] = useState(false); const locale = useLocale(); const pathname = usePathname(); - const searchParams = useSearchParams(); const t = useTranslations("nav"); const isHome = pathname === `/${locale}` || pathname === `/${locale}/`; - useEffect(() => { - const handleScroll = () => { - setScrolled(window.scrollY > 50); - }; - - window.addEventListener("scroll", handleScroll); - return () => window.removeEventListener("scroll", handleScroll); - }, []); - const navItems = [ { name: t("home"), href: `/${locale}` }, { name: t("about"), href: isHome ? "#about" : `/${locale}#about` }, @@ -35,234 +23,83 @@ const Header = () => { { name: t("contact"), href: isHome ? "#contact" : `/${locale}#contact` }, ]; - const socialLinks = [ - { icon: SiGithub, href: "https://github.com/Denshooter", label: "GitHub" }, - { - icon: SiLinkedin, - href: "https://linkedin.com/in/dkonkol", - label: "LinkedIn", - }, - { icon: Mail, href: "mailto:contact@dk0.dev", label: "Email" }, - ]; - - const pathWithoutLocale = pathname.replace(new RegExp(`^/${locale}`), "") || ""; - const qs = searchParams.toString(); - const query = qs ? `?${qs}` : ""; - const enHref = `/en${pathWithoutLocale}${query}`; - const deHref = `/de${pathWithoutLocale}${query}`; - - // Always render to prevent flash, but use opacity transition - return ( <> - -
+ - - + dk + + + {/* Desktop Nav Items */} +
+ {navItems.map((item) => ( - dk0 + {item.name} - + ))} +
-
+ + {/* Mobile Menu Overlay */} + + {isOpen && ( + +
{navItems.map((item) => ( - setIsOpen(false)} + className="px-4 py-3 text-lg font-medium text-stone-900 dark:text-stone-100 bg-stone-50 dark:bg-stone-800/50 rounded-xl" > - { - if (item.href.startsWith("#")) { - e.preventDefault(); - const element = document.querySelector(item.href); - if (element) { - element.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - } - } - }} - > - {item.name} - - - - ))} - - -
-
- - EN + {item.name} - - DE - -
- - {socialLinks.map((social) => ( - - - ))}
- - setIsOpen(!isOpen)} - className="md:hidden p-2 rounded-full bg-white/40 hover:bg-white/60 text-stone-800 transition-colors liquid-hover" - aria-label={isOpen ? "Close menu" : "Open menu"} - > - {isOpen ? : } - -
- - - {isOpen && ( - <> - setIsOpen(false)} - /> - -
- {navItems.map((item, index) => ( - - { - setIsOpen(false); - if (item.href.startsWith("#")) { - e.preventDefault(); - setTimeout(() => { - const element = document.querySelector(item.href); - if (element) { - element.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - } - }, 100); - } - }} - className="block text-stone-600 hover:text-stone-900 hover:bg-white/50 transition-all font-medium py-3 px-4 rounded-xl" - > - {item.name} - - - ))} - -
-
- - {socialLinks.map((social, index) => ( - - - - ))} -
-
-
-
- - )} -
-
+ )} + ); }; diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 6889428..9284ad5 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -1,251 +1,100 @@ "use client"; import { motion } from "framer-motion"; -import { ArrowDown, Code, Zap, Rocket } from "lucide-react"; -import { useEffect, useState } from "react"; +import { ArrowDown, Github, Linkedin, Mail } from "lucide-react"; import { useLocale, useTranslations } from "next-intl"; -import type { JSONContent } from "@tiptap/react"; -import RichTextClient from "./RichTextClient"; const Hero = () => { const locale = useLocale(); const t = useTranslations("home.hero"); - const [cmsDoc, setCmsDoc] = useState(null); - - useEffect(() => { - (async () => { - try { - const res = await fetch( - `/api/content/page?key=${encodeURIComponent("home-hero")}&locale=${encodeURIComponent(locale)}`, - ); - const data = await res.json(); - // Only use CMS content if it exists for the active locale. - // If the API falls back to another locale, keep showing next-intl strings - // so the locale switch visibly changes the page. - 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]); - - const features = [ - { icon: Code, text: t("features.f1") }, - { icon: Zap, text: t("features.f2") }, - { icon: Rocket, text: t("features.f3") }, - ]; return ( -
-
- {/* Profile Image with Organic Blob Mask */} - + {/* Dynamic Background */} +
+
+
+
+
+ +
+ {/* Availability Badge */} + -
- {/* Large Rotating Liquid Blobs behind image - Very slow and smooth */} - - - - {/* The Image Container with Organic Border Radius */} - - {/* Use a plain to fully bypass Next.js image optimizer (dev 400 issue). */} - Dennis Konkol - - {/* Glossy Overlay for Liquid Feel */} -
- - {/* Inner Border/Highlight */} -
- - - {/* Domain Badge - repositioned below image */} - -
- dk0.dev -
-
- - {/* Floating Badges - subtle animations */} - - - - - - -
+ + + + + Available for work {/* Main Title */} - -

- Dennis Konkol -

-

- Software Engineer -

-
- - {/* Description */} - - {cmsDoc ? ( - - ) : ( -

{t("description")}

- )} -
- - {/* Features */} - - {features.map((feature, index) => ( - - - - {feature.text} - - - ))} - - - {/* CTA Buttons */} - - + - {t("ctaWork")} - - - - + - {t("ctaContact")} - + Digital Products + + + + {/* Subtitle */} + + I'm Dennis, a Software Engineer crafting polished web & mobile experiences with a focus on performance and design. + + + {/* Buttons */} + + + View My Work + +
+ + {/* Scroll Indicator */} + + +
); }; diff --git a/app/components/Projects.tsx b/app/components/Projects.tsx index a7ae43e..eb78c08 100644 --- a/app/components/Projects.tsx +++ b/app/components/Projects.tsx @@ -1,35 +1,12 @@ "use client"; import { useState, useEffect } from "react"; -import { motion, Variants } from "framer-motion"; -import { ExternalLink, Github, ArrowRight, Calendar } from "lucide-react"; +import { motion } from "framer-motion"; +import { ArrowUpRight } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; import { useLocale, useTranslations } from "next-intl"; -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 staggerContainer: Variants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.2, - delayChildren: 0.1, - }, - }, -}; - interface Project { id: number; slug: string; @@ -53,214 +30,83 @@ const Projects = () => { useEffect(() => { const loadProjects = async () => { try { - const response = await fetch( - "/api/projects?featured=true&published=true&limit=6", - ); + const response = await fetch("/api/projects?featured=true&published=true&limit=6"); if (response.ok) { const data = await response.json(); setProjects(data.projects || []); } - } catch (error) { - if (process.env.NODE_ENV === "development") { - console.error("Error loading projects:", error); - } - } + } catch (error) {} }; loadProjects(); }, []); return ( -
+
- -

- {t("title")} -

-

- {t("subtitle")} -

-
+
+
+

+ Selected Work +

+

+ Projects that pushed my boundaries. +

+
+ + View Archive + +
- +
{projects.map((project) => ( - {/* Project Cover / Image Area */} -
- {project.imageUrl ? ( - <> + + {/* Image Card */} +
+ {project.imageUrl ? ( {project.title} -
- - ) : ( -
-
-
-
- -
- - {project.title.charAt(0)} + ) : ( +
+ {project.title.charAt(0)} +
+ )} + {/* Overlay on Hover */} +
+
+ + {/* Text Content */} +
+
+

+ {project.title} +

+

+ {project.description} +

+
+
+ {project.tags.slice(0, 2).map(tag => ( + + {tag} -
-
- )} - - {/* Texture/Grain Overlay */} -
- - {/* Animated Shine Effect */} -
- - {/* Featured Badge */} - {project.featured && ( -
-
- {t("featured")} -
-
- )} - - {/* Overlay Links */} -
- {project.github && ( - e.stopPropagation()} - > - - - )} - {project.live && !project.title.toLowerCase().includes('kernel panic') && ( - e.stopPropagation()} - > - - - )} -
-
- - {/* Content */} -
- {/* Stretched Link covering the whole card (including image area) */} - - -
-

- {project.title} -

-
- - - {(() => { - const d = new Date(project.date); - return isNaN(d.getTime()) ? project.date : d.getFullYear(); - })()} - + ))}
- -

- {project.description} -

- -
- {project.tags.slice(0, 4).map((tag) => ( - - {tag} - - ))} - {project.tags.length > 4 && ( - + {project.tags.length - 4} - )} -
- -
-
- {project.github && ( - e.stopPropagation()} - > - - - )} - {project.live && !project.title.toLowerCase().includes('kernel panic') && ( - e.stopPropagation()} - > - - - )} -
-
-
+ ))} - - - - - {t("viewAll")} - - +
); diff --git a/app/components/ui/BentoGrid.tsx b/app/components/ui/BentoGrid.tsx new file mode 100644 index 0000000..e7ea2a5 --- /dev/null +++ b/app/components/ui/BentoGrid.tsx @@ -0,0 +1,60 @@ +import { cn } from "@/lib/utils"; +import { motion } from "framer-motion"; + +export const BentoGrid = ({ + className, + children, +}: { + className?: string; + children?: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + +export const BentoGridItem = ({ + className, + title, + description, + header, + icon, + onClick, +}: { + className?: string; + title?: string | React.ReactNode; + description?: string | React.ReactNode; + header?: React.ReactNode; + icon?: React.ReactNode; + onClick?: () => void; +}) => { + return ( + + {header} +
+ {icon} +
+ {title} +
+
+ {description} +
+
+
+ ); +};