feat: implement dark mode infrastructure, optimize images, and add SEO structured data
Some checks failed
Dev Deployment (Zero Downtime) / deploy-dev (push) Failing after 10m16s

This commit is contained in:
2026-02-15 22:20:43 +01:00
parent 92e5b4936e
commit 0766b46cc8
17 changed files with 440 additions and 52 deletions

View File

@@ -7,6 +7,7 @@ import { ToastProvider } from "@/components/Toast";
import ErrorBoundary from "@/components/ErrorBoundary";
import { AnalyticsProvider } from "@/components/AnalyticsProvider";
import { ConsentProvider, useConsent } from "./ConsentProvider";
import { ThemeProvider } from "./ThemeProvider";
// Dynamic import with SSR disabled to avoid framer-motion issues
const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), {
@@ -72,9 +73,11 @@ export default function ClientProviders({
<ErrorBoundary>
<ErrorBoundary>
<ConsentProvider>
<GatedProviders mounted={mounted} is404Page={is404Page}>
{children}
</GatedProviders>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
<GatedProviders mounted={mounted} is404Page={is404Page}>
{children}
</GatedProviders>
</ThemeProvider>
</ConsentProvider>
</ErrorBoundary>
</ErrorBoundary>

View File

@@ -4,6 +4,7 @@ import { motion } from "framer-motion";
import { BookOpen } from "lucide-react";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import Image from "next/image";
interface CurrentlyReading {
title: string;
@@ -107,11 +108,12 @@ const CurrentlyReading = () => {
className="flex-shrink-0"
>
<div className="relative w-24 h-36 sm:w-28 sm:h-40 rounded-lg overflow-hidden shadow-lg border-2 border-white/50">
<img
<Image
src={book.image}
alt={book.title}
className="w-full h-full object-cover"
loading="lazy"
fill
className="object-cover"
sizes="(max-width: 640px) 96px, 112px"
/>
{/* Glossy Overlay */}
<div className="absolute inset-0 bg-gradient-to-tr from-white/20 via-transparent to-white/10 pointer-events-none" />

View File

@@ -7,6 +7,7 @@ import { SiGithub, SiLinkedin } from "react-icons/si";
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
import { usePathname, useSearchParams } from "next/navigation";
import { ThemeToggle } from "./ThemeToggle";
const Header = () => {
const [isOpen, setIsOpen] = useState(false);
@@ -155,6 +156,7 @@ const Header = () => {
DE
</Link>
</div>
<ThemeToggle />
{socialLinks.map((social) => (
<motion.a
key={social.label}
@@ -233,7 +235,8 @@ const Header = () => {
))}
<div className="pt-6 mt-4 border-t border-stone-200">
<div className="flex justify-center space-x-4">
<div className="flex justify-center items-center space-x-4">
<ThemeToggle />
{socialLinks.map((social, index) => (
<motion.a
key={social.label}

View File

@@ -41,7 +41,7 @@ const Hero = () => {
];
return (
<section className="relative min-h-screen flex items-center justify-center overflow-hidden pt-32 pb-16 bg-gradient-to-br from-liquid-mint/10 via-liquid-lavender/10 to-liquid-rose/10">
<section className="relative min-h-screen flex items-center justify-center overflow-hidden pt-32 pb-16 bg-gradient-to-br from-liquid-mint/10 via-liquid-lavender/10 to-liquid-rose/10 dark:from-stone-900 dark:via-stone-900 dark:to-stone-800 transition-colors duration-500">
<div className="relative z-10 text-center px-4 max-w-5xl mx-auto">
{/* Profile Image with Organic Blob Mask */}
<motion.div
@@ -53,7 +53,7 @@ const Hero = () => {
<div className="relative w-64 h-64 md:w-80 md:h-80 flex items-center justify-center">
{/* Large Rotating Liquid Blobs behind image - Very slow and smooth */}
<motion.div
className="absolute w-[150%] h-[150%] bg-gradient-to-tr from-liquid-mint/40 via-liquid-blue/30 to-liquid-lavender/40 blur-3xl -z-10"
className="absolute w-[150%] h-[150%] bg-gradient-to-tr from-liquid-mint/40 via-liquid-blue/30 to-liquid-lavender/40 blur-3xl -z-10 dark:from-liquid-mint/20 dark:via-liquid-blue/15 dark:to-liquid-lavender/20"
animate={{
borderRadius: [
"60% 40% 30% 70%/60% 30% 70% 40%",
@@ -71,7 +71,7 @@ const Hero = () => {
}}
/>
<motion.div
className="absolute w-[130%] h-[130%] bg-gradient-to-bl from-liquid-rose/35 via-purple-200/25 to-liquid-mint/35 blur-2xl -z-10"
className="absolute w-[130%] h-[130%] bg-gradient-to-bl from-liquid-rose/35 via-purple-200/25 to-liquid-mint/35 blur-2xl -z-10 dark:from-liquid-rose/15 dark:via-purple-900/10 dark:to-liquid-mint/15"
animate={{
borderRadius: [
"40% 60% 70% 30%/40% 50% 60% 50%",
@@ -91,7 +91,7 @@ const Hero = () => {
{/* The Image Container with Organic Border Radius */}
<motion.div
className="absolute inset-0 overflow-hidden bg-stone-100"
className="absolute inset-0 overflow-hidden bg-stone-100 dark:bg-stone-800"
style={{
filter: "drop-shadow(0 20px 40px rgba(0,0,0,0.1))",
willChange: "border-radius",
@@ -133,7 +133,7 @@ const Hero = () => {
transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
className="absolute -bottom-8 left-1/2 -translate-x-1/2 z-30"
>
<div className="px-6 py-2.5 rounded-full bg-white/90 backdrop-blur-xl text-stone-900 font-sans font-bold text-sm tracking-wide shadow-lg border-2 border-stone-300">
<div className="px-6 py-2.5 rounded-full bg-white/90 dark:bg-stone-800/90 backdrop-blur-xl text-stone-900 dark:text-stone-50 font-sans font-bold text-sm tracking-wide shadow-lg border-2 border-stone-300 dark:border-stone-700">
dk<span className="text-red-500 font-extrabold">0</span>.dev
</div>
</motion.div>
@@ -144,7 +144,7 @@ const Hero = () => {
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.4, duration: 0.5, ease: "easeOut" }}
whileHover={{ scale: 1.1, rotate: 5 }}
className="absolute -top-4 right-0 md:-right-4 p-3 bg-white/95 backdrop-blur-md shadow-lg rounded-full text-stone-700 z-30"
className="absolute -top-4 right-0 md:-right-4 p-3 bg-white/95 dark:bg-stone-800/95 backdrop-blur-md shadow-lg rounded-full text-stone-700 dark:text-stone-300 z-30"
>
<Code size={24} />
</motion.div>
@@ -153,7 +153,7 @@ const Hero = () => {
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.5, duration: 0.5, ease: "easeOut" }}
whileHover={{ scale: 1.1, rotate: -5 }}
className="absolute bottom-4 -left-4 md:-left-8 p-3 bg-white/95 backdrop-blur-md shadow-lg rounded-full text-stone-700 z-30"
className="absolute bottom-4 -left-4 md:-left-8 p-3 bg-white/95 dark:bg-stone-800/95 backdrop-blur-md shadow-lg rounded-full text-stone-700 dark:text-stone-300 z-30"
>
<Zap size={24} />
</motion.div>
@@ -167,10 +167,10 @@ const Hero = () => {
transition={{ duration: 0.6, delay: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
className="mb-8 flex flex-col items-center justify-center relative"
>
<h1 className="text-5xl md:text-8xl font-bold tracking-tighter text-stone-900 mb-2">
<h1 className="text-5xl md:text-8xl font-bold tracking-tighter text-stone-900 dark:text-stone-50 mb-2">
Dennis Konkol
</h1>
<h2 className="text-2xl md:text-4xl font-light tracking-wide text-stone-600 mt-2">
<h2 className="text-2xl md:text-4xl font-light tracking-wide text-stone-600 dark:text-stone-400 mt-2">
Software Engineer
</h2>
</motion.div>
@@ -180,10 +180,10 @@ const Hero = () => {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="text-lg md:text-xl text-stone-700 mb-12 max-w-2xl mx-auto leading-relaxed"
className="text-lg md:text-xl text-stone-700 dark:text-stone-300 mb-12 max-w-2xl mx-auto leading-relaxed"
>
{cmsDoc ? (
<RichTextClient doc={cmsDoc} className="prose prose-stone max-w-none" />
<RichTextClient doc={cmsDoc} className="prose prose-stone dark:prose-invert max-w-none" />
) : (
<p>{t("description")}</p>
)}
@@ -207,10 +207,10 @@ const Hero = () => {
ease: [0.25, 0.1, 0.25, 1],
}}
whileHover={{ scale: 1.03, y: -3 }}
className="flex items-center space-x-2 px-5 py-2.5 rounded-full bg-white/85 border-2 border-stone-300 shadow-md backdrop-blur-lg"
className="flex items-center space-x-2 px-5 py-2.5 rounded-full bg-white/85 dark:bg-stone-800/85 border-2 border-stone-300 dark:border-stone-700 shadow-md backdrop-blur-lg"
>
<feature.icon className="w-4 h-4 text-stone-800" />
<span className="text-stone-800 font-semibold text-sm">
<feature.icon className="w-4 h-4 text-stone-800 dark:text-stone-200" />
<span className="text-stone-800 dark:text-stone-200 font-semibold text-sm">
{feature.text}
</span>
</motion.div>

View File

@@ -4,6 +4,7 @@ import { motion } from "framer-motion";
import { BookCheck, Star, ChevronDown, ChevronUp } from "lucide-react";
import { useEffect, useState } from "react";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
interface BookReview {
id: string;
@@ -134,11 +135,12 @@ const ReadBooks = () => {
className="flex-shrink-0"
>
<div className="relative w-20 h-[7.5rem] sm:w-24 sm:h-32 rounded-lg overflow-hidden shadow-lg border-2 border-white/50">
<img
<Image
src={review.book_image}
alt={review.book_title}
className="w-full h-full object-cover"
loading="lazy"
fill
className="object-cover"
sizes="(max-width: 640px) 80px, 96px"
/>
<div className="absolute inset-0 bg-gradient-to-tr from-white/20 via-transparent to-white/10 pointer-events-none" />
</div>

View File

@@ -0,0 +1,11 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
export function ThemeProvider({
children,
...props
}: React.ComponentProps<typeof NextThemesProvider>) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

View File

@@ -0,0 +1,35 @@
"use client";
import * as React from "react";
import { Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { motion } from "framer-motion";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div className="w-9 h-9" />;
}
return (
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="p-2 rounded-full bg-stone-100 dark:bg-stone-800 text-stone-800 dark:text-stone-100 hover:bg-stone-200 dark:hover:bg-stone-700 transition-colors border border-stone-200 dark:border-stone-700 shadow-sm"
aria-label="Toggle theme"
>
{theme === "dark" ? (
<Sun size={18} className="text-amber-400" />
) : (
<Moon size={18} className="text-stone-600" />
)}
</motion.button>
);
}