ThreadCount Community edition

Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
ThreadCount
2026-09-13 08:45:19 +10:00
commit 1bc2de655a
505 changed files with 56223 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
import Link from "next/link";
import JsonLd from "@/components/JsonLd";
import { graph, softwareApplication } from "@/lib/schema";
import { Band, CtaBand, SiteNav, body, h2, kicker, small, wrap } from "@/components/site";
import { PRICES } from "@/lib/plan";
import { plansLive } from "@/lib/plans-live";
import { COMMUNITY } from "@/lib/edition";
import { redirect } from "next/navigation";
export const metadata = { alternates: { canonical: "/" } };
const CARDS = [
{ k: "Know", t: "Whats on the shelf", b: "On-hand by size, updated as you issue, and flagged before a size runs out on you." },
{ k: "Prove", t: "Where it went", b: "Every garment against the staff member who took it, dated, on a slip they signed." },
{ k: "Charge", t: "The right ward or clinic", b: "Issues carry the wearers cost centre at the price you paid, so the journal is already written." },
];
const LOOP = [
{ n: "1", t: "Issue", b: "Scan the garment, pick the staff member, done. Their ward or clinic, cost centre and last sizes are already there, so a repeat visit takes seconds.", pad: 0 },
{ n: "2", t: "Replenish", b: "What you take off the shelf lands on a draft order for that supplier. Check it, add their order number, send. Nothing you issue goes quietly unreplaced.", pad: 36 },
{ n: "3", t: "Receive", b: "Tick lines off against the invoice as you unpack, then send each one to the shelf or to whoever is waiting. A short delivery becomes a back order on its own.", pad: 72 },
];
const row: React.CSSProperties = { display: "flex", justifyContent: "space-between", padding: "8px 0", borderBottom: "1px solid var(--color-divider)", fontSize: 13 };
export default async function Home() {
// A Community instance is the product, not the website: its front door is the sign-in.
if (COMMUNITY) redirect("/auth");
const live = await plansLive();
return (
<>
{/* The product, with its price as it stands today, on the page that is actually about it. */}
<JsonLd data={graph(softwareApplication(live))} />
<SiteNav />
{/* Hero: full-bleed greyscale photograph with a solid ink panel flush left over it. */}
<section style={{ position: "relative", borderBottom: "2px solid var(--color-text)", minHeight: "clamp(440px,50vw,640px)", display: "flex", alignItems: "stretch" }}>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img className="grayscale" src="/photos/tc-photo-ward.jpg" alt="" aria-hidden="true" style={{ position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", objectPosition: "center 30%" }} />
<div style={{ ...wrap, position: "relative", zIndex: 2, display: "flex", alignItems: "flex-start" }}>
{/* The panel sizes to its widest line rather than a fixed 760: the headline scales to 84px,
where "ACCOUNTED FOR." is 753px and was breaking out of the box onto the photograph
from 1440px up. Hugging the content means the ink always contains the words. */}
<div style={{ background: "var(--color-text)", color: "#fff", width: "fit-content", maxWidth: "min(880px, 100%)", padding: "clamp(32px,6vw,52px) clamp(20px,5vw,48px) clamp(56px,9vw,104px)" }}>
<div style={{ ...kicker, color: "var(--color-accent-300)", display: "flex", alignItems: "center", gap: 10 }}>
<span style={{ width: 28, height: 3, background: "var(--color-accent)" }} />{live ? "For hospitals, aged care and clinics" : "Free for hospitals, aged care and clinics"}
</div>
<h1 style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: "clamp(30px,7.2vw,84px)", lineHeight: 0.95, letterSpacing: "-0.035em", textTransform: "uppercase", margin: "24px 0 0" }}>Every garment out the door, accounted for.</h1>
<p style={{ fontSize: 18, lineHeight: 1.6, color: "var(--color-neutral-200)", maxWidth: "48ch", margin: "24px 0 0" }}>ThreadCount follows a garment from the shelf, to the person wearing it, to the order that puts another one back.</p>
<div style={{ display: "flex", gap: 12, marginTop: 30, flexWrap: "wrap" }}>
<Link href="/demo" data-umami-event="open-demo" data-umami-event-placement="hero" className="btn" style={{ background: "#fff", color: "var(--color-accent-700)", border: "2px solid #fff", fontWeight: 700 }}>Open the working demo</Link>
<Link href="/how-it-works" className="btn" style={{ background: "transparent", color: "#fff", border: "2px solid #fff", fontWeight: 700 }}>How it works</Link>
</div>
</div>
</div>
</section>
{/* Card row pulls up over the hero's bottom padding. */}
<section style={{ background: "var(--color-bg)" }}>
<div style={{ ...wrap, position: "relative", zIndex: 5 }}>
<div className="tcm-3col tcm-pullup" style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", border: "2px solid var(--color-text)", background: "var(--color-bg)", marginTop: -58 }}>
{CARDS.map((c, i) => (
<div key={c.k} style={{ padding: "26px 28px 30px", borderRight: i < 2 ? "1px solid var(--color-divider)" : undefined }}>
<div style={kicker}>{c.k}</div>
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 24, letterSpacing: "-0.02em", marginTop: 10 }}>{c.t}</div>
<p style={{ fontSize: 14.5, lineHeight: 1.65, color: "var(--color-neutral-800)", margin: "8px 0 0" }}>{c.b}</p>
</div>
))}
</div>
</div>
</section>
{/* Proposition split */}
<Band pad="110px 40px clamp(64px,7vw,88px)">
<div className="tcm-split" style={{ display: "grid", gridTemplateColumns: "1.25fr 1fr", gap: 64 }}>
<h2 style={h2}>One coordinator can run the whole room from a phone.</h2>
<div>
<p style={{ ...body, margin: 0 }}>Scan the garment, pick the staff member, record it. The order to replace it starts itself, the cost lands on the right ward, wing or clinic, and the month-end pack is a button rather than a weekend.</p>
<Link href="/features" style={{ display: "inline-block", marginTop: 22, textDecoration: "none", fontSize: 14, fontWeight: 800, letterSpacing: "0.04em", textTransform: "uppercase", color: "var(--color-text)", borderBottom: "3px solid var(--color-accent)", paddingBottom: 3 }}>See everything it does</Link>
</div>
</div>
</Band>
{/* The loop, with the designed vertical stagger */}
<Band tone="surface">
<div style={kicker}>The loop</div>
<div className="tcm-3col tcm-stagger" style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 40, marginTop: 28 }}>
{LOOP.map((l) => (
<div key={l.n} style={{ paddingTop: l.pad }}>
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 88, lineHeight: 1, color: "var(--color-neutral-300)" }}>{l.n}</div>
<div style={{ borderTop: "4px solid var(--color-accent)", paddingTop: 12, marginTop: 6, fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 22, display: "inline-block" }}>{l.t}</div>
<p style={{ fontSize: 15, lineHeight: 1.7, color: "var(--color-neutral-800)", margin: "10px 0 0", maxWidth: "38ch" }}>{l.b}</p>
</div>
))}
</div>
</Band>
{/* Pricing band */}
<Band>
<div className="tcm-split" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 56, alignItems: "center" }}>
<div>
<div style={kicker}>Pricing</div>
{live
? <div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: "clamp(44px,7vw,104px)", lineHeight: 0.9, letterSpacing: "-0.04em", textTransform: "uppercase", marginTop: 10 }}>Free to run.<br />Paid to host.</div>
: <div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: "clamp(80px,13vw,190px)", lineHeight: 0.82, letterSpacing: "-0.04em", textTransform: "uppercase", marginTop: 10 }}>Free</div>}
</div>
<div>
<p style={{ fontSize: "clamp(17px,1.9vw,22px)", lineHeight: 1.55, color: "var(--color-neutral-800)", margin: 0 }}>
{live
? `Every feature in every edition. Run it yourself for nothing, or have it hosted from $${PRICES.hostedAnnual.toLocaleString("en-AU")} a year — free for a room under ${PRICES.freeStaff} staff records.`
: "No licence, no per-device charge, no per-user charge. Every feature, every report, everyone in the room."}
</p>
<Link href="/pricing" style={{ display: "inline-block", marginTop: 20, textDecoration: "none", fontSize: 14, fontWeight: 800, letterSpacing: "0.04em", textTransform: "uppercase", color: "var(--color-text)", borderBottom: "3px solid var(--color-accent)", paddingBottom: 3 }}>{live ? "The plans" : "What free covers"}</Link>
</div>
</div>
</Band>
{/* Recreated product UI */}
<Band tone="surface">
<div style={kicker}>On the screen</div>
<h2 style={{ ...h2, marginTop: 12 }}>The whole counter on one screen.</h2>
<div className="table-wrap" style={{ marginTop: 30 }}>
<div style={{ border: "2px solid var(--color-text)", background: "var(--color-bg)", minWidth: 620 }}>
<div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 16px", borderBottom: "2px solid var(--color-text)" }}>
<span style={{ width: 9, height: 9, border: "2px solid var(--color-text)" }} />
<span style={{ width: 9, height: 9, border: "2px solid var(--color-text)" }} />
<span style={{ width: 9, height: 9, background: "var(--color-accent)" }} />
<span style={{ ...kicker, color: "var(--color-neutral-700)", fontSize: 11, marginLeft: 10 }}>ThreadCount Dashboard</span>
<span style={{ marginLeft: "auto", fontSize: 11, color: "var(--color-neutral-600)" }}>Signed in · Coordinator</span>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", borderBottom: "2px solid var(--color-text)" }}>
{[["Orders", "3 open", "1 overdue — NL-48211, 6 days", true], ["Stock", "$18,240", "7 sizes at or below reorder", false], ["Staff", "212 active", "14 issues recorded this week", false]].map(([k, v, s, red], i) => (
<div key={String(k)} style={{ padding: "18px 22px", borderRight: i < 2 ? "1px solid var(--color-divider)" : undefined }}>
<div style={{ ...kicker, color: "var(--color-neutral-700)", fontSize: 10 }}>{k}</div>
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 30, marginTop: 4 }}>{v}</div>
<div style={{ fontSize: 12, color: red ? "var(--color-accent-700)" : "var(--color-neutral-700)", fontWeight: red ? 600 : 400 }}>{s}</div>
</div>
))}
</div>
<div style={{ display: "grid", gridTemplateColumns: "3fr 2fr" }}>
<div style={{ padding: "16px 22px", borderRight: "1px solid var(--color-divider)" }}>
<div style={{ ...kicker, color: "var(--color-text)", fontSize: 11, borderBottom: "2px solid var(--color-text)", paddingBottom: 6 }}>Awaiting pickup call list</div>
{[["K. Osei · Softshell Jacket M", "16 days", "tag tag-accent", "Contacted"], ["T. Nguyen · Polo Shirt L ×2", "4 days", "tag tag-neutral", "Call"], ["R. Patel · Cargo Pant 88R", "1 day", "tag tag-neutral", "Call"]].map(([n, d, cls, act]) => (
<div key={String(n)} style={{ display: "flex", gap: 12, alignItems: "center", padding: "8px 0", borderBottom: "1px solid var(--color-divider)", fontSize: 13 }}>
<b style={{ flex: 1 }}>{n}</b><span className={String(cls)}>{d}</span><span className="tag tag-outline">{act}</span>
</div>
))}
</div>
<div style={{ padding: "16px 22px" }}>
<div style={{ ...kicker, color: "var(--color-text)", fontSize: 11, borderBottom: "2px solid var(--color-text)", paddingBottom: 6 }}>Reorder flags</div>
{[["RN Scrub Top · S", "2 left"], ["Chef Jacket · L", "0 left"], ["Security Polo · XL", "3 left"]].map(([n, v]) => (
<div key={n} style={row}><span>{n}</span><b style={{ color: "var(--color-accent-700)" }}>{v}</b></div>
))}
</div>
</div>
</div>
</div>
<div style={{ ...small, marginTop: 14 }}>Inventory, issuing, ordering, stocktakes and reports all work the same way, so there is only one thing to learn.</div>
</Band>
{/* The guides. Someone arriving from a search for their problem rather than for a product
should find the useful writing without hunting for it — and someone weighing this up
wants evidence the person behind it knows the job. These do both. */}
<Band>
<div style={kicker}>Guides</div>
<h2 style={{ ...h2, marginTop: 12 }}>Written from the counter, not the brochure.</h2>
<p style={{ ...body, marginTop: 16, maxWidth: "58ch" }}>
Notes on running a linen room &mdash; in a hospital, an aged-care home or a clinic &mdash; that are useful whether or not you ever use ThreadCount.
Take the method and use a clipboard if that&rsquo;s what you have.
</p>
<div className="tcm-2col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", columnGap: 56, rowGap: 0, marginTop: 24 }}>
{[
["How to run a uniform stocktake", "/guides/uniform-stocktake"],
["Where the uniforms actually go", "/guides/uniform-loss"],
["Charging uniforms to the right cost centre", "/guides/cost-centre-reporting"],
["Entitlements and manager approvals", "/guides/manager-approvals"],
].map(([label, href]) => (
<Link key={href} href={href} style={{ textDecoration: "none", color: "var(--color-text)", borderBottom: "1px solid var(--color-divider)", padding: "18px 0", display: "flex", alignItems: "baseline", gap: 14 }}>
<span style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 18 }}>{label}</span>
<span aria-hidden style={{ marginLeft: "auto", color: "var(--color-accent)", fontWeight: 800 }}>&rarr;</span>
</Link>
))}
</div>
</Band>
<CtaBand title="I built this for my own linen room." lede="I coordinate uniforms at a hospital, and I got tired of a spreadsheet that never matched the shelf. If your room has the same problem, its yours to use." />
</>
);
}