返回 CodeWhale
merch-size-guide.tsx
根目录 / web / components / merch-size-guide.tsx
1 "use client";
2
3 import { useId, useState } from "react";
4 import { getMerchSizingCopy, merchSizingGarmentLabel } from "@/lib/content/merch-sizing";
5 import { MERCH_SIZE_CHARTS, merchMeasurement, type MerchSizingProductCode, type MerchSizingUnit } from "@/lib/merch/sizing";
6 import styles from "./merch-size-guide.module.css";
7
8 /** Supplier guidance only. This component does not choose a purchase size or enable variants. */
9 export function MerchSizeGuide({ locale, initialProductCode = "DJCTX" }: {
10 locale: string;
11 initialProductCode?: MerchSizingProductCode;
12 }) {
13 const d = getMerchSizingCopy(locale);
14 const id = useId();
15 const [productCode, setProductCode] = useState<MerchSizingProductCode>(initialProductCode);
16 const [unit, setUnit] = useState<MerchSizingUnit>("cm");
17 const chart = MERCH_SIZE_CHARTS.find((item) => item.code === productCode)!;
18 const garment = merchSizingGarmentLabel(chart.code, locale);
19 const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
20 const measurement = (value: number | null) => value === null ? "—" : number.format(merchMeasurement(value, unit));
21 const caption = d.caption.replace("{garment}", garment).replace("{unit}", d[unit]);
22 const tolerance = d.tolerance.replace("{value}", measurement(chart.toleranceCm)).replace("{unit}", d[unit]);
23 const tableNotes = `${id}-bust ${id}-tolerance${chart.warning ? ` ${id}-warning` : ""}`;
24
25 return <section className={styles.guide} aria-labelledby={`${id}-title`}>
26 <h2 id={`${id}-title`}>{d.title}</h2>
27 <p>{d.introduction}</p>
28 <p className={styles.note}>{d.guidance}</p>
29 <div className={styles.controls}>
30 <div className={styles.field}>
31 <label htmlFor={`${id}-garment`}>{d.garment}</label>
32 <select id={`${id}-garment`} value={productCode} onChange={(event) => {
33 const selected = MERCH_SIZE_CHARTS.find((item) => item.code === event.target.value);
34 if (selected) setProductCode(selected.code);
35 }}>
36 {MERCH_SIZE_CHARTS.map((item) => <option key={item.code} value={item.code}>{merchSizingGarmentLabel(item.code, locale)}</option>)}
37 </select>
38 </div>
39 <fieldset className={styles.units}>
40 <legend>{d.units}</legend>
41 {(["cm", "in"] as const).map((value) => <label key={value}>
42 <input type="radio" name={`${id}-unit`} value={value} checked={unit === value} onChange={() => setUnit(value)} />
43 {d[value]}
44 </label>)}
45 </fieldset>
46 </div>
47 {unit === "in" ? <p className={styles.note}>{d.conversion}</p> : null}
48 <div className={styles.tableScroll} role="region" aria-label={d.tableRegion} tabIndex={0}>
49 <table aria-describedby={tableNotes}>
50 <caption>{caption}</caption>
51 <thead><tr>
52 <th scope="col">{d.size}</th><th scope="col">{d.length}</th><th scope="col">{d.shoulder}</th><th scope="col">{d.bust}</th><th scope="col">{d.sleeve}</th>
53 </tr></thead>
54 <tbody>{chart.rows.map((row) => <tr key={row.size} data-confirmation={chart.confirmationNeededSizes.includes(row.size) || undefined}>
55 <th scope="row">{row.size}{chart.confirmationNeededSizes.includes(row.size) ? <abbr title={d.confirmationRequired}> *</abbr> : null}</th>
56 <td>{measurement(row.lengthCm)}</td><td>{measurement(row.shoulderCm)}</td><td>{measurement(row.bustCm)}</td><td>{measurement(row.sleeveCm)}</td>
57 </tr>)}</tbody>
58 </table>
59 </div>
60 <p id={`${id}-bust`} className={styles.note}>{d.bustNote}</p>
61 <p id={`${id}-tolerance`} className={styles.note}>{tolerance}</p>
62 {chart.rows.some((row) => row.sleeveCm === null) ? <p className={styles.note}>{d.missingSleeve}</p> : null}
63 {chart.warning ? <p id={`${id}-warning`} className={styles.warning}>{d[chart.warning]}</p> : null}
64 <p className={styles.note}><a href={chart.sourceUrl} target="_blank" rel="noopener noreferrer">{d.source} · {chart.code}</a></p>
65 <p className={styles.note}>{d.dated}</p>
66 </section>;
67 }
68
68 lines Plain Text