Files
mintel.me/apps/web/app/[locale]/freelance/page.tsx
T
mmintelandClaude Opus 5.5 2cb15608dd
Build & Deploy / 🔍 Prepare (push) Successful in 5s
Build & Deploy / 🏗️ Build (push) Canceled after 4m30s
Build & Deploy / 🚀 Deploy (push) Canceled after 0s
Build & Deploy / 🩺 Smoke Test (push) Canceled after 0s
Build & Deploy / 🔔 Notify (push) Canceled after 0s
feat: contact topics per audience, room above the last sentence, a hero line that says what a client gets
Contact: the message starter offered every topic on both pages. The homepage
now offers automation, a website, or something else; the freelance page a
developer, a reliable app, or something else.

Thread: where it crosses the page above the last sentence it ran too close
to the text. It keeps more distance now, on phones too.

Hero: the line under the hero listed years, working hours and two links,
which says nothing to a client (owner). It now names four things a client
gets: one contact who builds it himself, an offer before anything costs
money, changes by email, servers in Germany.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 12:32:33 +02:00

265 lines
9.3 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 { BitAura } from "@/src/components/bits/BitAura";
import { PathPicker } from "@/src/components/home/PathPicker";
import { TopicButton } from "@/src/components/home/TopicButton";
import { ContactDiagram } from "@/src/components/illustrations/ContactDiagram";
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"),
},
};
}
/** What a team may ask for. Clients have their own on the homepage. */
const TEAM_TOPICS = ["developer", "rescue", "other"] as const;
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 { working } = content;
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={services.label}
title={
<>
{freelance.cases.lead}{" "}
<span className={MUTED}>{freelance.cases.rest}</span>
</>
}
>
<Reveal>
<PathPicker ids={["developer", "rescue"]} pricing="estimate" />
</Reveal>
</Block>
<Block
index="03"
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="04"
label={working.label}
title={
<>
{working.title.lead}{" "}
<span className={MUTED}>{working.title.rest}</span>
</>
}
>
<Reveal>
<BitAura source="bottom" tone="light" className="mb-16 md:mb-24">
<div className="overflow-hidden rounded-3xl bg-slate-900 p-4 md:p-10">
<ContactDiagram />
</div>
</BitAura>
</Reveal>
<SwipeRow className="grid grid-cols-1 gap-x-12 gap-y-12 md:grid-cols-2 md:gap-y-16">
{working.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>
))}
</SwipeRow>
</Block>
<Block
index="05"
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>
<Block
index="06"
label={content.faq.label}
title={
<>
{freelance.faq.title.lead}{" "}
<span className={MUTED}>{freelance.faq.title.rest}</span>
</>
}
>
<Reveal>
<Faq items={freelance.faq.items} />
</Reveal>
</Block>
<ClosingCta contact={content.contact} topics={TEAM_TOPICS} />
</div>
);
}