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 <noreply@anthropic.com>
This commit is contained in:
denshooter
2026-10-15 23:48:39 +02:00
committed by GitHub
parent 02914d6145
commit 96ec2d867f
19 changed files with 355 additions and 584 deletions
+28 -48
View File
@@ -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<string, string> = {
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
<img
src={src}
alt={alt}
data-fill={fill?.toString()}
data-priority={priority?.toString()}
{...props}
/>
),
getTranslations: () =>
Promise.resolve((key: string, values?: { days?: number }) => {
const messages: Record<string, string> = {
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');
});
});