Drawings: "How I work" and the step drawings under "How it runs" now use the same language as the story thread. Systems and people are chips with pins, connections are tracks with 45 degree bends and solder points, and what travels is a bit instead of a round dot (circuit.tsx). Hero: the digits in the background were close to invisible on white. They are about twice as strong now, and the ones near the pointer light up indigo. Homepage vs. freelance page: the homepage is for clients who buy a result, so it shows three cases. The developer case (100 EUR per hour, booked by the month) and its steps live on the freelance page only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
209 lines
7.6 KiB
TypeScript
209 lines
7.6 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 { PathStory } from "@/src/components/home/PathPicker";
|
|
import { TopicButton } from "@/src/components/home/TopicButton";
|
|
import { PipelineRun } from "@/src/components/illustrations/PipelineRun";
|
|
import { Reveal } from "@/src/components/Reveal";
|
|
import { SwipeRow } from "@/src/components/SwipeRow";
|
|
import { contentFor } from "@/src/content";
|
|
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 { freelance } = contentFor(locale);
|
|
return {
|
|
title: { absolute: freelance.metaTitle },
|
|
description: freelance.metaDescription,
|
|
alternates: {
|
|
canonical: pathFor("freelance", locale),
|
|
languages: languageAlternates("freelance"),
|
|
},
|
|
};
|
|
}
|
|
|
|
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 md:text-2xl";
|
|
|
|
/**
|
|
* The page for people who develop themselves or lead a team: which projects
|
|
* fit, the tools, and how a change is secured before it goes live.
|
|
*/
|
|
export default async function FreelancePage({ params }: Props) {
|
|
const locale = parseLocale((await params).locale) ?? DEFAULT_LOCALE;
|
|
const content = contentFor(locale);
|
|
const { freelance, technology } = content;
|
|
const { booking } = freelance;
|
|
const { services } = content;
|
|
const teamPath = services.paths.find((path) => path.id === "developer");
|
|
|
|
return (
|
|
<div className="relative flex flex-col bg-white">
|
|
<section className="bg-white">
|
|
<div className="mx-auto w-full max-w-[1400px] px-5 pb-4 pt-14 md:px-12 md:pb-10 md:pt-28">
|
|
<p className="m-0 font-mono text-[11px] uppercase tracking-[0.25em] text-slate-500">
|
|
{freelance.label}
|
|
</p>
|
|
<h1 className="m-0 mt-8 max-w-6xl font-sans text-4xl font-bold leading-[0.95] tracking-tighter text-slate-900 md:text-7xl lg:text-[6.5rem]">
|
|
{freelance.title.lead}{" "}
|
|
<span className={MUTED}>{freelance.title.rest}</span>
|
|
</h1>
|
|
<p className={`${BODY} mt-10 max-w-3xl md:mt-14`}>
|
|
{freelance.intro}
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<Block
|
|
index="01"
|
|
label={technology.label}
|
|
title={
|
|
<>
|
|
{technology.title.lead}{" "}
|
|
<span className={MUTED}>{technology.title.rest}</span>
|
|
</>
|
|
}
|
|
>
|
|
<SwipeRow className="grid grid-cols-1 gap-12 md:grid-cols-3 md:gap-10">
|
|
{technology.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"}`}
|
|
>
|
|
<h3
|
|
className={`${LIST_TITLE} ${i === 2 ? "text-slate-400" : "text-slate-900"}`}
|
|
>
|
|
{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 md:text-xl ${
|
|
i === 2 ? "text-slate-400" : "text-slate-900"
|
|
}`}
|
|
>
|
|
{item}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</Reveal>
|
|
))}
|
|
</SwipeRow>
|
|
|
|
<Reveal>
|
|
<div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-24 md:p-10">
|
|
<h3 className={`${LIST_TITLE} text-slate-900`}>
|
|
{technology.techLabel}
|
|
</h3>
|
|
<div className="mt-6 grid grid-cols-1 gap-7 sm:grid-cols-2 md:mt-8 md:gap-10 lg:grid-cols-4">
|
|
{technology.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>
|
|
</Block>
|
|
|
|
<Block
|
|
index="02"
|
|
label={freelance.pipelineLabel}
|
|
title={technology.pipelineTitle}
|
|
>
|
|
<Reveal>
|
|
<p className={`${BODY} mb-12 max-w-3xl`}>
|
|
{technology.pipelineIntro}
|
|
</p>
|
|
</Reveal>
|
|
<Reveal>
|
|
<PipelineRun />
|
|
</Reveal>
|
|
</Block>
|
|
|
|
{teamPath && (
|
|
<Block index="03" label={services.howItRuns} title={teamPath.headline}>
|
|
<Reveal>
|
|
<p className={`${BODY} mb-12 max-w-3xl`}>{teamPath.intro}</p>
|
|
</Reveal>
|
|
<Reveal>
|
|
<PathStory path={teamPath} active />
|
|
</Reveal>
|
|
</Block>
|
|
)}
|
|
|
|
<Block
|
|
index="04"
|
|
label={booking.label}
|
|
title={
|
|
<>
|
|
{booking.title.lead}{" "}
|
|
<span className={MUTED}>{booking.title.rest}</span>
|
|
</>
|
|
}
|
|
>
|
|
<Reveal>
|
|
<div className="grid grid-cols-1 gap-12 md:grid-cols-12 md:gap-12">
|
|
<div className="md:col-span-6">
|
|
<p className={BODY}>{booking.text}</p>
|
|
<ul className="m-0 mt-8 list-none p-0">
|
|
{booking.terms.map((term) => (
|
|
<li
|
|
key={term}
|
|
className="border-t border-slate-200 py-4 font-sans text-lg text-slate-900 md:text-xl"
|
|
>
|
|
{term}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<div className="md:col-span-6">
|
|
<div className="rounded-3xl bg-slate-50 p-6 md:p-10">
|
|
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2">
|
|
{[booking.rate, booking.example].map((price) => (
|
|
<div key={price.label}>
|
|
<p className="m-0 font-sans text-base text-slate-500 md:text-lg">
|
|
{price.label}
|
|
</p>
|
|
<p className="m-0 mt-2 font-sans text-3xl font-black tracking-tighter text-slate-900 md:text-5xl">
|
|
{price.amount}
|
|
</p>
|
|
<p className="m-0 mt-2 font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500">
|
|
{price.note}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="mt-10 border-t border-slate-200 pt-8">
|
|
<TopicButton topic="developer">{booking.cta}</TopicButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Reveal>
|
|
</Block>
|
|
|
|
<ClosingCta contact={content.contact} />
|
|
</div>
|
|
);
|
|
}
|