@bytebrand/fe-ui-core-autobahn 1.0.14 → 1.0.16
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 +15 -3
- package/locales/en/autobahn.json +15 -3
- package/package.json +1 -1
- package/theme/md3.css +14 -1
- package/vdp/FinanceCalculator.tsx +139 -78
- package/vdp/VdpModules.tsx +425 -110
- package/vdp/index.ts +5 -0
package/locales/de/autobahn.json
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"termSelection": "Laufzeit",
|
|
57
57
|
"headerTitle": "Dein Angebot",
|
|
58
58
|
"adjustRate": "Rate anpassen",
|
|
59
|
-
"secureOffer": "Angebot
|
|
59
|
+
"secureOffer": "Angebot anfragen",
|
|
60
60
|
"trustLine": "Unverbindlich · kostenlos · 14 Tage Rückgaberecht",
|
|
61
61
|
"downPaymentLabel": "€ Anzahlung",
|
|
62
62
|
"monthsLabel": "{{months}} Monat",
|
|
@@ -84,7 +84,16 @@
|
|
|
84
84
|
"kmPerYear": "{{value}} km/Jahr",
|
|
85
85
|
"perMonth": "/mtl.",
|
|
86
86
|
"leasingDetails": "Leasingdetails",
|
|
87
|
-
"financingDetails": "Finanzierungsdetails"
|
|
87
|
+
"financingDetails": "Finanzierungsdetails",
|
|
88
|
+
"personalConsultation": "Persönliche Beratung",
|
|
89
|
+
"personalConsultationSub": "Wir sind für Sie da",
|
|
90
|
+
"financingLeasingPossible": "Finanzierung & Leasing möglich",
|
|
91
|
+
"requestRate": "Rate anfragen",
|
|
92
|
+
"callCta": "Anrufen",
|
|
93
|
+
"finalInstallmentToggle": "Mit Schlussrate",
|
|
94
|
+
"addServicesToggle": "Rundum-Sorglos-Paket",
|
|
95
|
+
"addServicesPerMonth": "+{{value}} € / Monat",
|
|
96
|
+
"addServicesOnce": "+{{value}} €"
|
|
88
97
|
},
|
|
89
98
|
"specTable": {
|
|
90
99
|
"title": "Fahrzeugdaten",
|
|
@@ -603,7 +612,10 @@
|
|
|
603
612
|
"totalAmount": "Gesamtbetrag",
|
|
604
613
|
"totalRepayment": "Gesamtrückzahlungsbetrag",
|
|
605
614
|
"legalLeasing": "Unverbindliches Leasingbeispiel (Kilometerleasing). Bonität vorausgesetzt. Dieses freibleibende Angebot wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt.",
|
|
606
|
-
"legalFinancing": "Repräsentatives Berechnungsbeispiel gem. § 17a Abs. 4 PAngV, das 2/3 aller Kunden — Bonität vorausgesetzt — erhalten. Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Dieses freibleibende Angebot der Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt."
|
|
615
|
+
"legalFinancing": "Repräsentatives Berechnungsbeispiel gem. § 17a Abs. 4 PAngV, das 2/3 aller Kunden — Bonität vorausgesetzt — erhalten. Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Dieses freibleibende Angebot der Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt.",
|
|
616
|
+
"sheetTitleFinancing": "Kredit-Rate anpassen",
|
|
617
|
+
"sheetTitleLeasing": "Leasing-Rate anpassen",
|
|
618
|
+
"sheetTitleCash": "Kaufen - Details"
|
|
607
619
|
},
|
|
608
620
|
"ratings": {
|
|
609
621
|
"showLess": "Weniger anzeigen",
|
package/locales/en/autobahn.json
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"termSelection": "Term",
|
|
57
57
|
"headerTitle": "Your offer",
|
|
58
58
|
"adjustRate": "Adjust rate",
|
|
59
|
-
"secureOffer": "
|
|
59
|
+
"secureOffer": "Request offer",
|
|
60
60
|
"trustLine": "Non-binding · free · 14-day return right",
|
|
61
61
|
"downPaymentLabel": "€ down payment",
|
|
62
62
|
"monthsLabel": "{{months}} month",
|
|
@@ -84,7 +84,16 @@
|
|
|
84
84
|
"kmPerYear": "{{value}} km/year",
|
|
85
85
|
"perMonth": "/mo.",
|
|
86
86
|
"leasingDetails": "Leasing details",
|
|
87
|
-
"financingDetails": "Financing details"
|
|
87
|
+
"financingDetails": "Financing details",
|
|
88
|
+
"personalConsultation": "Personal advice",
|
|
89
|
+
"personalConsultationSub": "We're here for you",
|
|
90
|
+
"financingLeasingPossible": "Financing & leasing available",
|
|
91
|
+
"requestRate": "Request rate",
|
|
92
|
+
"callCta": "Call",
|
|
93
|
+
"finalInstallmentToggle": "With final installment",
|
|
94
|
+
"addServicesToggle": "All-round care package",
|
|
95
|
+
"addServicesPerMonth": "+€{{value}} / month",
|
|
96
|
+
"addServicesOnce": "+€{{value}}"
|
|
88
97
|
},
|
|
89
98
|
"specTable": {
|
|
90
99
|
"title": "Vehicle data",
|
|
@@ -603,7 +612,10 @@
|
|
|
603
612
|
"totalAmount": "Total amount",
|
|
604
613
|
"totalRepayment": "Total repayment amount",
|
|
605
614
|
"legalLeasing": "Non-binding leasing example (mileage leasing). Subject to creditworthiness. This non-binding offer is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, which acts as a non-tied intermediary and does not provide advice. Errors excepted. Prices incl. VAT where applicable.",
|
|
606
|
-
"legalFinancing": "Representative calculation example pursuant to § 17a (4) PAngV, obtained by 2/3 of all customers — subject to creditworthiness. For your financing requests we will submit a credit inquiry. This non-binding offer from Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, which acts as a non-tied intermediary and does not provide advice. Errors excepted. Prices incl. VAT where applicable."
|
|
615
|
+
"legalFinancing": "Representative calculation example pursuant to § 17a (4) PAngV, obtained by 2/3 of all customers — subject to creditworthiness. For your financing requests we will submit a credit inquiry. This non-binding offer from Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, which acts as a non-tied intermediary and does not provide advice. Errors excepted. Prices incl. VAT where applicable.",
|
|
616
|
+
"sheetTitleFinancing": "Adjust credit rate",
|
|
617
|
+
"sheetTitleLeasing": "Adjust leasing rate",
|
|
618
|
+
"sheetTitleCash": "Purchase - Details"
|
|
607
619
|
},
|
|
608
620
|
"ratings": {
|
|
609
621
|
"showLess": "Show less",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/fe-ui-core-autobahn",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"license": "UNLICENSED",
|
package/theme/md3.css
CHANGED
|
@@ -155,6 +155,12 @@
|
|
|
155
155
|
NOT the 38px tappable filter-chip the base .chip is. */
|
|
156
156
|
.md3 .listcard-chips .chip { height: 24px; padding: 0 10px; font-size: 11.5px; border-radius: 8px; cursor: default; }
|
|
157
157
|
.md3 .listcard-chips .chip:hover { background: var(--md-surface); border-color: var(--md-outline-variant); }
|
|
158
|
+
/* VDP trust row (Sofort verfügbar/views/favorites/Geprüft) — same compact non-interactive
|
|
159
|
+
sizing as the listcard equipment tags, standalone (not nested under .listcard-chips). */
|
|
160
|
+
.md3 .vdp-trust-chip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px;
|
|
161
|
+
border-radius: 8px; font-size: 11.5px; font-weight: 500; line-height: 1; cursor: default; white-space: nowrap;
|
|
162
|
+
background: var(--md-surface); color: var(--md-on-surface-variant); border: 1px solid var(--md-outline-variant); }
|
|
163
|
+
.md3 .vdp-trust-chip .lucide { width: 14px; height: 14px; flex: none; }
|
|
158
164
|
|
|
159
165
|
/* status / rating chip (no CTA) */
|
|
160
166
|
.md3 .statchip { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 10px;
|
|
@@ -1283,7 +1289,14 @@
|
|
|
1283
1289
|
.md3 .srp-rail { display: none; } /* rail is desktop-only; ≤980 uses the filter sheet */
|
|
1284
1290
|
.md3 .srp-grid { grid-template-columns: 1fr; }
|
|
1285
1291
|
.md3 .vdp-grid { display: flex; flex-direction: column; }
|
|
1286
|
-
.md3 .vdp-gallery { order: 1; }
|
|
1292
|
+
.md3 .vdp-gallery { order: 1; }
|
|
1293
|
+
/* mobile-only "first screen": identity + trust row BEFORE the finance card, highlights/
|
|
1294
|
+
condition tiles moved AFTER it (both split out of the desktop-only VdpHeaderCard,
|
|
1295
|
+
which stays bundled inside .vdp-gallery, hidden on mobile via hide-mobile). */
|
|
1296
|
+
.md3 .vdp-header-summary { order: 2; padding: 0 4px; margin-bottom: 4px; }
|
|
1297
|
+
.md3 .vdp-aside { order: 3; }
|
|
1298
|
+
.md3 .vdp-header-extras { order: 4; margin-top: 16px; }
|
|
1299
|
+
.md3 .vdp-details { order: 5; }
|
|
1287
1300
|
.md3 .vdp-aside { position: static; }
|
|
1288
1301
|
.md3 .price-card { position: static; }
|
|
1289
1302
|
.md3 .fin-call { display: block; } /* tap-to-call restored on touch */
|
|
@@ -158,8 +158,11 @@ export interface FinanceCalculatorProps {
|
|
|
158
158
|
onSecure?: () => void;
|
|
159
159
|
/** secondary CTA click (phone). */
|
|
160
160
|
onCall?: () => void;
|
|
161
|
-
/** open the Finanzierungs-/Leasingdetails popup (full breakdown + legal). Omit to hide the link. */
|
|
161
|
+
/** open the Finanzierungs-/Leasingdetails popup (full breakdown + legal, expanded). Omit to hide the link. */
|
|
162
162
|
onShowDetails?: () => void;
|
|
163
|
+
/** mobile only: open the "Rate anpassen" / "Details anzeigen" sheet (collapsed/default view).
|
|
164
|
+
* Omit to hide the trigger row (falls back to the desktop inline expander). */
|
|
165
|
+
onOpenAdjust?: () => void;
|
|
163
166
|
/** mode segment changed. */
|
|
164
167
|
onModeChange: (mode: FinanceMode) => void;
|
|
165
168
|
/** down-payment slider changed. */
|
|
@@ -204,7 +207,7 @@ const DetailsLink: React.FunctionComponent<{ label: string; onClick: () => void
|
|
|
204
207
|
);
|
|
205
208
|
|
|
206
209
|
/** "Rate anpassen" / "Details anzeigen" expander toggle button. */
|
|
207
|
-
const expanderBtnStyle: React.CSSProperties = {
|
|
210
|
+
export const expanderBtnStyle: React.CSSProperties = {
|
|
208
211
|
width: '100%',
|
|
209
212
|
background: 'none',
|
|
210
213
|
border: 0,
|
|
@@ -243,6 +246,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
243
246
|
onSecure,
|
|
244
247
|
onCall,
|
|
245
248
|
onShowDetails,
|
|
249
|
+
onOpenAdjust,
|
|
246
250
|
onModeChange,
|
|
247
251
|
onDownChange,
|
|
248
252
|
onTermChange,
|
|
@@ -364,100 +368,157 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
364
368
|
</div>
|
|
365
369
|
</div>
|
|
366
370
|
|
|
367
|
-
{/*
|
|
368
|
-
<div className="
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
{
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<
|
|
381
|
-
|
|
382
|
-
{t('autobahn:finance.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 8 }}>
|
|
387
|
-
<div>
|
|
388
|
-
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
389
|
-
<span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
|
|
390
|
-
<span className="num" style={{ fontWeight: 700 }}>
|
|
391
|
-
{eur(down)} €
|
|
392
|
-
</span>
|
|
393
|
-
</div>
|
|
394
|
-
<Slider
|
|
395
|
-
min={downMin}
|
|
396
|
-
max={downMax}
|
|
397
|
-
step={downStep}
|
|
398
|
-
value={down}
|
|
399
|
-
onChange={onDownChange}
|
|
400
|
-
fmt={(v) => `${eur(Number(v))} €`}
|
|
401
|
-
/>
|
|
371
|
+
{/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
|
|
372
|
+
<div className="hide-mobile">
|
|
373
|
+
{/* RATE EDIT (Rate anpassen) — between the rate and the request CTA */}
|
|
374
|
+
<div className="fin-adjust">
|
|
375
|
+
<button onClick={() => setAdjust((a) => !a)} style={expanderBtnStyle}>
|
|
376
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
|
|
377
|
+
<Icon name={isCash ? 'list' : 'sliders-horizontal'} size={16} color="var(--md-primary)" />
|
|
378
|
+
{isCash ? t('autobahn:finance.detailsShow') : t('autobahn:finance.adjustRate')}
|
|
379
|
+
</span>
|
|
380
|
+
<Icon name={adjust ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" />
|
|
381
|
+
</button>
|
|
382
|
+
{adjust &&
|
|
383
|
+
(isCash ? (
|
|
384
|
+
<div style={{ marginTop: 4 }}>
|
|
385
|
+
<CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
|
|
386
|
+
<CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
|
|
387
|
+
<CalcRow label={t('autobahn:finance.deliveryLabel')} last>
|
|
388
|
+
{t('autobahn:finance.deliveryValue')}
|
|
389
|
+
</CalcRow>
|
|
402
390
|
</div>
|
|
403
|
-
|
|
391
|
+
) : (
|
|
392
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 8 }}>
|
|
404
393
|
<div>
|
|
405
394
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
406
|
-
<span className="md-title-sm">{t('autobahn:finance.
|
|
395
|
+
<span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
|
|
407
396
|
<span className="num" style={{ fontWeight: 700 }}>
|
|
408
|
-
{eur(
|
|
397
|
+
{eur(down)} €
|
|
409
398
|
</span>
|
|
410
399
|
</div>
|
|
411
400
|
<Slider
|
|
412
|
-
min={
|
|
413
|
-
max={
|
|
414
|
-
step={
|
|
415
|
-
value={
|
|
416
|
-
onChange={
|
|
417
|
-
fmt={(v) => `${eur(Number(v))}
|
|
401
|
+
min={downMin}
|
|
402
|
+
max={downMax}
|
|
403
|
+
step={downStep}
|
|
404
|
+
value={down}
|
|
405
|
+
onChange={onDownChange}
|
|
406
|
+
fmt={(v) => `${eur(Number(v))} €`}
|
|
418
407
|
/>
|
|
419
408
|
</div>
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
409
|
+
{annualMileage != null && onAnnualMileageChange && (
|
|
410
|
+
<div>
|
|
411
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
412
|
+
<span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
|
|
413
|
+
<span className="num" style={{ fontWeight: 700 }}>
|
|
414
|
+
{eur(annualMileage)} km
|
|
415
|
+
</span>
|
|
416
|
+
</div>
|
|
417
|
+
<Slider
|
|
418
|
+
min={annualMileageMin}
|
|
419
|
+
max={annualMileageMax}
|
|
420
|
+
step={annualMileageStep}
|
|
421
|
+
value={annualMileage}
|
|
422
|
+
onChange={onAnnualMileageChange}
|
|
423
|
+
fmt={(v) => `${eur(Number(v))} km`}
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
)}
|
|
427
|
+
<div>
|
|
428
|
+
<div className="md-title-sm" style={{ marginBottom: 8 }}>
|
|
429
|
+
{t('autobahn:finance.termSelection')}
|
|
430
|
+
</div>
|
|
431
|
+
<TermChips options={terms} value={term} onChange={onTermChange} t={t} />
|
|
432
|
+
</div>
|
|
433
|
+
<div>
|
|
434
|
+
<CalcRow label={t('autobahn:finance.purchasePrice')}>{eur(price)} €</CalcRow>
|
|
435
|
+
<CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
|
|
436
|
+
<CalcRow label={t('autobahn:finance.deliveryLabel')} last>
|
|
437
|
+
{t('autobahn:finance.deliveryValue')}
|
|
438
|
+
</CalcRow>
|
|
424
439
|
</div>
|
|
425
|
-
<TermChips options={terms} value={term} onChange={onTermChange} t={t} />
|
|
426
|
-
</div>
|
|
427
|
-
<div>
|
|
428
|
-
<CalcRow label={t('autobahn:finance.purchasePrice')}>{eur(price)} €</CalcRow>
|
|
429
|
-
<CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
|
|
430
|
-
<CalcRow label={t('autobahn:finance.deliveryLabel')} last>
|
|
431
|
-
{t('autobahn:finance.deliveryValue')}
|
|
432
|
-
</CalcRow>
|
|
433
440
|
</div>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
</div>
|
|
441
|
+
))}
|
|
442
|
+
</div>
|
|
437
443
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
</Button>
|
|
442
|
-
{/* phone CTA — touch only (hidden on desktop via .fin-call) */}
|
|
443
|
-
<div className="fin-call">
|
|
444
|
-
<Button variant="outlined" block icon="phone" onClick={onCall}>
|
|
445
|
-
{phone}
|
|
444
|
+
{/* REQUEST CTA */}
|
|
445
|
+
<Button variant="filled" block size="lg" icon="shield-check" style={{ marginTop: 14 }} onClick={onSecure}>
|
|
446
|
+
{t('autobahn:finance.secureOffer')}
|
|
446
447
|
</Button>
|
|
448
|
+
{/* phone CTA — touch only (hidden on desktop via .fin-call) */}
|
|
449
|
+
<div className="fin-call">
|
|
450
|
+
<Button variant="outlined" block icon="phone" onClick={onCall}>
|
|
451
|
+
{phone}
|
|
452
|
+
</Button>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
{/* auto.de-ID in front of the Finanzierungs-/Leasingdetails link — one row, under everything */}
|
|
456
|
+
{(autoDeId || (!isCash && onShowDetails)) && (
|
|
457
|
+
<div className="fin-info">
|
|
458
|
+
{autoDeId && <span className="fin-id">auto.de-ID {autoDeId}</span>}
|
|
459
|
+
{!isCash && onShowDetails && (
|
|
460
|
+
<DetailsLink
|
|
461
|
+
label={mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
|
|
462
|
+
onClick={onShowDetails}
|
|
463
|
+
/>
|
|
464
|
+
)}
|
|
465
|
+
</div>
|
|
466
|
+
)}
|
|
447
467
|
</div>
|
|
448
468
|
|
|
449
|
-
{/*
|
|
450
|
-
|
|
469
|
+
{/* ============================== MOBILE (≤600px) — new layout ============================== */}
|
|
470
|
+
<div className="show-mobile" style={{ flexDirection: 'column' }}>
|
|
471
|
+
{/* side-by-side CTAs */}
|
|
472
|
+
<div style={{ display: 'flex', gap: 10, marginTop: 14 }}>
|
|
473
|
+
<Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
|
|
474
|
+
{t('autobahn:finance.secureOffer')}
|
|
475
|
+
</Button>
|
|
476
|
+
<Button variant="outlined" size="lg" icon="phone" style={{ flex: 1 }} onClick={onCall}>
|
|
477
|
+
{t('autobahn:finance.callCta')}
|
|
478
|
+
</Button>
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
{/* Persönliche Beratung (left) · Finanzierungs-/Leasingdetails or hint (right) */}
|
|
451
482
|
<div className="fin-info">
|
|
452
|
-
{
|
|
453
|
-
|
|
454
|
-
<
|
|
455
|
-
label={
|
|
456
|
-
|
|
457
|
-
|
|
483
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
|
484
|
+
<Icon name="user" size={18} color="var(--md-on-surface-variant)" style={{ flex: 'none' }} />
|
|
485
|
+
<div style={{ minWidth: 0 }}>
|
|
486
|
+
<div className="md-label-sm" style={{ fontWeight: 700, color: 'var(--md-on-surface)' }}>
|
|
487
|
+
{t('autobahn:finance.personalConsultation')}
|
|
488
|
+
</div>
|
|
489
|
+
<div className="md-label-sm">{t('autobahn:finance.personalConsultationSub')}</div>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
{isCash ? (
|
|
493
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--md-on-surface-variant)' }}>
|
|
494
|
+
<Icon name="info" size={15} />
|
|
495
|
+
<span className="md-label-sm">{t('autobahn:finance.financingLeasingPossible')}</span>
|
|
496
|
+
</div>
|
|
497
|
+
) : (
|
|
498
|
+
onShowDetails && (
|
|
499
|
+
<DetailsLink
|
|
500
|
+
label={mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
|
|
501
|
+
onClick={onShowDetails}
|
|
502
|
+
/>
|
|
503
|
+
)
|
|
458
504
|
)}
|
|
459
505
|
</div>
|
|
460
|
-
|
|
506
|
+
|
|
507
|
+
{/* "Rate anpassen" / "Details anzeigen" — opens the full bottom sheet (not an inline expander) */}
|
|
508
|
+
{onOpenAdjust && (
|
|
509
|
+
<button
|
|
510
|
+
type="button"
|
|
511
|
+
onClick={onOpenAdjust}
|
|
512
|
+
style={{ ...expanderBtnStyle, borderTop: '1px solid var(--md-outline-variant)', marginTop: 4, padding: '14px 0 2px' }}
|
|
513
|
+
>
|
|
514
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
|
|
515
|
+
<Icon name={isCash ? 'list' : 'sliders-horizontal'} size={16} color="var(--md-primary)" />
|
|
516
|
+
{isCash ? t('autobahn:finance.detailsShow') : t('autobahn:finance.adjustRate')}
|
|
517
|
+
</span>
|
|
518
|
+
<Icon name="chevron-right" size={18} color="var(--md-primary)" />
|
|
519
|
+
</button>
|
|
520
|
+
)}
|
|
521
|
+
</div>
|
|
461
522
|
</div>
|
|
462
523
|
);
|
|
463
524
|
};
|
package/vdp/VdpModules.tsx
CHANGED
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
import React, { useState } from 'react';
|
|
25
25
|
import { TranslationFunction } from '../types';
|
|
26
26
|
|
|
27
|
-
import { Button, Icon, Spec, StatChip, Stars } from '../ui';
|
|
28
|
-
import { CO2_CLASSES, CO2_COLORS } from '../constants';
|
|
27
|
+
import { Button, Icon, Spec, StatChip, Stars, Switch } from '../ui';
|
|
28
|
+
import { CO2_CLASSES, CO2_COLORS, TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
|
|
29
29
|
import { defaultVdpSpecs, EquipmentGroups, VehicleDetail, VdpSpec, VdpReview } from './types';
|
|
30
|
-
import { Slider, TermChips } from './FinanceCalculator';
|
|
30
|
+
import { CalcRow, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
|
|
31
31
|
|
|
32
32
|
const eur = (n: number): string => n.toLocaleString('de-DE');
|
|
33
33
|
|
|
@@ -51,14 +51,10 @@ export interface VdpHeaderCardProps {
|
|
|
51
51
|
t: TranslationFunction;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
* is just the textual header card that sits beneath it.
|
|
59
|
-
*/
|
|
60
|
-
export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
|
|
61
|
-
const { condition } = vehicle;
|
|
54
|
+
/** headline + trim/power + rating pill, then the dot-separated fact line — the vehicle's
|
|
55
|
+
* identity block. Split out so the mobile-only layout can show this BEFORE the finance
|
|
56
|
+
* card (see VdpTrustRow) while the highlights/condition tiles move after it. */
|
|
57
|
+
export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
|
|
62
58
|
const factLine: ReadonlyArray<readonly [string, string]> = [
|
|
63
59
|
['gauge', fmtKm(vehicle.km)],
|
|
64
60
|
['calendar', vehicle.reg],
|
|
@@ -66,15 +62,8 @@ export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ veh
|
|
|
66
62
|
['fuel', stripParen(vehicle.fuel)],
|
|
67
63
|
['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
|
|
68
64
|
];
|
|
69
|
-
// condition.hu === 'Neu' compares the RAW backend data value, not a display string — left as-is.
|
|
70
|
-
const tiles: ReadonlyArray<readonly [string, string, string]> = [
|
|
71
|
-
['wrench', t('autobahn:header.huLabel'), condition.hu === 'Neu' ? t('autobahn:header.huNew') : t('autobahn:header.huUntil', { date: condition.hu })],
|
|
72
|
-
['shield-check', t('autobahn:header.accidentFree'), condition.unfallfrei ? t('autobahn:header.yes') : t('autobahn:header.no')],
|
|
73
|
-
['clipboard-check', t('autobahn:header.serviceHistory'), condition.scheckheft ? t('autobahn:header.yes') : t('autobahn:header.no')],
|
|
74
|
-
['award', t('autobahn:header.warranty'), condition.garantie],
|
|
75
|
-
];
|
|
76
65
|
return (
|
|
77
|
-
|
|
66
|
+
<>
|
|
78
67
|
<div className="vdp-headrow">
|
|
79
68
|
<div style={{ minWidth: 0 }}>
|
|
80
69
|
<h1 className="md-headline" style={{ margin: '0 0 4px' }}>
|
|
@@ -106,43 +95,112 @@ export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ veh
|
|
|
106
95
|
</span>
|
|
107
96
|
))}
|
|
108
97
|
</div>
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
109
101
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
>
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
</div>
|
|
102
|
+
/** hand-picked highlights — the single accent row of the header. */
|
|
103
|
+
export const VdpHeaderHighlights: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle }) => (
|
|
104
|
+
<div className="vdp-highlights" style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
|
|
105
|
+
{vehicle.highlights.slice(0, 6).map((f) => (
|
|
106
|
+
<span key={f} className="chip chip--filter chip--selected" style={{ cursor: 'default' }}>
|
|
107
|
+
<Icon name="check" size={14} stroke={2.4} />
|
|
108
|
+
{f}
|
|
109
|
+
</span>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
122
113
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
114
|
+
/** condition (zustand) tiles — TÜV/Unfallfrei/Scheckheft/Garantie. */
|
|
115
|
+
export const VdpHeaderCondition: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
|
|
116
|
+
const { condition } = vehicle;
|
|
117
|
+
// condition.hu === 'Neu' compares the RAW backend data value, not a display string — left as-is.
|
|
118
|
+
const tiles: ReadonlyArray<readonly [string, string, string]> = [
|
|
119
|
+
['wrench', t('autobahn:header.huLabel'), condition.hu === 'Neu' ? t('autobahn:header.huNew') : t('autobahn:header.huUntil', { date: condition.hu })],
|
|
120
|
+
['shield-check', t('autobahn:header.accidentFree'), condition.unfallfrei ? t('autobahn:header.yes') : t('autobahn:header.no')],
|
|
121
|
+
['clipboard-check', t('autobahn:header.serviceHistory'), condition.scheckheft ? t('autobahn:header.yes') : t('autobahn:header.no')],
|
|
122
|
+
['award', t('autobahn:header.warranty'), condition.garantie],
|
|
123
|
+
];
|
|
124
|
+
return (
|
|
125
|
+
<div className="zustand-grid">
|
|
126
|
+
{tiles.map(([ic, l, v]) => (
|
|
127
|
+
<div key={l} className="zustand-tile">
|
|
128
|
+
<Icon name={ic} size={19} color="var(--md-primary)" stroke={1.9} style={{ flex: 'none' }} />
|
|
129
|
+
<span style={{ minWidth: 0 }}>
|
|
130
|
+
<span
|
|
131
|
+
className="md-label-sm"
|
|
132
|
+
style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
|
133
|
+
>
|
|
134
|
+
{l}
|
|
138
135
|
</span>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
<span className="md-title-sm" style={{ display: 'block' }}>
|
|
137
|
+
{v}
|
|
138
|
+
</span>
|
|
139
|
+
</span>
|
|
140
|
+
</div>
|
|
141
|
+
))}
|
|
142
142
|
</div>
|
|
143
143
|
);
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* One container holding the entire vehicle identity: headline + trim/power, the
|
|
148
|
+
* dot-separated fact line, the hand-picked highlight chips, and the condition
|
|
149
|
+
* (zustand) tiles footer. The gallery itself is composed by the page (W3b) — this
|
|
150
|
+
* is just the textual header card that sits beneath it. Desktop-only usage (mobile
|
|
151
|
+
* splits these 3 pieces across the "before/after finance card" layout — see the page).
|
|
152
|
+
*/
|
|
153
|
+
export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => (
|
|
154
|
+
<div className="mcard" style={{ marginTop: 16, padding: '22px 24px 18px' }}>
|
|
155
|
+
<VdpHeaderSummary vehicle={vehicle} t={t} />
|
|
156
|
+
<div style={{ marginTop: 14 }}>
|
|
157
|
+
<VdpHeaderHighlights vehicle={vehicle} t={t} />
|
|
158
|
+
</div>
|
|
159
|
+
<VdpHeaderCondition vehicle={vehicle} t={t} />
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
export interface VdpTrustRowProps {
|
|
164
|
+
/** maps to carStore.isAvailable / state.isOnline — "Sofort verfügbar". */
|
|
165
|
+
available: boolean;
|
|
166
|
+
/** real totalCarImpCount from /service/user-actions/api/v1/stats/viewers — omit while loading. */
|
|
167
|
+
viewCount?: number;
|
|
168
|
+
/** real totalFavCount from the same endpoint — omit while loading. */
|
|
169
|
+
favCount?: number;
|
|
170
|
+
/** i18n function (drilled from the page). */
|
|
171
|
+
t: TranslationFunction;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** mobile-only trust-signal row shown between the identity block and the finance card:
|
|
175
|
+
* availability · view count · favorite count · "Geprüft" (every auto.de listing is
|
|
176
|
+
* 130-point inspected, so this one is always shown, not conditional real data). */
|
|
177
|
+
export const VdpTrustRow: React.FunctionComponent<VdpTrustRowProps> = ({ available, viewCount, favCount, t }) => (
|
|
178
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 6, marginTop: 10 }}>
|
|
179
|
+
{available && (
|
|
180
|
+
<span className="vdp-trust-chip">
|
|
181
|
+
<Icon name="check" size={13} stroke={2.4} color="var(--md-good)" />
|
|
182
|
+
{t('autobahn:availability.immediately')}
|
|
183
|
+
</span>
|
|
184
|
+
)}
|
|
185
|
+
{viewCount != null && (
|
|
186
|
+
<span className="vdp-trust-chip">
|
|
187
|
+
<Icon name="eye" size={13} />
|
|
188
|
+
{t('autobahn:activity.viewsCount', { count: viewCount })}
|
|
189
|
+
</span>
|
|
190
|
+
)}
|
|
191
|
+
{favCount != null && (
|
|
192
|
+
<span className="vdp-trust-chip">
|
|
193
|
+
<Icon name="heart" size={13} />
|
|
194
|
+
{t('autobahn:activity.favoritesCount', { count: favCount })}
|
|
195
|
+
</span>
|
|
196
|
+
)}
|
|
197
|
+
<span className="vdp-trust-chip">
|
|
198
|
+
<Icon name="shield-check" size={13} />
|
|
199
|
+
{t('autobahn:trust.checked')}
|
|
200
|
+
</span>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
|
|
146
204
|
// ---------------------------------------------------------------------------
|
|
147
205
|
// Fahrzeugdaten spec table (mobile clamp + "Mehr anzeigen")
|
|
148
206
|
// ---------------------------------------------------------------------------
|
|
@@ -934,6 +992,29 @@ export const InspectionBlock: React.FunctionComponent<InspectionBlockProps> = ({
|
|
|
934
992
|
// Finanzierungs-/Leasingdetails popup (desktop modal / mobile bottom-sheet)
|
|
935
993
|
// ---------------------------------------------------------------------------
|
|
936
994
|
|
|
995
|
+
/** full-width label(+optional trailing price) / Switch row — Switch itself is inline-flex
|
|
996
|
+
* (sized to its own content), so the space-between has to live on this wrapper, not on
|
|
997
|
+
* Switch's `label` prop (which renders track-then-label, not a full-width justified row). */
|
|
998
|
+
const ToggleRow: React.FunctionComponent<{
|
|
999
|
+
label: string;
|
|
1000
|
+
price?: string;
|
|
1001
|
+
checked: boolean;
|
|
1002
|
+
disabled?: boolean;
|
|
1003
|
+
onChange: (checked: boolean) => void;
|
|
1004
|
+
}> = ({ label, price, checked, disabled, onChange }) => (
|
|
1005
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
|
|
1006
|
+
<span className="md-title-sm">{label}</span>
|
|
1007
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
1008
|
+
{price && (
|
|
1009
|
+
<span className="num" style={{ fontWeight: 700, fontSize: 13.5 }}>
|
|
1010
|
+
{price}
|
|
1011
|
+
</span>
|
|
1012
|
+
)}
|
|
1013
|
+
<Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
|
|
1014
|
+
</div>
|
|
1015
|
+
</div>
|
|
1016
|
+
);
|
|
1017
|
+
|
|
937
1018
|
export interface FinanceDetailsModalProps {
|
|
938
1019
|
/** open flag (controlled by the page). */
|
|
939
1020
|
open: boolean;
|
|
@@ -971,6 +1052,25 @@ export interface FinanceDetailsModalProps {
|
|
|
971
1052
|
onTermChange?: (term: number) => void;
|
|
972
1053
|
onAnnualMileageChange?: (km: number) => void;
|
|
973
1054
|
onClose: () => void;
|
|
1055
|
+
/** mobile only — start with the Finanzierungs-/Leasingdetails breakdown already expanded
|
|
1056
|
+
* (opened directly via that link, skipping the "Rate anpassen" collapsed default view). */
|
|
1057
|
+
initialShowBreakdown?: boolean;
|
|
1058
|
+
/** mobile only — price-rating label, drives the eval pill/bar (same as the collapsed card). */
|
|
1059
|
+
ratingLabel?: string;
|
|
1060
|
+
/** mobile only — mode tabs inside the sheet (mirrors the collapsed card's tabs). */
|
|
1061
|
+
onModeChange?: (mode: FinanceMode) => void;
|
|
1062
|
+
/** mobile only — primary "Rate anfragen"/"Angebot anfragen" CTA. */
|
|
1063
|
+
onSecure?: () => void;
|
|
1064
|
+
/** mobile only — secondary call CTA. */
|
|
1065
|
+
onCall?: () => void;
|
|
1066
|
+
/** mobile only — "Mit Schlussrate" toggle (financing only; leasing has no balloon concept). */
|
|
1067
|
+
withFinalInstallment?: boolean;
|
|
1068
|
+
onToggleFinalInstallment?: (checked: boolean) => void;
|
|
1069
|
+
/** mobile only — "Rundum-Sorglos-Paket" toggle (all modes; forced-on + disabled for cash). */
|
|
1070
|
+
withAddServices?: boolean;
|
|
1071
|
+
addServicesCost?: number;
|
|
1072
|
+
addServicesDisabled?: boolean;
|
|
1073
|
+
onToggleAddServices?: (checked: boolean) => void;
|
|
974
1074
|
/** i18n function (drilled from the page). */
|
|
975
1075
|
t: TranslationFunction;
|
|
976
1076
|
}
|
|
@@ -1008,11 +1108,41 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1008
1108
|
onTermChange,
|
|
1009
1109
|
onAnnualMileageChange,
|
|
1010
1110
|
onClose,
|
|
1111
|
+
initialShowBreakdown,
|
|
1112
|
+
ratingLabel,
|
|
1113
|
+
onModeChange,
|
|
1114
|
+
onSecure,
|
|
1115
|
+
onCall,
|
|
1116
|
+
withFinalInstallment,
|
|
1117
|
+
onToggleFinalInstallment,
|
|
1118
|
+
withAddServices,
|
|
1119
|
+
addServicesCost,
|
|
1120
|
+
addServicesDisabled,
|
|
1121
|
+
onToggleAddServices,
|
|
1011
1122
|
t,
|
|
1012
1123
|
}) => {
|
|
1124
|
+
// mobile-only: the Finanzierungs-/Leasingdetails breakdown starts collapsed (opened via "Rate
|
|
1125
|
+
// anpassen") or expanded (opened directly via the details link) — reset on every open, since
|
|
1126
|
+
// this component stays mounted (the page always renders it, gated by `open`, not unmounted).
|
|
1127
|
+
const [showBreakdown, setShowBreakdown] = React.useState(!!initialShowBreakdown);
|
|
1128
|
+
React.useEffect(() => {
|
|
1129
|
+
if (open) setShowBreakdown(!!initialShowBreakdown);
|
|
1130
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1131
|
+
}, [open, initialShowBreakdown]);
|
|
1132
|
+
|
|
1013
1133
|
if (!open) return null;
|
|
1014
1134
|
const isLease = mode === 'leasing';
|
|
1135
|
+
const isCash = mode === 'cash';
|
|
1015
1136
|
const title = isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails');
|
|
1137
|
+
const mobileTitle = isCash
|
|
1138
|
+
? t('autobahn:modal.sheetTitleCash')
|
|
1139
|
+
: isLease
|
|
1140
|
+
? t('autobahn:modal.sheetTitleLeasing')
|
|
1141
|
+
: t('autobahn:modal.sheetTitleFinancing');
|
|
1142
|
+
// price-evaluation tier — identical derivation to the collapsed card (FinanceCalculator).
|
|
1143
|
+
const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
|
|
1144
|
+
const evalTier = tierIdx >= 0 ? tierIdx : 1;
|
|
1145
|
+
const evalTierLabel = t('autobahn:tier.' + TIER_KEYS[evalTier]);
|
|
1016
1146
|
// adjustable only for fin/leasing, and only when the page supplies the change handlers + bounds.
|
|
1017
1147
|
const adjustable = mode !== 'cash' && !!onDownChange && downMin != null && downMax != null && termMin != null && termMax != null;
|
|
1018
1148
|
// discrete term chip values (supplied periods, else derived from the bounds).
|
|
@@ -1041,89 +1171,274 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1041
1171
|
[isLease ? t('autobahn:modal.leasingRate') : t('autobahn:modal.monthlyRate'), t('autobahn:modal.ratePerMonth', { amount: eur(monthly) })],
|
|
1042
1172
|
...(grossAmount != null && grossAmount > 0 ? ([[isLease ? t('autobahn:modal.totalAmount') : t('autobahn:modal.totalRepayment'), `${eur(grossAmount)} €`]] as Array<[string, string]>) : []),
|
|
1043
1173
|
];
|
|
1174
|
+
const breakdownRows = rows.map(([l, v], i) => (
|
|
1175
|
+
<div
|
|
1176
|
+
key={l}
|
|
1177
|
+
style={{
|
|
1178
|
+
display: 'flex',
|
|
1179
|
+
justifyContent: 'space-between',
|
|
1180
|
+
gap: 12,
|
|
1181
|
+
padding: '9px 0',
|
|
1182
|
+
borderBottom: i < rows.length - 1 ? '1px solid var(--md-outline-variant)' : 0,
|
|
1183
|
+
}}
|
|
1184
|
+
>
|
|
1185
|
+
<span className="md-label-sm">{l}</span>
|
|
1186
|
+
<span className="num" style={{ fontSize: 13.5, fontWeight: 600 }}>
|
|
1187
|
+
{v}
|
|
1188
|
+
</span>
|
|
1189
|
+
</div>
|
|
1190
|
+
));
|
|
1191
|
+
|
|
1044
1192
|
return (
|
|
1045
1193
|
<div className="fin-modal-scrim" onClick={onClose}>
|
|
1046
1194
|
<div className="fin-modal" role="dialog" aria-modal="true" aria-label={title} onClick={(e) => e.stopPropagation()}>
|
|
1047
1195
|
<div className="fin-modal-head">
|
|
1048
|
-
<h2 className="md-headline-sm" style={{ margin: 0 }}>
|
|
1196
|
+
<h2 className="md-headline-sm hide-mobile" style={{ margin: 0 }}>
|
|
1049
1197
|
{title}
|
|
1050
1198
|
</h2>
|
|
1199
|
+
<h2 className="md-headline-sm show-mobile" style={{ margin: 0 }}>
|
|
1200
|
+
{mobileTitle}
|
|
1201
|
+
</h2>
|
|
1051
1202
|
<button className="icon-btn" onClick={onClose} aria-label={t('autobahn:modal.close')}>
|
|
1052
1203
|
<Icon name="x" size={20} />
|
|
1053
1204
|
</button>
|
|
1054
1205
|
</div>
|
|
1055
1206
|
<div className="fin-modal-body">
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
<span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
|
|
1068
|
-
<span className="num" style={{ fontWeight: 700 }}>
|
|
1069
|
-
{eur(down)} €
|
|
1070
|
-
</span>
|
|
1071
|
-
</div>
|
|
1072
|
-
<Slider
|
|
1073
|
-
min={downMin as number}
|
|
1074
|
-
max={downMax as number}
|
|
1075
|
-
step={downStep}
|
|
1076
|
-
value={down}
|
|
1077
|
-
onChange={onDownChange as (n: number) => void}
|
|
1078
|
-
fmt={(v) => `${eur(Number(v))} €`}
|
|
1079
|
-
/>
|
|
1080
|
-
</div>
|
|
1081
|
-
{isLease && annualMileage != null && onAnnualMileageChange && (
|
|
1207
|
+
{/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
|
|
1208
|
+
<div className="hide-mobile">
|
|
1209
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 14 }}>
|
|
1210
|
+
<span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
|
|
1211
|
+
{eur(monthly)} €
|
|
1212
|
+
</span>
|
|
1213
|
+
<span className="md-label-sm">{t('autobahn:finance.perMonth')}</span>
|
|
1214
|
+
</div>
|
|
1215
|
+
{/* adjustment sliders — change a value and every figure below recomputes live (via the page) */}
|
|
1216
|
+
{adjustable && (
|
|
1217
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 18 }}>
|
|
1082
1218
|
<div>
|
|
1083
1219
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
1084
|
-
<span className="md-title-sm">{t('autobahn:finance.
|
|
1220
|
+
<span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
|
|
1085
1221
|
<span className="num" style={{ fontWeight: 700 }}>
|
|
1086
|
-
{eur(
|
|
1222
|
+
{eur(down)} €
|
|
1087
1223
|
</span>
|
|
1088
1224
|
</div>
|
|
1089
1225
|
<Slider
|
|
1090
|
-
min={
|
|
1091
|
-
max={
|
|
1092
|
-
step={
|
|
1093
|
-
value={
|
|
1094
|
-
onChange={
|
|
1095
|
-
fmt={(v) => `${eur(Number(v))}
|
|
1226
|
+
min={downMin as number}
|
|
1227
|
+
max={downMax as number}
|
|
1228
|
+
step={downStep}
|
|
1229
|
+
value={down}
|
|
1230
|
+
onChange={onDownChange as (n: number) => void}
|
|
1231
|
+
fmt={(v) => `${eur(Number(v))} €`}
|
|
1096
1232
|
/>
|
|
1097
1233
|
</div>
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1234
|
+
{isLease && annualMileage != null && onAnnualMileageChange && (
|
|
1235
|
+
<div>
|
|
1236
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
1237
|
+
<span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
|
|
1238
|
+
<span className="num" style={{ fontWeight: 700 }}>
|
|
1239
|
+
{eur(annualMileage)} km
|
|
1240
|
+
</span>
|
|
1241
|
+
</div>
|
|
1242
|
+
<Slider
|
|
1243
|
+
min={annualMileageMin}
|
|
1244
|
+
max={annualMileageMax}
|
|
1245
|
+
step={annualMileageStep}
|
|
1246
|
+
value={annualMileage}
|
|
1247
|
+
onChange={onAnnualMileageChange}
|
|
1248
|
+
fmt={(v) => `${eur(Number(v))} km`}
|
|
1249
|
+
/>
|
|
1250
|
+
</div>
|
|
1251
|
+
)}
|
|
1252
|
+
<div>
|
|
1253
|
+
<div className="md-title-sm" style={{ marginBottom: 8 }}>
|
|
1254
|
+
{t('autobahn:finance.termSelection')}
|
|
1255
|
+
</div>
|
|
1256
|
+
<TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
|
|
1102
1257
|
</div>
|
|
1103
|
-
|
|
1258
|
+
</div>
|
|
1259
|
+
)}
|
|
1260
|
+
{breakdownRows}
|
|
1261
|
+
<p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
|
|
1262
|
+
{isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
|
|
1263
|
+
</p>
|
|
1264
|
+
</div>
|
|
1265
|
+
|
|
1266
|
+
{/* ============================== MOBILE (≤600px) — full "Rate anpassen" sheet ============================== */}
|
|
1267
|
+
<div className="show-mobile" style={{ flexDirection: 'column', gap: 18 }}>
|
|
1268
|
+
{onModeChange && (
|
|
1269
|
+
<div className="fin-tabs" role="tablist">
|
|
1270
|
+
{(
|
|
1271
|
+
[
|
|
1272
|
+
['fin', 'autobahn:finance.mode.financing'],
|
|
1273
|
+
['leasing', 'autobahn:finance.mode.leasing'],
|
|
1274
|
+
['cash', 'autobahn:finance.mode.cash'],
|
|
1275
|
+
] as Array<[FinanceMode, string]>
|
|
1276
|
+
).map(([k, labelKey]) => (
|
|
1277
|
+
<button
|
|
1278
|
+
key={k}
|
|
1279
|
+
type="button"
|
|
1280
|
+
role="tab"
|
|
1281
|
+
aria-selected={mode === k}
|
|
1282
|
+
className={'fin-tab' + (mode === k ? ' on' : '')}
|
|
1283
|
+
onClick={() => onModeChange(k)}
|
|
1284
|
+
>
|
|
1285
|
+
{t(labelKey)}
|
|
1286
|
+
</button>
|
|
1287
|
+
))}
|
|
1288
|
+
</div>
|
|
1289
|
+
)}
|
|
1290
|
+
|
|
1291
|
+
<div className="fin-price-grid">
|
|
1292
|
+
<div className="fin-rate-main">
|
|
1293
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 5 }}>
|
|
1294
|
+
<span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
|
|
1295
|
+
{isCash ? eur(price) : eur(Math.round(monthly))}
|
|
1296
|
+
</span>
|
|
1297
|
+
<span className="md-body-sm" style={{ fontWeight: 600 }}>
|
|
1298
|
+
{isCash ? '€' : `€ ${t('autobahn:finance.perMonth')}`}
|
|
1299
|
+
</span>
|
|
1300
|
+
</div>
|
|
1301
|
+
</div>
|
|
1302
|
+
<div>
|
|
1303
|
+
{ratingLabel && (
|
|
1304
|
+
<span
|
|
1305
|
+
className="fin-eval-pill"
|
|
1306
|
+
style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
|
|
1307
|
+
>
|
|
1308
|
+
<Icon
|
|
1309
|
+
name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
|
|
1310
|
+
size={14}
|
|
1311
|
+
stroke={2.6}
|
|
1312
|
+
color={TIER_COLORS[evalTier]}
|
|
1313
|
+
/>
|
|
1314
|
+
{evalTierLabel}
|
|
1315
|
+
</span>
|
|
1316
|
+
)}
|
|
1317
|
+
</div>
|
|
1318
|
+
<div />
|
|
1319
|
+
<div>
|
|
1320
|
+
{ratingLabel && (
|
|
1321
|
+
<div className="fin-eval-bar">
|
|
1322
|
+
<div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
|
|
1323
|
+
</div>
|
|
1324
|
+
)}
|
|
1104
1325
|
</div>
|
|
1105
1326
|
</div>
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1327
|
+
|
|
1328
|
+
{isCash ? (
|
|
1329
|
+
<>
|
|
1330
|
+
<div>
|
|
1331
|
+
<CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
|
|
1332
|
+
<CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
|
|
1333
|
+
<CalcRow label={t('autobahn:finance.deliveryLabel')} last>
|
|
1334
|
+
{t('autobahn:finance.deliveryValue')}
|
|
1335
|
+
</CalcRow>
|
|
1336
|
+
</div>
|
|
1337
|
+
{addServicesCost != null && (
|
|
1338
|
+
<ToggleRow
|
|
1339
|
+
label={t('autobahn:finance.addServicesToggle')}
|
|
1340
|
+
price={t('autobahn:finance.addServicesOnce', { value: eur(addServicesCost) })}
|
|
1341
|
+
checked={!!withAddServices}
|
|
1342
|
+
disabled={addServicesDisabled}
|
|
1343
|
+
onChange={(checked) => onToggleAddServices && onToggleAddServices(checked)}
|
|
1344
|
+
/>
|
|
1345
|
+
)}
|
|
1346
|
+
<div style={{ display: 'flex', gap: 10 }}>
|
|
1347
|
+
<Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
|
|
1348
|
+
{t('autobahn:finance.secureOffer')}
|
|
1349
|
+
</Button>
|
|
1350
|
+
<Button variant="outlined" size="lg" icon="phone" style={{ width: 52, flex: 'none' }} onClick={onCall} />
|
|
1351
|
+
</div>
|
|
1352
|
+
</>
|
|
1353
|
+
) : (
|
|
1354
|
+
<>
|
|
1355
|
+
{adjustable && (
|
|
1356
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
1357
|
+
<div>
|
|
1358
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
1359
|
+
<span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
|
|
1360
|
+
<span className="num" style={{ fontWeight: 700 }}>
|
|
1361
|
+
{eur(down)} €
|
|
1362
|
+
</span>
|
|
1363
|
+
</div>
|
|
1364
|
+
<Slider
|
|
1365
|
+
min={downMin as number}
|
|
1366
|
+
max={downMax as number}
|
|
1367
|
+
step={downStep}
|
|
1368
|
+
value={down}
|
|
1369
|
+
onChange={onDownChange as (n: number) => void}
|
|
1370
|
+
fmt={(v) => `${eur(Number(v))} €`}
|
|
1371
|
+
/>
|
|
1372
|
+
</div>
|
|
1373
|
+
{isLease && annualMileage != null && onAnnualMileageChange && (
|
|
1374
|
+
<div>
|
|
1375
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
1376
|
+
<span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
|
|
1377
|
+
<span className="num" style={{ fontWeight: 700 }}>
|
|
1378
|
+
{eur(annualMileage)} km
|
|
1379
|
+
</span>
|
|
1380
|
+
</div>
|
|
1381
|
+
<Slider
|
|
1382
|
+
min={annualMileageMin}
|
|
1383
|
+
max={annualMileageMax}
|
|
1384
|
+
step={annualMileageStep}
|
|
1385
|
+
value={annualMileage}
|
|
1386
|
+
onChange={onAnnualMileageChange}
|
|
1387
|
+
fmt={(v) => `${eur(Number(v))} km`}
|
|
1388
|
+
/>
|
|
1389
|
+
</div>
|
|
1390
|
+
)}
|
|
1391
|
+
<div>
|
|
1392
|
+
<div className="md-title-sm" style={{ marginBottom: 8 }}>
|
|
1393
|
+
{t('autobahn:finance.termSelection')}
|
|
1394
|
+
</div>
|
|
1395
|
+
<TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
|
|
1396
|
+
</div>
|
|
1397
|
+
</div>
|
|
1398
|
+
)}
|
|
1399
|
+
|
|
1400
|
+
{!isLease && onToggleFinalInstallment && (
|
|
1401
|
+
<ToggleRow
|
|
1402
|
+
label={t('autobahn:finance.finalInstallmentToggle')}
|
|
1403
|
+
checked={!!withFinalInstallment}
|
|
1404
|
+
onChange={onToggleFinalInstallment}
|
|
1405
|
+
/>
|
|
1406
|
+
)}
|
|
1407
|
+
{addServicesCost != null && onToggleAddServices && (
|
|
1408
|
+
<ToggleRow
|
|
1409
|
+
label={t('autobahn:finance.addServicesToggle')}
|
|
1410
|
+
price={t('autobahn:finance.addServicesPerMonth', { value: eur(addServicesCost) })}
|
|
1411
|
+
checked={!!withAddServices}
|
|
1412
|
+
disabled={addServicesDisabled}
|
|
1413
|
+
onChange={onToggleAddServices}
|
|
1414
|
+
/>
|
|
1415
|
+
)}
|
|
1416
|
+
|
|
1417
|
+
<div style={{ display: 'flex', gap: 10 }}>
|
|
1418
|
+
<Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
|
|
1419
|
+
{t('autobahn:finance.requestRate')}
|
|
1420
|
+
</Button>
|
|
1421
|
+
<Button variant="outlined" size="lg" icon="phone" style={{ width: 52, flex: 'none' }} onClick={onCall} />
|
|
1422
|
+
</div>
|
|
1423
|
+
|
|
1424
|
+
<button type="button" onClick={() => setShowBreakdown((v) => !v)} style={expanderBtnStyle}>
|
|
1425
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
|
|
1426
|
+
<Icon name="info" size={16} color="var(--md-primary)" />
|
|
1427
|
+
{isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails')}
|
|
1428
|
+
</span>
|
|
1429
|
+
<Icon name={showBreakdown ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" />
|
|
1430
|
+
</button>
|
|
1431
|
+
{showBreakdown && (
|
|
1432
|
+
<div>
|
|
1433
|
+
{breakdownRows}
|
|
1434
|
+
<p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
|
|
1435
|
+
{isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
|
|
1436
|
+
</p>
|
|
1437
|
+
</div>
|
|
1438
|
+
)}
|
|
1439
|
+
</>
|
|
1440
|
+
)}
|
|
1441
|
+
</div>
|
|
1127
1442
|
</div>
|
|
1128
1443
|
</div>
|
|
1129
1444
|
</div>
|
package/vdp/index.ts
CHANGED
|
@@ -23,6 +23,10 @@ export type {
|
|
|
23
23
|
// Remaining presentational modules.
|
|
24
24
|
export {
|
|
25
25
|
VdpHeaderCard,
|
|
26
|
+
VdpHeaderSummary,
|
|
27
|
+
VdpHeaderHighlights,
|
|
28
|
+
VdpHeaderCondition,
|
|
29
|
+
VdpTrustRow,
|
|
26
30
|
VdpSpecTable,
|
|
27
31
|
EquipmentList,
|
|
28
32
|
Co2Label,
|
|
@@ -41,6 +45,7 @@ export {
|
|
|
41
45
|
} from './VdpModules';
|
|
42
46
|
export type {
|
|
43
47
|
VdpHeaderCardProps,
|
|
48
|
+
VdpTrustRowProps,
|
|
44
49
|
VdpSpecTableProps,
|
|
45
50
|
EquipmentListProps,
|
|
46
51
|
Co2LabelProps,
|