1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
/**
* App.tsx — root component for the Bicycle Wheel Circumference calculator.
*
* State management:
* - Each selector (rim / tire) tracks two pieces of state:
* 1. The currently selected dropdown label (or "__custom__").
* 2. The raw text-input value (kept as a string for UX).
* - When a dropdown option is chosen its diameter_mm fills the text input.
* - When the user types a number, we search for the first option whose
* diameter_mm matches; if none match we set the dropdown to "Custom".
* - The circumference result is derived (not stored) on every render.
*/
import { useState, useMemo } from "react";
import tire_size_data from "./data/tire_sizes.json";
import type { TireSizeData, SizeOption } from "./types";
import SizeSelector, { CUSTOM_VALUE } from "./components/SizeSelector";
import ResultDisplay from "./components/ResultDisplay";
import { calculate_circumference } from "./utils/calculate_circumference";
/** Typed reference to the imported JSON. */
const data: TireSizeData = tire_size_data;
/**
* Given a numeric value, find the first option whose diameter_mm matches.
* Returns the option's label, or CUSTOM_VALUE when nothing matches.
*/
function find_matching_option(value: number, options: SizeOption[]): string {
const match = options.find((o) => o.diameter_mm === value);
return match ? match.label : CUSTOM_VALUE;
}
export default function App() {
/* ── Rim state ─────────────────────────────────────────────── */
const [rim_option, set_rim_option] = useState<string>("");
const [rim_input, set_rim_input] = useState<string>("");
/* ── Tire state ────────────────────────────────────────────── */
const [tire_option, set_tire_option] = useState<string>("");
const [tire_input, set_tire_input] = useState<string>("");
/* ── Derived circumference result ──────────────────────────── */
const result = useMemo(
() =>
calculate_circumference(parseFloat(rim_input), parseFloat(tire_input)),
[rim_input, tire_input],
);
/* ── Handlers: Rim ─────────────────────────────────────────── */
const handle_rim_option = (label: string) => {
set_rim_option(label);
// When "Custom" is chosen, leave the text input alone.
if (label === CUSTOM_VALUE) return;
const match = data.wheel_sizes.find((o) => o.label === label);
if (match) set_rim_input(String(match.diameter_mm));
};
const handle_rim_input = (raw: string) => {
set_rim_input(raw);
const num = parseFloat(raw);
if (isNaN(num)) {
set_rim_option(CUSTOM_VALUE);
return;
}
set_rim_option(find_matching_option(num, data.wheel_sizes));
};
/* ── Handlers: Tire ────────────────────────────────────────── */
const handle_tire_option = (label: string) => {
set_tire_option(label);
if (label === CUSTOM_VALUE) return;
const match = data.tire_sizes.find((o) => o.label === label);
if (match) set_tire_input(String(match.diameter_mm));
};
const handle_tire_input = (raw: string) => {
set_tire_input(raw);
const num = parseFloat(raw);
if (isNaN(num)) {
set_tire_option(CUSTOM_VALUE);
return;
}
set_tire_option(find_matching_option(num, data.tire_sizes));
};
/* ── Render ────────────────────────────────────────────────── */
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="card card-border bg-base-100 shadow-xl w-full max-w-lg">
<div className="card-body gap-6">
<h1 className="card-title justify-center text-2xl">
Tire Size Calculator
</h1>
{/* Rim size selector */}
<SizeSelector
label="Rim Size"
options={data.wheel_sizes}
selected_option={rim_option}
input_value={rim_input}
on_option_change={handle_rim_option}
on_input_change={handle_rim_input}
/>
{/* Tire size selector */}
<SizeSelector
label="Tire Size"
options={data.tire_sizes}
selected_option={tire_option}
input_value={tire_input}
on_option_change={handle_tire_option}
on_input_change={handle_tire_input}
/>
<div className="divider">Results</div>
{/* Computed output */}
<ResultDisplay result={result} />
</div>
</div>
</div>
);
}
|