From e7c69ea6f7358e78acad36eca99c96eeeed096ce Mon Sep 17 00:00:00 2001
From: Adam Malczewski <github@tradam.dev>
Date: Tue, 10 Mar 2026 16:55:36 +0900
Subject: write app plus touchup

---
 src/App.tsx | 148 ++++++++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 118 insertions(+), 30 deletions(-)

(limited to 'src/App.tsx')

diff --git a/src/App.tsx b/src/App.tsx
index a062428..532597a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,35 +1,123 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+/**
+ * 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.
+ */
 
-function App() {
-  const [count, setCount] = useState(0)
+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>
-        <a href="https://vite.dev" target="_blank">
-          <img src={viteLogo} className="logo" alt="Vite logo" />
-        </a>
-        <a href="https://react.dev" target="_blank">
-          <img src={reactLogo} className="logo react" alt="React logo" />
-        </a>
-      </div>
-      <h1 className="bg-red-500">Vite + React</h1>
-      <div className="card">
-        <button onClick={() => setCount((count) => count + 1)}>
-          count is {count}
-        </button>
-        <p>
-          Edit <code>src/App.tsx</code> and save to test HMR
-        </p>
+    <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>
-      <p className="read-the-docs">
-        Click on the Vite and React logos to learn more
-      </p>
-    </>
-  )
+    </div>
+  );
 }
-
-export default App
-- 
cgit v1.2.3

