The homepage showed a price list per case, including a day rate. Buyers of a result need a way to a price, not rates: each case now shows three steps to an estimate, one line for orientation and a button that opens the contact message with that topic chosen. The developer case is booked by the month, not by the day. The day rate is gone; the freelance page has the booking terms: 100 EUR per hour, fixed days of the week agreed for one month at a time, an example month, monthly invoice, two weeks notice to the end of a month. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
195 lines
7.2 KiB
TypeScript
195 lines
7.2 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 { 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;
|
|
|
|
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>
|
|
|
|
<Block
|
|
index="03"
|
|
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>
|
|
);
|
|
}
|