@bytebrand/fe-ui-core-autobahn 1.0.55 → 1.0.56
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.
- package/constants/autobahn.ts +69 -68
- package/constants.ts +31 -31
- package/locales/de/autobahn.json +790 -783
- package/locales/en/autobahn.json +790 -783
- package/package.json +1 -1
- package/theme/autobahn.css +7 -4
- package/theme/md3.css +8184 -1585
- package/ui/CardCarousel.tsx +6 -2
- package/utils/financeFormat.ts +62 -62
- package/utils/priceRating.ts +58 -58
- package/vdp/FinanceCalculator.tsx +746 -651
- package/vdp/VdpModules.tsx +2995 -2384
- package/vdp/VdpViewer.tsx +698 -99
|
@@ -1,651 +1,746 @@
|
|
|
1
|
-
// Autobahn (auto.de redesign) — VDP offer / finance card (Finanzierung / Leasing /
|
|
2
|
-
// Barzahlung).
|
|
3
|
-
//
|
|
4
|
-
// Structure (desktop rethink, tabs-on-top): payment-type TABS at the top (MD3 underline tabs),
|
|
5
|
-
// then the rate (with any discount), then the "Rate anpassen" rate-editor, then the primary
|
|
6
|
-
// "Angebot sichern" request CTA, and finally the Finanzierungs-/Leasingdetails info + popup link
|
|
7
|
-
// at the foot. Laufzeit is chosen via chips (discrete terms), not a slider.
|
|
8
|
-
//
|
|
9
|
-
// CONTROLLED / PRESENTATIONAL — computes NOTHING financial: `mode`, `monthly`, `down`, `term` and
|
|
10
|
-
// the slider/chip bounds all arrive as props; segment/slider/chip changes are reported back via
|
|
11
|
-
// `onModeChange` / `onDownChange` / `onTermChange`. The real numbers come from `financingStore`.
|
|
12
|
-
// The only local state is the `adjust` expander (pure view state).
|
|
13
|
-
//
|
|
14
|
-
// Renders correctly only inside a `<div className="md3">` subtree.
|
|
15
|
-
|
|
16
|
-
import React, { useState } from 'react';
|
|
17
|
-
import { TranslationFunction } from '../types';
|
|
18
|
-
|
|
19
|
-
import { Button, Icon } from '../ui';
|
|
20
|
-
import { TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
|
|
21
|
-
|
|
22
|
-
/** Payment mode segment keys. Mirrors the design's ['fin','leasing','cash']. */
|
|
23
|
-
export type FinanceMode = 'fin' | 'leasing' | 'cash';
|
|
24
|
-
|
|
25
|
-
export interface SliderProps {
|
|
26
|
-
min: number;
|
|
27
|
-
max: number;
|
|
28
|
-
step?: number;
|
|
29
|
-
value: number;
|
|
30
|
-
onChange: (value: number) => void;
|
|
31
|
-
/** formats the min/max end labels (and is reused by callers for the value). */
|
|
32
|
-
fmt?: (v: number) => React.ReactNode;
|
|
33
|
-
/** render the current value in a floating bubble above the thumb (Figma sheet sliders). */
|
|
34
|
-
bubble?: boolean;
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
)}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
</
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
{
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
</
|
|
484
|
-
|
|
485
|
-
)
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
{
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
</
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
)}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
|
|
1
|
+
// Autobahn (auto.de redesign) — VDP offer / finance card (Finanzierung / Leasing /
|
|
2
|
+
// Barzahlung).
|
|
3
|
+
//
|
|
4
|
+
// Structure (desktop rethink, tabs-on-top): payment-type TABS at the top (MD3 underline tabs),
|
|
5
|
+
// then the rate (with any discount), then the "Rate anpassen" rate-editor, then the primary
|
|
6
|
+
// "Angebot sichern" request CTA, and finally the Finanzierungs-/Leasingdetails info + popup link
|
|
7
|
+
// at the foot. Laufzeit is chosen via chips (discrete terms), not a slider.
|
|
8
|
+
//
|
|
9
|
+
// CONTROLLED / PRESENTATIONAL — computes NOTHING financial: `mode`, `monthly`, `down`, `term` and
|
|
10
|
+
// the slider/chip bounds all arrive as props; segment/slider/chip changes are reported back via
|
|
11
|
+
// `onModeChange` / `onDownChange` / `onTermChange`. The real numbers come from `financingStore`.
|
|
12
|
+
// The only local state is the `adjust` expander (pure view state).
|
|
13
|
+
//
|
|
14
|
+
// Renders correctly only inside a `<div className="md3">` subtree.
|
|
15
|
+
|
|
16
|
+
import React, { useState } from 'react';
|
|
17
|
+
import { TranslationFunction } from '../types';
|
|
18
|
+
|
|
19
|
+
import { Button, Icon } from '../ui';
|
|
20
|
+
import { TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
|
|
21
|
+
|
|
22
|
+
/** Payment mode segment keys. Mirrors the design's ['fin','leasing','cash']. */
|
|
23
|
+
export type FinanceMode = 'fin' | 'leasing' | 'cash';
|
|
24
|
+
|
|
25
|
+
export interface SliderProps {
|
|
26
|
+
min: number;
|
|
27
|
+
max: number;
|
|
28
|
+
step?: number;
|
|
29
|
+
value: number;
|
|
30
|
+
onChange: (value: number) => void;
|
|
31
|
+
/** formats the min/max end labels (and is reused by callers for the value). */
|
|
32
|
+
fmt?: (v: number) => React.ReactNode;
|
|
33
|
+
/** render the current value in a floating bubble above the thumb (Figma sheet sliders). */
|
|
34
|
+
bubble?: boolean;
|
|
35
|
+
/** space reserved below the rail for the bubble's tail (Figma differs per sheet: fin vs leasing). */
|
|
36
|
+
bubblePaddingBottom?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Range slider with a filled track and min/max end labels. The track fill is a
|
|
41
|
+
* linear-gradient driven by the value's position; the design's `.mslider` class
|
|
42
|
+
* styles the thumb/rail. With `bubble` the current value floats above the thumb
|
|
43
|
+
* (mockup "Rate anpassen" sliders) instead of the caller printing it elsewhere.
|
|
44
|
+
*/
|
|
45
|
+
export const Slider: React.FunctionComponent<SliderProps> = ({
|
|
46
|
+
min,
|
|
47
|
+
max,
|
|
48
|
+
step = 1,
|
|
49
|
+
value,
|
|
50
|
+
onChange,
|
|
51
|
+
fmt = (v) => v,
|
|
52
|
+
bubble,
|
|
53
|
+
bubblePaddingBottom = 34,
|
|
54
|
+
}) => {
|
|
55
|
+
const span = max - min;
|
|
56
|
+
const pct = span > 0 ? ((value - min) / span) * 100 : 0;
|
|
57
|
+
return (
|
|
58
|
+
<div>
|
|
59
|
+
<div style={{ position: 'relative', paddingTop: bubble ? 44 : 0, paddingBottom: bubble ? bubblePaddingBottom : 0 }}>
|
|
60
|
+
{bubble && (
|
|
61
|
+
// thumb is 22px wide: interpolate the bubble anchor between half-thumb insets so it
|
|
62
|
+
// tracks the thumb center (a plain pct% drifts at the rail ends)
|
|
63
|
+
<span className="slider-bubble num" style={{ left: `calc(11px + (100% - 22px) * ${pct / 100})` }}>
|
|
64
|
+
{fmt(value)}
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
67
|
+
<input
|
|
68
|
+
type="range"
|
|
69
|
+
className="mslider"
|
|
70
|
+
min={min}
|
|
71
|
+
max={max}
|
|
72
|
+
step={step}
|
|
73
|
+
value={value}
|
|
74
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
75
|
+
style={{ background: `linear-gradient(90deg, var(--md-primary) ${pct}%, var(--md-sc-highest) ${pct}%)` }}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
{!bubble && (
|
|
79
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
|
|
80
|
+
<span className="md-label-sm">{fmt(min)}</span>
|
|
81
|
+
<span className="md-label-sm">{fmt(max)}</span>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Segmented price-evaluation bar (Figma: 5 discrete tier segments, NOT a gradient) with a
|
|
90
|
+
* white ring marker. `leftPct` overrides the marker position (the Marktwert gauge positions
|
|
91
|
+
* it continuously); default = centered on the tier segment.
|
|
92
|
+
*/
|
|
93
|
+
export const EvalBar: React.FunctionComponent<{ tier: number; leftPct?: number }> = ({ tier, leftPct }) => (
|
|
94
|
+
<div className="fin-eval-bar">
|
|
95
|
+
{TIER_COLORS.map((c, i) => (
|
|
96
|
+
<span key={i} className="fin-eval-seg" style={{ background: c }} />
|
|
97
|
+
))}
|
|
98
|
+
<div
|
|
99
|
+
className={`fin-eval-thumb${TIER_KEYS[Math.max(0, Math.min(4, tier))] === 'good' ? ' fin-eval-thumb--good' : ''}`}
|
|
100
|
+
style={{ left: `${leftPct != null ? leftPct : (tier + 0.5) * 20}%`, borderColor: TIER_COLORS[Math.max(0, Math.min(4, tier))] }}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
/** Figma CTA icon — document with a check-in-circle badge over its bottom-right corner
|
|
106
|
+
* (no single lucide glyph matches; composed from file-text + a knocked-out check-circle).
|
|
107
|
+
* The badge backdrop assumes a filled (primary) button. */
|
|
108
|
+
export const DocCheckIcon: React.FunctionComponent<{ size?: number }> = ({ size = 19 }) => {
|
|
109
|
+
// inline width/height on BOTH icons — `.mbtn .lucide { width/height: 18px }` otherwise
|
|
110
|
+
// inflates the badge to full icon size and it swallows the document
|
|
111
|
+
const badge = Math.max(9, Math.round(size * 0.5));
|
|
112
|
+
return (
|
|
113
|
+
<span style={{ position: 'relative', display: 'inline-flex', flex: 'none' }}>
|
|
114
|
+
<Icon name="file-text" size={size} stroke={2} style={{ width: size, height: size }} />
|
|
115
|
+
<span
|
|
116
|
+
style={{
|
|
117
|
+
position: 'absolute',
|
|
118
|
+
right: -3,
|
|
119
|
+
bottom: -2,
|
|
120
|
+
display: 'flex',
|
|
121
|
+
borderRadius: '50%',
|
|
122
|
+
background: 'var(--md-primary)',
|
|
123
|
+
padding: 1,
|
|
124
|
+
}}
|
|
125
|
+
>
|
|
126
|
+
<Icon name="check-circle" size={badge} stroke={2.8} style={{ width: badge, height: badge }} />
|
|
127
|
+
</span>
|
|
128
|
+
</span>
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export interface TermChipsProps {
|
|
133
|
+
/** the selectable term values (months), e.g. [12,24,36,48,60,72]. */
|
|
134
|
+
options: number[];
|
|
135
|
+
value: number;
|
|
136
|
+
onChange: (term: number) => void;
|
|
137
|
+
/** i18n function (drilled from the page). */
|
|
138
|
+
t: TranslationFunction;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Laufzeit selector — discrete term values as tappable chips (replaces the fiddly slider). */
|
|
142
|
+
export const TermChips: React.FunctionComponent<TermChipsProps> = ({ options, value, onChange, t }) => (
|
|
143
|
+
<div className="term-chips" role="group" aria-label={t('autobahn:finance.termSelection')}>
|
|
144
|
+
{options.map((m) => (
|
|
145
|
+
<button
|
|
146
|
+
key={m}
|
|
147
|
+
type="button"
|
|
148
|
+
aria-pressed={m === value}
|
|
149
|
+
className={'term-chip' + (m === value ? ' on' : '')}
|
|
150
|
+
onClick={() => onChange(m)}
|
|
151
|
+
>
|
|
152
|
+
{m}
|
|
153
|
+
<span className="tc-unit">{t('autobahn:finance.monthUnit')}</span>
|
|
154
|
+
</button>
|
|
155
|
+
))}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
export interface CalcRowProps {
|
|
160
|
+
label: React.ReactNode;
|
|
161
|
+
/** when true, drops the bottom hairline (last row). */
|
|
162
|
+
last?: boolean;
|
|
163
|
+
children?: React.ReactNode;
|
|
164
|
+
className?: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** One label/value row inside the "Rate anpassen" / "Details" expander. */
|
|
168
|
+
export const CalcRow: React.FunctionComponent<CalcRowProps> = ({ label, last, children, className }) => (
|
|
169
|
+
<div
|
|
170
|
+
className={'calcrow' + (className ? ' ' + className : '')}
|
|
171
|
+
style={{
|
|
172
|
+
display: 'flex',
|
|
173
|
+
justifyContent: 'space-between',
|
|
174
|
+
alignItems: 'baseline',
|
|
175
|
+
gap: 12,
|
|
176
|
+
padding: '8px 0',
|
|
177
|
+
borderBottom: last ? 0 : '1px solid var(--md-outline-variant)',
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
<span className="md-label-sm calcrow-label">{label}</span>
|
|
181
|
+
<span className="num calcrow-value" style={{ fontSize: 13.5, fontWeight: 600 }}>
|
|
182
|
+
{children}
|
|
183
|
+
</span>
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
export interface FinanceCalculatorProps {
|
|
188
|
+
/** active payment mode (controlled). */
|
|
189
|
+
mode: FinanceMode;
|
|
190
|
+
/** purchase price in EUR (the cash mode hero). */
|
|
191
|
+
price: number;
|
|
192
|
+
/** previous/list price — when > price, shown struck through as the UVP in cash mode (real discount). */
|
|
193
|
+
listPrice?: number;
|
|
194
|
+
/** auto.de-ID reference code — shown with the financing/leasing condition line. */
|
|
195
|
+
autoDeId?: string;
|
|
196
|
+
/** price-rating label, e.g. 'Guter Preis' — drives the Preisbewertung indicator. */
|
|
197
|
+
ratingLabel?: string;
|
|
198
|
+
/** computed monthly rate in EUR for the current mode — supplied, NOT computed here. */
|
|
199
|
+
monthly: number;
|
|
200
|
+
/** struck old monthly rate (fe-financing oldMonthlyInstallment, isStrikeShown-gated) —
|
|
201
|
+
* rendered as the red line-through figure next to the hero rate (fin/leasing only). */
|
|
202
|
+
oldMonthly?: number;
|
|
203
|
+
/** effective annual rate (%) for the current mode — supplied, NOT computed here. */
|
|
204
|
+
apr: number;
|
|
205
|
+
/** current down payment in EUR (controlled). */
|
|
206
|
+
down: number;
|
|
207
|
+
/** current term in months (controlled). */
|
|
208
|
+
term: number;
|
|
209
|
+
/** down-payment slider bounds. */
|
|
210
|
+
downMin: number;
|
|
211
|
+
downMax: number;
|
|
212
|
+
/** down-payment slider step (EUR). */
|
|
213
|
+
downStep?: number;
|
|
214
|
+
/** term slider bounds (months) — used to derive chip options when termOptions is absent. */
|
|
215
|
+
termMin: number;
|
|
216
|
+
termMax: number;
|
|
217
|
+
/** term slider step (months). */
|
|
218
|
+
termStep?: number;
|
|
219
|
+
/** the discrete selectable terms (months) for the Laufzeit chips. */
|
|
220
|
+
termOptions?: number[];
|
|
221
|
+
/** annual mileage in km (controlled) — leasing/financing Laufleistung/Jahr. Omit to hide the slider. */
|
|
222
|
+
annualMileage?: number;
|
|
223
|
+
/** annual-mileage slider bounds + step (km). */
|
|
224
|
+
annualMileageMin?: number;
|
|
225
|
+
annualMileageMax?: number;
|
|
226
|
+
annualMileageStep?: number;
|
|
227
|
+
/** annual-mileage slider changed. */
|
|
228
|
+
onAnnualMileageChange?: (km: number) => void;
|
|
229
|
+
/** phone number shown on the secondary CTA (touch only). */
|
|
230
|
+
phone?: string;
|
|
231
|
+
/** primary CTA click (Angebot sichern). */
|
|
232
|
+
onSecure?: () => void;
|
|
233
|
+
/** secondary CTA click (phone). */
|
|
234
|
+
onCall?: () => void;
|
|
235
|
+
/** open the Finanzierungs-/Leasingdetails popup (full breakdown + legal, expanded). Omit to hide the link. */
|
|
236
|
+
onShowDetails?: () => void;
|
|
237
|
+
/** mobile only: open the "Rate anpassen" / "Details anzeigen" sheet (collapsed/default view).
|
|
238
|
+
* Omit to hide the trigger row (falls back to the desktop inline expander). */
|
|
239
|
+
onOpenAdjust?: () => void;
|
|
240
|
+
/** mode segment changed. */
|
|
241
|
+
onModeChange: (mode: FinanceMode) => void;
|
|
242
|
+
/** down-payment slider changed. */
|
|
243
|
+
onDownChange: (down: number) => void;
|
|
244
|
+
/** term changed. */
|
|
245
|
+
onTermChange: (term: number) => void;
|
|
246
|
+
/** i18n function (drilled from the page). */
|
|
247
|
+
t: TranslationFunction;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const eur = (n: number): string => n.toLocaleString('de-DE');
|
|
251
|
+
|
|
252
|
+
/** build a [min..max] step range (fallback term options when none are supplied). */
|
|
253
|
+
const range = (min: number, max: number, step: number): number[] => {
|
|
254
|
+
const out: number[] = [];
|
|
255
|
+
for (let v = min; v <= max; v += step || 12) out.push(v);
|
|
256
|
+
return out.length ? out : [min];
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** quiet primary-colored text link (Finanzierungsdetails / Details anzeigen). */
|
|
260
|
+
const DetailsLink: React.FunctionComponent<{ label: string; onClick: () => void }> = ({ label, onClick }) => (
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
onClick={onClick}
|
|
264
|
+
style={{
|
|
265
|
+
display: 'inline-flex',
|
|
266
|
+
alignItems: 'center',
|
|
267
|
+
gap: 5,
|
|
268
|
+
background: 'none',
|
|
269
|
+
border: 0,
|
|
270
|
+
padding: 0,
|
|
271
|
+
cursor: 'pointer',
|
|
272
|
+
color: 'var(--md-primary)',
|
|
273
|
+
font: 'inherit',
|
|
274
|
+
fontSize: 13,
|
|
275
|
+
fontWeight: 600,
|
|
276
|
+
}}
|
|
277
|
+
>
|
|
278
|
+
<Icon name="info" size={15} color="var(--md-primary)" />
|
|
279
|
+
{label}
|
|
280
|
+
</button>
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
/** "Rate anpassen" / "Details anzeigen" expander toggle button. */
|
|
284
|
+
export const expanderBtnStyle: React.CSSProperties = {
|
|
285
|
+
width: '100%',
|
|
286
|
+
background: 'none',
|
|
287
|
+
border: 0,
|
|
288
|
+
cursor: 'pointer',
|
|
289
|
+
display: 'flex',
|
|
290
|
+
alignItems: 'center',
|
|
291
|
+
justifyContent: 'space-between',
|
|
292
|
+
padding: '10px 0 2px',
|
|
293
|
+
color: 'var(--md-primary)',
|
|
294
|
+
fontWeight: 600,
|
|
295
|
+
fontSize: 14,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
299
|
+
mode,
|
|
300
|
+
price,
|
|
301
|
+
listPrice,
|
|
302
|
+
autoDeId,
|
|
303
|
+
ratingLabel,
|
|
304
|
+
monthly,
|
|
305
|
+
oldMonthly,
|
|
306
|
+
down,
|
|
307
|
+
term,
|
|
308
|
+
downMin,
|
|
309
|
+
downMax,
|
|
310
|
+
downStep = 500,
|
|
311
|
+
termMin,
|
|
312
|
+
termMax,
|
|
313
|
+
termStep = 6,
|
|
314
|
+
termOptions,
|
|
315
|
+
annualMileage,
|
|
316
|
+
annualMileageMin = 5000,
|
|
317
|
+
annualMileageMax = 60000,
|
|
318
|
+
annualMileageStep = 5000,
|
|
319
|
+
onAnnualMileageChange,
|
|
320
|
+
phone = '0800 40 30 182',
|
|
321
|
+
onSecure,
|
|
322
|
+
onCall,
|
|
323
|
+
onShowDetails,
|
|
324
|
+
onOpenAdjust,
|
|
325
|
+
onModeChange,
|
|
326
|
+
onDownChange,
|
|
327
|
+
onTermChange,
|
|
328
|
+
t,
|
|
329
|
+
}) => {
|
|
330
|
+
// Pure view state only — whether the adjust/details expander is open.
|
|
331
|
+
const [adjust, setAdjust] = useState(false);
|
|
332
|
+
const isCash = mode === 'cash';
|
|
333
|
+
const terms = termOptions && termOptions.length ? termOptions : range(termMin, termMax, termStep);
|
|
334
|
+
const showStrike = listPrice != null && listPrice > price;
|
|
335
|
+
// price-evaluation tier from the rating label (defaults to 'Guter Preis' tier when unmapped).
|
|
336
|
+
const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
|
|
337
|
+
const evalTier = tierIdx >= 0 ? tierIdx : 1;
|
|
338
|
+
const evalTierLabel = t('autobahn:tier.' + TIER_KEYS[evalTier]);
|
|
339
|
+
// condition summary (current selection) — the auto.de-ID lives with the details link, not here.
|
|
340
|
+
const condParts: string[] = [];
|
|
341
|
+
if (!isCash) {
|
|
342
|
+
condParts.push(
|
|
343
|
+
mode === 'leasing' && annualMileage != null
|
|
344
|
+
? t('autobahn:finance.kmPerYear', { value: eur(annualMileage) })
|
|
345
|
+
: t('autobahn:finance.downPaymentLabel', { value: eur(down) }),
|
|
346
|
+
);
|
|
347
|
+
condParts.push(t('autobahn:finance.monthsLabel', { months: term, count: term }));
|
|
348
|
+
}
|
|
349
|
+
const condLine = condParts.length ? `${t('autobahn:finance.exampleLabel')} ${condParts.join(' · ')}` : '';
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<div className="mcard mcard--elevated fin-calc-card" style={{ padding: 20 }}>
|
|
353
|
+
{/* header — desktop only; mobile goes straight into the tabs (see mockup: no card
|
|
354
|
+
title above Finanzierung/Leasing/Kaufen on phone). */}
|
|
355
|
+
<div className="fin-card-header hide-mobile" style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
|
|
356
|
+
<span
|
|
357
|
+
style={{
|
|
358
|
+
width: 34,
|
|
359
|
+
height: 34,
|
|
360
|
+
borderRadius: 'var(--md-r-full)',
|
|
361
|
+
background: 'var(--md-primary-container)',
|
|
362
|
+
display: 'flex',
|
|
363
|
+
alignItems: 'center',
|
|
364
|
+
justifyContent: 'center',
|
|
365
|
+
flex: 'none',
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
<Icon name="tag" size={18} color="var(--md-on-primary-container)" />
|
|
369
|
+
</span>
|
|
370
|
+
<div style={{ fontFamily: 'var(--md-font-display)', fontSize: 18, fontWeight: 700 }}>{t('autobahn:finance.headerTitle')}</div>
|
|
371
|
+
</div>
|
|
372
|
+
|
|
373
|
+
{/* payment-type TABS on top */}
|
|
374
|
+
<div className="fin-tabs" role="tablist">
|
|
375
|
+
{(
|
|
376
|
+
[
|
|
377
|
+
['fin', 'autobahn:finance.mode.financing'],
|
|
378
|
+
['leasing', 'autobahn:finance.mode.leasing'],
|
|
379
|
+
['cash', 'autobahn:finance.mode.cash'],
|
|
380
|
+
] as Array<[FinanceMode, string]>
|
|
381
|
+
).map(([k, labelKey]) => (
|
|
382
|
+
<button
|
|
383
|
+
key={k}
|
|
384
|
+
type="button"
|
|
385
|
+
role="tab"
|
|
386
|
+
aria-selected={mode === k}
|
|
387
|
+
className={'fin-tab' + (mode === k ? ' on' : '')}
|
|
388
|
+
onClick={() => onModeChange(k)}
|
|
389
|
+
>
|
|
390
|
+
{k === 'cash' ? (
|
|
391
|
+
<>
|
|
392
|
+
{/* mockup names the cash tab "Kaufen" on phone; desktop keeps "Barzahlung" */}
|
|
393
|
+
<span className="hide-mobile">{t(labelKey)}</span>
|
|
394
|
+
<span className="show-mobile" style={{ justifyContent: 'center' }}>
|
|
395
|
+
{t('autobahn:card.price.buy')}
|
|
396
|
+
</span>
|
|
397
|
+
</>
|
|
398
|
+
) : (
|
|
399
|
+
t(labelKey)
|
|
400
|
+
)}
|
|
401
|
+
</button>
|
|
402
|
+
))}
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
{/* 2-col grid: rate|pill (row 1) + condLine|bar (row 2) — fixed right column prevents wrapping */}
|
|
406
|
+
<div className="fin-price-grid" style={isCash ? { gap: '0px 16px' } : undefined}>
|
|
407
|
+
{/* col 1 row 1: hero rate — Figma: "329 €" all-big, then a small stacked column with
|
|
408
|
+
the struck old rate ABOVE "/mtl." (round 11; was a single baseline row) */}
|
|
409
|
+
<div className="fin-rate-main">
|
|
410
|
+
{/* mobile Kaufen (Figma round 14): struck list price ABOVE the price, right-aligned,
|
|
411
|
+
tight to the figure — replaces the Festpreis + UVP lines, which stay desktop-only */}
|
|
412
|
+
{isCash && showStrike && (
|
|
413
|
+
<div className="show-mobile" style={{ justifyContent: 'flex-end', lineHeight: 1, marginTop: 7 }}>
|
|
414
|
+
<s className="num fin-rate-old fin-rate-old-list">
|
|
415
|
+
{eur(listPrice as number)} €<sup>2</sup>
|
|
416
|
+
</s>
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
|
420
|
+
{isCash ? (
|
|
421
|
+
<>
|
|
422
|
+
{/* phone: ONE unbreakable "13.249 €" span (a separate € span wraps to its own
|
|
423
|
+
line); desktop keeps the number + quiet small € pair */}
|
|
424
|
+
<span
|
|
425
|
+
className={`num show-mobile fin-rate-cash-mobile${
|
|
426
|
+
(() => {
|
|
427
|
+
const digits = String(Math.round(price as number)).length;
|
|
428
|
+
if (digits >= 8) return ' fin-rate-cash-mobile--len8';
|
|
429
|
+
if (digits === 7) return ' fin-rate-cash-mobile--len7';
|
|
430
|
+
if (digits === 6) return ' fin-rate-cash-mobile--len6';
|
|
431
|
+
return '';
|
|
432
|
+
})()
|
|
433
|
+
}`}
|
|
434
|
+
style={{ fontSize: 48, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-2px', whiteSpace: 'nowrap' }}
|
|
435
|
+
>
|
|
436
|
+
{eur(price)} €<sup>1</sup>
|
|
437
|
+
</span>
|
|
438
|
+
<span
|
|
439
|
+
className="num hide-mobile"
|
|
440
|
+
style={{ fontSize: 48, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-2px' }}
|
|
441
|
+
>
|
|
442
|
+
{eur(price)}
|
|
443
|
+
</span>
|
|
444
|
+
<span className="md-body-sm hide-mobile" style={{ fontWeight: 600 }}>€</span>
|
|
445
|
+
</>
|
|
446
|
+
) : (
|
|
447
|
+
<>
|
|
448
|
+
<span
|
|
449
|
+
className={`num fin-rate-hero${
|
|
450
|
+
(() => {
|
|
451
|
+
const digits = String(Math.round(monthly)).length;
|
|
452
|
+
if (digits >= 6) return ' fin-rate-hero--len6';
|
|
453
|
+
if (digits === 5) return ' fin-rate-hero--len5';
|
|
454
|
+
if (digits === 4) return ' fin-rate-hero--len4';
|
|
455
|
+
return '';
|
|
456
|
+
})()
|
|
457
|
+
}`}
|
|
458
|
+
style={{ fontSize: 48, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-2px' }}
|
|
459
|
+
>
|
|
460
|
+
{eur(Math.round(monthly))}<span style={{ marginLeft: 6 }}>€</span>
|
|
461
|
+
</span>
|
|
462
|
+
<span className="fin-rate-unit">
|
|
463
|
+
{/* struck old rate (REAL oldMonthlyInstallment; SRP strikes the same source) */}
|
|
464
|
+
{oldMonthly != null && (
|
|
465
|
+
<>
|
|
466
|
+
<span className="fin-rate-example-label">{t('autobahn:finance.exampleLabel')}</span>
|
|
467
|
+
<s
|
|
468
|
+
className={`num fin-rate-old fin-rate-example-price${
|
|
469
|
+
String(Math.round(oldMonthly)).length >= 5
|
|
470
|
+
? ' fin-rate-example-price--len5'
|
|
471
|
+
: String(Math.round(oldMonthly)).length === 4
|
|
472
|
+
? ' fin-rate-example-price--len4'
|
|
473
|
+
: ''
|
|
474
|
+
}`}
|
|
475
|
+
>
|
|
476
|
+
{eur(Math.round(oldMonthly))} €<sup>2</sup>
|
|
477
|
+
</s>
|
|
478
|
+
</>
|
|
479
|
+
)}
|
|
480
|
+
<span className="fin-rate-example-suffix">
|
|
481
|
+
{t('autobahn:finance.perMonth')}<sup>3</sup>
|
|
482
|
+
</span>
|
|
483
|
+
</span>
|
|
484
|
+
</>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
{isCash && <div className="md-label-sm hide-mobile">{t('autobahn:finance.fixedPrice')}</div>}
|
|
488
|
+
{isCash && showStrike && (
|
|
489
|
+
<div className="fin-kauf hide-mobile">
|
|
490
|
+
{t('autobahn:finance.uvp')} <s>{eur(listPrice as number)} €</s>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
</div>
|
|
494
|
+
{/* col 2: pill + eval bar grouped together (display:contents on desktop — no-op,
|
|
495
|
+
preserves the original rate|pill / cond|bar auto-placement; mobile turns this into
|
|
496
|
+
a real flex column so the two stack tight with a fixed 16px gap). */}
|
|
497
|
+
<div className="fin-eval-col">
|
|
498
|
+
{/* col 2 row 1: price-rating pill (Figma: bold colored label, NO icon) */}
|
|
499
|
+
<div className="fin-eval-pill-cell">
|
|
500
|
+
{ratingLabel && (
|
|
501
|
+
<span
|
|
502
|
+
className={`fin-eval-pill${TIER_KEYS[evalTier] === 'good' ? ' fin-eval-pill--good' : ''}`}
|
|
503
|
+
style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
|
|
504
|
+
aria-label={t('autobahn:finance.priceRatingAria', { rating: evalTierLabel })}
|
|
505
|
+
>
|
|
506
|
+
{evalTierLabel}
|
|
507
|
+
</span>
|
|
508
|
+
)}
|
|
509
|
+
</div>
|
|
510
|
+
{/* segmented evaluation bar (Figma: discrete tiers, not a gradient) */}
|
|
511
|
+
<div className="fin-eval-bar-cell">{ratingLabel && <EvalBar tier={evalTier} />}</div>
|
|
512
|
+
</div>
|
|
513
|
+
{/* col 1 row 2: condition line */}
|
|
514
|
+
<div className="fin-cond">{condLine}</div>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
{/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
|
|
518
|
+
<div className="hide-mobile">
|
|
519
|
+
{/* RATE EDIT (Rate anpassen) — between the rate and the request CTA */}
|
|
520
|
+
<div className="fin-adjust">
|
|
521
|
+
<button onClick={() => setAdjust((a) => !a)} style={expanderBtnStyle}>
|
|
522
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
|
|
523
|
+
<Icon name={isCash ? 'list' : 'sliders-horizontal'} size={16} color="var(--md-primary)" />
|
|
524
|
+
{isCash ? t('autobahn:finance.detailsShow') : t('autobahn:finance.adjustRate')}
|
|
525
|
+
</span>
|
|
526
|
+
<Icon name={adjust ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" />
|
|
527
|
+
</button>
|
|
528
|
+
{adjust &&
|
|
529
|
+
(isCash ? (
|
|
530
|
+
<div style={{ marginTop: 4 }}>
|
|
531
|
+
<CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
|
|
532
|
+
<CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
|
|
533
|
+
<CalcRow label={t('autobahn:finance.deliveryLabel')} last>
|
|
534
|
+
{t('autobahn:finance.deliveryValue')}
|
|
535
|
+
</CalcRow>
|
|
536
|
+
</div>
|
|
537
|
+
) : (
|
|
538
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 8 }}>
|
|
539
|
+
<div>
|
|
540
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
541
|
+
<span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
|
|
542
|
+
<span className="num" style={{ fontWeight: 700 }}>
|
|
543
|
+
{eur(down)} €
|
|
544
|
+
</span>
|
|
545
|
+
</div>
|
|
546
|
+
<Slider
|
|
547
|
+
min={downMin}
|
|
548
|
+
max={downMax}
|
|
549
|
+
step={downStep}
|
|
550
|
+
value={down}
|
|
551
|
+
onChange={onDownChange}
|
|
552
|
+
fmt={(v) => `${eur(Number(v))} €`}
|
|
553
|
+
/>
|
|
554
|
+
</div>
|
|
555
|
+
{annualMileage != null && onAnnualMileageChange && (
|
|
556
|
+
<div>
|
|
557
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
558
|
+
<span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
|
|
559
|
+
<span className="num" style={{ fontWeight: 700 }}>
|
|
560
|
+
{eur(annualMileage)} km
|
|
561
|
+
</span>
|
|
562
|
+
</div>
|
|
563
|
+
<Slider
|
|
564
|
+
min={annualMileageMin}
|
|
565
|
+
max={annualMileageMax}
|
|
566
|
+
step={annualMileageStep}
|
|
567
|
+
value={annualMileage}
|
|
568
|
+
onChange={onAnnualMileageChange}
|
|
569
|
+
fmt={(v) => `${eur(Number(v))} km`}
|
|
570
|
+
/>
|
|
571
|
+
</div>
|
|
572
|
+
)}
|
|
573
|
+
<div>
|
|
574
|
+
<div className="md-title-sm" style={{ marginBottom: 8 }}>
|
|
575
|
+
{t('autobahn:finance.termSelection')}
|
|
576
|
+
</div>
|
|
577
|
+
<TermChips options={terms} value={term} onChange={onTermChange} t={t} />
|
|
578
|
+
</div>
|
|
579
|
+
<div>
|
|
580
|
+
<CalcRow label={t('autobahn:finance.purchasePrice')}>{eur(price)} €</CalcRow>
|
|
581
|
+
<CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
|
|
582
|
+
<CalcRow label={t('autobahn:finance.deliveryLabel')} last>
|
|
583
|
+
{t('autobahn:finance.deliveryValue')}
|
|
584
|
+
</CalcRow>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
))}
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
{/* REQUEST CTA */}
|
|
591
|
+
<Button variant="filled" block size="lg" icon="shield-check" style={{ marginTop: 14 }} onClick={onSecure}>
|
|
592
|
+
{t('autobahn:finance.secureOffer')}
|
|
593
|
+
</Button>
|
|
594
|
+
{/* phone CTA — touch only (hidden on desktop via .fin-call) */}
|
|
595
|
+
<div className="fin-call">
|
|
596
|
+
<Button variant="outlined" block icon="phone" onClick={onCall}>
|
|
597
|
+
{phone}
|
|
598
|
+
</Button>
|
|
599
|
+
</div>
|
|
600
|
+
|
|
601
|
+
{/* auto.de-ID in front of the Finanzierungs-/Leasingdetails link — one row, under everything */}
|
|
602
|
+
{(autoDeId || (!isCash && onShowDetails)) && (
|
|
603
|
+
<div className="fin-info">
|
|
604
|
+
{autoDeId && <span className="fin-id">auto.de-ID {autoDeId}</span>}
|
|
605
|
+
{!isCash && onShowDetails && (
|
|
606
|
+
<DetailsLink
|
|
607
|
+
label={mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
|
|
608
|
+
onClick={onShowDetails}
|
|
609
|
+
/>
|
|
610
|
+
)}
|
|
611
|
+
</div>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
{/* ============================== MOBILE (≤600px) — new layout ============================== */}
|
|
616
|
+
<div className="show-mobile" style={{ flexDirection: 'column' }}>
|
|
617
|
+
{/* side-by-side CTAs — mockup ratio ~2:1, with the tiny auto.de-ID sitting
|
|
618
|
+
directly under the Anrufen button (not in its own full-width row). */}
|
|
619
|
+
<div className="fin-cta-row" style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
|
|
620
|
+
{/* Figma CTA icon: document + check badge (composed — no single lucide glyph) */}
|
|
621
|
+
<Button
|
|
622
|
+
variant="filled"
|
|
623
|
+
size="lg"
|
|
624
|
+
style={{ width: 216, height: 50, paddingLeft: 0, paddingRight: 9, gap: 18 }}
|
|
625
|
+
onClick={onSecure}
|
|
626
|
+
>
|
|
627
|
+
<DocCheckIcon size={23} />
|
|
628
|
+
{t('autobahn:finance.secureOffer')}
|
|
629
|
+
</Button>
|
|
630
|
+
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
|
|
631
|
+
<Button variant="outlined" size="lg" icon="phone" style={{ width: 112, height: 50 }} onClick={onCall}>
|
|
632
|
+
{t('autobahn:finance.callCta')}
|
|
633
|
+
</Button>
|
|
634
|
+
{autoDeId && (
|
|
635
|
+
<span
|
|
636
|
+
className="num"
|
|
637
|
+
style={{ fontSize: 10, fontWeight: 700, color: 'var(--md-primary)', textAlign: 'center', whiteSpace: 'nowrap' }}
|
|
638
|
+
>
|
|
639
|
+
auto.de ID: {autoDeId}
|
|
640
|
+
</span>
|
|
641
|
+
)}
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
{/* Persönliche Beratung (left) · divider · Finanzierungs-/Leasingdetails or hint (right) */}
|
|
646
|
+
<div className="fin-info">
|
|
647
|
+
<div className="fin-info-main" style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, flex: 1 }}>
|
|
648
|
+
<Icon name="user" size={26} color="var(--md-on-surface-variant)" style={{ flex: 'none' }} />
|
|
649
|
+
<div style={{ minWidth: 0 }}>
|
|
650
|
+
<div className="md-label-sm" style={{ fontWeight: 700, color: 'var(--md-on-surface)' }}>
|
|
651
|
+
{t('autobahn:finance.personalConsultation')}
|
|
652
|
+
</div>
|
|
653
|
+
<div className="md-label-sm fin-info-sub">{t('autobahn:finance.personalConsultationSub')}</div>
|
|
654
|
+
</div>
|
|
655
|
+
</div>
|
|
656
|
+
<span
|
|
657
|
+
style={{
|
|
658
|
+
width: 1,
|
|
659
|
+
alignSelf: 'stretch',
|
|
660
|
+
background: 'var(--md-outline-variant)',
|
|
661
|
+
flex: 'none',
|
|
662
|
+
marginLeft: 13,
|
|
663
|
+
marginRight: 13,
|
|
664
|
+
}}
|
|
665
|
+
/>
|
|
666
|
+
{!isCash && onShowDetails ? (
|
|
667
|
+
<button
|
|
668
|
+
type="button"
|
|
669
|
+
className="fin-info-details-btn"
|
|
670
|
+
onClick={onShowDetails}
|
|
671
|
+
style={{
|
|
672
|
+
display: 'flex',
|
|
673
|
+
alignItems: 'center',
|
|
674
|
+
gap: 8,
|
|
675
|
+
background: 'none',
|
|
676
|
+
border: 0,
|
|
677
|
+
padding: 0,
|
|
678
|
+
cursor: 'pointer',
|
|
679
|
+
font: 'inherit',
|
|
680
|
+
color: 'var(--md-on-surface)',
|
|
681
|
+
textAlign: 'left',
|
|
682
|
+
}}
|
|
683
|
+
>
|
|
684
|
+
<Icon name="info" size={24} color="var(--md-on-surface-variant)" style={{ flex: 'none' }} />
|
|
685
|
+
{/* no max-width: "Finanzierungsdetails" is one unbreakable word — capping the
|
|
686
|
+
span clipped it mid-word instead of wrapping */}
|
|
687
|
+
<span className="md-label-sm fin-info-details-label">
|
|
688
|
+
{mode === 'leasing' ? (
|
|
689
|
+
t('autobahn:finance.leasingDetails')
|
|
690
|
+
) : (
|
|
691
|
+
<>
|
|
692
|
+
Finanzierungs-
|
|
693
|
+
<br />
|
|
694
|
+
Details
|
|
695
|
+
</>
|
|
696
|
+
)}
|
|
697
|
+
</span>
|
|
698
|
+
</button>
|
|
699
|
+
) : (
|
|
700
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--md-on-surface-variant)' }}>
|
|
701
|
+
<Icon name="info" size={24} />
|
|
702
|
+
<span className="md-label-sm fin-financing-leasing-note">
|
|
703
|
+
{(() => {
|
|
704
|
+
// hard break right after "&" (not a soft wrap-if-needed hint) — always
|
|
705
|
+
// two lines: "Finanzierung &" / "Leasing möglich"; derived from the
|
|
706
|
+
// translated string itself so it works for either locale.
|
|
707
|
+
const raw = t('autobahn:finance.financingLeasingPossible');
|
|
708
|
+
const splitAt = raw.indexOf(' & ');
|
|
709
|
+
if (splitAt === -1) return raw;
|
|
710
|
+
const before = raw.slice(0, splitAt).split(' ').join(' ');
|
|
711
|
+
const after = raw.slice(splitAt + 3).split(' ').join(' ');
|
|
712
|
+
return (
|
|
713
|
+
<>
|
|
714
|
+
{before} &
|
|
715
|
+
<br />
|
|
716
|
+
{after}
|
|
717
|
+
</>
|
|
718
|
+
);
|
|
719
|
+
})()}
|
|
720
|
+
</span>
|
|
721
|
+
</div>
|
|
722
|
+
)}
|
|
723
|
+
</div>
|
|
724
|
+
|
|
725
|
+
{/* "Rate anpassen" / "Details anzeigen" — opens the full bottom sheet (not an inline expander) */}
|
|
726
|
+
{onOpenAdjust && (
|
|
727
|
+
<button
|
|
728
|
+
type="button"
|
|
729
|
+
onClick={onOpenAdjust}
|
|
730
|
+
style={{ ...expanderBtnStyle, borderTop: '1px solid var(--md-outline-variant)', marginTop: 11, padding: '11px 0 2px 21px' }}
|
|
731
|
+
>
|
|
732
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 21 }}>
|
|
733
|
+
<Icon name="sliders-horizontal" size={23} color="var(--md-primary)" />
|
|
734
|
+
<span className="fin-adjust-btn-label">
|
|
735
|
+
{isCash ? t('autobahn:finance.detailsShow') : t('autobahn:finance.adjustRate')}
|
|
736
|
+
</span>
|
|
737
|
+
</span>
|
|
738
|
+
<Icon name="chevron-right" size={23} color="var(--md-primary)" />
|
|
739
|
+
</button>
|
|
740
|
+
)}
|
|
741
|
+
</div>
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
export default FinanceCalculator;
|