Skip to content

Commit 06271a3

Browse files
mighteaclaude
andcommitted
feat: tire pressure configurations with sidecar support
The tire pressure form now edits one riding configuration at a time via a daisyUI select (Solo / Mit Sozius / Offroad) with completeness markers per option; every configuration is optional and independently deletable — the delete button removes only the selected set, or the whole record when it is the last one. The sidecar wheel is a third tire position inside each configuration, shown only for motorcycles with the new Beiwagen (Gespann) checkbox in the vehicle form. The read-only card and the print view render the sets as a matrix: one row per tire position, one labeled column per recorded configuration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01654vNu6Z4iFjHuPfm4ny4t
1 parent 27309f7 commit 06271a3

6 files changed

Lines changed: 440 additions & 155 deletions

File tree

app/components/add-motorcycle-form.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ export function AddMotorcycleForm({
211211
// un-archiving and clearing the veteran flag actually save.
212212
formData.set("isVeteran", formData.has("isVeteran") ? "true" : "false");
213213
formData.set("isArchived", formData.has("isArchived") ? "true" : "false");
214+
formData.set("hasSidecar", formData.has("hasSidecar") ? "true" : "false");
214215

215216
if (croppedImageBlob) {
216217
formData.append("image", croppedImageBlob, "motorcycle.jpg");
@@ -471,6 +472,18 @@ export function AddMotorcycleForm({
471472
Veteranen-Status
472473
</label>
473474
</div>
475+
<div className="flex items-center gap-3">
476+
<input
477+
type="checkbox"
478+
name="hasSidecar"
479+
id="hasSidecar"
480+
value="true"
481+
defaultChecked={Boolean(initialValues?.hasSidecar)}
482+
className="h-5 w-5 rounded border-gray-300 text-primary focus:ring-primary dark:border-navy-500 dark:bg-navy-900 dark:checked:bg-primary" />
483+
<label htmlFor="hasSidecar" className="text-sm font-medium text-foreground dark:text-white">
484+
Beiwagen (Gespann)
485+
</label>
486+
</div>
474487
<div className="flex items-center gap-3">
475488
<input
476489
type="checkbox"

app/components/tire-pressure-card.tsx

Lines changed: 108 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,62 +7,133 @@ interface TirePressureCardProps {
77
}
88

99
/**
10-
* Read-only display of one motorcycle's recommended tire pressures.
11-
* Three rows max — Vorne / Hinten / Beiwagen — each shows the user's
12-
* preferred unit prominently with the converted unit in a small
13-
* mono-uppercase secondary line.
10+
* Read-only display of one motorcycle's recommended tire pressures as a
11+
* matrix: one row per tire position (Vorne / Hinten / Beiwagen), one
12+
* column per riding configuration (Solo / Sozius / Offroad). Only
13+
* recorded configurations get a column; with a single configuration no
14+
* column headers render and the card stays a simple two-row list.
1415
*/
1516
export function TirePressureCard({ pressure }: TirePressureCardProps) {
17+
// Only recorded configurations become columns — every set is optional.
18+
const variants = [
19+
{
20+
key: "solo",
21+
label: "Solo",
22+
front: pressure.frontBar,
23+
rear: pressure.rearBar,
24+
sidecar: pressure.sidecarBar,
25+
},
26+
{
27+
key: "passenger",
28+
label: "Sozius",
29+
front: pressure.frontPassengerBar,
30+
rear: pressure.rearPassengerBar,
31+
sidecar: pressure.sidecarPassengerBar,
32+
},
33+
{
34+
key: "offroad",
35+
label: "Offroad",
36+
front: pressure.frontOffroadBar,
37+
rear: pressure.rearOffroadBar,
38+
sidecar: pressure.sidecarOffroadBar,
39+
},
40+
].filter((v) => v.front != null || v.rear != null);
41+
// Headers render for several columns — and for a single non-solo column,
42+
// where values without their configuration label would be ambiguous.
43+
const showHeader = variants.length > 1 || variants.some((v) => v.key !== "solo");
44+
const hasSidecar = variants.some((v) => v.sidecar != null);
45+
1646
return (
1747
<Card>
18-
<div className="px-4 py-4">
19-
<dl className="space-y-3">
20-
<PressureRow
21-
label="Vorne"
22-
bar={pressure.frontBar}
23-
preferred={pressure.preferredUnit}
24-
/>
25-
<PressureRow
26-
label="Hinten"
27-
bar={pressure.rearBar}
28-
preferred={pressure.preferredUnit}
29-
/>
30-
{pressure.sidecarBar != null && (
31-
<PressureRow
32-
label="Beiwagen"
33-
bar={pressure.sidecarBar}
48+
{/* overflow-x-auto: three value columns plus labels can exceed a
49+
narrow phone viewport; the card must scroll, not the page. */}
50+
<div className="overflow-x-auto px-4 py-4">
51+
<table className="w-full">
52+
{showHeader && (
53+
<thead>
54+
<tr>
55+
<th aria-label="Reifenposition" />
56+
{variants.map((v) => (
57+
<th
58+
key={v.key}
59+
scope="col"
60+
className="pb-2 text-right font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-base-content/45 dark:text-navy-500"
61+
>
62+
{v.label}
63+
</th>
64+
))}
65+
</tr>
66+
</thead>
67+
)}
68+
<tbody>
69+
<PositionRow
70+
label="Vorne"
71+
values={variants.map((v) => ({ key: v.key, bar: v.front }))}
3472
preferred={pressure.preferredUnit}
3573
/>
36-
)}
37-
</dl>
74+
<PositionRow
75+
label="Hinten"
76+
values={variants.map((v) => ({ key: v.key, bar: v.rear }))}
77+
preferred={pressure.preferredUnit}
78+
/>
79+
{hasSidecar && (
80+
<PositionRow
81+
label="Beiwagen"
82+
values={variants.map((v) => ({ key: v.key, bar: v.sidecar }))}
83+
preferred={pressure.preferredUnit}
84+
/>
85+
)}
86+
</tbody>
87+
</table>
3888
</div>
3989
</Card>
4090
);
4191
}
4292

43-
function PressureRow({
93+
function PositionRow({
4494
label,
45-
bar,
95+
values,
4696
preferred,
4797
}: {
4898
label: string;
99+
values: { key: string; bar: number | null }[];
100+
preferred: TirePressure["preferredUnit"];
101+
}) {
102+
return (
103+
<tr className="border-b border-base-200/70 last:border-b-0 dark:border-navy-700/60">
104+
<th
105+
scope="row"
106+
className="py-2.5 pr-4 text-left align-baseline font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-base-content/60 dark:text-navy-400"
107+
>
108+
{label}
109+
</th>
110+
{values.map(({ key, bar }) => (
111+
<td key={key} className="py-2.5 pl-4 text-right align-baseline">
112+
{bar != null ? <PressureValue bar={bar} preferred={preferred} /> : (
113+
<span aria-hidden="true" className="text-base-content/30"></span>
114+
)}
115+
</td>
116+
))}
117+
</tr>
118+
);
119+
}
120+
121+
function PressureValue({
122+
bar,
123+
preferred,
124+
}: {
49125
bar: number;
50126
preferred: TirePressure["preferredUnit"];
51127
}) {
52128
const formatted = formatPressure(bar, preferred);
53129
return (
54-
<div className="flex items-baseline justify-between gap-4 border-b border-base-200/70 pb-2 last:border-b-0 last:pb-0 dark:border-navy-700/60">
55-
<dt className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-base-content/60 dark:text-navy-400">
56-
{label}
57-
</dt>
58-
<dd className="flex items-baseline gap-2">
59-
<span className="font-numeric text-base font-semibold tabular-nums text-base-content dark:text-white">
60-
{formatted.primary}
61-
</span>
62-
<span className="font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-base-content/50">
63-
{formatted.secondary}
64-
</span>
65-
</dd>
66-
</div>
130+
<span className="inline-flex flex-col items-end gap-0.5 sm:flex-row sm:items-baseline sm:gap-2">
131+
<span className="font-numeric text-base font-semibold tabular-nums text-base-content dark:text-white">
132+
{formatted.primary}
133+
</span>
134+
<span className="whitespace-nowrap font-mono text-[10px] font-semibold uppercase tracking-[0.14em] text-base-content/50">
135+
{formatted.secondary}
136+
</span>
137+
</span>
67138
);
68139
}

0 commit comments

Comments
 (0)