1bc2de655a
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
190 lines
14 KiB
TypeScript
190 lines
14 KiB
TypeScript
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: "What’s 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 wearer’s 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 — in a hospital, an aged-care home or a clinic — that are useful whether or not you ever use ThreadCount.
|
||
Take the method and use a clipboard if that’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 }}>→</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, it’s yours to use." />
|
||
</>
|
||
);
|
||
}
|