Files
mintel.me/apps/web/app/[locale]/freelance/page.tsx
T
mmintelandClaude Opus 5.5 c7f8672cd0 feat: estimates on the homepage, monthly booking on the freelance page
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>
2026-10-05 10:01:23 +02:00

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>
);
}