Files
threadcount-community/app/(site)/pricing/page.tsx
T
ThreadCount 344b1701dd ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
2026-09-13 08:54:35 +10:00

196 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import type { Metadata } from "next";
import { Band, CtaBand, SiteNav, body, h2, h3, kicker, small, wrap } from "@/components/site";
import { PRICES } from "@/lib/plan";
import { plansLive } from "@/lib/plans-live";
/* Two pages in one file, and which one renders is decided by the console's plans switch, not by a
* deploy — see lib/plans-live.ts for why. The first is the page as it stood while ThreadCount was
* free, kept word for word: its promises are the reason the second page reads the way it does.
* The founder's sentence under "Why …" is the same on both; only the heading's object moves. */
export async function generateMetadata(): Promise<Metadata> {
const live = await plansLive();
return live
? { title: "Pricing", description: `ThreadCount is free to run yourself and free to host for rooms under ${PRICES.freeStaff} staff records. Hosting a facility is $${PRICES.hostedAnnual} a year. Every feature is in every edition.`, alternates: { canonical: "/pricing" } }
: { title: "Pricing", description: "ThreadCount is free. No licence, no per-device charge, no per-user charge, and no tier above this one holding the useful parts back.", alternates: { canonical: "/pricing" } };
}
const INCLUDED = [
{ t: "Every person, every device", b: "Access is per person and costs nothing, so the counter, the ward or wing, and the coordinators phone can all be signed in at once." },
{ t: "Every feature", b: "Issuing, ordering, receiving, stocktakes, all nine report tabs and the month-end pack. There is no locked tier." },
{ t: "Every record, exportable", b: "Every report and register exports as CSV. One backup file carries the whole facility out. Nothing is held back to make leaving hard." },
];
const COVERED = [
"Unlimited staff records", "Unlimited garments and sizes", "Unlimited issues and receipts",
"Scanning by USB scanner, or by phone camera where the browser reads one", "Draft replenishment orders", "Printable purchase orders and slips",
"Stocktakes with blind counting", "All nine report tabs", "Journal CSV for finance", "The one-click month-end pack",
];
const HONEST = [
{ q: "Is there a paid tier coming?", a: "Nothing is planned. If that ever changes, the rooms using it will hear before the website does." },
{ q: "Whats the catch?", a: "Its maintained by one person alongside a day job. Thats the honest limitation — not a cost, but a support surface worth discussing before you commit a whole site to it." },
{ q: "Do we need to buy hardware?", a: "No. It runs on the phones and computers the room already has. A USB scanner speeds up the counter but isnt required." },
{ q: "What about hosting costs?", a: "There is nothing for you to pay. If your facility needs it hosted inside their own environment, thats a conversation worth having early." },
];
/* ---------- the page once plans are live ---------- */
const P = PRICES;
const money = (n: number) => `$${n.toLocaleString("en-AU")}`;
const PLANS = [
{
name: "Community", price: "$0", per: "", who: "Run it on your own server. The source is published; every feature is in it.",
rows: ["Everything, no ceiling", "Your Postgres, your backups", "Your own single sign-on broker, if you want one", "A public issue tracker"],
cta: ["Read the install guide", "/getting-started"], lead: false,
},
{
name: "Hosted", price: money(P.hostedMonthly), per: "a facility, a month", who: `Or ${money(P.hostedAnnual)} a year. Free for a room under ${P.freeStaff} staff records.`,
rows: ["Everything", "Australian hosting, nightly backups kept 35 days", "Point-in-time restore", "Single sign-on set up for you", "Email support, next business day"],
cta: ["Start a 60-day trial", "/auth?mode=signup"], lead: true,
},
{
name: "Health Service", price: money(P.healthServiceAnnual), per: "a year", who: `Up to ${P.healthServiceFacilities} facilities, then ${money(P.healthServiceExtra)} each.`,
rows: ["Everything, across every site", "One sign-in, switch between facilities", "Roll-up reports and a shared catalogue", "Named support, same business day", "Invoice, purchase order, security questionnaire"],
cta: ["Talk about a pilot", "/contact"], lead: false,
},
];
const HONEST_LIVE = [
{ q: "Is there a locked tier?", a: "No. Community, Hosted and Health Service run the same code. The differences are who runs it, how long the backups are kept, and how many sites it covers." },
{ q: "We signed up when it was free. What changes?", a: "Nothing. Every facility created before this page changed stays on the free hosted plan, with everything, for as long as it exists. That was promised here, and it is written into the terms." },
{ q: `Why ${P.freeStaff} staff records?`, a: "It is roughly where a room stops being one person and a cupboard and starts being a job. A clinic, a dental practice or a small home sits under it and pays nothing; a hospital ward is over it on day one." },
{ q: "What happens if we stop paying?", a: "The facility goes read-only after a fortnights grace. Every report, export and the full backup keep working. Nothing is deleted, and writing starts again when the invoice is paid." },
{ q: "How do we pay?", a: "By invoice, annually, against your purchase order. Prices are in Australian dollars before GST." },
{ q: "Whats the catch?", a: "It is still maintained by one person alongside a day job. Hosted buys a response time; it does not buy a team." },
];
function FreePage() {
return (
<>
<section style={{ background: "var(--color-accent-600)", color: "#fff", borderBottom: "2px solid var(--color-text)" }}>
<div style={{ ...wrap, padding: "52px 40px 56px" }}>
<div style={{ ...kicker, color: "#fff" }}>Pricing</div>
<h1 style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: "clamp(52px,7vw,104px)", lineHeight: 0.9, letterSpacing: "-0.04em", textTransform: "uppercase", margin: "12px 0 0" }}>Free</h1>
<p style={{ fontSize: 17.5, lineHeight: 1.6, maxWidth: "46ch", marginTop: 20 }}>No licence. No per-device charge. No per-user charge. That is the whole page.</p>
</div>
</section>
<Band>
<h2 style={h2}>What free actually covers.</h2>
<p style={{ ...body, marginTop: 16, maxWidth: "56ch" }}>Everything. There is no tier above this one holding the useful parts back.</p>
<div className="tcm-3col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 0, marginTop: 34, border: "2px solid var(--color-text)" }}>
{INCLUDED.map((c, i) => (
<div key={c.t} style={{ padding: "26px 28px 30px", borderRight: i < 2 ? "1px solid var(--color-divider)" : undefined }}>
<h3 style={{ ...h3, fontSize: 20 }}>{c.t}</h3>
<p style={{ fontSize: 14.5, lineHeight: 1.65, color: "var(--color-neutral-800)", marginTop: 10 }}>{c.b}</p>
</div>
))}
</div>
</Band>
<Band tone="surface">
<div style={kicker}>In the box</div>
<div className="tcm-2col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0 56px", marginTop: 20 }}>
{COVERED.map((c) => (
<div key={c} style={{ display: "flex", gap: 14, alignItems: "baseline", padding: "12px 0", borderBottom: "1px solid var(--color-divider)", fontSize: 15.5 }}>
<span style={{ color: "var(--color-accent)", fontWeight: 800 }}></span>{c}
</div>
))}
</div>
</Band>
<Band>
<div className="tcm-split" style={{ display: "grid", gridTemplateColumns: "1fr 1.15fr", gap: 56 }}>
<div>
<h2 style={h2}>Why it costs nothing.</h2>
<p style={{ ...body, marginTop: 18, maxWidth: "40ch" }}>It was built by a hospital uniform coordinator to solve their own room&rsquo;s problem. It already exists, it already works, and charging for it was never the point.</p>
</div>
<div>
{HONEST.map((h) => (
<div key={h.q} style={{ padding: "18px 0", borderBottom: "1px solid var(--color-divider)" }}>
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 17 }}>{h.q}</div>
<p style={{ fontSize: 15, lineHeight: 1.65, color: "var(--color-neutral-800)", margin: "7px 0 0", maxWidth: "56ch" }}>{h.a}</p>
</div>
))}
</div>
</div>
</Band>
</>
);
}
function LivePage() {
return (
<>
<section style={{ background: "var(--color-accent-600)", color: "#fff", borderBottom: "2px solid var(--color-text)" }}>
<div style={{ ...wrap, padding: "52px 40px 56px" }}>
<div style={{ ...kicker, color: "#fff" }}>Pricing</div>
<h1 style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: "clamp(40px,6vw,88px)", lineHeight: 0.92, letterSpacing: "-0.04em", textTransform: "uppercase", margin: "12px 0 0", maxWidth: "12ch" }}>Free to run. Paid to host.</h1>
<p style={{ fontSize: 17.5, lineHeight: 1.6, maxWidth: "50ch", marginTop: 20 }}>Every feature is in every edition. You pay for someone to keep it running, backed up and supported. A room under {P.freeStaff} staff records pays nothing either way.</p>
</div>
</section>
<Band pad="0 40px">
<div className="tcm-3col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 0, borderLeft: "2px solid var(--color-text)", borderRight: "2px solid var(--color-text)" }}>
{PLANS.map((p, i) => (
<div key={p.name} style={{ padding: "30px 26px 34px", borderRight: i < 2 ? "1px solid var(--color-divider)" : undefined, background: p.lead ? "var(--color-surface)" : undefined, display: "flex", flexDirection: "column" }}>
<h2 style={{ ...h3, fontSize: 22 }}>{p.name}</h2>
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 44, letterSpacing: "-0.04em", lineHeight: 1, marginTop: 16 }}>{p.price}</div>
{p.per && <div style={{ ...small, marginTop: 6 }}>{p.per}</div>}
<p style={{ fontSize: 14, lineHeight: 1.6, color: "var(--color-neutral-800)", margin: "10px 0 0", minHeight: 44 }}>{p.who}</p>
<ul style={{ listStyle: "none", padding: 0, margin: "18px 0 0", flex: 1 }}>
{p.rows.map((r) => <li key={r} style={{ padding: "8px 0", borderBottom: "1px solid var(--color-divider)", fontSize: 14.5 }}>{r}</li>)}
</ul>
<Link href={p.cta[1]} className="btn" data-umami-event={p.lead ? "signup-cta" : undefined} data-umami-event-placement={p.lead ? "pricing-hosted" : undefined} style={{ marginTop: 22, textAlign: "center", fontWeight: 700, ...(p.lead ? { background: "var(--color-text)", color: "var(--color-bg)", border: "2px solid var(--color-text)" } : { border: "2px solid var(--color-text)", color: "var(--color-text)" }) }}>{p.cta[0]}</Link>
</div>
))}
</div>
</Band>
<Band tone="surface">
<div style={kicker}>In every edition</div>
<div className="tcm-2col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0 56px", marginTop: 20 }}>
{COVERED.map((c) => (
<div key={c} style={{ display: "flex", gap: 14, alignItems: "baseline", padding: "12px 0", borderBottom: "1px solid var(--color-divider)", fontSize: 15.5 }}>
<span style={{ color: "var(--color-accent)", fontWeight: 800 }}></span>{c}
</div>
))}
</div>
<p style={{ ...small, marginTop: 18, maxWidth: "60ch" }}>&ldquo;Unlimited staff records&rdquo; is true of Community, Hosted Facility and Health Service. The free hosted room holds {P.freeStaff}; past that it is a Hosted facility.</p>
</Band>
<Band>
<div className="tcm-split" style={{ display: "grid", gridTemplateColumns: "1fr 1.15fr", gap: 56 }}>
<div>
<h2 style={h2}>Why the software costs nothing.</h2>
<p style={{ ...body, marginTop: 18, maxWidth: "40ch" }}>It was built by a hospital uniform coordinator to solve their own room&rsquo;s problem. It already exists, it already works, and charging for it was never the point.</p>
<p style={{ ...body, marginTop: 14, maxWidth: "40ch" }}>What costs money is running it for other people, carefully: the servers, the backups, and being the person who answers.</p>
</div>
<div>
{HONEST_LIVE.map((h) => (
<div key={h.q} style={{ padding: "18px 0", borderBottom: "1px solid var(--color-divider)" }}>
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 17 }}>{h.q}</div>
<p style={{ fontSize: 15, lineHeight: 1.65, color: "var(--color-neutral-800)", margin: "7px 0 0", maxWidth: "56ch" }}>{h.a}</p>
</div>
))}
</div>
</div>
</Band>
</>
);
}
export default async function Pricing() {
const live = await plansLive();
return (
<>
<SiteNav current="pricing" />
{live ? <LivePage /> : <FreePage />}
<Band tone="ink">
<h2 style={{ ...h2, color: "#fff" }}>Nothing to sign. Open it and look.</h2>
<p style={{ fontSize: 16.5, lineHeight: 1.7, color: "var(--color-neutral-200)", maxWidth: "50ch", marginTop: 18 }}>The demo runs on demonstration data. Nothing you do in it touches a real record.</p>
<div style={{ display: "flex", gap: 12, marginTop: 28, flexWrap: "wrap" }}>
<Link href="/demo" data-umami-event="open-demo" data-umami-event-placement="pricing" className="btn" style={{ background: "#fff", color: "var(--color-accent-700)", border: "2px solid #fff", fontWeight: 700 }}>Open the working demo</Link>
<Link href="/getting-started" className="btn" style={{ background: "transparent", color: "#fff", border: "2px solid #fff", fontWeight: 700 }}>Getting started</Link>
</div>
</Band>
<CtaBand />
</>
);
}