Moves the Next.js app's contents from new-site/ to the repository root and deletes the previous Hugo site (assets/, content/, themes/, hugo.toml, etc.). Also retires the AWS Amplify config and old Netlify _redirects file — the new site deploys to Vercel. Updates STRATEGY.md path references to drop the new-site/ prefix. LICENSE preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
374 lines
13 KiB
TypeScript
374 lines
13 KiB
TypeScript
import { Button } from "@/components/button";
|
|
import { Container } from "@/components/container";
|
|
import { GuaranteesMini } from "@/components/guarantees";
|
|
import { JsonLd } from "@/components/jsonld";
|
|
import { Eyebrow, SectionHeading } from "@/components/section-heading";
|
|
import { serviceSchema } from "@/lib/jsonld";
|
|
import {
|
|
blocks,
|
|
formatPrice,
|
|
retainer,
|
|
saleActive,
|
|
saleLabel,
|
|
type CheckoutOption,
|
|
} from "@/lib/pricing";
|
|
import { SITE_URL } from "@/lib/site";
|
|
import type { Metadata } from "next";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "Services & Engagements",
|
|
description:
|
|
"Three ways to work with Five Devs: a one-shot project, a monthly retainer for ongoing help, or a block of senior engineering time you can spend as you go.",
|
|
};
|
|
|
|
function PriceLine({
|
|
option,
|
|
perSuffix,
|
|
invert,
|
|
size = "lg",
|
|
}: {
|
|
option: CheckoutOption;
|
|
perSuffix?: string;
|
|
invert?: boolean;
|
|
size?: "lg" | "md";
|
|
}) {
|
|
const muted = invert ? "text-cream/60" : "text-muted";
|
|
const strong = invert ? "text-cream" : "text-ink";
|
|
const showSale = option.salePrice !== undefined;
|
|
const priceClass =
|
|
size === "lg"
|
|
? "text-3xl font-semibold tracking-tight"
|
|
: "text-2xl font-semibold tracking-tight";
|
|
const strikeClass = size === "lg" ? "text-lg line-through" : "text-base line-through";
|
|
return (
|
|
<div className="space-y-1">
|
|
<div className="flex items-baseline gap-3">
|
|
{showSale ? (
|
|
<>
|
|
<span className={`${priceClass} ${strong}`}>
|
|
{formatPrice(option.salePrice!)}
|
|
{perSuffix ? (
|
|
<span className={`text-base font-normal ${muted}`}>
|
|
{perSuffix}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
<span className={`${strikeClass} ${muted}`}>
|
|
{formatPrice(option.regularPrice)}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span className={`${priceClass} ${strong}`}>
|
|
{formatPrice(option.regularPrice)}
|
|
{perSuffix ? (
|
|
<span className={`text-base font-normal ${muted}`}>
|
|
{perSuffix}
|
|
</span>
|
|
) : null}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className={`text-sm ${muted}`}>
|
|
{showSale && option.saleEffectiveHourly !== undefined
|
|
? `${formatPrice(option.saleEffectiveHourly)}/hr effective`
|
|
: `${formatPrice(option.effectiveHourly)}/hr effective`}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function BlockRow({ option }: { option: CheckoutOption }) {
|
|
const wired = !!option.buyHref;
|
|
const inner = (
|
|
<div className="flex flex-col gap-2 py-5 sm:flex-row sm:items-center sm:justify-between">
|
|
<div className="flex items-baseline gap-4">
|
|
<p className="font-medium text-ink">{option.label}</p>
|
|
{wired ? (
|
|
<span
|
|
aria-hidden="true"
|
|
className="font-mono text-base text-muted opacity-0 transition-all group-hover:translate-x-1 group-hover:text-accent group-hover:opacity-100"
|
|
>
|
|
→
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
<PriceLine option={option} size="md" />
|
|
</div>
|
|
);
|
|
|
|
if (!wired) return <li className="px-1">{inner}</li>;
|
|
|
|
return (
|
|
<li>
|
|
<a
|
|
href={option.buyHref}
|
|
className="group block -mx-3 rounded-lg px-3 transition-colors hover:bg-cream"
|
|
aria-label={`Buy ${option.label}`}
|
|
>
|
|
{inner}
|
|
</a>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
function SaleBadge({ invert }: { invert?: boolean }) {
|
|
if (!saleActive) return null;
|
|
return (
|
|
<span
|
|
className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium uppercase tracking-wider ${
|
|
invert
|
|
? "bg-cream/10 text-cream ring-1 ring-cream/30"
|
|
: "bg-accent/10 text-accent ring-1 ring-accent/30"
|
|
}`}
|
|
>
|
|
{saleLabel}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
const servicesUrl = `${SITE_URL}/services`;
|
|
|
|
const serviceSchemas = [
|
|
serviceSchema({
|
|
name: "Monthly PHP development retainer",
|
|
description:
|
|
"Reserved senior PHP development hours each month. Ideal for teams without a senior PHP developer in-house.",
|
|
url: servicesUrl,
|
|
price: retainer.salePrice ?? retainer.regularPrice,
|
|
priceType: "Subscription",
|
|
}),
|
|
...blocks.map((b) =>
|
|
serviceSchema({
|
|
name: `${b.label} of senior PHP development time`,
|
|
description: `Block of ${b.label.replace("-hour block", " hours")} of senior PHP development time, usable within 6 months.`,
|
|
url: servicesUrl,
|
|
price: b.salePrice ?? b.regularPrice,
|
|
priceType: "OneTime",
|
|
}),
|
|
),
|
|
serviceSchema({
|
|
name: "Project sprint",
|
|
description:
|
|
"Fixed-scope, fixed-price PHP development engagement quoted after a paid discovery sprint.",
|
|
url: servicesUrl,
|
|
}),
|
|
];
|
|
|
|
export default function ServicesPage() {
|
|
const retainerWired = !!retainer.buyHref;
|
|
return (
|
|
<>
|
|
{serviceSchemas.map((schema, i) => (
|
|
<JsonLd key={i} data={schema} />
|
|
))}
|
|
<section className="py-20 sm:py-28">
|
|
<Container>
|
|
<div className="max-w-2xl">
|
|
<Eyebrow>Services</Eyebrow>
|
|
<h1 className="mt-6 font-serif text-4xl font-semibold leading-tight tracking-tight text-ink sm:text-6xl">
|
|
Three ways to work together.
|
|
</h1>
|
|
<p className="mt-7 text-lg leading-relaxed text-ink-soft">
|
|
Most freelance engagements fall into one of three shapes.
|
|
Pick the one that matches how your work actually arrives.
|
|
Block and retainer engagements are{" "}
|
|
<span className="font-medium text-ink">self-serve</span>{" "}
|
|
— check out below, or book a call first if
|
|
you’d rather talk.
|
|
</p>
|
|
{saleActive ? (
|
|
<div className="mt-8 flex flex-wrap items-center gap-3">
|
|
<SaleBadge />
|
|
<p className="text-sm text-ink-soft">
|
|
Half off retainer and block engagements while the
|
|
redesign is fresh. First clients lock it in.
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
|
|
<section className="pb-24 sm:pb-32">
|
|
<Container>
|
|
<ul className="grid gap-6 lg:grid-cols-3">
|
|
{/* Project sprint — no checkout */}
|
|
<li className="flex flex-col rounded-2xl border border-line/80 bg-cream-soft p-8">
|
|
<h2 className="font-serif text-2xl font-semibold text-ink">
|
|
Project sprint
|
|
</h2>
|
|
<p className="mt-3 text-lg leading-snug text-ink-soft">
|
|
A defined piece of work, scoped, quoted, shipped.
|
|
</p>
|
|
|
|
<div className="mt-7 space-y-1">
|
|
<p className="text-3xl font-semibold tracking-tight text-ink">
|
|
Custom quote
|
|
</p>
|
|
<p className="text-sm text-muted">
|
|
Fixed-price after a paid discovery
|
|
</p>
|
|
</div>
|
|
|
|
<ul className="mt-7 space-y-3 text-sm leading-relaxed text-ink-soft">
|
|
{[
|
|
"Two weeks to two months",
|
|
"Daily progress in shared Slack or GitHub",
|
|
"Documented handoff at the end",
|
|
].map((b) => (
|
|
<li key={b} className="flex gap-3">
|
|
<span className="mt-2 inline-block h-1 w-1 flex-none rounded-full bg-accent" />
|
|
<span>{b}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className="mt-7 text-sm italic text-muted">
|
|
Best when you know exactly what you need built or fixed.
|
|
</p>
|
|
|
|
<div className="mt-auto pt-7">
|
|
<Button href="/contact" variant="primary">
|
|
Discuss a project →
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
|
|
{/* Monthly retainer — featured, with checkout */}
|
|
<li className="flex flex-col rounded-2xl border border-ink bg-ink p-8 text-cream">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<h2 className="font-serif text-2xl font-semibold text-cream">
|
|
Monthly retainer
|
|
</h2>
|
|
<SaleBadge invert />
|
|
</div>
|
|
<p className="mt-3 text-lg leading-snug text-cream/80">
|
|
Quiet, predictable senior help on the long tail.
|
|
</p>
|
|
|
|
<div className="mt-7">
|
|
<PriceLine option={retainer} perSuffix="/mo" invert />
|
|
<p className="mt-2 text-sm text-cream/70">
|
|
{retainer.hoursLabel}
|
|
</p>
|
|
</div>
|
|
|
|
<ul className="mt-7 space-y-3 text-sm leading-relaxed text-cream/85">
|
|
{[
|
|
"Reserved hours each month",
|
|
"Priority on incidents and requests",
|
|
"Quarterly roadmap & tech-health check-ins",
|
|
"Pause or cancel with 30 days notice",
|
|
].map((b) => (
|
|
<li key={b} className="flex gap-3">
|
|
<span className="mt-2 inline-block h-1 w-1 flex-none rounded-full bg-cream/70" />
|
|
<span>{b}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className="mt-7 text-sm italic text-cream/70">
|
|
Best for teams without a senior PHP developer in-house.
|
|
</p>
|
|
|
|
<div className="mt-auto space-y-3 pt-7">
|
|
<Button
|
|
href={retainer.buyHref ?? "/contact"}
|
|
variant="secondary"
|
|
className="w-full justify-center border-cream/30 text-cream hover:border-cream hover:bg-cream/10"
|
|
>
|
|
{retainerWired
|
|
? "Subscribe — $2,500/mo →"
|
|
: "Talk first →"}
|
|
</Button>
|
|
<Button
|
|
href="/contact"
|
|
variant="ghost"
|
|
className="w-full justify-center text-cream/70 hover:text-cream"
|
|
>
|
|
Or book a call first
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
|
|
{/* Block of time — selectable rows */}
|
|
<li className="flex flex-col rounded-2xl border border-line/80 bg-cream-soft p-8">
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<h2 className="font-serif text-2xl font-semibold text-ink">
|
|
Block of time
|
|
</h2>
|
|
<SaleBadge />
|
|
</div>
|
|
<p className="mt-3 text-lg leading-snug text-ink-soft">
|
|
Buy a block, spend it as needs come up.
|
|
</p>
|
|
|
|
<ul className="mt-7 divide-y divide-line/80 border-y border-line/80">
|
|
{blocks.map((b) => (
|
|
<BlockRow key={b.id} option={b} />
|
|
))}
|
|
</ul>
|
|
|
|
<p className="mt-7 text-sm italic text-muted">
|
|
Best for work that comes in bursts — integrations,
|
|
migrations, audits.
|
|
</p>
|
|
|
|
<div className="mt-auto pt-7">
|
|
<Button
|
|
href="/contact"
|
|
variant="secondary"
|
|
className="w-full justify-center"
|
|
>
|
|
Or book a call first
|
|
</Button>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
|
|
<p className="mt-10 max-w-2xl text-sm text-muted">
|
|
All purchases trigger a confirmation email and a link to
|
|
book your kickoff call. You’ll hear from me within
|
|
one business day.
|
|
</p>
|
|
|
|
<div className="mt-12">
|
|
<GuaranteesMini />
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
|
|
<section className="border-t border-line/70 bg-cream-soft py-24 sm:py-32">
|
|
<Container width="narrow">
|
|
<SectionHeading
|
|
eyebrow="A note on rates"
|
|
title="Senior, transparent, and worth it."
|
|
subtitle="Rates anchor in the senior end of the market. Retainer and block rates are lower per hour because I can plan around them."
|
|
/>
|
|
<div className="mt-12 space-y-6 text-ink-soft">
|
|
<p>
|
|
Project sprints are quoted as a fixed price after a short
|
|
paid discovery so we both know what we’re
|
|
committing to. Retainer and block prices are listed above
|
|
and you can check out directly — no email
|
|
ping-pong.
|
|
</p>
|
|
<p>
|
|
I do not subcontract or hand work off to junior
|
|
developers without telling you. If a project genuinely
|
|
needs a second pair of hands, I’ll tell you who and
|
|
why before they touch your code.
|
|
</p>
|
|
</div>
|
|
<div className="mt-12 flex flex-wrap items-center gap-4">
|
|
<Button href="/contact" variant="primary">
|
|
Book a call
|
|
</Button>
|
|
<Button href="mailto:chris@fivedevs.com" variant="ghost">
|
|
chris@fivedevs.com
|
|
</Button>
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|