@bytebrand/fe-ui-core-autobahn 1.0.29 → 1.0.30
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 +28 -7
- package/locales/en/autobahn.json +728 -707
- package/package.json +1 -1
- package/theme/md3.css +104 -22
- package/ui/Icon.tsx +2 -0
- package/vdp/VdpModules.tsx +263 -66
package/vdp/VdpModules.tsx
CHANGED
|
@@ -61,6 +61,9 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
|
|
|
61
61
|
['cog', vehicle.gear],
|
|
62
62
|
['fuel', stripParen(vehicle.fuel)],
|
|
63
63
|
['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
|
|
64
|
+
// body type closes the fact grid (mockup: km|EZ / Getriebe|Kraftstoff / Hand|Karosserie);
|
|
65
|
+
// the compact phone dot-line hides everything from "Hand" on via CSS (nth-child(n+5)).
|
|
66
|
+
...(vehicle.body ? ([['car-front', vehicle.body]] as const) : []),
|
|
64
67
|
];
|
|
65
68
|
return (
|
|
66
69
|
<>
|
|
@@ -99,10 +102,12 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
|
|
|
99
102
|
);
|
|
100
103
|
};
|
|
101
104
|
|
|
102
|
-
/** hand-picked highlights — the
|
|
105
|
+
/** hand-picked highlights — the accent rows of the header. ALL highlights are shown
|
|
106
|
+
* (review round 9: "all highlights shown", no 6-chip cap); the Figma pill colors live
|
|
107
|
+
* on .vdp-highlights .chip in the theme. */
|
|
103
108
|
export const VdpHeaderHighlights: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle }) => (
|
|
104
109
|
<div className="vdp-highlights" style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
|
|
105
|
-
{vehicle.highlights.
|
|
110
|
+
{vehicle.highlights.map((f) => (
|
|
106
111
|
<span key={f} className="chip chip--filter chip--selected" style={{ cursor: 'default' }}>
|
|
107
112
|
<Icon name="check" size={14} stroke={2.4} />
|
|
108
113
|
{f}
|
|
@@ -297,7 +302,7 @@ export const EquipmentList: React.FunctionComponent<EquipmentListProps> = ({ gro
|
|
|
297
302
|
<div className="eq-cols">
|
|
298
303
|
{entries.map(([g, items]) => (
|
|
299
304
|
<div key={g} style={{ breakInside: 'avoid', marginBottom: 18 }}>
|
|
300
|
-
<div className="md-title-sm" style={{ color: 'var(--md-
|
|
305
|
+
<div className="md-title-sm" style={{ color: 'var(--md-link)', marginBottom: 8 }}>
|
|
301
306
|
{g}
|
|
302
307
|
</div>
|
|
303
308
|
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 7 }}>
|
|
@@ -341,10 +346,14 @@ export interface Co2LabelProps {
|
|
|
341
346
|
t: TranslationFunction;
|
|
342
347
|
}
|
|
343
348
|
|
|
344
|
-
/** CO₂ / energy-efficiency label (EU-style stepped colour band
|
|
349
|
+
/** CO₂ / energy-efficiency label (EU-style stepped colour band, Figma pastel scale).
|
|
350
|
+
* Handles PHEV with BOTH class markers (gew. komb. + entladen komb.) on the scale. */
|
|
345
351
|
export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t }) => {
|
|
346
352
|
const p = vehicle.phev;
|
|
347
353
|
const idx = Math.max(0, CO2_CLASSES.indexOf((p ? p.co2classW : vehicle.co2class) as (typeof CO2_CLASSES)[number]));
|
|
354
|
+
// discharged-class marker (PHEV only); -1 when absent or same as the weighted class.
|
|
355
|
+
const idxD = p ? CO2_CLASSES.indexOf(p.co2classD as (typeof CO2_CLASSES)[number]) : -1;
|
|
356
|
+
const hasMarks = !!p && (idx >= 0 || idxD >= 0);
|
|
348
357
|
const stats: ReadonlyArray<readonly [string, string]> = p
|
|
349
358
|
? [
|
|
350
359
|
[t('autobahn:eco.stat.powerConsumptionWeighted'), p.consEl],
|
|
@@ -374,8 +383,27 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
|
|
|
374
383
|
</div>
|
|
375
384
|
<div className="co2-scale">
|
|
376
385
|
{CO2_CLASSES.map((c, i) => (
|
|
377
|
-
<div key={c} className=
|
|
378
|
-
{
|
|
386
|
+
<div key={c} className="co2-col">
|
|
387
|
+
{/* PHEV: tiny class markers above the weighted + discharged segments (mockup) */}
|
|
388
|
+
{hasMarks && (
|
|
389
|
+
<span className="co2-mark">
|
|
390
|
+
{i === idx
|
|
391
|
+
? t('autobahn:eco.weightedShort')
|
|
392
|
+
: i === idxD && idxD !== idx
|
|
393
|
+
? t('autobahn:eco.dischargedShort')
|
|
394
|
+
: ' '}
|
|
395
|
+
</span>
|
|
396
|
+
)}
|
|
397
|
+
<div
|
|
398
|
+
className={
|
|
399
|
+
'co2-seg' +
|
|
400
|
+
(i === idx ? ' co2-seg--on' : '') +
|
|
401
|
+
(i === idxD && idxD !== idx ? ' co2-seg--on2' : '')
|
|
402
|
+
}
|
|
403
|
+
style={{ ['--seg' as string]: CO2_COLORS[i] } as React.CSSProperties}
|
|
404
|
+
>
|
|
405
|
+
{c}
|
|
406
|
+
</div>
|
|
379
407
|
</div>
|
|
380
408
|
))}
|
|
381
409
|
</div>
|
|
@@ -450,8 +478,8 @@ export const VehicleDescription: React.FunctionComponent<VehicleDescriptionProps
|
|
|
450
478
|
// Trust strip
|
|
451
479
|
// ---------------------------------------------------------------------------
|
|
452
480
|
|
|
453
|
-
/** [kebab-case lucide icon, label]. */
|
|
454
|
-
export type TrustItem = readonly [icon: string, label: string];
|
|
481
|
+
/** [kebab-case lucide icon, label, optional subline (mobile Figma tiles)]. */
|
|
482
|
+
export type TrustItem = readonly [icon: string, label: string, sub?: string];
|
|
455
483
|
|
|
456
484
|
export interface TrustStripProps {
|
|
457
485
|
/** trust badges; defaults to auto.de's four standard promises. */
|
|
@@ -460,21 +488,25 @@ export interface TrustStripProps {
|
|
|
460
488
|
t: TranslationFunction;
|
|
461
489
|
}
|
|
462
490
|
|
|
463
|
-
/** auto.de's four standard promises — [icon, i18n label key]. */
|
|
464
|
-
const DEFAULT_TRUST: ReadonlyArray<
|
|
465
|
-
['shield-check', 'autobahn:trust.warranty'],
|
|
466
|
-
['undo-2', 'autobahn:trust.return'],
|
|
467
|
-
['truck', 'autobahn:trust.delivery'],
|
|
468
|
-
['badge-check', 'autobahn:trust.inspection'],
|
|
491
|
+
/** auto.de's four standard promises — [icon, i18n label key, i18n subline key]. */
|
|
492
|
+
const DEFAULT_TRUST: ReadonlyArray<readonly [string, string, string]> = [
|
|
493
|
+
['shield-check', 'autobahn:trust.warranty', 'autobahn:trust.warrantySub'],
|
|
494
|
+
['undo-2', 'autobahn:trust.return', 'autobahn:trust.returnSub'],
|
|
495
|
+
['truck', 'autobahn:trust.delivery', 'autobahn:trust.deliverySub'],
|
|
496
|
+
['badge-check', 'autobahn:trust.inspection', 'autobahn:trust.inspectionSub'],
|
|
469
497
|
];
|
|
470
498
|
|
|
471
|
-
/**
|
|
499
|
+
/** Trust-signal strip: quiet one-liner on desktop, Figma 2×2 tile card on mobile
|
|
500
|
+
* (the .trust-sub sublines only surface in the mobile tile layout via CSS). */
|
|
472
501
|
export const TrustStrip: React.FunctionComponent<TrustStripProps> = ({ items, t }) => (
|
|
473
502
|
<div className="trust-strip">
|
|
474
|
-
{(items ?? DEFAULT_TRUST.map(([ic, key]) => [ic, t(key)] as TrustItem)).map(([ic, label]) => (
|
|
503
|
+
{(items ?? DEFAULT_TRUST.map(([ic, key, subKey]) => [ic, t(key), t(subKey)] as TrustItem)).map(([ic, label, sub]) => (
|
|
475
504
|
<div key={label}>
|
|
476
505
|
<Icon name={ic} size={17} color="var(--md-primary)" stroke={2} style={{ flex: 'none' }} />
|
|
477
|
-
{
|
|
506
|
+
<span style={{ minWidth: 0 }}>
|
|
507
|
+
{label}
|
|
508
|
+
{sub && <span className="trust-sub">{sub}</span>}
|
|
509
|
+
</span>
|
|
478
510
|
</div>
|
|
479
511
|
))}
|
|
480
512
|
</div>
|
|
@@ -930,32 +962,111 @@ export interface VdpBottomBarProps {
|
|
|
930
962
|
* after scrolling past the main offer card.
|
|
931
963
|
*/
|
|
932
964
|
show: boolean;
|
|
965
|
+
/** active payment mode — drives the tabs + the displayed figure (rate vs price). */
|
|
966
|
+
mode?: FinanceMode;
|
|
967
|
+
/** mode tab tapped; when omitted the tabs row is not rendered. */
|
|
968
|
+
onModeChange?: (mode: FinanceMode) => void;
|
|
969
|
+
/** monthly rate for the CURRENT mode (fin/leasing differ); falls back to vehicle.monthly. */
|
|
970
|
+
monthly?: number;
|
|
971
|
+
/** price-rating label — drives the eval pill + gradient bar (same as the offer card). */
|
|
972
|
+
ratingLabel?: string;
|
|
933
973
|
/** primary CTA click. */
|
|
934
974
|
onSecure?: () => void;
|
|
975
|
+
/** phone CTA click. */
|
|
976
|
+
onCall?: () => void;
|
|
977
|
+
/** opens the "Rate anpassen" sheet; when omitted the button is not rendered. */
|
|
978
|
+
onOpenAdjust?: () => void;
|
|
935
979
|
/** i18n function (drilled from the page). */
|
|
936
980
|
t: TranslationFunction;
|
|
937
981
|
}
|
|
938
982
|
|
|
939
|
-
/** Mobile sticky bottom
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
983
|
+
/** Mobile sticky bottom bar (Figma "Sticky Footer"): mode tabs, rate + price rating,
|
|
984
|
+
* then Angebot anfragen · phone · Rate anpassen. No struck old rate — there is no
|
|
985
|
+
* real previous-rate source (needs-data discipline), so nothing is fabricated. */
|
|
986
|
+
export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
|
|
987
|
+
vehicle,
|
|
988
|
+
show,
|
|
989
|
+
mode = 'fin',
|
|
990
|
+
onModeChange,
|
|
991
|
+
monthly,
|
|
992
|
+
ratingLabel,
|
|
993
|
+
onSecure,
|
|
994
|
+
onCall,
|
|
995
|
+
onOpenAdjust,
|
|
996
|
+
t,
|
|
997
|
+
}) => {
|
|
998
|
+
const isCash = mode === 'cash';
|
|
999
|
+
const rate = monthly != null ? monthly : vehicle.monthly;
|
|
1000
|
+
const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
|
|
1001
|
+
const evalTier = tierIdx >= 0 ? tierIdx : 1;
|
|
1002
|
+
return (
|
|
1003
|
+
<div className={'vdp-bottom-bar show-mobile' + (show ? ' vdp-bottom-bar--on' : '')}>
|
|
1004
|
+
{onModeChange && (
|
|
1005
|
+
<div className="fin-tabs bb-tabs" role="tablist">
|
|
1006
|
+
{(
|
|
1007
|
+
[
|
|
1008
|
+
['fin', 'autobahn:finance.mode.financing'],
|
|
1009
|
+
['leasing', 'autobahn:finance.mode.leasing'],
|
|
1010
|
+
['cash', 'autobahn:card.price.buy'],
|
|
1011
|
+
] as Array<[FinanceMode, string]>
|
|
1012
|
+
).map(([k, labelKey]) => (
|
|
1013
|
+
<button
|
|
1014
|
+
key={k}
|
|
1015
|
+
type="button"
|
|
1016
|
+
role="tab"
|
|
1017
|
+
aria-selected={mode === k}
|
|
1018
|
+
className={'fin-tab' + (mode === k ? ' on' : '')}
|
|
1019
|
+
onClick={() => onModeChange(k)}
|
|
1020
|
+
>
|
|
1021
|
+
{t(labelKey)}
|
|
1022
|
+
</button>
|
|
1023
|
+
))}
|
|
1024
|
+
</div>
|
|
1025
|
+
)}
|
|
1026
|
+
<div className="bb-price-row">
|
|
1027
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 5, whiteSpace: 'nowrap', minWidth: 0 }}>
|
|
1028
|
+
<span className="num" style={{ fontWeight: 800, fontSize: 30, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
|
|
1029
|
+
{isCash ? eur(vehicle.price) : eur(Math.round(rate))} €
|
|
1030
|
+
</span>
|
|
1031
|
+
<span className="md-label-sm" style={{ fontWeight: 600 }}>
|
|
1032
|
+
{isCash ? t('autobahn:finance.fixedPrice') : `${t('autobahn:finance.exampleLabel')} ${t('autobahn:bottom.perMonth')}`}
|
|
1033
|
+
</span>
|
|
1034
|
+
</div>
|
|
1035
|
+
{ratingLabel && (
|
|
1036
|
+
<div className="bb-eval">
|
|
1037
|
+
<span
|
|
1038
|
+
className="fin-eval-pill"
|
|
1039
|
+
style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
|
|
1040
|
+
>
|
|
1041
|
+
<Icon
|
|
1042
|
+
name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
|
|
1043
|
+
size={13}
|
|
1044
|
+
stroke={2.6}
|
|
1045
|
+
color={TIER_COLORS[evalTier]}
|
|
1046
|
+
/>
|
|
1047
|
+
{t('autobahn:tier.' + TIER_KEYS[evalTier])}
|
|
1048
|
+
</span>
|
|
1049
|
+
<div className="fin-eval-bar">
|
|
1050
|
+
<div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
)}
|
|
949
1054
|
</div>
|
|
950
|
-
<div className="
|
|
951
|
-
{
|
|
1055
|
+
<div className="bb-cta-row">
|
|
1056
|
+
<Button variant="filled" icon="file-check" style={{ flex: 1, minWidth: 0 }} onClick={onSecure}>
|
|
1057
|
+
{t('autobahn:bottom.secureOffer')}
|
|
1058
|
+
</Button>
|
|
1059
|
+
{/* padding:0 — icon-only button (same fix as the sheet's phone button) */}
|
|
1060
|
+
{onCall && <Button variant="outlined" icon="phone" style={{ width: 46, flex: 'none', padding: 0 }} onClick={onCall} />}
|
|
1061
|
+
{onOpenAdjust && !isCash && (
|
|
1062
|
+
<Button variant="outlined" icon="sliders-horizontal" className="bb-adjust" style={{ flex: 'none' }} onClick={onOpenAdjust}>
|
|
1063
|
+
{t('autobahn:finance.adjustRate')}
|
|
1064
|
+
</Button>
|
|
1065
|
+
)}
|
|
952
1066
|
</div>
|
|
953
1067
|
</div>
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
</Button>
|
|
957
|
-
</div>
|
|
958
|
-
);
|
|
1068
|
+
);
|
|
1069
|
+
};
|
|
959
1070
|
|
|
960
1071
|
// ---------------------------------------------------------------------------
|
|
961
1072
|
// 130-Punkte-Check block
|
|
@@ -1483,6 +1594,12 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1483
1594
|
export interface CustomerRatingsProps {
|
|
1484
1595
|
/** the loaded reviews (REAL — from the WP json-quotes source). */
|
|
1485
1596
|
reviews: VdpReview[];
|
|
1597
|
+
/**
|
|
1598
|
+
* Overall auto.de Google rating to display instead of the average of the few
|
|
1599
|
+
* loaded reviews (which skews to 5,0 — the review feed only carries 5-star
|
|
1600
|
+
* quotes). The page passes the real aggregate (4,6 per Google, review round 9).
|
|
1601
|
+
*/
|
|
1602
|
+
average?: number;
|
|
1486
1603
|
/** i18n function (drilled from the page). */
|
|
1487
1604
|
t: TranslationFunction;
|
|
1488
1605
|
}
|
|
@@ -1495,11 +1612,11 @@ export interface CustomerRatingsProps {
|
|
|
1495
1612
|
* the WordPress json-quotes feed); the page loads them and passes them in. When no
|
|
1496
1613
|
* reviews load, the page renders nothing (no fabricated content).
|
|
1497
1614
|
*/
|
|
1498
|
-
export const CustomerRatings: React.FunctionComponent<CustomerRatingsProps> = ({ reviews, t }) => {
|
|
1615
|
+
export const CustomerRatings: React.FunctionComponent<CustomerRatingsProps> = ({ reviews, average, t }) => {
|
|
1499
1616
|
const [showAll, setShowAll] = useState(false);
|
|
1500
1617
|
if (!reviews || !reviews.length) return null;
|
|
1501
1618
|
const count = reviews.length;
|
|
1502
|
-
const avg = reviews.reduce((s, r) => s + (r.rating || 0), 0) / count;
|
|
1619
|
+
const avg = average != null ? average : reviews.reduce((s, r) => s + (r.rating || 0), 0) / count;
|
|
1503
1620
|
const shown = showAll ? reviews : reviews.slice(0, 4);
|
|
1504
1621
|
return (
|
|
1505
1622
|
<section className="mcard" id="vdp-bewertungen" style={{ padding: 22, marginTop: 44 }}>
|
|
@@ -1608,15 +1725,13 @@ const enquiryInput: React.CSSProperties = {
|
|
|
1608
1725
|
fontSize: 14,
|
|
1609
1726
|
};
|
|
1610
1727
|
|
|
1611
|
-
/** field with
|
|
1612
|
-
const
|
|
1728
|
+
/** outlined field with the label floating ON the top border (Figma phone/message fields). */
|
|
1729
|
+
const FloatField: React.FunctionComponent<{
|
|
1613
1730
|
label: string;
|
|
1614
1731
|
children: React.ReactNode;
|
|
1615
1732
|
}> = ({ label, children }) => (
|
|
1616
|
-
<label
|
|
1617
|
-
<span className="
|
|
1618
|
-
{label}
|
|
1619
|
-
</span>
|
|
1733
|
+
<label className="enq-float">
|
|
1734
|
+
<span className="enq-float-label">{label}</span>
|
|
1620
1735
|
{children}
|
|
1621
1736
|
</label>
|
|
1622
1737
|
);
|
|
@@ -1640,9 +1755,12 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
1640
1755
|
onSubmit,
|
|
1641
1756
|
t,
|
|
1642
1757
|
}) => {
|
|
1643
|
-
const [
|
|
1758
|
+
const [firstName, setFirstName] = useState('');
|
|
1759
|
+
const [lastName, setLastName] = useState('');
|
|
1644
1760
|
const [email, setEmail] = useState('');
|
|
1645
|
-
const [
|
|
1761
|
+
const [areaCode, setAreaCode] = useState('');
|
|
1762
|
+
const [phoneNum, setPhoneNum] = useState('');
|
|
1763
|
+
const [consent, setConsent] = useState(false);
|
|
1646
1764
|
const [pref, setPref] = useState<'email' | 'phone'>('email');
|
|
1647
1765
|
const [message, setMessage] = useState(
|
|
1648
1766
|
autoDeId
|
|
@@ -1654,9 +1772,16 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
1654
1772
|
const [error, setError] = useState('');
|
|
1655
1773
|
|
|
1656
1774
|
const emailOk = /.+@.+\..+/.test(email);
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1659
|
-
|
|
1775
|
+
const phoneDigits = (areaCode + phoneNum).replace(/\D/g, '');
|
|
1776
|
+
const phoneOk = phoneDigits.length >= 6;
|
|
1777
|
+
// Figma round 9: Vorname/Nachname/E-Mail are always required (*), phone additionally
|
|
1778
|
+
// when it's the preferred channel; the consent checkbox gates the send.
|
|
1779
|
+
const valid =
|
|
1780
|
+
firstName.trim().length > 0 &&
|
|
1781
|
+
lastName.trim().length > 0 &&
|
|
1782
|
+
emailOk &&
|
|
1783
|
+
consent &&
|
|
1784
|
+
(pref !== 'phone' || phoneOk);
|
|
1660
1785
|
|
|
1661
1786
|
const submit = async (): Promise<void> => {
|
|
1662
1787
|
if (!valid || sending) return;
|
|
@@ -1665,8 +1790,14 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
1665
1790
|
const note = t('autobahn:enquiry.preferredContact', {
|
|
1666
1791
|
channel: pref === 'email' ? t('autobahn:enquiry.contactMethod.email') : t('autobahn:enquiry.contactMethod.phone'),
|
|
1667
1792
|
});
|
|
1793
|
+
const phone = phoneDigits ? `+49 ${areaCode.trim()} ${phoneNum.trim()}`.replace(/\s+/g, ' ').trim() : '';
|
|
1668
1794
|
try {
|
|
1669
|
-
await onSubmit({
|
|
1795
|
+
await onSubmit({
|
|
1796
|
+
name: `${firstName.trim()} ${lastName.trim()}`,
|
|
1797
|
+
email: email.trim(),
|
|
1798
|
+
phone,
|
|
1799
|
+
message: message.trim() + note,
|
|
1800
|
+
});
|
|
1670
1801
|
setSent(true);
|
|
1671
1802
|
} catch (e) {
|
|
1672
1803
|
setError(t('autobahn:enquiry.form.sendError'));
|
|
@@ -1744,38 +1875,103 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
1744
1875
|
</button>
|
|
1745
1876
|
</div>
|
|
1746
1877
|
</div>
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1878
|
+
{/* Vorname* / Nachname* — two required fields (Figma round 9), placeholder-labeled */}
|
|
1879
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12 }}>
|
|
1880
|
+
<input
|
|
1881
|
+
style={enquiryInput}
|
|
1882
|
+
placeholder={t('autobahn:enquiry.form.firstNamePlaceholder')}
|
|
1883
|
+
value={firstName}
|
|
1884
|
+
onChange={(e) => setFirstName(e.target.value)}
|
|
1885
|
+
aria-label={t('autobahn:enquiry.form.firstNameAria')}
|
|
1886
|
+
/>
|
|
1887
|
+
<input
|
|
1888
|
+
style={enquiryInput}
|
|
1889
|
+
placeholder={t('autobahn:enquiry.form.lastNamePlaceholder')}
|
|
1890
|
+
value={lastName}
|
|
1891
|
+
onChange={(e) => setLastName(e.target.value)}
|
|
1892
|
+
aria-label={t('autobahn:enquiry.form.lastNameAria')}
|
|
1893
|
+
/>
|
|
1757
1894
|
</div>
|
|
1758
1895
|
<div style={{ marginTop: 12 }}>
|
|
1759
|
-
<
|
|
1896
|
+
<input
|
|
1897
|
+
style={enquiryInput}
|
|
1898
|
+
type="email"
|
|
1899
|
+
placeholder={t('autobahn:enquiry.form.emailPlaceholder')}
|
|
1900
|
+
value={email}
|
|
1901
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
1902
|
+
aria-label={t('autobahn:enquiry.contactMethod.email')}
|
|
1903
|
+
/>
|
|
1904
|
+
</div>
|
|
1905
|
+
{/* Land (+49 fixed) · Vorwahl · Telefonnummer — outlined float-label row (Figma) */}
|
|
1906
|
+
<div className="enq-phone-row" style={{ marginTop: 16 }}>
|
|
1907
|
+
<FloatField label={t('autobahn:enquiry.form.countryLabel')}>
|
|
1908
|
+
<span className="enq-country num">
|
|
1909
|
+
+49
|
|
1910
|
+
<Icon name="chevron-down" size={14} color="var(--md-on-surface-variant)" />
|
|
1911
|
+
</span>
|
|
1912
|
+
</FloatField>
|
|
1913
|
+
<FloatField label={t('autobahn:enquiry.form.areaCodeLabel')}>
|
|
1914
|
+
<input
|
|
1915
|
+
type="tel"
|
|
1916
|
+
value={areaCode}
|
|
1917
|
+
onChange={(e) => setAreaCode(e.target.value)}
|
|
1918
|
+
aria-label={t('autobahn:enquiry.form.areaCodeLabel')}
|
|
1919
|
+
/>
|
|
1920
|
+
</FloatField>
|
|
1921
|
+
<FloatField label={t('autobahn:enquiry.form.phoneNumberLabel')}>
|
|
1922
|
+
<input
|
|
1923
|
+
type="tel"
|
|
1924
|
+
value={phoneNum}
|
|
1925
|
+
onChange={(e) => setPhoneNum(e.target.value)}
|
|
1926
|
+
aria-label={t('autobahn:enquiry.form.phoneNumberLabel')}
|
|
1927
|
+
/>
|
|
1928
|
+
</FloatField>
|
|
1929
|
+
</div>
|
|
1930
|
+
<div style={{ marginTop: 16 }}>
|
|
1931
|
+
<FloatField label={t('autobahn:enquiry.form.messageLabel')}>
|
|
1760
1932
|
<textarea
|
|
1761
|
-
style={{ ...enquiryInput, height: 92, padding: '10px 14px', resize: 'vertical' }}
|
|
1762
1933
|
value={message}
|
|
1763
1934
|
onChange={(e) => setMessage(e.target.value)}
|
|
1764
1935
|
aria-label={t('autobahn:enquiry.form.messageAria')}
|
|
1765
1936
|
/>
|
|
1766
|
-
</
|
|
1937
|
+
</FloatField>
|
|
1767
1938
|
</div>
|
|
1939
|
+
{/* Nutzungsbedingungen sentence + revocable-consent checkbox (Figma round 9) */}
|
|
1940
|
+
<p className="md-body-sm" style={{ margin: '14px 0 0' }}>
|
|
1941
|
+
{t('autobahn:enquiry.form.termsPrefix')}{' '}
|
|
1942
|
+
<a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>
|
|
1943
|
+
{t('autobahn:enquiry.form.termsLink')}
|
|
1944
|
+
</a>{' '}
|
|
1945
|
+
{t('autobahn:enquiry.form.termsSuffix')}
|
|
1946
|
+
</p>
|
|
1947
|
+
<label className="enq-consent">
|
|
1948
|
+
<input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)} />
|
|
1949
|
+
<span>
|
|
1950
|
+
{t('autobahn:enquiry.form.consentPart1')}{' '}
|
|
1951
|
+
<a href="/datenschutz/" target="_blank" rel="noopener noreferrer">
|
|
1952
|
+
{t('autobahn:enquiry.form.consentLinkHere')}
|
|
1953
|
+
</a>{' '}
|
|
1954
|
+
{t('autobahn:enquiry.form.consentPart2')}{' '}
|
|
1955
|
+
<a href="/datenschutz/" target="_blank" rel="noopener noreferrer">
|
|
1956
|
+
{t('autobahn:enquiry.form.consentLinkPrivacy')}
|
|
1957
|
+
</a>
|
|
1958
|
+
{t('autobahn:enquiry.form.consentPart3')}
|
|
1959
|
+
</span>
|
|
1960
|
+
</label>
|
|
1768
1961
|
{error && (
|
|
1769
1962
|
<div className="md-label-sm" style={{ color: 'var(--md-error)', marginTop: 8 }}>
|
|
1770
1963
|
{error}
|
|
1771
1964
|
</div>
|
|
1772
1965
|
)}
|
|
1773
|
-
<div style={{
|
|
1774
|
-
<Button variant="filled" size="lg" icon="send" disabled={!valid || sending} onClick={submit}>
|
|
1966
|
+
<div style={{ marginTop: 16 }}>
|
|
1967
|
+
<Button variant="filled" block size="lg" icon="send" disabled={!valid || sending} onClick={submit}>
|
|
1775
1968
|
{sending ? t('autobahn:enquiry.form.sending') : t('autobahn:enquiry.sendButton')}
|
|
1776
1969
|
</Button>
|
|
1777
|
-
<span
|
|
1778
|
-
|
|
1970
|
+
<span
|
|
1971
|
+
className="md-label-sm"
|
|
1972
|
+
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5, marginTop: 10, color: 'var(--md-good)' }}
|
|
1973
|
+
>
|
|
1974
|
+
<Icon name="lock" size={13} color="var(--md-good)" />
|
|
1779
1975
|
{t('autobahn:enquiry.security')}
|
|
1780
1976
|
</span>
|
|
1781
1977
|
</div>
|
|
@@ -1816,7 +2012,8 @@ export const HowItWorks: React.FunctionComponent<HowItWorksProps> = ({ steps, t
|
|
|
1816
2012
|
steps ?? DEFAULT_STEPS.map(([titleKey, descKey]) => [t(titleKey), t(descKey)] as HowItWorksStep);
|
|
1817
2013
|
return (
|
|
1818
2014
|
<section className="hiw-section" style={{ marginTop: 44 }}>
|
|
1819
|
-
|
|
2015
|
+
{/* alignment is breakpoint-owned: centered on desktop, left on the mobile card (CSS) */}
|
|
2016
|
+
<h2 className="md-headline-sm hiw-heading" style={{ marginBottom: 28 }}>
|
|
1820
2017
|
{t('autobahn:howItWorks.title')}
|
|
1821
2018
|
</h2>
|
|
1822
2019
|
<div className="steps-4 hiw-grid">
|