diff options
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ResultDisplay.tsx | 48 | ||||
| -rw-r--r-- | src/components/SizeSelector.tsx | 81 |
2 files changed, 129 insertions, 0 deletions
diff --git a/src/components/ResultDisplay.tsx b/src/components/ResultDisplay.tsx new file mode 100644 index 0000000..3f3f81a --- /dev/null +++ b/src/components/ResultDisplay.tsx @@ -0,0 +1,48 @@ +/** + * ResultDisplay.tsx + * + * Shows the computed circumference in several units inside a styled card. + * When inputs are incomplete / invalid the component shows a helpful prompt. + */ + +import type { CircumferenceResult } from "../types"; + +interface ResultDisplayProps { + result: CircumferenceResult | null; +} + +export default function ResultDisplay({ result }: ResultDisplayProps) { + if (!result) { + return ( + <div className="card card-border bg-base-200"> + <div className="card-body items-center text-center"> + <p className="text-base-content/60"> + Select or enter both rim and tire sizes to see the circumference. + </p> + </div> + </div> + ); + } + + /** Helper to render a single stat inside the stats bar. */ + const stat_item = (title: string, value: string | number, desc: string) => ( + <div className="stat" key={title}> + <div className="stat-title">{title}</div> + <div className="stat-value text-lg">{value}</div> + <div className="stat-desc">{desc}</div> + </div> + ); + + return ( + <div className="card card-border bg-base-200"> + <div className="card-body"> + <h2 className="card-title justify-center">Wheel Circumference</h2> + <div className="stats stats-vertical sm:stats-horizontal shadow w-full"> + {stat_item("Millimeters", result.mm, "mm")} + {stat_item("Centimeters", result.cm, "cm")} + {stat_item("Inches", result.inches, "in")} + </div> + </div> + </div> + ); +} diff --git a/src/components/SizeSelector.tsx b/src/components/SizeSelector.tsx new file mode 100644 index 0000000..cb4baef --- /dev/null +++ b/src/components/SizeSelector.tsx @@ -0,0 +1,81 @@ +/** + * SizeSelector.tsx + * + * A reusable pair of <select> + <input> for choosing a rim or tire size. + * + * Behaviour: + * - Selecting a dropdown option fills the text input with its diameter_mm. + * - Typing a number in the text input auto-selects the first matching + * dropdown option, or falls back to "Custom" if no match exists. + * - Choosing "Custom" in the dropdown does nothing to the text input. + */ + +import type { SizeOption } from "../types"; + +/** Sentinel value used as the <option> value for "Custom". */ +const CUSTOM_VALUE = "__custom__"; + +interface SizeSelectorProps { + /** Human-readable label shown above the controls. */ + label: string; + /** The list of predefined size options to populate the dropdown. */ + options: SizeOption[]; + /** Currently selected dropdown value (a label string or CUSTOM_VALUE). */ + selected_option: string; + /** Current numeric value in the text input (as a string so we can allow empty). */ + input_value: string; + /** Called when the dropdown selection changes. */ + on_option_change: (option_value: string) => void; + /** Called when the text input value changes. */ + on_input_change: (value: string) => void; +} + +export default function SizeSelector({ + label, + options, + selected_option, + input_value, + on_option_change, + on_input_change, +}: SizeSelectorProps) { + return ( + <fieldset className="fieldset"> + <legend className="fieldset-legend">{label}</legend> + + {/* Dropdown for predefined sizes */} + <select + className="select select-bordered w-full" + value={selected_option} + aria-label={`${label} preset`} + onChange={(e) => on_option_change(e.target.value)} + > + <option disabled value=""> + Pick a size + </option> + {options.map((opt, idx) => ( + <option key={`${opt.label}-${idx}`} value={opt.label}> + {opt.label} ({opt.diameter_mm} mm) + </option> + ))} + <option value={CUSTOM_VALUE}>Custom</option> + </select> + + {/* Numeric text input — mirrors / overrides the dropdown */} + <label className="input w-full mt-2"> + <input + type="number" + step="any" + min="0" + className="grow" + placeholder="mm" + value={input_value} + aria-label={`${label} diameter in mm`} + onChange={(e) => on_input_change(e.target.value)} + /> + <span className="label">mm</span> + </label> + </fieldset> + ); +} + +export { CUSTOM_VALUE }; |
