1bc2de655a
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
106 lines
4.6 KiB
TypeScript
106 lines
4.6 KiB
TypeScript
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 doesn’t 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 }}>→</span>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</Band>
|
||
<CtaBand />
|
||
</>
|
||
);
|
||
}
|