@bytebrand/fe-ui-core-autobahn 1.0.59 → 1.0.61
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 +55 -19
- package/cards/CarRow.tsx +66 -16
- package/cards/cardParts.tsx +303 -31
- package/cards/index.ts +4 -0
- package/cards/types.ts +7 -0
- package/constants.ts +31 -31
- package/data/toAutobahnCar.ts +13 -3
- package/locales/de/autobahn.json +34 -20
- package/locales/en/autobahn.json +33 -19
- package/package.json +1 -1
- package/theme/md3.css +557 -51
- package/utils/financeFormat.ts +62 -62
- package/utils/priceRating.ts +58 -58
- package/vdp/FinanceCalculator.tsx +127 -96
- package/vdp/VdpModules.tsx +219 -44
package/cards/CarCard.tsx
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import React, { useState } from 'react';
|
|
19
19
|
import { TranslationFunction } from '../types';
|
|
20
20
|
|
|
21
|
-
import { CardCarousel, IconButton,
|
|
21
|
+
import { CardCarousel, IconButton, SaveChip, Button } from '../ui';
|
|
22
22
|
import {
|
|
23
23
|
SpecLine,
|
|
24
24
|
EcoLine,
|
|
@@ -28,15 +28,18 @@ import {
|
|
|
28
28
|
OfferBadge,
|
|
29
29
|
PriceTabs,
|
|
30
30
|
PriceHero,
|
|
31
|
-
|
|
31
|
+
PriceEvalCol,
|
|
32
|
+
PriceTerms,
|
|
32
33
|
CardCtaRow,
|
|
33
34
|
GalleryCounter,
|
|
35
|
+
GalleryActivityPill,
|
|
34
36
|
cardSpec,
|
|
35
37
|
PaymentMode,
|
|
36
|
-
ratingTone,
|
|
37
38
|
callDealerSalesPhone,
|
|
39
|
+
DEALER_SALES_PHONE,
|
|
38
40
|
} from './cardParts';
|
|
39
41
|
import { AutobahnCar } from './types';
|
|
42
|
+
import VdpViewer from '../vdp/VdpViewer';
|
|
40
43
|
|
|
41
44
|
export interface CarCardProps {
|
|
42
45
|
/** fully-resolved car view-model. */
|
|
@@ -85,6 +88,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
85
88
|
}) => {
|
|
86
89
|
const [mode, setMode] = useState<PaymentMode>('financing');
|
|
87
90
|
const [photoIndex, setPhotoIndex] = useState(0);
|
|
91
|
+
const [viewerOpen, setViewerOpen] = useState(false);
|
|
88
92
|
const showSave = car.savings != null && car.savings > 600;
|
|
89
93
|
const showOfferBadge = !!(car.isNew || car.drop != null || showSave || car.dropPct != null || car.zeroDown);
|
|
90
94
|
return (
|
|
@@ -102,6 +106,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
102
106
|
<CardCarousel images={car.gallery} height="100%" fit="contain" onChange={setPhotoIndex} />
|
|
103
107
|
{/* max two overlays: heart + ONE deal signal — rating moved next to the price */}
|
|
104
108
|
<div
|
|
109
|
+
className="cc-fav"
|
|
105
110
|
style={{ position: 'absolute', top: 6, right: 6, zIndex: 3 }}
|
|
106
111
|
onClick={(e) => e.stopPropagation()}
|
|
107
112
|
>
|
|
@@ -112,6 +117,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
112
117
|
SaveChip; each only renders when its real data exists. */}
|
|
113
118
|
{(showOfferBadge || car.dropPct != null) && (
|
|
114
119
|
<div
|
|
120
|
+
className="cc-badges"
|
|
115
121
|
style={{
|
|
116
122
|
position: 'absolute',
|
|
117
123
|
top: 10,
|
|
@@ -133,12 +139,22 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
133
139
|
<OfferBadge show={showOfferBadge} t={t} />
|
|
134
140
|
</div>
|
|
135
141
|
)}
|
|
136
|
-
{/* bottom-left: expand icon + "current|total" photo counter (Figma
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
{/* bottom-left: expand icon + "current | total" photo counter (Figma light pill, node
|
|
143
|
+
281:1016) — opens the full VdpViewer lightbox; stopPropagation keeps the click off
|
|
144
|
+
the card's own onOpen (React portals still bubble through the React tree, not the
|
|
145
|
+
DOM tree, so this guard is needed even though VdpViewer renders via createPortal). */}
|
|
146
|
+
<div
|
|
147
|
+
style={{ position: 'absolute', bottom: 10, left: 10, zIndex: 3 }}
|
|
148
|
+
onClick={(e) => e.stopPropagation()}
|
|
149
|
+
>
|
|
150
|
+
<GalleryCounter current={photoIndex + 1} total={car.photos} light onClick={() => setViewerOpen(true)} />
|
|
151
|
+
</div>
|
|
152
|
+
{/* bottom-right: eye/heart activity pill — real stats only (Figma "Push Chips / Likes", node 297:1037) */}
|
|
153
|
+
<div style={{ position: 'absolute', bottom: 10, right: 10, zIndex: 3 }}>
|
|
154
|
+
<GalleryActivityPill viewsCount={car.viewsCount} favoritesCount={car.favoritesCount} />
|
|
139
155
|
</div>
|
|
140
156
|
</div>
|
|
141
|
-
<div style={{ padding: '
|
|
157
|
+
<div style={{ padding: '10px 18px', display: 'flex', flexDirection: 'column', gap: 0, flex: 1 }}>
|
|
142
158
|
<div>
|
|
143
159
|
<div className="md-title" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
144
160
|
{car.make} {car.model}
|
|
@@ -148,23 +164,30 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
148
164
|
</div>
|
|
149
165
|
</div>
|
|
150
166
|
<SpecLine items={cardSpec(car)} oneLine />
|
|
151
|
-
<div style={{
|
|
152
|
-
<div className="cc-price" style={{ marginTop: 4, borderTop: '1px solid var(--md-outline-variant)', paddingTop: 10 }}>
|
|
167
|
+
<div className="cc-price" style={{ marginTop: 4, borderTop: '1px solid var(--md-outline-variant)' }}>
|
|
153
168
|
<PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
|
|
154
169
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 10 }}>
|
|
155
170
|
<PriceHero car={car} mode={mode} t={t} />
|
|
156
|
-
{car
|
|
157
|
-
<StatChip
|
|
158
|
-
tone={ratingTone(car.rating)}
|
|
159
|
-
icon={ratingTone(car.rating) === 'good' ? 'check' : 'minus'}
|
|
160
|
-
>
|
|
161
|
-
{t('autobahn:tier.' + car.rating)}
|
|
162
|
-
</StatChip>
|
|
163
|
-
)}
|
|
171
|
+
<PriceEvalCol car={car} t={t} />
|
|
164
172
|
</div>
|
|
165
|
-
|
|
173
|
+
{/* same fallback CardCtaRow already uses below: pages that haven't wired a real
|
|
174
|
+
"Rate anpassen" sheet yet still get a working "Details" link (opens the car). */}
|
|
175
|
+
<PriceTerms car={car} mode={mode} onOpenRateAdjust={onOpenRateAdjust || (() => onOpen(car))} t={t} />
|
|
166
176
|
</div>
|
|
167
|
-
<EcoLine
|
|
177
|
+
<EcoLine
|
|
178
|
+
t={t}
|
|
179
|
+
car={car}
|
|
180
|
+
style={{
|
|
181
|
+
marginTop: 'auto',
|
|
182
|
+
fontFamily: 'Arial, Helvetica, sans-serif',
|
|
183
|
+
fontStyle: 'normal',
|
|
184
|
+
fontWeight: 400,
|
|
185
|
+
fontSize: 11,
|
|
186
|
+
lineHeight: '11px',
|
|
187
|
+
textAlign: 'justify',
|
|
188
|
+
color: '#0e2238',
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
168
191
|
{/* Mobile-only row (CardCtaRow itself carries the show-mobile class) — always
|
|
169
192
|
rendered, with real fallbacks the page can override once it wires its own
|
|
170
193
|
handlers (e.g. the "Rate anpassen" FinanceDetailsModal sheet). Never hidden. */}
|
|
@@ -191,6 +214,19 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
191
214
|
</div>
|
|
192
215
|
)}
|
|
193
216
|
</div>
|
|
217
|
+
{viewerOpen && (
|
|
218
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
219
|
+
<VdpViewer
|
|
220
|
+
images={car.galleryLarge || car.gallery}
|
|
221
|
+
start={photoIndex}
|
|
222
|
+
onClose={() => setViewerOpen(false)}
|
|
223
|
+
t={t}
|
|
224
|
+
phone={DEALER_SALES_PHONE}
|
|
225
|
+
onCall={onCall || callDealerSalesPhone}
|
|
226
|
+
onEmail={onRequestOffer || (() => onOpen(car))}
|
|
227
|
+
/>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
194
230
|
</article>
|
|
195
231
|
);
|
|
196
232
|
};
|
package/cards/CarRow.tsx
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
OfferBadge,
|
|
32
32
|
PriceTabs,
|
|
33
33
|
PriceHero,
|
|
34
|
+
PricePaths,
|
|
35
|
+
PriceKauf,
|
|
34
36
|
DropChip,
|
|
35
37
|
PercentChip,
|
|
36
38
|
ZeroDownChip,
|
|
@@ -104,7 +106,8 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
|
|
|
104
106
|
<div style={{ display: 'flex', flexWrap: 'nowrap', alignItems: 'center', gap: 5 }}>
|
|
105
107
|
{car.isNew && <span className="neu-badge">NEU</span>}
|
|
106
108
|
{car.drop != null ? <DropChip t={t} amount={car.drop} /> : showSave && <SaveChip t={t} amount={car.savings!} />}
|
|
107
|
-
<ZeroDownChip t={t} show={car.zeroDown} compact />
|
|
109
|
+
<ZeroDownChip t={t} show={car.zeroDown} compact className="show-mobile" />
|
|
110
|
+
<ZeroDownChip t={t} show={car.zeroDown} className="hide-mobile" />
|
|
108
111
|
<PercentChip pct={car.dropPct} />
|
|
109
112
|
</div>
|
|
110
113
|
<OfferBadge show={showOfferBadge} t={t} />
|
|
@@ -121,6 +124,14 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
|
|
|
121
124
|
>
|
|
122
125
|
<IconButton name="heart" filled active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
|
|
123
126
|
</div>
|
|
127
|
+
{/* desktop: heart sits on the photo itself (Figma "mcard"), not next to the title. */}
|
|
128
|
+
<div
|
|
129
|
+
onClick={(e) => e.stopPropagation()}
|
|
130
|
+
className="hide-mobile"
|
|
131
|
+
style={{ position: 'absolute', top: 10, right: 10, zIndex: 3 }}
|
|
132
|
+
>
|
|
133
|
+
<IconButton name="heart" active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
|
|
134
|
+
</div>
|
|
124
135
|
{/* activity/trust badges — overlaid on the studio-white floor at the bottom of the photo
|
|
125
136
|
itself (PM ref), not a separate divided strip below it. Bottom-right, raised above the
|
|
126
137
|
carousel dots and the photo-count badge (positioning lives in .gallery-badges, md3.css). */}
|
|
@@ -128,17 +139,12 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
|
|
|
128
139
|
</div>
|
|
129
140
|
|
|
130
141
|
<div className="listcard-info">
|
|
131
|
-
<div style={{
|
|
132
|
-
<div style={{
|
|
133
|
-
|
|
134
|
-
{car.make} {car.model}
|
|
135
|
-
</div>
|
|
136
|
-
<div className="md-body-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
137
|
-
{car.trim}
|
|
138
|
-
</div>
|
|
142
|
+
<div style={{ minWidth: 0 }}>
|
|
143
|
+
<div className="md-title" style={{ fontSize: 18, lineHeight: 1.2 }}>
|
|
144
|
+
{car.make} {car.model}
|
|
139
145
|
</div>
|
|
140
|
-
<div
|
|
141
|
-
|
|
146
|
+
<div className="md-body-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
147
|
+
{car.trim}
|
|
142
148
|
</div>
|
|
143
149
|
</div>
|
|
144
150
|
<div className="specstrip">
|
|
@@ -160,12 +166,13 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
|
|
|
160
166
|
<EcoLine t={t} car={car} />
|
|
161
167
|
</div>
|
|
162
168
|
|
|
163
|
-
{/* price column
|
|
164
|
-
|
|
165
|
-
|
|
169
|
+
{/* price column — mobile keeps the Finanzierung/Leasing/Kaufen tab switcher (ONE hero price
|
|
170
|
+
at a time); desktop shows all three payment paths stacked at once (Figma "mcard"), via
|
|
171
|
+
the previously-unused PricePaths/PriceKauf pair. Both variants share the same
|
|
172
|
+
Preisbewertung meter + CTA, anchored to the bottom (margin-top:auto on .pp-purchase). */}
|
|
166
173
|
<div className="listcard-price">
|
|
167
|
-
<PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
|
|
168
|
-
<div className="pp-purchase">
|
|
174
|
+
<PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} className="show-mobile" />
|
|
175
|
+
<div className="pp-purchase show-mobile">
|
|
169
176
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
170
177
|
<PriceHero car={car} mode={mode} t={t} />
|
|
171
178
|
{car.rating && (
|
|
@@ -204,6 +211,49 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
|
|
|
204
211
|
)}
|
|
205
212
|
<PpActivity t={t} car={car} />
|
|
206
213
|
</div>
|
|
214
|
+
|
|
215
|
+
<PricePaths car={car} t={t} className="hide-mobile" />
|
|
216
|
+
<div className="pp-purchase hide-mobile">
|
|
217
|
+
<PriceKauf car={car} t={t} />
|
|
218
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
219
|
+
{car.rating && (
|
|
220
|
+
<StatChip tone={ratingTone(car.rating)} icon={ratingTone(car.rating) === 'good' ? 'check' : 'minus'}>
|
|
221
|
+
{t('autobahn:tier.' + car.rating)}
|
|
222
|
+
</StatChip>
|
|
223
|
+
)}
|
|
224
|
+
<div style={{ flex: 1 }}>
|
|
225
|
+
<PriceMeter t={t} car={car} />
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
<div className="pp-cta">
|
|
229
|
+
<Button
|
|
230
|
+
variant="filled"
|
|
231
|
+
block
|
|
232
|
+
iconRight="arrow-right"
|
|
233
|
+
onClick={(e) => {
|
|
234
|
+
e.stopPropagation();
|
|
235
|
+
onOpen(car);
|
|
236
|
+
}}
|
|
237
|
+
>
|
|
238
|
+
{t('autobahn:card.cta.viewOffer')}
|
|
239
|
+
</Button>
|
|
240
|
+
</div>
|
|
241
|
+
{adminAction && (
|
|
242
|
+
<div className="pp-cta">
|
|
243
|
+
<Button
|
|
244
|
+
variant="outlined"
|
|
245
|
+
block
|
|
246
|
+
onClick={(e) => {
|
|
247
|
+
e.stopPropagation();
|
|
248
|
+
adminAction.onClick();
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
{adminAction.label}
|
|
252
|
+
</Button>
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
<PpActivity t={t} car={car} />
|
|
256
|
+
</div>
|
|
207
257
|
</div>
|
|
208
258
|
</article>
|
|
209
259
|
);
|