Owner: the homepage is for people who need a website or process automation
and have no app of their own; companies that book by the hour belong on the
freelance page.
Homepage: two cases instead of three (automation, website). The price box
with its steps and figures is replaced by one box: you get an offer before
anything costs money. No rates or weekly prices there any more.
Freelance page: gets the developer case and "make an app reliable" with
their steps and prices, and the section on how working together goes.
Headlines the owner called unclear are rewritten: the automation section
("Your tools do the routine work. Nobody retypes anything.") and the
working section ("How working together goes. Direct and easy to follow.").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
278 lines
10 KiB
TypeScript
278 lines
10 KiB
TypeScript
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import { BitAura } from "@/src/components/bits/BitAura";
|
|
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 {
|
|
PresentationCompare,
|
|
ReasonDrawing,
|
|
} from "@/src/components/illustrations/WebPresentation";
|
|
import { WorkShowcase } from "@/src/components/illustrations/WorkShowcase";
|
|
import { Reveal } from "@/src/components/Reveal";
|
|
import { StoryThread } from "@/src/components/story/StoryThread";
|
|
import { SwipeRow } from "@/src/components/SwipeRow";
|
|
import { contentFor } from "@/src/content";
|
|
import type { SplitTitle } from "@/src/content/types";
|
|
import { DEFAULT_LOCALE, parseLocale } from "@/src/domain/locale";
|
|
import { languageAlternates, pathFor } from "@/src/domain/routes";
|
|
|
|
type Props = { readonly params: Promise<{ readonly locale: string }> };
|
|
|
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
|
const { site } = contentFor(locale);
|
|
return {
|
|
title: { absolute: site.title },
|
|
description: site.description,
|
|
alternates: {
|
|
canonical: pathFor("home", locale),
|
|
languages: languageAlternates("home"),
|
|
},
|
|
};
|
|
}
|
|
|
|
const MUTED = "text-slate-400";
|
|
const BODY = "m-0 font-sans text-lg leading-relaxed text-slate-500 md:text-xl";
|
|
const LIST_TITLE =
|
|
"m-0 font-sans text-xl font-bold tracking-tight text-slate-900 md:text-2xl";
|
|
|
|
const Title: React.FC<{ title: SplitTitle; muted?: string }> = ({
|
|
title,
|
|
muted = MUTED,
|
|
}) => (
|
|
<>
|
|
{title.lead} <span className={muted}>{title.rest}</span>
|
|
</>
|
|
);
|
|
|
|
export default async function LandingPage({ params }: Props) {
|
|
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
|
const content = contentFor(locale);
|
|
const {
|
|
hello,
|
|
services,
|
|
automation,
|
|
presentations,
|
|
story,
|
|
freelance,
|
|
work,
|
|
faq,
|
|
} = content;
|
|
|
|
return (
|
|
<div className="relative flex flex-col bg-white">
|
|
<HomeJsonLd locale={locale} />
|
|
<HeroStatement hero={content.hero} />
|
|
<StickyBook />
|
|
|
|
<PortraitSection
|
|
id="hello"
|
|
index="01"
|
|
label={hello.label}
|
|
title={hello.title}
|
|
portraitAlt={hello.portraitAlt}
|
|
story={story.hello}
|
|
>
|
|
<p className="m-0 font-sans text-2xl leading-snug text-white md:text-4xl">
|
|
{hello.lead}
|
|
</p>
|
|
{hello.paragraphs.map((paragraph, i) => (
|
|
<p
|
|
key={paragraph}
|
|
className={`m-0 font-sans text-lg leading-relaxed text-slate-300 md:text-xl ${i === 0 ? "mt-8" : "mt-6"}`}
|
|
>
|
|
{paragraph}
|
|
</p>
|
|
))}
|
|
</PortraitSection>
|
|
|
|
<section className="relative overflow-clip bg-slate-900">
|
|
<div className="mx-auto w-full max-w-[1400px] px-5 pb-20 pt-4 md:px-12 md:pb-36 md:pt-0">
|
|
<p className="m-0 mb-10 font-mono text-[11px] uppercase tracking-[0.2em] text-slate-400">
|
|
{hello.careerLabel}
|
|
</p>
|
|
<CareerLine stations={hello.career} />
|
|
</div>
|
|
</section>
|
|
|
|
<Block
|
|
index="02"
|
|
label={services.label}
|
|
chapter={{ line: story.services, figure: "diamond", flip: false }}
|
|
title={<Title title={services.title} />}
|
|
>
|
|
<Reveal>
|
|
<PathPicker ids={["automation", "website"]} pricing="offer" />
|
|
</Reveal>
|
|
<Reveal>
|
|
<Link
|
|
href={pathFor("freelance", locale)}
|
|
className="group mt-14 flex flex-col gap-5 rounded-3xl bg-slate-50 p-6 no-underline transition-colors duration-300 hover:bg-slate-100 md:mt-24 md:flex-row md:items-center md:justify-between md:p-10"
|
|
>
|
|
<span className="block max-w-2xl">
|
|
<span className="block font-sans text-2xl font-bold tracking-tight text-slate-900 md:text-3xl">
|
|
{freelance.teaser.title}
|
|
</span>
|
|
<span className="mt-3 block font-sans text-base leading-relaxed text-slate-500 md:text-lg">
|
|
{freelance.teaser.text}
|
|
</span>
|
|
</span>
|
|
<span className="inline-flex shrink-0 items-center gap-3 self-start rounded-full bg-slate-900 px-6 py-3 font-sans text-[11px] font-bold uppercase tracking-[0.15em] text-white md:self-auto">
|
|
{freelance.teaser.cta}
|
|
<span
|
|
aria-hidden="true"
|
|
className="transition-transform duration-300 group-hover:translate-x-1"
|
|
>
|
|
→
|
|
</span>
|
|
</span>
|
|
</Link>
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<Block
|
|
index="03"
|
|
label={automation.label}
|
|
chapter={{ line: story.automation, figure: "hexagon", flip: true }}
|
|
title={<Title title={automation.title} />}
|
|
>
|
|
<Reveal>
|
|
<p className={`${BODY} mb-12 max-w-3xl`}>{automation.intro}</p>
|
|
</Reveal>
|
|
<Reveal>
|
|
<AutomationFlow />
|
|
</Reveal>
|
|
<SwipeRow className="mt-12 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">
|
|
<h3 className={LIST_TITLE}>{group.title}</h3>
|
|
<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>
|
|
))}
|
|
</SwipeRow>
|
|
</Block>
|
|
|
|
<Block
|
|
index="04"
|
|
label={presentations.label}
|
|
chapter={{ line: story.presentations, figure: "screen", flip: false }}
|
|
tone="dark"
|
|
title={<Title title={presentations.title} muted="text-slate-500" />}
|
|
>
|
|
<Reveal>
|
|
<p className="m-0 mb-12 max-w-3xl font-sans text-lg leading-relaxed text-slate-300 md:mb-16 md:text-xl">
|
|
{presentations.intro}
|
|
</p>
|
|
</Reveal>
|
|
<Reveal>
|
|
<BitAura source="around" tone="dark">
|
|
<div className="rounded-3xl border border-slate-700 bg-slate-950 px-4 py-8 md:px-10 md:py-12">
|
|
<PresentationCompare />
|
|
</div>
|
|
</BitAura>
|
|
</Reveal>
|
|
|
|
<SwipeRow className="mt-12 grid grid-cols-1 gap-x-10 gap-y-14 md:mt-28 md:grid-cols-2 lg:grid-cols-4">
|
|
{presentations.reasons.map((reason, i) => (
|
|
<Reveal key={reason.title} delay={0.08 * i}>
|
|
<div className="rounded-2xl border border-slate-800 bg-slate-950/60 p-4">
|
|
<ReasonDrawing index={i} />
|
|
</div>
|
|
<h3 className="m-0 mt-6 font-sans text-2xl font-bold tracking-tight text-white">
|
|
{reason.title}
|
|
</h3>
|
|
<p className="m-0 mt-3 font-sans text-base leading-relaxed text-slate-300 md:text-lg">
|
|
{reason.text}
|
|
</p>
|
|
</Reveal>
|
|
))}
|
|
</SwipeRow>
|
|
|
|
<div className="mt-16 grid grid-cols-1 gap-12 md:mt-28 md:grid-cols-12 md:gap-12">
|
|
<Reveal className="md:col-span-6">
|
|
<div className="border-t border-slate-700 pt-6">
|
|
<h3 className="m-0 font-sans text-xl font-bold tracking-tight text-white md:text-2xl">
|
|
{presentations.goodFor.title}
|
|
</h3>
|
|
<ul className="m-0 mt-4 list-none p-0">
|
|
{presentations.goodFor.items.map((item) => (
|
|
<li
|
|
key={item}
|
|
className="border-b border-slate-800 py-4 font-sans text-lg text-slate-100 md:text-xl"
|
|
>
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</Reveal>
|
|
<Reveal delay={0.1} className="md:col-span-6">
|
|
<div className="rounded-3xl border border-slate-700 bg-slate-950/60 p-6 md:p-10">
|
|
<h3 className="m-0 font-sans text-2xl font-bold tracking-tight text-white md:text-3xl">
|
|
{presentations.honest.title}
|
|
</h3>
|
|
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-300 md:text-lg">
|
|
{presentations.honest.text}
|
|
</p>
|
|
<a
|
|
href="#contact"
|
|
className="bit-cta bit-cta-light mt-8 inline-flex items-center gap-3 rounded-full bg-white px-6 py-3 font-sans text-[11px] font-bold uppercase tracking-[0.15em] text-slate-900 no-underline transition-transform duration-300 hover:-translate-y-0.5"
|
|
>
|
|
{presentations.cta} <span aria-hidden="true">→</span>
|
|
</a>
|
|
</div>
|
|
</Reveal>
|
|
</div>
|
|
</Block>
|
|
|
|
<Block
|
|
index="05"
|
|
label={work.label}
|
|
chapter={{ line: story.work, figure: "square", flip: true }}
|
|
tone="dark"
|
|
title={<Title title={work.title} muted="text-slate-500" />}
|
|
>
|
|
<WorkShowcase />
|
|
<Reveal>
|
|
<p className="m-0 mt-20 max-w-3xl border-t border-slate-800 pt-8 font-sans text-lg leading-relaxed text-slate-300 md:mt-28 md:text-xl">
|
|
{work.note}
|
|
</p>
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<Block
|
|
index="06"
|
|
label={faq.label}
|
|
chapter={{ line: story.faq, figure: "triangle", flip: false }}
|
|
title={<Title title={faq.title} />}
|
|
>
|
|
<Reveal>
|
|
<Faq items={faq.items} />
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<ClosingCta contact={content.contact} story={story.contact} />
|
|
<StoryThread />
|
|
</div>
|
|
);
|
|
}
|