Section headlines are written out behind a column of digits, mono labels resolve from binary, the portrait assembles itself from bits, section indexes rattle and keep one bit alive, cards get corner marks, the header shows the scroll position as an eight-bit number, pressed links and buttons shed a few digits, and the work stage leans towards the pointer. All effects respect reduced motion and leave the content readable without JavaScript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { DecodeText } from "@/src/components/bits/DecodeText";
|
|
import { Block } from "@/src/components/home/Block";
|
|
import { ClosingCta } from "@/src/components/home/ClosingCta";
|
|
import { Faq } from "@/src/components/home/Faq";
|
|
import { HeroStatement } from "@/src/components/home/HeroStatement";
|
|
import { HomeJsonLd } from "@/src/components/home/HomeJsonLd";
|
|
import { PathPicker } from "@/src/components/home/PathPicker";
|
|
import { PortraitSection } from "@/src/components/home/PortraitSection";
|
|
import { StickyBook } from "@/src/components/home/StickyBook";
|
|
import { AutomationFlow } from "@/src/components/illustrations/AutomationFlow";
|
|
import { CareerLine } from "@/src/components/illustrations/CareerLine";
|
|
import { ContactDiagram } from "@/src/components/illustrations/ContactDiagram";
|
|
import { PipelineRun } from "@/src/components/illustrations/PipelineRun";
|
|
import {
|
|
WorkShowcase,
|
|
type WorkItem,
|
|
} from "@/src/components/illustrations/WorkShowcase";
|
|
import { Reveal } from "@/src/components/Reveal";
|
|
import { CAREER, FIT, TECH } from "@/src/content/about";
|
|
import { SITE_DESCRIPTION, SITE_TITLE } from "@/src/content/site";
|
|
import { AUTOMATION_NOTES } from "@/src/content/automation";
|
|
|
|
export const metadata: Metadata = {
|
|
title: { absolute: SITE_TITLE },
|
|
description: SITE_DESCRIPTION,
|
|
alternates: { canonical: "/" },
|
|
};
|
|
|
|
const MUTED = "text-slate-400";
|
|
const BODY = "m-0 font-sans text-lg leading-relaxed text-slate-500 md:text-xl";
|
|
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500";
|
|
|
|
const PRINCIPLES = [
|
|
{
|
|
title: "One contact",
|
|
text: "You talk to me, and I am the one who does the work.",
|
|
},
|
|
{
|
|
title: "Hours you can check",
|
|
text: "I track my time. The monthly invoice shows what the hours were spent on.",
|
|
},
|
|
{
|
|
title: "Careful work",
|
|
text: "I test what I build before I hand it over, and I say so when something is not finished or I am not sure.",
|
|
},
|
|
{
|
|
title: "What I build is yours",
|
|
text: "Code and design belong to you. You can hand them to another developer.",
|
|
},
|
|
] as const;
|
|
|
|
const WORK: readonly WorkItem[] = [
|
|
{
|
|
name: "klz-cables.com",
|
|
href: "https://klz-cables.com",
|
|
client: "Cable supplier",
|
|
text: "The company website of a supplier of energy cables, in German and English.",
|
|
did: [
|
|
"Product catalogue with categories and detail pages",
|
|
"Datasheets generated from the product data",
|
|
"Content the client can edit in a CMS",
|
|
],
|
|
tech: ["Next.js", "TypeScript", "Headless CMS", "Two languages"],
|
|
desktop: { src: "/work/klz-long.webp", width: 1200, height: 1680 },
|
|
mobile: { src: "/work/klz-mobile.webp", width: 390, height: 3600 },
|
|
},
|
|
{
|
|
name: "e-tib.com",
|
|
href: "https://e-tib.com",
|
|
client: "Cable network construction",
|
|
text: "The company website of a group in cable network construction, in German and English.",
|
|
did: [
|
|
"Pages for services, references, careers and events",
|
|
"The same content in two languages",
|
|
"Works on phone, tablet and desktop",
|
|
],
|
|
tech: ["Next.js", "TypeScript", "Headless CMS", "Two languages"],
|
|
desktop: { src: "/work/etib-long.webp", width: 1200, height: 4333 },
|
|
mobile: { src: "/work/etib-mobile.webp", width: 390, height: 3600 },
|
|
},
|
|
];
|
|
|
|
export default function LandingPage() {
|
|
return (
|
|
<div className="relative flex flex-col bg-white">
|
|
<HomeJsonLd />
|
|
<HeroStatement />
|
|
<StickyBook />
|
|
|
|
<PortraitSection
|
|
id="hello"
|
|
index="01"
|
|
label="Hello"
|
|
title={<>I am Marc.</>}
|
|
>
|
|
<p className="m-0 font-sans text-2xl leading-snug text-white md:text-4xl">
|
|
I have been building for the web for 15 years: first in agencies,
|
|
later as a senior developer, and now on my own.
|
|
</p>
|
|
<p className="m-0 mt-8 font-sans text-lg leading-relaxed text-slate-300 md:text-xl">
|
|
My strength is the part people see and use: design and interface. I am
|
|
also at home with what lies behind it, so I can build a complete
|
|
application and not just its surface.
|
|
</p>
|
|
<p className="m-0 mt-6 font-sans text-lg leading-relaxed text-slate-300 md:text-xl">
|
|
You work with me directly. Whether you run a product team or are
|
|
commissioning something like this for the first time.
|
|
</p>
|
|
</PortraitSection>
|
|
|
|
<section className="bg-white">
|
|
<div className="mx-auto w-full max-w-[1400px] px-5 py-16 md:px-12 md:py-28">
|
|
<p className={`${MONO} m-0 mb-10`}>The path so far</p>
|
|
<CareerLine stations={CAREER} />
|
|
</div>
|
|
</section>
|
|
|
|
<Block
|
|
index="02"
|
|
label="Services"
|
|
title={
|
|
<>
|
|
What do you need? <span className={MUTED}>Four typical cases.</span>
|
|
</>
|
|
}
|
|
>
|
|
<Reveal>
|
|
<PathPicker />
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<Block
|
|
index="03"
|
|
label="Process automation"
|
|
title={
|
|
<>
|
|
Let the systems <span className={MUTED}>do the copying.</span>
|
|
</>
|
|
}
|
|
>
|
|
<Reveal>
|
|
<p className={`${BODY} mb-12 max-w-3xl`}>
|
|
In most companies, people move information from one tool to another:
|
|
read it here, type it in there, send an email about it. Automation
|
|
connects those tools so that the work passes from one to the next on
|
|
its own. Three typical examples:
|
|
</p>
|
|
</Reveal>
|
|
<Reveal>
|
|
<AutomationFlow />
|
|
</Reveal>
|
|
<div className="mt-16 grid grid-cols-1 gap-12 md:mt-24 md:grid-cols-3 md:gap-10">
|
|
{AUTOMATION_NOTES.map((group, i) => (
|
|
<Reveal key={group.title} delay={0.1 * i}>
|
|
<div className="border-t border-slate-900 pt-6">
|
|
<p className={`${MONO} m-0`}>{group.title}</p>
|
|
<ul className="m-0 mt-4 list-none p-0">
|
|
{group.items.map((item) => (
|
|
<li
|
|
key={item}
|
|
className="border-b border-slate-100 py-4 font-sans text-lg text-slate-900 md:text-xl"
|
|
>
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</Reveal>
|
|
))}
|
|
</div>
|
|
</Block>
|
|
|
|
<Block
|
|
index="04"
|
|
label="Technology"
|
|
title={
|
|
<>
|
|
Which projects <span className={MUTED}>I am right for.</span>
|
|
</>
|
|
}
|
|
>
|
|
<div className="grid grid-cols-1 gap-12 md:grid-cols-3 md:gap-10">
|
|
{FIT.map((group, i) => (
|
|
<Reveal key={group.title} delay={0.1 * i}>
|
|
<div
|
|
className={`border-t pt-6 ${i === 2 ? "border-slate-300" : "border-slate-900"}`}
|
|
>
|
|
<p className={`${MONO} m-0`}>{group.title}</p>
|
|
<ul className="m-0 mt-4 list-none p-0">
|
|
{group.items.map((item) => (
|
|
<li
|
|
key={item}
|
|
className={`border-b border-slate-100 py-4 font-sans text-lg md:text-xl ${
|
|
i === 2 ? "text-slate-400" : "text-slate-900"
|
|
}`}
|
|
>
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</Reveal>
|
|
))}
|
|
</div>
|
|
|
|
<Reveal>
|
|
<div className="mt-16 rounded-3xl bg-slate-50 p-6 md:mt-24 md:p-10">
|
|
<p className={`${MONO} m-0`}>What I work with</p>
|
|
<div className="mt-8 grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
|
{TECH.map((group) => (
|
|
<div key={group.area}>
|
|
<p className="m-0 font-sans text-xl font-bold tracking-tight text-slate-900 md:text-2xl">
|
|
{group.area}
|
|
</p>
|
|
<ul className="m-0 mt-4 flex list-none flex-wrap gap-2 p-0">
|
|
{group.items.map((item) => (
|
|
<li
|
|
key={item}
|
|
className="rounded-full border border-slate-200 bg-white px-4 py-2 font-mono text-[11px] uppercase tracking-[0.12em] text-slate-700"
|
|
>
|
|
<DecodeText text={item} trigger="both" />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Reveal>
|
|
|
|
<div className="mt-16 md:mt-24">
|
|
<Reveal>
|
|
<h3 className="m-0 font-sans text-3xl font-bold tracking-tighter text-slate-900 md:text-5xl">
|
|
What happens before a change goes live.
|
|
</h3>
|
|
<p className={`${BODY} mb-12 mt-6 max-w-3xl`}>
|
|
Software is connected: a change in one place can break another. I
|
|
secure what I build with automated tests, and every change has to
|
|
pass them before it is released. This is what such a run looks
|
|
like.
|
|
</p>
|
|
</Reveal>
|
|
<Reveal>
|
|
<PipelineRun />
|
|
</Reveal>
|
|
</div>
|
|
</Block>
|
|
|
|
<Block
|
|
index="05"
|
|
label="Working with me"
|
|
title={
|
|
<>
|
|
How I work. <span className={MUTED}>Nothing unusual.</span>
|
|
</>
|
|
}
|
|
>
|
|
<Reveal>
|
|
<div className="mb-16 overflow-hidden rounded-3xl bg-slate-900 p-4 md:mb-24 md:p-10">
|
|
<ContactDiagram />
|
|
</div>
|
|
</Reveal>
|
|
<div className="grid grid-cols-1 gap-x-12 gap-y-12 md:grid-cols-2 md:gap-y-16">
|
|
{PRINCIPLES.map((item, i) => (
|
|
<Reveal key={item.title} delay={0.08 * i}>
|
|
<div className="border-t border-slate-900 pt-6">
|
|
<h3 className="m-0 font-sans text-3xl font-bold tracking-tight text-slate-900 md:text-4xl">
|
|
{item.title}
|
|
</h3>
|
|
<p className={`${BODY} mt-4`}>{item.text}</p>
|
|
</div>
|
|
</Reveal>
|
|
))}
|
|
</div>
|
|
</Block>
|
|
|
|
<Block
|
|
index="06"
|
|
label="My work"
|
|
title={
|
|
<>
|
|
Two sites I built. <span className={MUTED}>Both are live.</span>
|
|
</>
|
|
}
|
|
>
|
|
<WorkShowcase items={WORK} />
|
|
<Reveal>
|
|
<p
|
|
className={`${BODY} mt-20 max-w-3xl border-t border-slate-200 pt-8 md:mt-28`}
|
|
>
|
|
Besides client work I build my own software, among other things a
|
|
system in Rust and Solidity that runs on seven blockchains.
|
|
</p>
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<Block
|
|
index="07"
|
|
label="Questions"
|
|
title={
|
|
<>
|
|
Good to know <span className={MUTED}>before we talk.</span>
|
|
</>
|
|
}
|
|
>
|
|
<Reveal>
|
|
<Faq />
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<ClosingCta />
|
|
</div>
|
|
);
|
|
}
|