feat: the homepage speaks to people who commission a result
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>
This commit is contained in:
@@ -7,21 +7,16 @@ import { DecodeText } from "@/src/components/bits/DecodeText";
|
||||
import { PathScene } from "@/src/components/illustrations/PathScenes";
|
||||
import { useContent } from "@/src/components/ContentProvider";
|
||||
import { TopicButton } from "./TopicButton";
|
||||
import type { ServicePath } from "@/src/content/types";
|
||||
import type { ServicePath, ServicePathId } from "@/src/content/types";
|
||||
|
||||
const MONO = "font-mono text-[11px] uppercase tracking-[0.2em] text-slate-500";
|
||||
const STEP_MS = 4800;
|
||||
/**
|
||||
* Booked by the hour by a team, not bought as a result: this case is not on the
|
||||
* homepage. It lives on the freelance page.
|
||||
*/
|
||||
const TEAM_PATH = "developer";
|
||||
|
||||
/**
|
||||
* PathStory: the four steps of one path, each with a drawing that explains it.
|
||||
* The steps advance on their own until the visitor picks one.
|
||||
*/
|
||||
export const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
|
||||
const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
|
||||
path,
|
||||
active,
|
||||
}) => {
|
||||
@@ -151,10 +146,17 @@ export const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
|
||||
* what they get, how it runs, what it costs, what to do next.
|
||||
* All three panels stay in the DOM so the content is readable without JavaScript.
|
||||
*/
|
||||
export const PathPicker: React.FC = () => {
|
||||
export const PathPicker: React.FC<{
|
||||
/** The cases to show, in this order. */
|
||||
ids: readonly ServicePathId[];
|
||||
/** `offer`: ask for an offer, no figures. `estimate`: the way to a price, with figures. */
|
||||
pricing: "offer" | "estimate";
|
||||
}> = ({ ids, pricing }) => {
|
||||
const { services } = useContent();
|
||||
const { estimate } = services;
|
||||
const paths = services.paths.filter((path) => path.id !== TEAM_PATH);
|
||||
const paths = ids.flatMap((id) =>
|
||||
services.paths.filter((path) => path.id === id),
|
||||
);
|
||||
const [activeId, setActiveId] = React.useState(paths[0]?.id ?? "");
|
||||
|
||||
return (
|
||||
@@ -162,7 +164,7 @@ export const PathPicker: React.FC = () => {
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={services.tablistLabel}
|
||||
className="grid grid-cols-1 gap-2 md:grid-cols-3 md:gap-4"
|
||||
className="grid grid-cols-1 gap-2 md:grid-cols-2 md:gap-4"
|
||||
>
|
||||
{paths.map((path, index) => {
|
||||
const selected = path.id === activeId;
|
||||
@@ -249,48 +251,70 @@ export const PathPicker: React.FC = () => {
|
||||
<PathStory path={path} active={selected} />
|
||||
</div>
|
||||
|
||||
<div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-16 md:p-10">
|
||||
<p className={`${MONO} m-0`}>{estimate.label}</p>
|
||||
<div className="mt-6 grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-12">
|
||||
<div className="md:col-span-7">
|
||||
{pricing === "offer" ? (
|
||||
<div className="mt-12 flex flex-col gap-8 rounded-3xl bg-slate-50 p-6 md:mt-16 md:flex-row md:items-center md:justify-between md:gap-12 md:p-10">
|
||||
<div className="max-w-2xl">
|
||||
<p className="m-0 font-sans text-2xl font-bold leading-tight tracking-tight text-slate-900 md:text-4xl">
|
||||
{estimate.title}
|
||||
{services.offer.title}
|
||||
</p>
|
||||
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500 md:text-lg">
|
||||
{services.offer.text}
|
||||
</p>
|
||||
<ol className="m-0 mt-6 list-none p-0">
|
||||
{estimate.steps.map((item, index) => (
|
||||
<li
|
||||
key={item.title}
|
||||
className="flex gap-5 border-t border-slate-200 py-4"
|
||||
>
|
||||
<span className="w-6 shrink-0 font-sans text-2xl font-black leading-none tracking-tighter text-slate-300">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>
|
||||
<span className="block font-sans text-lg font-bold tracking-tight text-slate-900">
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="mt-1 block font-sans text-base leading-relaxed text-slate-500">
|
||||
{item.text}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<div className="flex flex-col md:col-span-5">
|
||||
<p className={`${MONO} m-0`}>{estimate.orientationLabel}</p>
|
||||
<p className="m-0 mt-3 font-sans text-2xl font-black leading-tight tracking-tighter text-slate-900 md:text-4xl">
|
||||
{path.orientation}
|
||||
</p>
|
||||
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500">
|
||||
{path.orientationNote} {estimate.net}
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col items-start gap-5 md:mt-auto md:pt-8">
|
||||
<TopicButton topic={path.id}>{estimate.cta}</TopicButton>
|
||||
<div className="shrink-0">
|
||||
<TopicButton topic={path.id}>
|
||||
{services.offer.cta}
|
||||
</TopicButton>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-12 rounded-3xl bg-slate-50 p-6 md:mt-16 md:p-10">
|
||||
<p className={`${MONO} m-0`}>{estimate.label}</p>
|
||||
<div className="mt-6 grid grid-cols-1 gap-10 md:grid-cols-12 md:gap-12">
|
||||
<div className="md:col-span-7">
|
||||
<p className="m-0 font-sans text-2xl font-bold leading-tight tracking-tight text-slate-900 md:text-4xl">
|
||||
{estimate.title}
|
||||
</p>
|
||||
<ol className="m-0 mt-6 list-none p-0">
|
||||
{estimate.steps.map((item, index) => (
|
||||
<li
|
||||
key={item.title}
|
||||
className="flex gap-5 border-t border-slate-200 py-4"
|
||||
>
|
||||
<span className="w-6 shrink-0 font-sans text-2xl font-black leading-none tracking-tighter text-slate-300">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>
|
||||
<span className="block font-sans text-lg font-bold tracking-tight text-slate-900">
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="mt-1 block font-sans text-base leading-relaxed text-slate-500">
|
||||
{item.text}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<div className="flex flex-col md:col-span-5">
|
||||
<p className={`${MONO} m-0`}>
|
||||
{estimate.orientationLabel}
|
||||
</p>
|
||||
<p className="m-0 mt-3 font-sans text-2xl font-black leading-tight tracking-tighter text-slate-900 md:text-4xl">
|
||||
{path.orientation}
|
||||
</p>
|
||||
<p className="m-0 mt-4 font-sans text-base leading-relaxed text-slate-500">
|
||||
{path.orientationNote} {estimate.net}
|
||||
</p>
|
||||
<div className="mt-8 flex flex-col items-start gap-5 md:mt-auto md:pt-8">
|
||||
<TopicButton topic={path.id}>
|
||||
{estimate.cta}
|
||||
</TopicButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user