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
+105
View File
@@ -0,0 +1,105 @@
import Link from "next/link";
import { Band, CtaBand, PageHead, SiteNav, body, h3, small } from "@/components/site";
import JsonLd from "@/components/JsonLd";
import { graph } from "@/lib/schema";
/* Shared furniture for the guides.
*
* These pages exist to be found by someone searching for the problem rather than the product — a
* linen services manager working out how to run a stocktake, not someone shopping for software.
* So they are written to be useful on their own: a person who reads one, does the thing manually
* and never signs up has still been helped, and the page has still done its job.
*
* The product is mentioned where it genuinely bears on the step and nowhere else. A guide that
* turns into an advertisement three paragraphs in is one people stop trusting, and stop linking
* to, which defeats the entire point of writing it. */
const SITE = process.env.NEXT_PUBLIC_SITE_URL || "https://threadcount.tech";
export type Step = { n: string; h: string; p: React.ReactNode };
export function GuideMeta({ slug, title, description, updated }: { slug: string; title: string; description: string; updated: string }) {
return (
<JsonLd
data={graph({
"@type": "Article",
"@id": `${SITE}/guides/${slug}#article`,
headline: title,
description,
datePublished: updated,
dateModified: updated,
inLanguage: "en-AU",
isAccessibleForFree: true,
author: { "@id": `${SITE}/#organization` },
publisher: { "@id": `${SITE}/#organization` },
mainEntityOfPage: { "@type": "WebPage", "@id": `${SITE}/guides/${slug}` },
})}
/>
);
}
export function GuideHead({ kicker, title, lede }: { kicker: string; title: string; lede: string }) {
return (
<>
<SiteNav />
<PageHead kicker={kicker} title={title} lede={lede} />
</>
);
}
/** A numbered step. The number carries the sequence, so the prose doesnt have to.
*
* The index is drawn in neutral-600, not the neutral-400 it used to be: on the paper ground that
* is 1.9:1, which is under every threshold there is, and a step number nobody can read is a
* numbered list that has stopped being numbered. neutral-400 stays fine where it is used on the
* ink panels — it is only text on paper that fails. */
export function Steps({ steps }: { steps: Step[] }) {
return (
<div>
{steps.map((s) => (
<div key={s.n} style={{ display: "grid", gridTemplateColumns: "44px 1fr", gap: 20, padding: "30px 0", borderBottom: "1px solid var(--color-divider)" }}>
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 15, color: "var(--color-neutral-600)" }}>{s.n}</div>
<div>
<h2 style={{ ...h3, fontSize: "clamp(19px,2vw,25px)", margin: 0 }}>{s.h}</h2>
<div style={{ ...body, margin: "10px 0 0", maxWidth: "66ch" }}>{s.p}</div>
</div>
</div>
))}
</div>
);
}
/** The thing that goes wrong, stated plainly. These are the parts people actually search for. */
export function Pitfalls({ items }: { items: [string, string][] }) {
return (
<div className="tcm-2col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", columnGap: 56, rowGap: 0, marginTop: 6 }}>
{items.map(([h, p]) => (
<div key={h} style={{ padding: "22px 0", borderBottom: "1px solid var(--color-divider)" }}>
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 17 }}>{h}</div>
<p style={{ ...small, margin: "7px 0 0", maxWidth: "46ch" }}>{p}</p>
</div>
))}
</div>
);
}
export function GuideFoot({ related }: { related: [string, string][] }) {
return (
<>
<Band tone="surface">
<div style={{ fontSize: 11.5, letterSpacing: "0.12em", textTransform: "uppercase", fontWeight: 800, borderBottom: "2px solid var(--color-text)", paddingBottom: 9 }}>
Read next
</div>
<div className="tcm-2col" style={{ display: "grid", gridTemplateColumns: "1fr 1fr", columnGap: 56, rowGap: 0 }}>
{related.map(([label, href]) => (
<Link key={href} href={href} style={{ textDecoration: "none", color: "var(--color-text)", borderBottom: "1px solid var(--color-divider)", padding: "20px 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 />
</>
);
}