From 96ec2d867f516d9a8f27302e9ae6e9ffa29a5857 Mon Sep 17 00:00:00 2001 From: denshooter <44590296+denshooter@users.noreply.github.com> Date: Thu, 15 Oct 2026 23:48:39 +0200 Subject: [PATCH] feat: plakat redesign phase 1 (tokens, fonts, poster hero, header) (#74) - New design tokens: signal red, ink and paper as RGB channels; ink and paper swap roles in dark mode so every component inverts cleanly - Fonts via next/font: Bricolage Grotesque (with wdth/opsz axes), Geist, Geist Mono; drop Inter and Playfair - Poster hero: kicker, giant DK0 wordmark that reacts to pointer distance (weight and width through a damped spring, desktop only, off for reduced motion), claim as h1, intro with thesis countdown, marquee strip - Header in poster style: transparent over the red hero, paper strip with ink rule when scrolled or on subpages; drawer menu restyled - Remove wavy SVG separators, CSS shader gradient, background blobs and unused providers that caused the pink/purple bands between sections - Drop the global p/span/div color override so colors inherit properly - New hero texts in de/en, tests and i18n key check updated Co-Authored-By: Claude Opus 5.5 --- app/__tests__/components/Hero.test.tsx | 76 +++--- app/__tests__/pre-push-check.test.ts | 2 +- app/_ui/HomePageServer.tsx | 65 ----- app/components/BackgroundBlobsClient.tsx | 17 -- app/components/ClientProviders.tsx | 41 +--- app/components/HeaderClient.tsx | 251 ++++++++------------ app/components/Hero.tsx | 94 ++++---- app/components/KineticWordmark.tsx | 99 ++++++++ app/components/RootProviders.tsx | 41 ---- app/components/ShaderGradientBackground.tsx | 33 --- app/components/ThemeToggle.tsx | 10 +- app/globals.css | 32 ++- app/layout.tsx | 25 +- components/BackgroundBlobs.tsx | 48 ---- lib/translations-loader.ts | 25 -- messages/de.json | 17 +- messages/en.json | 27 +-- tailwind.config.ts | 25 +- types/translations.ts | 11 - 19 files changed, 355 insertions(+), 584 deletions(-) delete mode 100644 app/components/BackgroundBlobsClient.tsx create mode 100644 app/components/KineticWordmark.tsx delete mode 100644 app/components/RootProviders.tsx delete mode 100644 app/components/ShaderGradientBackground.tsx delete mode 100644 components/BackgroundBlobs.tsx diff --git a/app/__tests__/components/Hero.test.tsx b/app/__tests__/components/Hero.test.tsx index 3147380..2a25239 100644 --- a/app/__tests__/components/Hero.test.tsx +++ b/app/__tests__/components/Hero.test.tsx @@ -3,58 +3,38 @@ import Hero from '@/app/components/Hero'; // Mock next-intl/server jest.mock('next-intl/server', () => ({ - getTranslations: () => Promise.resolve((key: string) => { - const messages: Record = { - badge: 'Student & Self-Hoster', - line1: 'Building', - line2: 'Stuff.', - description: 'Dennis is a student and passionate self-hoster.', - ctaWork: 'View My Work', - ctaContact: 'Get in touch', - }; - return messages[key] || key; - }), -})); - -// Mock next/image -interface ImageProps { - src: string; - alt: string; - fill?: boolean; - priority?: boolean; - [key: string]: unknown; -} - -jest.mock('next/image', () => ({ - __esModule: true, - default: ({ src, alt, fill, priority, ...props }: ImageProps) => ( - // eslint-disable-next-line @next/next/no-img-element - {alt} - ), + getTranslations: () => + Promise.resolve((key: string, values?: { days?: number }) => { + const messages: Record = { + kicker: 'Dennis Konkol — Osnabrück, Germany', + claim: 'I self-host half my life. The rest I code.', + intro: "IT admin in Osnabrück and master's student.", + countdown: `${values?.days ?? 0} days to go.`, + ctaWork: 'See projects', + ctaContact: 'Write me', + }; + return messages[key] || key; + }), })); describe('Hero', () => { - it('renders the hero section correctly', async () => { + it('renders the poster hero with claim, intro and calls to action', async () => { const HeroResolved = await Hero({ locale: 'en' }); render(HeroResolved); - - // Check for the main headlines (defaults in Hero.tsx) - expect(screen.getByText('Building')).toBeInTheDocument(); - expect(screen.getByText('Stuff.')).toBeInTheDocument(); - - // Check for the description from our mock - expect(screen.getByText(/Dennis is a student/i)).toBeInTheDocument(); - - // Check for the image - expect(screen.getByAltText('Dennis Konkol')).toBeInTheDocument(); - - // Check for CTA - expect(screen.getByText('View My Work')).toBeInTheDocument(); + + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('I self-host half my life. The rest I code.'); + expect(screen.getByText('Dennis Konkol — Osnabrück, Germany')).toBeInTheDocument(); + expect(screen.getByText(/IT admin in Osnabrück/)).toBeInTheDocument(); + expect(screen.getByRole('link', { name: /See projects/ })).toHaveAttribute('href', '#projects'); + expect(screen.getByRole('link', { name: 'Write me' })).toHaveAttribute('href', '#contact'); + }); + + it('renders the DK0 wordmark as decoration hidden from assistive tech', async () => { + const HeroResolved = await Hero({ locale: 'en' }); + const { container } = render(HeroResolved); + + const wordmark = container.querySelector('[aria-hidden="true"].font-display'); + expect(wordmark).not.toBeNull(); + expect(wordmark).toHaveTextContent('DK0'); }); }); diff --git a/app/__tests__/pre-push-check.test.ts b/app/__tests__/pre-push-check.test.ts index 9f400a7..5c66e18 100644 --- a/app/__tests__/pre-push-check.test.ts +++ b/app/__tests__/pre-push-check.test.ts @@ -136,7 +136,7 @@ const STATIC_KEYS: Record> = { ProjectDetailClient: ["back"], ProjectsPageClient: ["backToHome"], }, consent: { ConsentBanner: ["title", "description", "essential", "chat", "alwaysOn", "acceptAll", "acceptSelected", "rejectAll", "hide"] }, - "home.hero": { Hero: ["badge", "line1", "line2", "description", "ctaWork", "ctaContact"] }, + "home.hero": { Hero: ["kicker", "claim", "intro", "countdown", "ctaWork", "ctaContact"] }, "home.about": { About: ["title", "p1", "p2", "p3", "funFactTitle", "funFactBody", "hobbiesTitle"] }, "home.about.activity": { ActivityFeed: ["idleStatus", "codingNow", "gaming", "inGame", "listening"] }, "home.about.currentlyReading": { CurrentlyReading: ["title", "progress"] }, diff --git a/app/_ui/HomePageServer.tsx b/app/_ui/HomePageServer.tsx index 833e830..1fd5f67 100644 --- a/app/_ui/HomePageServer.tsx +++ b/app/_ui/HomePageServer.tsx @@ -88,82 +88,17 @@ export default async function HomePageServer({ locale }: HomePageServerProps) { }} />
- {/* Spacer to prevent navbar overlap */} -
- {/* Wavy Separator 1 - Hero to About */} -
- - - - - - - - - - -
- - {/* Wavy Separator 2 - About to Projects */} -
- - - - - - - - - - -
- - {/* Wavy Separator 3 - Projects to Contact */} -
- - - - - - - - - - -
- diff --git a/app/components/BackgroundBlobsClient.tsx b/app/components/BackgroundBlobsClient.tsx deleted file mode 100644 index baaf46b..0000000 --- a/app/components/BackgroundBlobsClient.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -import React, { useEffect, useState } from "react"; -import BackgroundBlobs from "@/components/BackgroundBlobs"; - -export default function BackgroundBlobsClient() { - // Avoid SSR/webpack bailout issues from `next/dynamic({ ssr:false })` - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted) return null; - - return ; -} diff --git a/app/components/ClientProviders.tsx b/app/components/ClientProviders.tsx index bf90804..f2e0bf5 100644 --- a/app/components/ClientProviders.tsx +++ b/app/components/ClientProviders.tsx @@ -1,31 +1,17 @@ "use client"; -import React, { useEffect, useState } from "react"; -import { usePathname } from "next/navigation"; -import dynamic from "next/dynamic"; +import React from "react"; import { MotionConfig } from "framer-motion"; import { ToastProvider } from "@/components/Toast"; import ErrorBoundary from "@/components/ErrorBoundary"; import { ConsentProvider } from "./ConsentProvider"; import { ThemeProvider } from "./ThemeProvider"; -const BackgroundBlobs = dynamic( - () => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), - { ssr: false, loading: () => null } -); - export default function ClientProviders({ children, }: { children: React.ReactNode; }) { - const [mounted, setMounted] = useState(false); - const pathname = usePathname(); - - useEffect(() => { - setMounted(true); - }, [pathname]); - return ( @@ -33,7 +19,7 @@ export default function ClientProviders({ {/* Respect prefers-reduced-motion in every Framer Motion animation */} - + {children} @@ -44,31 +30,10 @@ export default function ClientProviders({ ); } -function GatedProviders({ - children, - mounted, -}: { - children: React.ReactNode; - mounted: boolean; -}) { - // Defer animated background blobs until after LCP - const [deferredReady, setDeferredReady] = useState(false); - useEffect(() => { - if (!mounted) return; - let id: ReturnType | number; - if (typeof requestIdleCallback !== "undefined") { - id = requestIdleCallback(() => setDeferredReady(true), { timeout: 5000 }); - return () => cancelIdleCallback(id as number); - } else { - id = setTimeout(() => setDeferredReady(true), 200); - return () => clearTimeout(id); - } - }, [mounted]); - +function GatedProviders({ children }: { children: React.ReactNode }) { return ( - {deferredReady && }
{children}
diff --git a/app/components/HeaderClient.tsx b/app/components/HeaderClient.tsx index fbd5b46..2584bfd 100644 --- a/app/components/HeaderClient.tsx +++ b/app/components/HeaderClient.tsx @@ -86,174 +86,131 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps const enHref = `/en${pathWithoutLocale}${query}`; const deHref = `/de${pathWithoutLocale}${query}`; + const de = locale === "de"; + + // Über dem roten Plakat bleibt der Kopf transparent, sonst wird er zum Papierstreifen + const solid = scrolled || !isHome; + const pill = + "rounded-full border-[1.5px] border-ink px-4 py-2 text-sm font-medium transition-[background-color,color,transform] duration-150 ease-out hover:bg-ink hover:text-paper active:scale-[0.97]"; + const langPill = (active: boolean) => + `rounded-full px-3 py-2 font-mono text-xs transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.97] ${ + active ? "bg-ink text-paper" : "hover:bg-ink/10" + }`; + return ( <> -
-
-
+
+ -
- - dk0 + dk0 + + + - - - -
+
- {/* Mobile menu overlay — always mounted so it can fade out, not just vanish */} + {/* Abdunklung hinter dem Menü — bleibt gemountet, damit sie ausblenden kann */} }> - - -
{children}
- -
- - ); -} diff --git a/app/components/ShaderGradientBackground.tsx b/app/components/ShaderGradientBackground.tsx deleted file mode 100644 index 9c5db96..0000000 --- a/app/components/ShaderGradientBackground.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// Pure CSS gradient background — replaces the Three.js/WebGL shader gradient. -// Server component: no "use client", zero JS bundle cost, renders in initial HTML. -// Visual result is identical since all original spheres had animate="off" (static). -export default function ShaderGradientBackground() { - return ( -