@burdenoff/microfe-movethewheels 2026.918.1 → 2026.920.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Shipping Rates Page
3
+ *
4
+ * Compare live courier rates between two pincodes across the platform's
5
+ * carriers (Shiprocket first). Nothing to set up: the carrier accounts belong
6
+ * to the platform. Read-only — comparing never books a shipment or spends
7
+ * carrier credit.
8
+ */
9
+
10
+ import { useState, type FormEvent } from 'react';
11
+ import { Scale } from 'lucide-react';
12
+
13
+ import { FormError } from '../components/FormError';
14
+ import { PageLayout } from '../components/PageLayout';
15
+ import { ShippingQuoteResults } from '../components/ShippingQuoteResults';
16
+ import {
17
+ Button,
18
+ Card,
19
+ CardContent,
20
+ CardHeader,
21
+ CardTitle,
22
+ Input,
23
+ Label,
24
+ Switch,
25
+ } from '../components/ui';
26
+ import { useTr } from '../hooks';
27
+ import {
28
+ useShippingQuotes,
29
+ type ShippingQuoteInput,
30
+ type ShippingQuoteResult,
31
+ } from '../hooks/useShippingCarriers';
32
+
33
+ const POSTCODE = /^[1-9]\d{5}$/;
34
+
35
+ const optionalNumber = (value: string): number | undefined => {
36
+ const parsed = Number(value);
37
+ return value.trim() !== '' && Number.isFinite(parsed) ? parsed : undefined;
38
+ };
39
+
40
+ export default function ShippingRatesPage() {
41
+ const tr = useTr();
42
+ const fetchQuotes = useShippingQuotes();
43
+
44
+ const [pickup, setPickup] = useState('');
45
+ const [delivery, setDelivery] = useState('');
46
+ const [weight, setWeight] = useState('');
47
+ const [length, setLength] = useState('');
48
+ const [width, setWidth] = useState('');
49
+ const [height, setHeight] = useState('');
50
+ const [cod, setCod] = useState(false);
51
+ const [distance, setDistance] = useState('');
52
+ const [costPerKm, setCostPerKm] = useState('');
53
+ const [fixedCost, setFixedCost] = useState('');
54
+ const [isLoading, setIsLoading] = useState(false);
55
+ const [error, setError] = useState<string | null>(null);
56
+ const [result, setResult] = useState<ShippingQuoteResult | null>(null);
57
+
58
+ const handleCompare = async (event: FormEvent) => {
59
+ event.preventDefault();
60
+ setError(null);
61
+
62
+ const weightKg = optionalNumber(weight);
63
+ if (!POSTCODE.test(pickup) || !POSTCODE.test(delivery)) {
64
+ setError(
65
+ tr(
66
+ 'movethewheels.shipping.form.postcodeError',
67
+ 'Enter valid 6-digit pickup and delivery postcodes'
68
+ )
69
+ );
70
+ return;
71
+ }
72
+ if (weightKg === undefined || weightKg <= 0) {
73
+ setError(tr('movethewheels.shipping.form.weightError', 'Enter the package weight in kg'));
74
+ return;
75
+ }
76
+ const dims = [optionalNumber(length), optionalNumber(width), optionalNumber(height)];
77
+ const given = dims.filter((d) => d !== undefined).length;
78
+ if (given !== 0 && given !== 3) {
79
+ setError(
80
+ tr(
81
+ 'movethewheels.shipping.form.dimensionsError',
82
+ 'Enter length, width and height together, or leave all three empty'
83
+ )
84
+ );
85
+ return;
86
+ }
87
+
88
+ const fleetDistance = optionalNumber(distance);
89
+ const fleetCostPerKm = optionalNumber(costPerKm);
90
+ const fleetFixed = optionalNumber(fixedCost);
91
+ const fleetGiven = fleetDistance !== undefined || fleetCostPerKm !== undefined;
92
+ if (
93
+ fleetGiven &&
94
+ (!fleetDistance || fleetDistance <= 0 || !fleetCostPerKm || fleetCostPerKm <= 0)
95
+ ) {
96
+ setError(
97
+ tr(
98
+ 'movethewheels.shipping.form.ownFleetError',
99
+ 'To compare with your own vehicle, enter both the distance and your cost per km'
100
+ )
101
+ );
102
+ return;
103
+ }
104
+ if (fleetFixed !== undefined && fleetFixed < 0) {
105
+ setError(tr('movethewheels.shipping.form.fixedCostError', 'Fixed cost cannot be negative'));
106
+ return;
107
+ }
108
+
109
+ const input: ShippingQuoteInput = {
110
+ pickupPostcode: pickup,
111
+ deliveryPostcode: delivery,
112
+ weightKg,
113
+ cod,
114
+ ...(fleetGiven && fleetDistance && fleetCostPerKm
115
+ ? {
116
+ ownFleet: {
117
+ distanceKm: fleetDistance,
118
+ costPerKm: fleetCostPerKm,
119
+ ...(fleetFixed ? { fixedCost: fleetFixed } : {}),
120
+ },
121
+ }
122
+ : {}),
123
+ ...(given === 3 ? { lengthCm: dims[0], widthCm: dims[1], heightCm: dims[2] } : {}),
124
+ };
125
+
126
+ setIsLoading(true);
127
+ try {
128
+ setResult(await fetchQuotes(input));
129
+ } catch (e: unknown) {
130
+ setResult(null);
131
+ setError(
132
+ e instanceof Error
133
+ ? e.message
134
+ : tr('movethewheels.shipping.form.failed', 'Could not fetch rates')
135
+ );
136
+ } finally {
137
+ setIsLoading(false);
138
+ }
139
+ };
140
+
141
+ return (
142
+ <PageLayout
143
+ title={tr('movethewheels.shipping.title', 'Compare Rates')}
144
+ subtitle={tr(
145
+ 'movethewheels.shipping.subtitle',
146
+ 'Enter a pickup and a delivery pincode to see every courier available for that route, with price and delivery time.'
147
+ )}
148
+ icon={<Scale size={24} />}
149
+ >
150
+ <div className="space-y-6">
151
+ <Card>
152
+ <CardHeader>
153
+ <CardTitle>{tr('movethewheels.shipping.form.title', 'Shipment details')}</CardTitle>
154
+ </CardHeader>
155
+ <CardContent>
156
+ <form onSubmit={(event) => void handleCompare(event)} className="space-y-4">
157
+ <div className="grid gap-3 sm:grid-cols-3">
158
+ <div className="space-y-1">
159
+ <Label htmlFor="rate-pickup">
160
+ {tr('movethewheels.shipping.form.pickup', 'Pickup postcode')}
161
+ </Label>
162
+ <Input
163
+ id="rate-pickup"
164
+ inputMode="numeric"
165
+ maxLength={6}
166
+ value={pickup}
167
+ onChange={(e) => setPickup(e.target.value.trim())}
168
+ />
169
+ </div>
170
+ <div className="space-y-1">
171
+ <Label htmlFor="rate-delivery">
172
+ {tr('movethewheels.shipping.form.delivery', 'Delivery postcode')}
173
+ </Label>
174
+ <Input
175
+ id="rate-delivery"
176
+ inputMode="numeric"
177
+ maxLength={6}
178
+ value={delivery}
179
+ onChange={(e) => setDelivery(e.target.value.trim())}
180
+ />
181
+ </div>
182
+ <div className="space-y-1">
183
+ <Label htmlFor="rate-weight">
184
+ {tr('movethewheels.shipping.form.weight', 'Weight (kg)')}
185
+ </Label>
186
+ <Input
187
+ id="rate-weight"
188
+ inputMode="decimal"
189
+ value={weight}
190
+ onChange={(e) => setWeight(e.target.value)}
191
+ />
192
+ </div>
193
+ </div>
194
+ <div className="grid gap-3 sm:grid-cols-3">
195
+ <div className="space-y-1">
196
+ <Label htmlFor="rate-length">
197
+ {tr('movethewheels.shipping.form.length', 'Length (cm, optional)')}
198
+ </Label>
199
+ <Input
200
+ id="rate-length"
201
+ inputMode="decimal"
202
+ value={length}
203
+ onChange={(e) => setLength(e.target.value)}
204
+ />
205
+ </div>
206
+ <div className="space-y-1">
207
+ <Label htmlFor="rate-width">
208
+ {tr('movethewheels.shipping.form.width', 'Width (cm, optional)')}
209
+ </Label>
210
+ <Input
211
+ id="rate-width"
212
+ inputMode="decimal"
213
+ value={width}
214
+ onChange={(e) => setWidth(e.target.value)}
215
+ />
216
+ </div>
217
+ <div className="space-y-1">
218
+ <Label htmlFor="rate-height">
219
+ {tr('movethewheels.shipping.form.height', 'Height (cm, optional)')}
220
+ </Label>
221
+ <Input
222
+ id="rate-height"
223
+ inputMode="decimal"
224
+ value={height}
225
+ onChange={(e) => setHeight(e.target.value)}
226
+ />
227
+ </div>
228
+ </div>
229
+ <fieldset className="space-y-3 rounded-lg border p-4">
230
+ <legend className="px-1 text-sm font-medium">
231
+ {tr(
232
+ 'movethewheels.shipping.form.ownFleetTitle',
233
+ 'Compare with your own vehicle (optional)'
234
+ )}
235
+ </legend>
236
+ <p className="text-sm text-text-secondary">
237
+ {tr(
238
+ 'movethewheels.shipping.form.ownFleetHelp',
239
+ 'Enter the road distance and what it costs you to run a vehicle per km, and we will show it next to the courier prices.'
240
+ )}
241
+ </p>
242
+ <div className="grid gap-3 sm:grid-cols-3">
243
+ <div className="space-y-1">
244
+ <Label htmlFor="rate-distance">
245
+ {tr('movethewheels.shipping.form.distance', 'Distance (km)')}
246
+ </Label>
247
+ <Input
248
+ id="rate-distance"
249
+ inputMode="decimal"
250
+ value={distance}
251
+ onChange={(e) => setDistance(e.target.value)}
252
+ />
253
+ </div>
254
+ <div className="space-y-1">
255
+ <Label htmlFor="rate-cost-per-km">
256
+ {tr('movethewheels.shipping.form.costPerKm', 'Your cost per km (₹)')}
257
+ </Label>
258
+ <Input
259
+ id="rate-cost-per-km"
260
+ inputMode="decimal"
261
+ value={costPerKm}
262
+ onChange={(e) => setCostPerKm(e.target.value)}
263
+ />
264
+ </div>
265
+ <div className="space-y-1">
266
+ <Label htmlFor="rate-fixed-cost">
267
+ {tr(
268
+ 'movethewheels.shipping.form.fixedCost',
269
+ 'Fixed cost per trip (₹, optional)'
270
+ )}
271
+ </Label>
272
+ <Input
273
+ id="rate-fixed-cost"
274
+ inputMode="decimal"
275
+ value={fixedCost}
276
+ onChange={(e) => setFixedCost(e.target.value)}
277
+ />
278
+ </div>
279
+ </div>
280
+ </fieldset>
281
+ <div className="flex items-center gap-2">
282
+ <Switch id="rate-cod" checked={cod} onCheckedChange={setCod} />
283
+ <Label htmlFor="rate-cod">
284
+ {tr('movethewheels.shipping.form.cod', 'Cash on delivery')}
285
+ </Label>
286
+ </div>
287
+ <FormError message={error} />
288
+ <Button type="submit" disabled={isLoading}>
289
+ {isLoading
290
+ ? tr('movethewheels.shipping.form.comparing', 'Comparing…')
291
+ : tr('movethewheels.shipping.form.compare', 'Compare rates')}
292
+ </Button>
293
+ </form>
294
+ </CardContent>
295
+ </Card>
296
+
297
+ {result && <ShippingQuoteResults result={result} t={tr} />}
298
+ </div>
299
+ </PageLayout>
300
+ );
301
+ }