@bytebrand/fe-ui-core-autobahn 1.0.43 → 1.0.45
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/locales/de/autobahn.json +19 -10
- package/locales/en/autobahn.json +17 -8
- package/package.json +1 -1
- package/theme/md3.css +39 -2
- package/vdp/FinanceCalculator.tsx +622 -579
- package/vdp/VdpModules.tsx +182 -98
- package/vdp/index.ts +1 -0
package/vdp/VdpModules.tsx
CHANGED
|
@@ -27,10 +27,29 @@ import { TranslationFunction } from '../types';
|
|
|
27
27
|
import { Button, Icon, Spec, StatChip, Stars, Switch } from '../ui';
|
|
28
28
|
import { CO2_CLASSES, CO2_COLORS, TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
|
|
29
29
|
import { defaultVdpSpecs, EquipmentGroups, PriceRatingBands, VehicleDetail, VdpSpec, VdpReview } from './types';
|
|
30
|
-
import { CalcRow, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
|
|
30
|
+
import { CalcRow, DocCheckIcon, EvalBar, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
|
|
31
31
|
|
|
32
32
|
const eur = (n: number): string => n.toLocaleString('de-DE');
|
|
33
33
|
|
|
34
|
+
/** Figma "check with open arc" glyph (modern lucide circle-check-big; the bundled lucide
|
|
35
|
+
* 0.288 only ships the closed-circle variant, so the two paths are inlined). */
|
|
36
|
+
const ArcCheckIcon: React.FunctionComponent<{ size?: number; color?: string }> = ({ size = 20, color = 'currentColor' }) => (
|
|
37
|
+
<svg
|
|
38
|
+
width={size}
|
|
39
|
+
height={size}
|
|
40
|
+
viewBox="0 0 24 24"
|
|
41
|
+
fill="none"
|
|
42
|
+
stroke={color}
|
|
43
|
+
strokeWidth={2.4}
|
|
44
|
+
strokeLinecap="round"
|
|
45
|
+
strokeLinejoin="round"
|
|
46
|
+
style={{ flex: 'none' }}
|
|
47
|
+
>
|
|
48
|
+
<path d="M21.801 10A10 10 0 1 1 17 3.335" />
|
|
49
|
+
<path d="m9 11 3 3L22 4" />
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
|
|
34
53
|
/** mileage → 'X.XXX km' (de-DE grouping). */
|
|
35
54
|
function fmtKm(n: number): string {
|
|
36
55
|
return `${n.toLocaleString('de-DE')} km`;
|
|
@@ -102,6 +121,30 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
|
|
|
102
121
|
);
|
|
103
122
|
};
|
|
104
123
|
|
|
124
|
+
/** the 6-fact icon grid (km/EZ · Getriebe/Kraftstoff · Hand/Karosserie) — reused by the
|
|
125
|
+
* mobile highlights card, which repeats the vehicle identity per the Figma master
|
|
126
|
+
* ("Vehicle Highlights / Card" carries its own title + facts above the pills). */
|
|
127
|
+
export const VdpFactsGrid: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
|
|
128
|
+
const facts: ReadonlyArray<readonly [string, string]> = [
|
|
129
|
+
['gauge', fmtKm(vehicle.km)],
|
|
130
|
+
['calendar', vehicle.reg],
|
|
131
|
+
['cog', vehicle.gear],
|
|
132
|
+
['fuel', stripParen(vehicle.fuel)],
|
|
133
|
+
['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
|
|
134
|
+
...(vehicle.body ? ([['car-front', vehicle.body]] as const) : []),
|
|
135
|
+
];
|
|
136
|
+
return (
|
|
137
|
+
<div className="vdp-meta" style={{ marginTop: 0 }}>
|
|
138
|
+
{facts.map(([ic, v]) => (
|
|
139
|
+
<span key={ic}>
|
|
140
|
+
<Icon name={ic} size={16} color="var(--md-on-surface-variant)" stroke={1.8} />
|
|
141
|
+
{v}
|
|
142
|
+
</span>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
105
148
|
/** hand-picked highlights — the accent rows of the header. ALL highlights are shown
|
|
106
149
|
* (review round 9: "all highlights shown", no 6-chip cap); the Figma pill colors live
|
|
107
150
|
* on .vdp-highlights .chip in the theme. */
|
|
@@ -362,9 +405,9 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
|
|
|
362
405
|
[t('autobahn:eco.stat.co2WeightedCombined'), p.co2w],
|
|
363
406
|
]
|
|
364
407
|
: [
|
|
408
|
+
// Figma: two stats only — the class letter lives on the scale, not as a stat row
|
|
365
409
|
[t('autobahn:eco.stat.consumptionCombined'), vehicle.cons],
|
|
366
410
|
[t('autobahn:eco.stat.co2Emissions'), vehicle.co2],
|
|
367
|
-
[t('autobahn:eco.stat.co2Class'), vehicle.co2class],
|
|
368
411
|
];
|
|
369
412
|
return (
|
|
370
413
|
<div className="mcard" style={{ padding: 22 }}>
|
|
@@ -381,6 +424,10 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
|
|
|
381
424
|
</div>
|
|
382
425
|
))}
|
|
383
426
|
</div>
|
|
427
|
+
{/* scale caption (Figma) — replaces the old CO₂-Klasse stat row + EnVKV footer line */}
|
|
428
|
+
<div className="md-label-sm" style={{ margin: '0 0 8px' }}>
|
|
429
|
+
{t('autobahn:eco.co2ClassScaleLabel')}
|
|
430
|
+
</div>
|
|
384
431
|
<div className="co2-scale">
|
|
385
432
|
{CO2_CLASSES.map((c, i) => (
|
|
386
433
|
<div key={c} className="co2-col">
|
|
@@ -407,17 +454,8 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
|
|
|
407
454
|
</div>
|
|
408
455
|
))}
|
|
409
456
|
</div>
|
|
410
|
-
{
|
|
411
|
-
|
|
412
|
-
{t('autobahn:eco.co2ClassPrefix')} <b style={{ color: 'var(--md-on-surface)' }}>{p.co2classW}</b>{' '}
|
|
413
|
-
{t('autobahn:eco.weightedCombined')} · <b style={{ color: 'var(--md-on-surface)' }}>{p.co2classD}</b>{' '}
|
|
414
|
-
{t('autobahn:eco.dischargedCombined')} · {t('autobahn:eco.energyClassWltpSuffix')}
|
|
415
|
-
</p>
|
|
416
|
-
) : (
|
|
417
|
-
<p className="md-label-sm" style={{ margin: '10px 0 0' }}>
|
|
418
|
-
{t('autobahn:eco.energyClass')}
|
|
419
|
-
</p>
|
|
420
|
-
)}
|
|
457
|
+
{/* no EnVKV footer line — Figma drops it (the * on the heading refers to the page-footer
|
|
458
|
+
disclaimer instead); the scale caption above carries the WLTP-kombiniert wording */}
|
|
421
459
|
</div>
|
|
422
460
|
);
|
|
423
461
|
};
|
|
@@ -617,28 +655,12 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
|
|
|
617
655
|
<div className="md-title-sm" style={{ marginBottom: 12 }}>
|
|
618
656
|
{t('autobahn:marketValue.heading')}
|
|
619
657
|
</div>
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}}
|
|
627
|
-
>
|
|
628
|
-
<span
|
|
629
|
-
style={{
|
|
630
|
-
position: 'absolute',
|
|
631
|
-
top: '50%',
|
|
632
|
-
left: `calc(${mpos * 100}% - 9px)`,
|
|
633
|
-
transform: 'translateY(-50%)',
|
|
634
|
-
width: 18,
|
|
635
|
-
height: 18,
|
|
636
|
-
borderRadius: '50%',
|
|
637
|
-
background: '#fff',
|
|
638
|
-
boxShadow: 'var(--md-e2), 0 0 0 2px var(--md-primary)',
|
|
639
|
-
}}
|
|
640
|
-
/>
|
|
641
|
-
</div>
|
|
658
|
+
{/* segmented tier bar (Figma — discrete colors, not a gradient), continuous marker;
|
|
659
|
+
ring color follows the car's actual rating tone */}
|
|
660
|
+
<EvalBar
|
|
661
|
+
tier={{ top: 0, good: 1, fair: 2, raised: 3, high: 4 }[vehicle.rating as 'top' | 'good' | 'fair' | 'raised' | 'high'] ?? 2}
|
|
662
|
+
leftPct={mpos * 100}
|
|
663
|
+
/>
|
|
642
664
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8 }}>
|
|
643
665
|
<span className="md-label-sm">{eur(vehicle.marketLow!)} €</span>
|
|
644
666
|
<span className="md-label-sm">{eur(vehicle.marketHigh!)} €</span>
|
|
@@ -650,13 +672,13 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
|
|
|
650
672
|
alignItems: 'center',
|
|
651
673
|
gap: 8,
|
|
652
674
|
marginTop: 12,
|
|
653
|
-
padding: '
|
|
675
|
+
padding: '13px 14px',
|
|
654
676
|
borderRadius: 'var(--md-r-sm)',
|
|
655
677
|
background: 'var(--md-good-container)',
|
|
656
678
|
}}
|
|
657
679
|
>
|
|
658
|
-
<
|
|
659
|
-
<span style={{ fontSize:
|
|
680
|
+
<ArcCheckIcon size={20} color="var(--md-good)" />
|
|
681
|
+
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--md-good)' }}>
|
|
660
682
|
{t('autobahn:marketValue.belowAverage', { rating: t('autobahn:tier.' + vehicle.rating), amount: eur(delta) })}
|
|
661
683
|
</span>
|
|
662
684
|
</div>
|
|
@@ -720,8 +742,41 @@ const RATING_TIERS: ReadonlyArray<readonly [keyof PriceRatingBands, string]> = [
|
|
|
720
742
|
['highPrice', 'var(--md-error)'],
|
|
721
743
|
];
|
|
722
744
|
|
|
745
|
+
/** collapsible sub-card header (blue icon + bold title + chevron) — Figma round 12. */
|
|
746
|
+
const BreakdownCardHead: React.FunctionComponent<{
|
|
747
|
+
icon: string;
|
|
748
|
+
title: string;
|
|
749
|
+
open: boolean;
|
|
750
|
+
onToggle: () => void;
|
|
751
|
+
}> = ({ icon, title, open, onToggle }) => (
|
|
752
|
+
<button
|
|
753
|
+
type="button"
|
|
754
|
+
onClick={onToggle}
|
|
755
|
+
style={{
|
|
756
|
+
display: 'flex',
|
|
757
|
+
alignItems: 'center',
|
|
758
|
+
gap: 10,
|
|
759
|
+
width: '100%',
|
|
760
|
+
background: 'none',
|
|
761
|
+
border: 0,
|
|
762
|
+
padding: 0,
|
|
763
|
+
cursor: 'pointer',
|
|
764
|
+
font: 'inherit',
|
|
765
|
+
textAlign: 'left',
|
|
766
|
+
}}
|
|
767
|
+
>
|
|
768
|
+
<Icon name={icon} size={19} color="var(--md-primary)" stroke={2} style={{ flex: 'none' }} />
|
|
769
|
+
<span className="md-title-sm" style={{ flex: 1, minWidth: 0, fontSize: 15 }}>
|
|
770
|
+
{title}
|
|
771
|
+
</span>
|
|
772
|
+
<Icon name={open ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" style={{ flex: 'none' }} />
|
|
773
|
+
</button>
|
|
774
|
+
);
|
|
775
|
+
|
|
723
776
|
const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t: TranslationFunction }> = ({ vehicle, t }) => {
|
|
724
777
|
const [open, setOpen] = useState(false);
|
|
778
|
+
const [criteriaOpen, setCriteriaOpen] = useState(true);
|
|
779
|
+
const [categoriesOpen, setCategoriesOpen] = useState(true);
|
|
725
780
|
const bands = vehicle.priceRatingBands;
|
|
726
781
|
if (!bands) return null;
|
|
727
782
|
|
|
@@ -752,11 +807,15 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
|
|
|
752
807
|
</button>
|
|
753
808
|
{open && (
|
|
754
809
|
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 18 }}>
|
|
755
|
-
<div className="mcard mcard--filled" style={{ padding: 16 }}>
|
|
756
|
-
<
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
810
|
+
<div className="mcard mcard--filled" style={{ padding: 16, flexDirection: 'column' }}>
|
|
811
|
+
<BreakdownCardHead
|
|
812
|
+
icon="list"
|
|
813
|
+
title={t('autobahn:priceMarket.criteriaTitle')}
|
|
814
|
+
open={criteriaOpen}
|
|
815
|
+
onToggle={() => setCriteriaOpen((o) => !o)}
|
|
816
|
+
/>
|
|
817
|
+
{criteriaOpen && (
|
|
818
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, marginTop: 14 }}>
|
|
760
819
|
{criteria.map(([ic, label, value]) => (
|
|
761
820
|
<div key={label} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, minWidth: '44%', flex: '1 1 44%' }}>
|
|
762
821
|
<Icon name={ic} size={17} color="var(--md-primary)" stroke={1.8} style={{ marginTop: 1, flex: 'none' }} />
|
|
@@ -771,14 +830,18 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
|
|
|
771
830
|
</div>
|
|
772
831
|
))}
|
|
773
832
|
</div>
|
|
833
|
+
)}
|
|
774
834
|
</div>
|
|
775
835
|
|
|
776
|
-
<div className="mcard mcard--filled" style={{ padding: 16 }}>
|
|
777
|
-
<
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
836
|
+
<div className="mcard mcard--filled" style={{ padding: 16, flexDirection: 'column' }}>
|
|
837
|
+
<BreakdownCardHead
|
|
838
|
+
icon="bar-chart-3"
|
|
839
|
+
title={t('autobahn:priceMarket.categoriesTitle')}
|
|
840
|
+
open={categoriesOpen}
|
|
841
|
+
onToggle={() => setCategoriesOpen((o) => !o)}
|
|
842
|
+
/>
|
|
843
|
+
{categoriesOpen && (
|
|
844
|
+
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
|
|
782
845
|
{RATING_TIERS.map(([key, color], i) => (
|
|
783
846
|
<div
|
|
784
847
|
key={key}
|
|
@@ -824,6 +887,7 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
|
|
|
824
887
|
</p>
|
|
825
888
|
</div>
|
|
826
889
|
</div>
|
|
890
|
+
)}
|
|
827
891
|
</div>
|
|
828
892
|
</div>
|
|
829
893
|
)}
|
|
@@ -1198,22 +1262,15 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
|
|
|
1198
1262
|
className="fin-eval-pill"
|
|
1199
1263
|
style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
|
|
1200
1264
|
>
|
|
1201
|
-
<Icon
|
|
1202
|
-
name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
|
|
1203
|
-
size={13}
|
|
1204
|
-
stroke={2.6}
|
|
1205
|
-
color={TIER_COLORS[evalTier]}
|
|
1206
|
-
/>
|
|
1207
1265
|
{t('autobahn:tier.' + TIER_KEYS[evalTier])}
|
|
1208
1266
|
</span>
|
|
1209
|
-
<
|
|
1210
|
-
<div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
|
|
1211
|
-
</div>
|
|
1267
|
+
<EvalBar tier={evalTier} />
|
|
1212
1268
|
</div>
|
|
1213
1269
|
)}
|
|
1214
1270
|
</div>
|
|
1215
1271
|
<div className="bb-cta-row">
|
|
1216
|
-
<Button variant="filled"
|
|
1272
|
+
<Button variant="filled" style={{ flex: 1, minWidth: 0 }} onClick={onSecure}>
|
|
1273
|
+
<DocCheckIcon />
|
|
1217
1274
|
{t('autobahn:bottom.secureOffer')}
|
|
1218
1275
|
</Button>
|
|
1219
1276
|
{/* padding:0 — icon-only button (same fix as the sheet's phone button) */}
|
|
@@ -1308,6 +1365,8 @@ export interface FinanceDetailsModalProps {
|
|
|
1308
1365
|
monthly: number;
|
|
1309
1366
|
/** struck old rate (oldMonthlyInstallment, isStrikeShown-gated) — red line-through next to the rate. */
|
|
1310
1367
|
oldMonthly?: number;
|
|
1368
|
+
/** struck list price (common.highestPrice, isStrikeShown-gated) — cash-tab strike (Figma). */
|
|
1369
|
+
listPrice?: number;
|
|
1311
1370
|
apr: number;
|
|
1312
1371
|
down: number;
|
|
1313
1372
|
term: number;
|
|
@@ -1374,6 +1433,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1374
1433
|
price,
|
|
1375
1434
|
monthly,
|
|
1376
1435
|
oldMonthly,
|
|
1436
|
+
listPrice,
|
|
1377
1437
|
apr,
|
|
1378
1438
|
down,
|
|
1379
1439
|
term,
|
|
@@ -1514,6 +1574,20 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1514
1574
|
</div>
|
|
1515
1575
|
));
|
|
1516
1576
|
|
|
1577
|
+
// mobile sheet footnote — the Figma representative example: real figures inline
|
|
1578
|
+
// ("³ Fahrzeugpreis: … €; Effektiver Jahreszins: … %; …") followed by the PangV sentence.
|
|
1579
|
+
// Only REAL config figures are included; absent ones are simply omitted.
|
|
1580
|
+
const exampleParts: string[] = [
|
|
1581
|
+
`${t('autobahn:modal.exampleVehiclePrice')}: ${eur(price)} € ${t('autobahn:modal.examplePriceSuffix')}`,
|
|
1582
|
+
...(apr > 0 ? [`${t('autobahn:modal.exampleApr')}: ${pct(apr)}`] : []),
|
|
1583
|
+
...(nominalRate != null && nominalRate > 0 ? [`${t('autobahn:modal.exampleNominal')}: ${pct(nominalRate)}`] : []),
|
|
1584
|
+
`${t('autobahn:modal.exampleNet')}: ${eur(netto)} €`,
|
|
1585
|
+
`${t('autobahn:modal.exampleDown')}: ${eur(down)} €`,
|
|
1586
|
+
...(finalInstallment != null && finalInstallment > 0 ? [`${t('autobahn:modal.exampleFinal')}: ${eur(finalInstallment)} €`] : []),
|
|
1587
|
+
...(grossAmount != null && grossAmount > 0 ? [`${t('autobahn:modal.exampleGross')}: ${eur(grossAmount)} €`] : []),
|
|
1588
|
+
];
|
|
1589
|
+
const exampleLine = `³ ${exampleParts.join('; ')};`;
|
|
1590
|
+
|
|
1517
1591
|
return (
|
|
1518
1592
|
<div className="fin-modal-scrim" onClick={onClose}>
|
|
1519
1593
|
<div className="fin-modal" role="dialog" aria-modal="true" aria-label={title} ref={sheetRef} onClick={(e) => e.stopPropagation()}>
|
|
@@ -1625,16 +1699,27 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1625
1699
|
|
|
1626
1700
|
<div className="fin-price-grid">
|
|
1627
1701
|
<div className="fin-rate-main">
|
|
1628
|
-
<div style={{ display: 'flex', alignItems: 'center', gap:
|
|
1702
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
1629
1703
|
<span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
|
|
1630
1704
|
{isCash ? eur(price) : eur(Math.round(monthly))} €
|
|
1631
1705
|
</span>
|
|
1632
|
-
{
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
<
|
|
1706
|
+
{/* Figma Kaufen sheet: struck list price top-right beside the big figure */}
|
|
1707
|
+
{isCash && listPrice != null && listPrice > price && (
|
|
1708
|
+
<span className="fin-rate-unit" style={{ alignSelf: 'flex-start', paddingTop: 2 }}>
|
|
1709
|
+
<s className="num fin-rate-old">{eur(listPrice)} €</s>
|
|
1636
1710
|
</span>
|
|
1637
1711
|
)}
|
|
1712
|
+
{!isCash && (
|
|
1713
|
+
<>
|
|
1714
|
+
<span className="md-label-sm" style={{ fontWeight: 600, alignSelf: 'flex-end', paddingBottom: 5 }}>
|
|
1715
|
+
{t('autobahn:finance.exampleLabel')}
|
|
1716
|
+
</span>
|
|
1717
|
+
<span className="fin-rate-unit">
|
|
1718
|
+
{oldMonthly != null && <s className="num fin-rate-old">{eur(Math.round(oldMonthly))} €</s>}
|
|
1719
|
+
<span>{t('autobahn:finance.perMonth')}</span>
|
|
1720
|
+
</span>
|
|
1721
|
+
</>
|
|
1722
|
+
)}
|
|
1638
1723
|
</div>
|
|
1639
1724
|
</div>
|
|
1640
1725
|
<div>
|
|
@@ -1643,24 +1728,12 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1643
1728
|
className="fin-eval-pill"
|
|
1644
1729
|
style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
|
|
1645
1730
|
>
|
|
1646
|
-
<Icon
|
|
1647
|
-
name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
|
|
1648
|
-
size={14}
|
|
1649
|
-
stroke={2.6}
|
|
1650
|
-
color={TIER_COLORS[evalTier]}
|
|
1651
|
-
/>
|
|
1652
1731
|
{evalTierLabel}
|
|
1653
1732
|
</span>
|
|
1654
1733
|
)}
|
|
1655
1734
|
</div>
|
|
1656
1735
|
<div />
|
|
1657
|
-
<div>
|
|
1658
|
-
{ratingLabel && (
|
|
1659
|
-
<div className="fin-eval-bar">
|
|
1660
|
-
<div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
|
|
1661
|
-
</div>
|
|
1662
|
-
)}
|
|
1663
|
-
</div>
|
|
1736
|
+
<div>{ratingLabel && <EvalBar tier={evalTier} />}</div>
|
|
1664
1737
|
</div>
|
|
1665
1738
|
|
|
1666
1739
|
{isCash ? (
|
|
@@ -1675,14 +1748,15 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1675
1748
|
{addServicesCost != null && (
|
|
1676
1749
|
<ToggleRow
|
|
1677
1750
|
label={t('autobahn:finance.addServicesToggle')}
|
|
1678
|
-
price={t('autobahn:finance.addServicesOnce', { value: eur(addServicesCost) })}
|
|
1751
|
+
price={t('autobahn:finance.addServicesOnce', { value: eur(Math.round(addServicesCost)) })}
|
|
1679
1752
|
checked={!!withAddServices}
|
|
1680
1753
|
disabled={addServicesDisabled}
|
|
1681
1754
|
onChange={(checked) => onToggleAddServices && onToggleAddServices(checked)}
|
|
1682
1755
|
/>
|
|
1683
1756
|
)}
|
|
1684
1757
|
<div style={{ display: 'flex', gap: 10 }}>
|
|
1685
|
-
<Button variant="filled" size="lg"
|
|
1758
|
+
<Button variant="filled" size="lg" style={{ flex: 1 }} onClick={onSecure}>
|
|
1759
|
+
<DocCheckIcon />
|
|
1686
1760
|
{t('autobahn:finance.secureOffer')}
|
|
1687
1761
|
</Button>
|
|
1688
1762
|
{/* padding:0 — mbtn--lg's 30px side padding left no room for the icon at width 52 */}
|
|
@@ -1703,7 +1777,6 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1703
1777
|
{eur(down)} €
|
|
1704
1778
|
</span>
|
|
1705
1779
|
</div>
|
|
1706
|
-
<span className="fin-val-box num">{eur(down)} €</span>
|
|
1707
1780
|
<Slider
|
|
1708
1781
|
min={downMin as number}
|
|
1709
1782
|
max={downMax as number}
|
|
@@ -1711,6 +1784,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1711
1784
|
value={down}
|
|
1712
1785
|
onChange={onDownChange as (n: number) => void}
|
|
1713
1786
|
fmt={(v) => `${eur(Number(v))} €`}
|
|
1787
|
+
bubble
|
|
1714
1788
|
/>
|
|
1715
1789
|
</div>
|
|
1716
1790
|
{isLease && annualMileage != null && onAnnualMileageChange && (
|
|
@@ -1724,7 +1798,6 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1724
1798
|
{eur(annualMileage)} km
|
|
1725
1799
|
</span>
|
|
1726
1800
|
</div>
|
|
1727
|
-
<span className="fin-val-box num">{eur(annualMileage)} km</span>
|
|
1728
1801
|
<Slider
|
|
1729
1802
|
min={annualMileageMin}
|
|
1730
1803
|
max={annualMileageMax}
|
|
@@ -1732,6 +1805,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1732
1805
|
value={annualMileage}
|
|
1733
1806
|
onChange={onAnnualMileageChange}
|
|
1734
1807
|
fmt={(v) => `${eur(Number(v))} km`}
|
|
1808
|
+
bubble
|
|
1735
1809
|
/>
|
|
1736
1810
|
</div>
|
|
1737
1811
|
)}
|
|
@@ -1755,7 +1829,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1755
1829
|
{addServicesCost != null && onToggleAddServices && (
|
|
1756
1830
|
<ToggleRow
|
|
1757
1831
|
label={t('autobahn:finance.addServicesToggle')}
|
|
1758
|
-
price={t('autobahn:finance.addServicesPerMonth', { value: eur(addServicesCost) })}
|
|
1832
|
+
price={t('autobahn:finance.addServicesPerMonth', { value: eur(Math.round(addServicesCost)) })}
|
|
1759
1833
|
checked={!!withAddServices}
|
|
1760
1834
|
disabled={addServicesDisabled}
|
|
1761
1835
|
onChange={onToggleAddServices}
|
|
@@ -1763,7 +1837,8 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1763
1837
|
)}
|
|
1764
1838
|
|
|
1765
1839
|
<div style={{ display: 'flex', gap: 10 }}>
|
|
1766
|
-
<Button variant="filled" size="lg"
|
|
1840
|
+
<Button variant="filled" size="lg" style={{ flex: 1 }} onClick={onSecure}>
|
|
1841
|
+
<DocCheckIcon />
|
|
1767
1842
|
{t('autobahn:finance.requestRate')}
|
|
1768
1843
|
</Button>
|
|
1769
1844
|
{/* padding:0 — mbtn--lg's 30px side padding left no room for the icon at width 52 */}
|
|
@@ -1777,11 +1852,13 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1777
1852
|
{isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails')}
|
|
1778
1853
|
</span>
|
|
1779
1854
|
</button>
|
|
1780
|
-
{/* mobile mockup shows NO row-by-row breakdown table (round 9 review) —
|
|
1781
|
-
|
|
1855
|
+
{/* mobile mockup shows NO row-by-row breakdown table (round 9 review) — the Figma
|
|
1856
|
+
footnote: real figures inline, then the PangV sentence (round 12). */}
|
|
1782
1857
|
{showBreakdown && (
|
|
1783
1858
|
<div>
|
|
1784
1859
|
<p className="md-label-sm" style={{ margin: 0, lineHeight: 1.55 }}>
|
|
1860
|
+
{exampleLine}
|
|
1861
|
+
<br />
|
|
1785
1862
|
{isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
|
|
1786
1863
|
</p>
|
|
1787
1864
|
</div>
|
|
@@ -1840,11 +1917,13 @@ export const ReviewsTeaser: React.FunctionComponent<ReviewsTeaserProps> = ({ cou
|
|
|
1840
1917
|
}}
|
|
1841
1918
|
>
|
|
1842
1919
|
<span style={{ minWidth: 0 }}>
|
|
1843
|
-
<span style={{ display: 'inline-flex', alignItems: 'center', gap:
|
|
1844
|
-
<Stars value={average} size={
|
|
1845
|
-
<span className="num
|
|
1920
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
|
|
1921
|
+
<Stars value={average} size={19} />
|
|
1922
|
+
<span className="num" style={{ fontWeight: 800, fontSize: 20, color: 'var(--fig-navy)' }}>
|
|
1923
|
+
{average.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}/5
|
|
1924
|
+
</span>
|
|
1846
1925
|
</span>
|
|
1847
|
-
<span
|
|
1926
|
+
<span style={{ display: 'block', marginTop: 4, fontSize: 14.5, color: 'var(--fig-navy)' }}>
|
|
1848
1927
|
{t('autobahn:reviews.teaserCount', { count })}
|
|
1849
1928
|
</span>
|
|
1850
1929
|
</span>
|
|
@@ -2036,14 +2115,14 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2036
2115
|
const emailOk = /.+@.+\..+/.test(email);
|
|
2037
2116
|
const phoneDigits = (areaCode + phoneNum).replace(/\D/g, '');
|
|
2038
2117
|
const phoneOk = phoneDigits.length >= 6;
|
|
2039
|
-
//
|
|
2040
|
-
//
|
|
2118
|
+
// Round 13 (explicit spec): PHONE is always required (the * sits on the country-code field),
|
|
2119
|
+
// e-mail is always optional (validated only when filled); names + consent always required.
|
|
2041
2120
|
const valid =
|
|
2042
2121
|
firstName.trim().length > 0 &&
|
|
2043
2122
|
lastName.trim().length > 0 &&
|
|
2044
|
-
emailOk &&
|
|
2045
2123
|
consent &&
|
|
2046
|
-
|
|
2124
|
+
phoneOk &&
|
|
2125
|
+
(email.trim().length === 0 || emailOk);
|
|
2047
2126
|
|
|
2048
2127
|
const submit = async (): Promise<void> => {
|
|
2049
2128
|
if (!valid || sending) return;
|
|
@@ -2159,7 +2238,8 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2159
2238
|
<input
|
|
2160
2239
|
style={enquiryInput}
|
|
2161
2240
|
type="email"
|
|
2162
|
-
|
|
2241
|
+
// e-mail is always optional (round 13) — no required-*
|
|
2242
|
+
placeholder={t('autobahn:enquiry.form.emailPlaceholder').replace(/\*$/, '')}
|
|
2163
2243
|
value={email}
|
|
2164
2244
|
onChange={(e) => setEmail(e.target.value)}
|
|
2165
2245
|
aria-label={t('autobahn:enquiry.contactMethod.email')}
|
|
@@ -2167,7 +2247,8 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2167
2247
|
</div>
|
|
2168
2248
|
{/* Land (+49 fixed) · Vorwahl · Telefonnummer — outlined float-label row (Figma) */}
|
|
2169
2249
|
<div className="enq-phone-row" style={{ marginTop: 16 }}>
|
|
2170
|
-
|
|
2250
|
+
{/* phone is always required — the * sits on the country-code field (round 13) */}
|
|
2251
|
+
<FloatField label={t('autobahn:enquiry.form.countryLabel') + '*'}>
|
|
2171
2252
|
<span className="enq-country num">
|
|
2172
2253
|
+49
|
|
2173
2254
|
<Icon name="chevron-down" size={14} color="var(--md-on-surface-variant)" />
|
|
@@ -2202,7 +2283,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2202
2283
|
{/* Nutzungsbedingungen sentence + revocable-consent checkbox (Figma round 9) */}
|
|
2203
2284
|
<p className="md-body-sm" style={{ margin: '14px 0 0' }}>
|
|
2204
2285
|
{t('autobahn:enquiry.form.termsPrefix')}{' '}
|
|
2205
|
-
<a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>
|
|
2286
|
+
<a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit', textDecoration: 'underline' }}>
|
|
2206
2287
|
{t('autobahn:enquiry.form.termsLink')}
|
|
2207
2288
|
</a>{' '}
|
|
2208
2289
|
{t('autobahn:enquiry.form.termsSuffix')}
|
|
@@ -2286,8 +2367,11 @@ export const HowItWorks: React.FunctionComponent<HowItWorksProps> = ({ steps, t
|
|
|
2286
2367
|
<Icon name={STEP_ICONS[i] || 'star'} size={24} color="#fff" stroke={1.8} />
|
|
2287
2368
|
<span className="hiw-num">{i + 1}</span>
|
|
2288
2369
|
</div>
|
|
2289
|
-
|
|
2290
|
-
<
|
|
2370
|
+
{/* title stacked ABOVE the description (Figma) — one text column beside the circle */}
|
|
2371
|
+
<div className="hiw-body">
|
|
2372
|
+
<div className="hiw-title">{title}</div>
|
|
2373
|
+
<p className="hiw-desc">{desc}</p>
|
|
2374
|
+
</div>
|
|
2291
2375
|
</div>
|
|
2292
2376
|
))}
|
|
2293
2377
|
</div>
|