Skip to content

Commit 11e283b

Browse files
committed
feat: tilt bike according to ground axis
1 parent 6a7e2c6 commit 11e283b

26 files changed

+643
-187
lines changed

ui/app/bike/components/Bike.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function Bike({
1212
const strokeColor = isShadow ? "black" : "blue";
1313

1414
return (
15-
<svg width="100%" height="100%" viewBox="0 0 1500 1500">
15+
<svg width="100%" height="100%" viewBox="0 0 1500 1000">
1616
<defs>
1717
<linearGradient id="rainbow">
1818
<stop offset="0%" stopColor="rgba(255, 0, 0, 1)" />
@@ -84,7 +84,12 @@ export default function Bike({
8484
/>
8585

8686
{/* Fork */}
87-
<path d={bike.fork.draw()} stroke={strokeColor} strokeWidth="5" fill="none" />
87+
<path
88+
d={bike.fork.draw()}
89+
stroke={strokeColor}
90+
strokeWidth="5"
91+
fill="none"
92+
/>
8893

8994
{/* Down tube */}
9095
<path

ui/app/bike/components/BikeForm.tsx

Lines changed: 189 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,141 +21,277 @@ import { setShadowBike, shadowBikeSelectors } from "../lib/shadowBikeSlice";
2121
import type { BikeState } from "../lib/bikeSlice";
2222
import BikeSelect from "./BikeSelect";
2323
import { Button } from "@/components/ui/button";
24-
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
24+
import {
25+
HoverCard,
26+
HoverCardContent,
27+
HoverCardTrigger,
28+
} from "@/components/ui/hover-card";
2529

26-
type BikeFormProps = { isShadow?: boolean; };
30+
type BikeFormProps = { isShadow?: boolean };
2731

2832
export default function BikeForm({ isShadow = false }: BikeFormProps) {
2933
const dispatch = useAppDispatch();
30-
const bike = useAppSelector(isShadow ? shadowBikeSelectors.selectShadowBike : bikeSelectors.selectBike);
34+
const bike = useAppSelector(
35+
isShadow ? shadowBikeSelectors.selectShadowBike : bikeSelectors.selectBike,
36+
);
3137

32-
const bikeAttributes: Record<keyof BikeState, { label: string; type: z.ZodTypeAny; warnings?: string[] }> = {
38+
const bikeAttributes: Record<
39+
keyof BikeState,
40+
{ label: string; type: z.ZodTypeAny; warnings?: string[] }
41+
> = {
3342
stack: {
3443
label: "Stack (mm)",
35-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
44+
type: z
45+
.number()
46+
.int()
47+
.min(0, "Value must be at least 0")
48+
.max(1000, "Value must be at most 1000"),
3649
},
3750
reach: {
3851
label: "Reach (mm)",
39-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000"),
52+
type: z
53+
.number()
54+
.int()
55+
.min(0, "Value must be at least 0")
56+
.max(1000, "Value must be at most 1000"),
4057
},
4158
headTube: {
4259
label: "Head tube length (mm)",
43-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000"),
44-
warnings: ["Include the external headset bottom cup stack height in the headtube length if applicable"]
60+
type: z
61+
.number()
62+
.int()
63+
.min(0, "Value must be at least 0")
64+
.max(1000, "Value must be at most 1000"),
65+
warnings: [
66+
"Include the external headset bottom cup stack height in the headtube length if applicable",
67+
],
4568
},
4669
headTubeAngle: {
4770
label: "Head tube angle (degrees)",
48-
type: z.number().min(0, "Angle must be at least 0 degrees").max(89, "Angle must be less than 90 degrees")
49-
.refine(n => !(n * 100).toString().includes("."), { message: "Max precision is 2 decimal places" }),
71+
type: z
72+
.number()
73+
.min(0, "Angle must be at least 0 degrees")
74+
.max(89, "Angle must be less than 90 degrees")
75+
.refine((n) => !(n * 100).toString().includes("."), {
76+
message: "Max precision is 2 decimal places",
77+
}),
5078
},
5179
chainStay: {
5280
label: "Chainstay length (mm)",
53-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
81+
type: z
82+
.number()
83+
.int()
84+
.min(0, "Value must be at least 0")
85+
.max(1000, "Value must be at most 1000"),
5486
},
5587
actualSeatTubeAngle: {
5688
label: "Actual seat tube angle (degrees)",
57-
type: z.number().min(0, "Angle must be at least 0 degrees").max(89, "Angle must be less than 90 degrees")
58-
.refine(n => !(n * 100).toString().includes("."), { message: "Max precision is 2 decimal places" }),
89+
type: z
90+
.number()
91+
.min(0, "Angle must be at least 0 degrees")
92+
.max(89, "Angle must be less than 90 degrees")
93+
.refine((n) => !(n * 100).toString().includes("."), {
94+
message: "Max precision is 2 decimal places",
95+
}),
5996
},
6097
effectiveSeatTubeAngle: {
6198
label: "Effective seat tube angle (degrees)",
62-
type: z.number().min(0, "Angle must be at least 0 degrees").max(89, "Angle must be less than 90 degrees")
63-
.refine(n => !(n * 100).toString().includes("."), { message: "Max precision is 2 decimal places" }),
99+
type: z
100+
.number()
101+
.min(0, "Angle must be at least 0 degrees")
102+
.max(89, "Angle must be less than 90 degrees")
103+
.refine((n) => !(n * 100).toString().includes("."), {
104+
message: "Max precision is 2 decimal places",
105+
}),
64106
},
65107
seatTube: {
66108
label: "Seat tube length (mm)",
67-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
109+
type: z
110+
.number()
111+
.int()
112+
.min(0, "Value must be at least 0")
113+
.max(1000, "Value must be at most 1000"),
68114
},
69115
bottomBracketDrop: {
70116
label: "Bottom bracket drop (mm)",
71-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
117+
type: z
118+
.number()
119+
.int()
120+
.min(0, "Value must be at least 0")
121+
.max(1000, "Value must be at most 1000"),
72122
},
73123
frontCenter: {
74124
label: "Front center (mm)",
75-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
125+
type: z
126+
.number()
127+
.int()
128+
.min(0, "Value must be at least 0")
129+
.max(1000, "Value must be at most 1000"),
76130
},
77131
wheelBase: {
78132
label: "Wheelbase (mm)",
79-
type: z.number().int().min(0, "Value must be at least 0").max(2000, "Value must be at most 2000")
133+
type: z
134+
.number()
135+
.int()
136+
.min(0, "Value must be at least 0")
137+
.max(2000, "Value must be at most 2000"),
80138
},
81139
forkAxleToCrown: {
82140
label: "Fork axle to crown (mm)",
83-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
141+
type: z
142+
.number()
143+
.int()
144+
.min(0, "Value must be at least 0")
145+
.max(1000, "Value must be at most 1000"),
84146
},
85147
forkOffset: {
86148
label: "Fork offset (mm)",
87-
type: z.number().int().min(0, "Value must be at least 0").max(100, "Value must be at most 100")
149+
type: z
150+
.number()
151+
.int()
152+
.min(0, "Value must be at least 0")
153+
.max(100, "Value must be at most 100"),
88154
},
89155
forkTravel: {
90156
label: "Fork travel (mm)",
91-
type: z.number().int().min(0, "Value must be at least 0").max(300, "Value must be at most 300")
157+
type: z
158+
.number()
159+
.int()
160+
.min(0, "Value must be at least 0")
161+
.max(300, "Value must be at most 300"),
92162
},
93163
crankLength: {
94164
label: "Crank length (mm)",
95-
type: z.number().int().min(0, "Value must be at least 0").max(300, "Value must be at most 300")
165+
type: z
166+
.number()
167+
.int()
168+
.min(0, "Value must be at least 0")
169+
.max(300, "Value must be at most 300"),
96170
},
97171
crankQFactor: {
98172
label: "Crank Q factor (mm)",
99-
type: z.number().int().min(0, "Value must be at least 0").max(300, "Value must be at most 300")
173+
type: z
174+
.number()
175+
.int()
176+
.min(0, "Value must be at least 0")
177+
.max(300, "Value must be at most 300"),
100178
},
101179
spacers: {
102180
label: "Spacers height (mm)",
103-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000"),
104-
warnings: ["Include the external headset top cup stack height in the calculated length if applicable", "Include the stem steerer height in the calculated length if applicable"]
181+
type: z
182+
.number()
183+
.int()
184+
.min(0, "Value must be at least 0")
185+
.max(1000, "Value must be at most 1000"),
186+
warnings: [
187+
"Include the external headset top cup stack height in the calculated length if applicable",
188+
"Include the stem steerer height in the calculated length if applicable",
189+
],
105190
},
106191
stemLength: {
107192
label: "Stem length (mm)",
108-
type: z.number().int().min(0, "Value must be at least 0").max(200, "Value must be at most 200")
193+
type: z
194+
.number()
195+
.int()
196+
.min(0, "Value must be at least 0")
197+
.max(200, "Value must be at most 200"),
109198
},
110199
stemAngle: {
111200
label: "Stem angle (degrees)",
112-
type: z.number().int().min(-89, "Angle must be at least -90 degrees").max(89, "Angle must be less than 90 degrees"),
201+
type: z
202+
.number()
203+
.int()
204+
.min(-89, "Angle must be at least -90 degrees")
205+
.max(89, "Angle must be less than 90 degrees"),
113206
},
114207
stemSteererHeight: {
115208
label: "Stem steerer height (mm)",
116-
type: z.number().int().min(0, "Value must be at least 0").max(100, "Value must be at most 100")
209+
type: z
210+
.number()
211+
.int()
212+
.min(0, "Value must be at least 0")
213+
.max(100, "Value must be at most 100"),
117214
},
118215
seatOffset: {
119216
label: "Seat offset (mm)",
120-
type: z.number().int().min(0, "Value must be at least 0").max(100, "Value must be at most 100")
217+
type: z
218+
.number()
219+
.int()
220+
.min(0, "Value must be at least 0")
221+
.max(100, "Value must be at most 100"),
121222
},
122223
handlebarWidth: {
123224
label: "Handlebar width (mm)",
124-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
225+
type: z
226+
.number()
227+
.int()
228+
.min(0, "Value must be at least 0")
229+
.max(1000, "Value must be at most 1000"),
125230
},
126231
handlebarReach: {
127232
label: "Handlebar reach (mm)",
128-
type: z.number().int().min(0, "Value must be at least 0").max(300, "Value must be at most 300")
233+
type: z
234+
.number()
235+
.int()
236+
.min(0, "Value must be at least 0")
237+
.max(300, "Value must be at most 300"),
129238
},
130239
handlebarRise: {
131240
label: "Handlebar rise (mm)",
132-
type: z.number().int().min(-100, "Handlebar rise must be at least -100mm").max(100, "Handlebar rise must be at most 100mm")
241+
type: z
242+
.number()
243+
.int()
244+
.min(-100, "Handlebar rise must be at least -100mm")
245+
.max(100, "Handlebar rise must be at most 100mm"),
133246
},
134247
tireFrontWidth: {
135248
label: "Front tire width (mm)",
136-
type: z.number().int().min(0, "Value must be at least 0").max(200, "Value must be at most 200")
249+
type: z
250+
.number()
251+
.int()
252+
.min(0, "Value must be at least 0")
253+
.max(200, "Value must be at most 200"),
137254
},
138255
tireRearWidth: {
139256
label: "Rear tire width (mm)",
140-
type: z.number().int().min(0, "Value must be at least 0").max(200, "Value must be at most 200")
257+
type: z
258+
.number()
259+
.int()
260+
.min(0, "Value must be at least 0")
261+
.max(200, "Value must be at most 200"),
141262
},
142263
wheelFrontDiameter: {
143264
label: "Front wheel diameter (mm)",
144-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
265+
type: z
266+
.number()
267+
.int()
268+
.min(0, "Value must be at least 0")
269+
.max(1000, "Value must be at most 1000"),
145270
},
146271
wheelRearDiameter: {
147272
label: "Rear wheel diameter (mm)",
148-
type: z.number().int().min(0, "Value must be at least 0").max(1000, "Value must be at most 1000")
273+
type: z
274+
.number()
275+
.int()
276+
.min(0, "Value must be at least 0")
277+
.max(1000, "Value must be at most 1000"),
149278
},
150279
};
151280

152-
const formSchema = z.object(Object.entries(bikeAttributes).reduce<Record<keyof BikeState, z.ZodTypeAny>>((acc, [key, value]) => ({ ...acc, [key]: value.type }), {} as Record<keyof BikeState, z.ZodTypeAny>));
281+
const formSchema = z.object(
282+
Object.entries(bikeAttributes).reduce<
283+
Record<keyof BikeState, z.ZodTypeAny>
284+
>(
285+
(acc, [key, value]) => ({ ...acc, [key]: value.type }),
286+
{} as Record<keyof BikeState, z.ZodTypeAny>,
287+
),
288+
);
153289
const formFields = formSchema.keyof().options;
154290

155291
const form = useForm<z.infer<typeof formSchema>>({
156292
resolver: zodResolver(formSchema),
157293
defaultValues: {
158-
...bike
294+
...bike,
159295
},
160296
});
161297

@@ -175,12 +311,9 @@ export default function BikeForm({ isShadow = false }: BikeFormProps) {
175311
<BikeSelect onChange={onBikeSelectChange} />
176312
</div>
177313
<Form {...form}>
178-
<form
179-
onSubmit={form.handleSubmit(onSubmit)}
180-
className="bg-inherit"
181-
>
314+
<form onSubmit={form.handleSubmit(onSubmit)} className="bg-inherit">
182315
<div className="grid gap-4 p-4">
183-
{formFields.map((key) => (
316+
{formFields.map((key) => (
184317
<FormField
185318
control={form.control}
186319
name={key}
@@ -196,7 +329,13 @@ export default function BikeForm({ isShadow = false }: BikeFormProps) {
196329
</HoverCardTrigger>
197330
<HoverCardContent className="w-80">
198331
<ul className="list-inside list-disc text-sm">
199-
{bikeAttributes[key].warnings.map((warning, idx) => <li key={`${key}-warning-${idx}`}>{warning}</li>)}
332+
{bikeAttributes[key].warnings.map(
333+
(warning, idx) => (
334+
<li key={`${key}-warning-${idx}`}>
335+
{warning}
336+
</li>
337+
),
338+
)}
200339
</ul>
201340
</HoverCardContent>
202341
</HoverCard>
@@ -206,9 +345,12 @@ export default function BikeForm({ isShadow = false }: BikeFormProps) {
206345
<Input
207346
{...field}
208347
type="number"
209-
value={field.value ?? ''}
348+
value={field.value ?? ""}
210349
onChange={(event) => {
211-
const value = event.target.value === '' ? undefined : Number(event.target.value);
350+
const value =
351+
event.target.value === ""
352+
? undefined
353+
: Number(event.target.value);
212354
field.onChange(value);
213355
onSubmit(form.getValues());
214356
}}

0 commit comments

Comments
 (0)