@bytebrand/fe-ui-core-autobahn 1.0.133 → 1.0.135
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/cards/CarCard.tsx +5 -19
- package/cards/CarRow.tsx +7 -20
- package/cards/cardParts.tsx +22 -55
- package/cards/types.ts +0 -10
- package/chrome/Footer.tsx +9 -1
- package/compat/VehicleSmallCard/VehicleData/VechiclePriceItem/VechiclePriceItem.tsx +6 -3
- package/compat/VehicleSmallCard/VehicleData/VehicleInfo/VehicleInfo.tsx +2 -2
- package/compat/VehicleSmallCard/VehicleSmallCard.tsx +4 -6
- package/data/toAutobahnCar.ts +0 -24
- package/package.json +1 -1
- package/theme/md3.css +1 -0
- package/vdp/VdpModules.tsx +21 -70
- package/vdp/index.ts +0 -2
package/cards/CarCard.tsx
CHANGED
|
@@ -51,14 +51,8 @@ export interface CarCardProps {
|
|
|
51
51
|
isFavorite: boolean;
|
|
52
52
|
/** toggle the favorite; the heart calls this (click is kept off the card). */
|
|
53
53
|
onToggleFavorite: () => void;
|
|
54
|
-
/** fired when the card is clicked (opens the detail page).
|
|
55
|
-
|
|
56
|
-
* `?mode=` query flag so the originating card and the CDP always agree on the product. */
|
|
57
|
-
onOpen: (car: AutobahnCar, mode?: PaymentMode) => void;
|
|
58
|
-
/** ADV-181 — fired when the user explicitly switches the card's payment tab. Pages use it to
|
|
59
|
-
* persist the per-car product choice (FinancingStore.setSelectedProduct) + analytics. The
|
|
60
|
-
* card still owns the visual tab state; this is a notification, not a controlled input. */
|
|
61
|
-
onModeChange?: (mode: PaymentMode) => void;
|
|
54
|
+
/** fired when the card is clicked (opens the detail page). */
|
|
55
|
+
onOpen: (car: AutobahnCar) => void;
|
|
62
56
|
/** fired on first hover/touch of the media box — the SRP uses it to lazy-load the gallery. */
|
|
63
57
|
onMediaEnter?: () => void;
|
|
64
58
|
/**
|
|
@@ -119,16 +113,8 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
119
113
|
onCall,
|
|
120
114
|
onOpenRateAdjust,
|
|
121
115
|
onShowFinancingDetails,
|
|
122
|
-
onModeChange,
|
|
123
116
|
}) => {
|
|
124
|
-
|
|
125
|
-
// financing vs leasing, or the user's saved per-car choice) — computed by toAutobahnCar,
|
|
126
|
-
// so SSR/hydration agree. Absent (older producer) → financing, the pre-ADV-181 default.
|
|
127
|
-
const [mode, setMode] = useState<PaymentMode>(car.defaultMode ?? 'financing');
|
|
128
|
-
const changeMode = (m: PaymentMode) => {
|
|
129
|
-
setMode(m);
|
|
130
|
-
onModeChange && onModeChange(m);
|
|
131
|
-
};
|
|
117
|
+
const [mode, setMode] = useState<PaymentMode>('financing');
|
|
132
118
|
const [photoIndex, setPhotoIndex] = useState(0);
|
|
133
119
|
const [viewerOpen, setViewerOpen] = useState(false);
|
|
134
120
|
const mediaRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -174,7 +160,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
174
160
|
return (
|
|
175
161
|
<article
|
|
176
162
|
className="mcard mcard--interactive cc-card"
|
|
177
|
-
onClick={() => onOpen(car
|
|
163
|
+
onClick={() => onOpen(car)}
|
|
178
164
|
style={{ display: 'flex', flexDirection: 'column' }}
|
|
179
165
|
>
|
|
180
166
|
<div
|
|
@@ -289,7 +275,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
289
275
|
</div>
|
|
290
276
|
<SpecLine items={cardSpec(car)} oneLine />
|
|
291
277
|
<div className="cc-price" style={{ marginTop: 0, borderTop: '1px solid var(--md-outline-variant)' }}>
|
|
292
|
-
<PriceTabs car={car} mode={mode} onModeChange={
|
|
278
|
+
<PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
|
|
293
279
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 10 }}>
|
|
294
280
|
<PriceHero car={car} mode={mode} t={t} alwaysShowUnit />
|
|
295
281
|
<PriceEvalCol car={car} t={t} />
|
package/cards/CarRow.tsx
CHANGED
|
@@ -52,13 +52,8 @@ export interface CarRowProps {
|
|
|
52
52
|
isFavorite: boolean;
|
|
53
53
|
/** toggle the favorite; the hearts call this (click is kept off the card). */
|
|
54
54
|
onToggleFavorite: () => void;
|
|
55
|
-
/** fired when the card (or the CTA) is clicked (opens the detail page).
|
|
56
|
-
|
|
57
|
-
* the VDP via `?mode=` so the originating card and the CDP always agree on the product. */
|
|
58
|
-
onOpen: (car: AutobahnCar, mode?: PaymentMode) => void;
|
|
59
|
-
/** ADV-181 — fired when the user explicitly switches the mobile payment tab. Pages use it to
|
|
60
|
-
* persist the per-car product choice (FinancingStore.setSelectedProduct) + analytics. */
|
|
61
|
-
onModeChange?: (mode: PaymentMode) => void;
|
|
55
|
+
/** fired when the card (or the CTA) is clicked (opens the detail page). */
|
|
56
|
+
onOpen: (car: AutobahnCar) => void;
|
|
62
57
|
/** fired on first hover/touch of the media box — the SRP uses it to lazy-load the gallery. */
|
|
63
58
|
onMediaEnter?: () => void;
|
|
64
59
|
/**
|
|
@@ -97,16 +92,8 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
97
92
|
t,
|
|
98
93
|
adminAction,
|
|
99
94
|
onShowFinancingDetails,
|
|
100
|
-
onModeChange,
|
|
101
95
|
}) => {
|
|
102
|
-
|
|
103
|
-
// Drives the mobile tab switcher AND the mode reported to onOpen; the desktop branch's
|
|
104
|
-
// untabbed PricePaths reads car.defaultMode directly.
|
|
105
|
-
const [mode, setMode] = useState<PaymentMode>(car.defaultMode ?? 'financing');
|
|
106
|
-
const changeMode = (m: PaymentMode) => {
|
|
107
|
-
setMode(m);
|
|
108
|
-
onModeChange && onModeChange(m);
|
|
109
|
-
};
|
|
96
|
+
const [mode, setMode] = useState<PaymentMode>('financing');
|
|
110
97
|
const [photoIndex, setPhotoIndex] = useState(0);
|
|
111
98
|
const mediaRef = useRef<HTMLDivElement | null>(null);
|
|
112
99
|
// always call the LATEST onMediaEnter without re-arming the observer on every render (the
|
|
@@ -159,7 +146,7 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
159
146
|
return (
|
|
160
147
|
<article
|
|
161
148
|
className="mcard mcard--interactive listcard"
|
|
162
|
-
onClick={() => onOpen(car
|
|
149
|
+
onClick={() => onOpen(car)}
|
|
163
150
|
style={{ background: 'var(--md-sc-lowest)' }}
|
|
164
151
|
>
|
|
165
152
|
<div className="listcard-media" ref={mediaRef} style={{ position: 'relative' }} onMouseEnter={onMediaEnter} onTouchStart={onMediaEnter}>
|
|
@@ -281,7 +268,7 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
281
268
|
the previously-unused PricePaths/PriceKauf pair. Both variants share the same
|
|
282
269
|
Preisbewertung meter + CTA, anchored to the bottom (margin-top:auto on .pp-purchase). */}
|
|
283
270
|
<div className="listcard-price">
|
|
284
|
-
<PriceTabs car={car} mode={mode} onModeChange={
|
|
271
|
+
<PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} className="show-mobile" />
|
|
285
272
|
<div className="pp-purchase show-mobile">
|
|
286
273
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
287
274
|
<PriceHero car={car} mode={mode} t={t} alwaysShowUnit />
|
|
@@ -300,7 +287,7 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
300
287
|
iconRight="arrow-right"
|
|
301
288
|
onClick={(e) => {
|
|
302
289
|
e.stopPropagation();
|
|
303
|
-
onOpen(car
|
|
290
|
+
onOpen(car);
|
|
304
291
|
}}
|
|
305
292
|
>
|
|
306
293
|
{t('autobahn:card.cta.viewOffer')}
|
|
@@ -349,7 +336,7 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
349
336
|
iconRight="arrow-right"
|
|
350
337
|
onClick={(e) => {
|
|
351
338
|
e.stopPropagation();
|
|
352
|
-
onOpen(car
|
|
339
|
+
onOpen(car);
|
|
353
340
|
}}
|
|
354
341
|
>
|
|
355
342
|
{t('autobahn:card.cta.viewOffer')}
|
package/cards/cardParts.tsx
CHANGED
|
@@ -1103,11 +1103,6 @@ const PpRow: React.FunctionComponent<PpRowProps> = ({ label, value, unit, old, f
|
|
|
1103
1103
|
*/
|
|
1104
1104
|
export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, className, onShowDetails }) => {
|
|
1105
1105
|
const hasLease = car.lease != null;
|
|
1106
|
-
// ADV-181: the hero rate follows the centrally-selected default product (cheaper valid rate,
|
|
1107
|
-
// computed in toAutobahnCar). Leasing leads only when it actually won AND is displayable;
|
|
1108
|
-
// 'buy'/absent both fall back to the financing hero (pre-ADV-181 layout) — the purchase price
|
|
1109
|
-
// keeps its own PriceKauf row below either way, per the desktop rate-first rule.
|
|
1110
|
-
const heroMode: PaymentMode = car.defaultMode === 'leasing' && hasLease ? 'leasing' : 'financing';
|
|
1111
1106
|
// Terms of the config the displayed rate was actually computed with (per-car:
|
|
1112
1107
|
// dealer rate + the user's saved localStorage adjustments) — default config
|
|
1113
1108
|
// terms only when the car doesn't carry its own.
|
|
@@ -1135,70 +1130,42 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, c
|
|
|
1135
1130
|
) : (
|
|
1136
1131
|
<span className="pp-details-link">{t('autobahn:card.price.details')}</span>
|
|
1137
1132
|
);
|
|
1138
|
-
const finTermsLine = (
|
|
1139
|
-
<>
|
|
1140
|
-
{t('autobahn:card.price.financeTerms', {
|
|
1141
|
-
firstInstallment: finFirstInstallment,
|
|
1142
|
-
months: finMonths,
|
|
1143
|
-
count: finMonths,
|
|
1144
|
-
})}{' '}
|
|
1145
|
-
· {detailsLink('financing')}
|
|
1146
|
-
</>
|
|
1147
|
-
);
|
|
1148
|
-
const leasCompactTerms = hasLeasTerms && (
|
|
1149
|
-
<div className="pp-sub-terms">
|
|
1150
|
-
{/* dedicated keys (not a rework of leaseTerms) so mobile/VDP's own reviewed
|
|
1151
|
-
wording there stays untouched, but includes the same Sonderzahlung figure.
|
|
1152
|
-
Split across two lines with an explicit <br/> (column is too narrow for all
|
|
1153
|
-
three figures + Details on one line) — forced right after km/Jahr so "36" and
|
|
1154
|
-
"Mon." always land on the same line instead of an awkward mid-number wrap. */}
|
|
1155
|
-
{t('autobahn:card.price.leaseTermsCompactLine1', {
|
|
1156
|
-
firstInstallment: leasFirstInstallment,
|
|
1157
|
-
mileage: leasMileageStr,
|
|
1158
|
-
})}{' · '}
|
|
1159
|
-
<br />
|
|
1160
|
-
{t('autobahn:card.price.leaseTermsCompactLine2', { months: leasMonths })}{' '}
|
|
1161
|
-
· {detailsLink('leasing')}
|
|
1162
|
-
</div>
|
|
1163
|
-
);
|
|
1164
1133
|
return (
|
|
1165
1134
|
<div className={'pricepaths' + (className ? ' ' + className : '')}>
|
|
1166
|
-
<div className="pp-eyebrow">
|
|
1167
|
-
|
|
1168
|
-
</div>
|
|
1169
|
-
{/* reuses PriceHero's own rendering — the exact same stacked "UVP-Rate ⓘ /
|
|
1135
|
+
<div className="pp-eyebrow">{t('autobahn:card.price.financingFrom')}</div>
|
|
1136
|
+
{/* reuses PriceHero's own financing rendering — the exact same stacked "UVP-Rate ⓘ /
|
|
1170
1137
|
struck price / mtl." group beside the rate that .cc-price and mobile's tab switcher
|
|
1171
1138
|
already use, so the rate + unit never fight for room in one flat row (the earlier
|
|
1172
1139
|
hand-rolled version here put them side by side and wrapped for any 4+-digit rate). */}
|
|
1173
|
-
<PriceHero car={car} mode=
|
|
1174
|
-
|
|
1175
|
-
|
|
1140
|
+
<PriceHero car={car} mode="financing" t={t} alwaysShowUnit />
|
|
1141
|
+
<div className="pp-hero-terms">
|
|
1142
|
+
{t('autobahn:card.price.financeTerms', {
|
|
1143
|
+
firstInstallment: finFirstInstallment,
|
|
1144
|
+
months: finMonths,
|
|
1145
|
+
count: finMonths,
|
|
1146
|
+
})}{' '}
|
|
1147
|
+
· {detailsLink('financing')}
|
|
1148
|
+
</div>
|
|
1149
|
+
{hasLease && (
|
|
1150
|
+
<div className="pp-lease-row">
|
|
1151
|
+
<PpRow label={t('autobahn:card.price.leasing')} value={car.lease as number} unit="mtl." t={t} />
|
|
1176
1152
|
{hasLeasTerms && (
|
|
1177
|
-
<div className="pp-
|
|
1153
|
+
<div className="pp-sub-terms">
|
|
1154
|
+
{/* dedicated keys (not a rework of leaseTerms) so mobile/VDP's own reviewed
|
|
1155
|
+
wording there stays untouched, but includes the same Sonderzahlung figure.
|
|
1156
|
+
Split across two lines with an explicit <br/> (column is too narrow for all
|
|
1157
|
+
three figures + Details on one line) — forced right after km/Jahr so "36" and
|
|
1158
|
+
"Mon." always land on the same line instead of an awkward mid-number wrap. */}
|
|
1178
1159
|
{t('autobahn:card.price.leaseTermsCompactLine1', {
|
|
1179
1160
|
firstInstallment: leasFirstInstallment,
|
|
1180
1161
|
mileage: leasMileageStr,
|
|
1181
1162
|
})}{' · '}
|
|
1163
|
+
<br />
|
|
1182
1164
|
{t('autobahn:card.price.leaseTermsCompactLine2', { months: leasMonths })}{' '}
|
|
1183
1165
|
· {detailsLink('leasing')}
|
|
1184
1166
|
</div>
|
|
1185
1167
|
)}
|
|
1186
|
-
|
|
1187
|
-
<div className="pp-lease-row">
|
|
1188
|
-
<PpRow label={t('autobahn:finance.mode.financing')} value={car.monthly} unit="mtl." t={t} />
|
|
1189
|
-
<div className="pp-sub-terms">{finTermsLine}</div>
|
|
1190
|
-
</div>
|
|
1191
|
-
</>
|
|
1192
|
-
) : (
|
|
1193
|
-
<>
|
|
1194
|
-
<div className="pp-hero-terms">{finTermsLine}</div>
|
|
1195
|
-
{hasLease && (
|
|
1196
|
-
<div className="pp-lease-row">
|
|
1197
|
-
<PpRow label={t('autobahn:card.price.leasing')} value={car.lease as number} unit="mtl." t={t} />
|
|
1198
|
-
{leasCompactTerms}
|
|
1199
|
-
</div>
|
|
1200
|
-
)}
|
|
1201
|
-
</>
|
|
1168
|
+
</div>
|
|
1202
1169
|
)}
|
|
1203
1170
|
</div>
|
|
1204
1171
|
);
|
package/cards/types.ts
CHANGED
|
@@ -57,16 +57,6 @@ export interface AutobahnCar {
|
|
|
57
57
|
// --- price (always real) ---
|
|
58
58
|
/** financing monthly rate in EUR — the LEAD price on every card. */
|
|
59
59
|
monthly: number;
|
|
60
|
-
/**
|
|
61
|
-
* ADV-181 — which payment product leads on this card. Computed centrally by
|
|
62
|
-
* toAutobahnCar via fe-financing's pickDefaultProduct (cheaper valid rate of
|
|
63
|
-
* financing vs leasing; tie → financing; neither valid → 'buy'), then
|
|
64
|
-
* overridden by the user's saved explicit per-car selection when the fe-main
|
|
65
|
-
* transform stamped one (storeCar.selectedProduct). Cards initialise their
|
|
66
|
-
* tab state from this; absent (older producer) → 'financing', the pre-ADV-181
|
|
67
|
-
* behavior. Derived purely from card data, so SSR and hydration agree.
|
|
68
|
-
*/
|
|
69
|
-
defaultMode?: 'financing' | 'leasing' | 'buy';
|
|
70
60
|
/**
|
|
71
61
|
* financing terms the `monthly` rate was computed with — the per-car merged
|
|
72
62
|
* config (dealer rate + the user's saved localStorage adjustments, ADV-157).
|
package/chrome/Footer.tsx
CHANGED
|
@@ -79,6 +79,13 @@ export interface FooterProps {
|
|
|
79
79
|
* strings as the legacy footer so /search stays compliant where prices/rates are shown.
|
|
80
80
|
*/
|
|
81
81
|
t: TranslationFunction;
|
|
82
|
+
/**
|
|
83
|
+
* Adds footnotes ⁵ (Pkw-EnVKV possible CO₂ costs) and ⁶ (§3d KraftStG EV tax exemption) to the
|
|
84
|
+
* disclaimer block, right after ⁴ (price evaluation) and before the WLTP consumption note. Real
|
|
85
|
+
* legal copy tied to the "Laufende Kosten pro Jahr" cards on the CDP — only pass true there
|
|
86
|
+
* (VehicleDetailedNextPage), never site-wide, since every other page shows only footnotes 1-4.
|
|
87
|
+
*/
|
|
88
|
+
showCostFootnotes?: boolean;
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
/**
|
|
@@ -86,7 +93,7 @@ export interface FooterProps {
|
|
|
86
93
|
* disclaimer block, and a copyright + "Nach Oben" bar. Pure/presentational MD3 chrome — renders
|
|
87
94
|
* correctly only inside a `<div className="md3">` subtree (scoped theme at source/theme/autobahn).
|
|
88
95
|
*/
|
|
89
|
-
const Footer: React.FunctionComponent<FooterProps> = ({ t }) => {
|
|
96
|
+
const Footer: React.FunctionComponent<FooterProps> = ({ t, showCostFootnotes }) => {
|
|
90
97
|
const scrollTop = (): void => window.scroll({ top: 0, left: 0, behavior: 'smooth' });
|
|
91
98
|
const now = new Date();
|
|
92
99
|
const year = now.getFullYear();
|
|
@@ -179,6 +186,7 @@ const Footer: React.FunctionComponent<FooterProps> = ({ t }) => {
|
|
|
179
186
|
t('footer:manufacturerUvp'),
|
|
180
187
|
t('footer:uvpComparisonRate'),
|
|
181
188
|
t('footer:priceEvaluation', { date: today }),
|
|
189
|
+
...(showCostFootnotes ? [t('footer:co2CostsInfo'), t('footer:evTaxExemption')] : []),
|
|
182
190
|
t('footer:consumptionInfo'),
|
|
183
191
|
t('footer:vehicleImages'),
|
|
184
192
|
].join('<br />'),
|
|
@@ -39,9 +39,10 @@ export interface IVehiclePriceItemProps {
|
|
|
39
39
|
isMarge?: boolean;
|
|
40
40
|
}
|
|
41
41
|
class VehiclePriceItem extends React.Component<IVehiclePriceItemProps> {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
routeToActiveTab = () => {
|
|
43
|
+
const { priceTitle } = this.props;
|
|
44
|
+
localStorage.setItem('activeTabCDP', priceTitle);
|
|
45
|
+
}
|
|
45
46
|
|
|
46
47
|
render(): React.ReactNode {
|
|
47
48
|
const {
|
|
@@ -102,6 +103,7 @@ class VehiclePriceItem extends React.Component<IVehiclePriceItemProps> {
|
|
|
102
103
|
...routeObj,
|
|
103
104
|
target,
|
|
104
105
|
rel,
|
|
106
|
+
onClick: this.routeToActiveTab,
|
|
105
107
|
className: priceItemCategoryClassName,
|
|
106
108
|
href: url
|
|
107
109
|
})}
|
|
@@ -186,6 +188,7 @@ class VehiclePriceItem extends React.Component<IVehiclePriceItemProps> {
|
|
|
186
188
|
...routeObj,
|
|
187
189
|
target,
|
|
188
190
|
rel,
|
|
191
|
+
onClick: this.routeToActiveTab,
|
|
189
192
|
className: styles.priceItemPrice,
|
|
190
193
|
href: url
|
|
191
194
|
})}
|
|
@@ -166,8 +166,8 @@ const VehicleInfo: React.FC<IVehicleInfoProps> = (props) => {
|
|
|
166
166
|
),
|
|
167
167
|
...routeObj,
|
|
168
168
|
className: wrapClasses,
|
|
169
|
-
// ADV-
|
|
170
|
-
|
|
169
|
+
// ADV-116: opening a CDP from a card defaults to the Financing tab.
|
|
170
|
+
onClick: () => localStorage.setItem('activeTabCDP', t('vehicleProps:title.financing')),
|
|
171
171
|
href: url,
|
|
172
172
|
target,
|
|
173
173
|
rel
|
|
@@ -319,9 +319,8 @@ const VehicleSmallCard: FunctionComponent<IVehicleSmallCardProps> = (props) => {
|
|
|
319
319
|
target,
|
|
320
320
|
rel,
|
|
321
321
|
className: `${styles.redirectClassLink} ${styles.gridAreaTitle}`,
|
|
322
|
-
// ADV-
|
|
323
|
-
|
|
324
|
-
// saved per-car choice / `?mode=` query flag).
|
|
322
|
+
// ADV-116: opening a CDP from a card defaults to the Financing tab.
|
|
323
|
+
onClick: () => localStorage.setItem('activeTabCDP', t('vehicleProps:title.financing')),
|
|
325
324
|
href: url
|
|
326
325
|
})}
|
|
327
326
|
|
|
@@ -374,9 +373,8 @@ const VehicleSmallCard: FunctionComponent<IVehicleSmallCardProps> = (props) => {
|
|
|
374
373
|
target,
|
|
375
374
|
rel,
|
|
376
375
|
className: `${styles.redirectClassLink} ${styles.gridAreaImage}`,
|
|
377
|
-
// ADV-
|
|
378
|
-
|
|
379
|
-
// saved per-car choice / `?mode=` query flag).
|
|
376
|
+
// ADV-116: opening a CDP from a card defaults to the Financing tab.
|
|
377
|
+
onClick: () => localStorage.setItem('activeTabCDP', t('vehicleProps:title.financing')),
|
|
380
378
|
href: url
|
|
381
379
|
})}
|
|
382
380
|
<VehicleInfo {...vehicleInfoProps} />
|
package/data/toAutobahnCar.ts
CHANGED
|
@@ -23,8 +23,6 @@
|
|
|
23
23
|
// Every field path below was confirmed against the real code (file:line in
|
|
24
24
|
// ./fieldMapping.ts), not guessed.
|
|
25
25
|
|
|
26
|
-
import { pickDefaultProduct } from '@bytebrand/fe-financing';
|
|
27
|
-
|
|
28
26
|
import { getPriceRating, getPredictablePrice } from '../utils/priceRating';
|
|
29
27
|
import { formatTimestamp } from '../utils/DateUtils';
|
|
30
28
|
import { getPowerLabel } from '../utils/power';
|
|
@@ -429,27 +427,6 @@ export function toAutobahnCar(storeCar: any, deps: ToAutobahnCarDeps = {}): Auto
|
|
|
429
427
|
const leaseMonths = leasingActive ? num(price?.leasing?.paybackPeriod) : undefined;
|
|
430
428
|
const leaseFirstInstallment = leasingActive ? num(price?.leasing?.firstInstallment) : undefined;
|
|
431
429
|
|
|
432
|
-
// --- ADV-181: leading payment product — ONE central rule for every card surface. ---
|
|
433
|
-
// Automatic default = fe-financing's pickDefaultProduct on the same displayed rates the
|
|
434
|
-
// card renders (cheaper valid rate wins, tie → financing, neither valid → 'buy'). The
|
|
435
|
-
// user's saved explicit per-car choice (storeCar.selectedProduct, stamped by fe-main's
|
|
436
|
-
// transformDataForVehicles from FinancingStore) overrides the automatic rule — but only
|
|
437
|
-
// while that product is actually displayable on this card, so a stale "leasing" choice on
|
|
438
|
-
// a car that aged out of leasing eligibility falls back to the automatic rule instead of
|
|
439
|
-
// selecting a product the card can't show. Pure input → pure output: SSR (fe-landing)
|
|
440
|
-
// computes the identical value from the same payload, so hydration never flips the tab.
|
|
441
|
-
const picked = pickDefaultProduct({
|
|
442
|
-
financing: { monthlyInstallment: monthly },
|
|
443
|
-
leasing: { isActive: leasingActive, monthlyInstallment: lease ?? null },
|
|
444
|
-
});
|
|
445
|
-
const savedProduct = (storeCar as any).selectedProduct;
|
|
446
|
-
const defaultMode: 'financing' | 'leasing' | 'buy' =
|
|
447
|
-
savedProduct === 'leasing' && lease != null
|
|
448
|
-
? 'leasing'
|
|
449
|
-
: savedProduct === 'financing' && monthly > 0
|
|
450
|
-
? 'financing'
|
|
451
|
-
: picked.product;
|
|
452
|
-
|
|
453
430
|
// --- optional: list price / savings — ONLY from a real strike price ---
|
|
454
431
|
// transformDataForVehicles exposes oldMonthlyInstallment / highestPrice via
|
|
455
432
|
// the financing spread, and price.historyPriceDifference. There is NO reliable
|
|
@@ -517,7 +494,6 @@ export function toAutobahnCar(storeCar: any, deps: ToAutobahnCarDeps = {}): Auto
|
|
|
517
494
|
|
|
518
495
|
// price
|
|
519
496
|
monthly,
|
|
520
|
-
defaultMode,
|
|
521
497
|
...(finFirstInstallment != null ? { finFirstInstallment } : {}),
|
|
522
498
|
...(finMonths != null ? { finMonths } : {}),
|
|
523
499
|
price: purchase,
|
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.135",
|
|
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
package/vdp/VdpModules.tsx
CHANGED
|
@@ -417,7 +417,7 @@ const footnoteStyle: React.CSSProperties = {
|
|
|
417
417
|
fontWeight: 700,
|
|
418
418
|
fontSize: 18,
|
|
419
419
|
lineHeight: '24px',
|
|
420
|
-
color: '#
|
|
420
|
+
color: '#0E2238',
|
|
421
421
|
};
|
|
422
422
|
|
|
423
423
|
const summaryCardTitleStyle: React.CSSProperties = {
|
|
@@ -445,9 +445,8 @@ export const VdpEnergyTaxSummary: React.FunctionComponent<VdpEnergyTaxSummaryPro
|
|
|
445
445
|
display: 'flex',
|
|
446
446
|
width: '100%',
|
|
447
447
|
gap: 4,
|
|
448
|
-
background: '
|
|
449
|
-
borderRadius:
|
|
450
|
-
border: '1px solid var(--md-outline-variant)',
|
|
448
|
+
background: '#F5F7FA',
|
|
449
|
+
borderRadius: 12,
|
|
451
450
|
}}
|
|
452
451
|
>
|
|
453
452
|
<div className="vdp-ets-col vdp-ets-col--first" style={{ flex: 1 }}>
|
|
@@ -500,9 +499,8 @@ export const VdpCo2CostsSummary: React.FunctionComponent<VdpCo2CostsSummaryProps
|
|
|
500
499
|
display: 'flex',
|
|
501
500
|
width: '100%',
|
|
502
501
|
gap: 4,
|
|
503
|
-
background: '
|
|
504
|
-
borderRadius:
|
|
505
|
-
border: '1px solid var(--md-outline-variant)',
|
|
502
|
+
background: '#F5F7FA',
|
|
503
|
+
borderRadius: 12,
|
|
506
504
|
}}
|
|
507
505
|
>
|
|
508
506
|
{rows.map((row, i) => (
|
|
@@ -520,60 +518,6 @@ export const VdpCo2CostsSummary: React.FunctionComponent<VdpCo2CostsSummaryProps
|
|
|
520
518
|
);
|
|
521
519
|
};
|
|
522
520
|
|
|
523
|
-
// ---------------------------------------------------------------------------
|
|
524
|
-
// Legal footnote text for the "⁵"/"⁶" markers used in VdpCo2CostsSummary/VdpEnergyTaxSummary
|
|
525
|
-
// above — real Pkw-EnVKV/§3d KraftStG disclosure copy (verbatim, not paraphrased), plain
|
|
526
|
-
// text (no card/background). Needs-data: each paragraph only shows when its corresponding
|
|
527
|
-
// card above actually rendered (never an orphaned footnote for a marker nobody saw).
|
|
528
|
-
// ---------------------------------------------------------------------------
|
|
529
|
-
|
|
530
|
-
export interface VdpCostFootnotesProps {
|
|
531
|
-
vehicle: VehicleDetail;
|
|
532
|
-
t: TranslationFunction;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const costFootnoteStyle: React.CSSProperties = {
|
|
536
|
-
fontFamily: 'Arimo',
|
|
537
|
-
fontStyle: 'normal',
|
|
538
|
-
fontWeight: 700,
|
|
539
|
-
fontSize: 13.5,
|
|
540
|
-
lineHeight: '16px',
|
|
541
|
-
color: '#000000',
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
export const VdpCostFootnotes: React.FunctionComponent<VdpCostFootnotesProps> = ({ vehicle, t }) => {
|
|
545
|
-
const showCo2Footnote = !!vehicle.co2CostsSummary;
|
|
546
|
-
const showTaxFootnote = !!vehicle.energyTaxSummary;
|
|
547
|
-
if (!showCo2Footnote && !showTaxFootnote) return null;
|
|
548
|
-
return (
|
|
549
|
-
<div id="vdp-cost-footnotes" style={costFootnoteStyle}>
|
|
550
|
-
{showCo2Footnote && (
|
|
551
|
-
<>
|
|
552
|
-
{t('vehicleProps:value.co2CostsFootnoteTitle')}
|
|
553
|
-
<br />
|
|
554
|
-
<br />
|
|
555
|
-
{t('vehicleProps:value.co2CostsFootnote')}
|
|
556
|
-
</>
|
|
557
|
-
)}
|
|
558
|
-
{showCo2Footnote && showTaxFootnote && (
|
|
559
|
-
<>
|
|
560
|
-
<br />
|
|
561
|
-
<br />
|
|
562
|
-
<br />
|
|
563
|
-
</>
|
|
564
|
-
)}
|
|
565
|
-
{showTaxFootnote && (
|
|
566
|
-
<>
|
|
567
|
-
{t('vehicleProps:value.taxExemptFootnoteTitle')}
|
|
568
|
-
<br />
|
|
569
|
-
<br />
|
|
570
|
-
{t('vehicleProps:value.taxExemptFootnote')}
|
|
571
|
-
</>
|
|
572
|
-
)}
|
|
573
|
-
</div>
|
|
574
|
-
);
|
|
575
|
-
};
|
|
576
|
-
|
|
577
521
|
// ---------------------------------------------------------------------------
|
|
578
522
|
// Equipment list (grouped + searchable)
|
|
579
523
|
// ---------------------------------------------------------------------------
|
|
@@ -734,6 +678,19 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
|
|
|
734
678
|
</div>
|
|
735
679
|
{/* no EnVKV footer line — Figma drops it (the * on the heading refers to the page-footer
|
|
736
680
|
disclaimer instead); the scale caption above carries the WLTP-kombiniert wording */}
|
|
681
|
+
|
|
682
|
+
{/* Pkw-EnVKV mandatory cost disclosure — moved to the bottom of this same card (Figma) rather
|
|
683
|
+
than standing as its own top-level block; each still gates on its own vehicle data. */}
|
|
684
|
+
{vehicle.energyTaxSummary && (
|
|
685
|
+
<div style={{ paddingTop: 20, borderTop: '1px solid #C5C6D2' }}>
|
|
686
|
+
<VdpEnergyTaxSummary vehicle={vehicle} t={t} />
|
|
687
|
+
</div>
|
|
688
|
+
)}
|
|
689
|
+
{vehicle.co2CostsSummary && (
|
|
690
|
+
<div style={{ marginTop: 22 }}>
|
|
691
|
+
<VdpCo2CostsSummary vehicle={vehicle} t={t} />
|
|
692
|
+
</div>
|
|
693
|
+
)}
|
|
737
694
|
</div>
|
|
738
695
|
);
|
|
739
696
|
};
|
|
@@ -1649,18 +1606,12 @@ export interface VdpStickyBarProps {
|
|
|
1649
1606
|
show: boolean;
|
|
1650
1607
|
/** primary CTA click. */
|
|
1651
1608
|
onSecure?: () => void;
|
|
1652
|
-
/** ADV-181: monthly rate for the CURRENTLY selected payment product (fin/leasing differ) —
|
|
1653
|
-
* falls back to vehicle.monthly (the financing rate) when omitted, the pre-ADV-181 display. */
|
|
1654
|
-
monthly?: number;
|
|
1655
|
-
/** ADV-181: label of the selected product ("Finanzierung"/"Leasing") — replaces the generic
|
|
1656
|
-
* "ab" eyebrow so the sticky bar's label always matches the rate it shows. */
|
|
1657
|
-
productLabel?: string;
|
|
1658
1609
|
/** i18n function (drilled from the page). */
|
|
1659
1610
|
t: TranslationFunction;
|
|
1660
1611
|
}
|
|
1661
1612
|
|
|
1662
1613
|
/** Desktop sticky summary bar (AS24 pattern) — docks to top, driven by `show`. */
|
|
1663
|
-
export const VdpStickyBar: React.FunctionComponent<VdpStickyBarProps> = ({ vehicle, image, show, onSecure,
|
|
1614
|
+
export const VdpStickyBar: React.FunctionComponent<VdpStickyBarProps> = ({ vehicle, image, show, onSecure, t }) => (
|
|
1664
1615
|
<div className={'vdp-stickytop hide-mobile' + (show ? ' vdp-stickytop--on' : '')}>
|
|
1665
1616
|
<div className="container" style={{ display: 'flex', alignItems: 'center', gap: 18, padding: '10px 24px' }}>
|
|
1666
1617
|
<img
|
|
@@ -1686,9 +1637,9 @@ export const VdpStickyBar: React.FunctionComponent<VdpStickyBarProps> = ({ vehic
|
|
|
1686
1637
|
</div>
|
|
1687
1638
|
<div style={{ textAlign: 'right', flex: 'none' }}>
|
|
1688
1639
|
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, justifyContent: 'flex-end' }}>
|
|
1689
|
-
<span className="md-label-sm">{
|
|
1640
|
+
<span className="md-label-sm">{t('autobahn:sticky.from')}</span>
|
|
1690
1641
|
<span className="num" style={{ fontWeight: 700, fontSize: 20, color: 'var(--md-primary)', letterSpacing: '-.4px' }}>
|
|
1691
|
-
{
|
|
1642
|
+
{vehicle.monthly} €
|
|
1692
1643
|
</span>
|
|
1693
1644
|
<span className="md-label-sm">{t('autobahn:sticky.perMonth')}</span>
|
|
1694
1645
|
</div>
|
package/vdp/index.ts
CHANGED
|
@@ -31,7 +31,6 @@ export {
|
|
|
31
31
|
VdpSpecTable,
|
|
32
32
|
VdpEnergyTaxSummary,
|
|
33
33
|
VdpCo2CostsSummary,
|
|
34
|
-
VdpCostFootnotes,
|
|
35
34
|
EquipmentList,
|
|
36
35
|
Co2Label,
|
|
37
36
|
VehicleDescription,
|
|
@@ -54,7 +53,6 @@ export type {
|
|
|
54
53
|
VdpSpecTableProps,
|
|
55
54
|
VdpEnergyTaxSummaryProps,
|
|
56
55
|
VdpCo2CostsSummaryProps,
|
|
57
|
-
VdpCostFootnotesProps,
|
|
58
56
|
EquipmentListProps,
|
|
59
57
|
Co2LabelProps,
|
|
60
58
|
VehicleDescriptionProps,
|