@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 CHANGED
@@ -18,7 +18,7 @@
18
18
  import React, { useState } from 'react';
19
19
  import { TranslationFunction } from '../types';
20
20
 
21
- import { CardCarousel, IconButton, StatChip, SaveChip, Button } from '../ui';
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
- PriceMeter,
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
- <div style={{ position: 'absolute', bottom: 10, left: 10, zIndex: 3 }}>
138
- <GalleryCounter current={photoIndex + 1} total={car.photos} />
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: '12px 14px 14px', display: 'flex', flexDirection: 'column', gap: 9, flex: 1 }}>
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={{ flex: 1 }} />
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.rating && (
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
- <PriceMeter car={car} t={t} />
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 t={t} car={car} style={{ marginTop: 8 }} />
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={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
132
- <div style={{ minWidth: 0 }}>
133
- <div className="md-title" style={{ fontSize: 18, lineHeight: 1.2 }}>
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 onClick={(e) => e.stopPropagation()} className="hide-mobile">
141
- <IconButton name="heart" active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
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: Finanzierung/Leasing/Kaufen tabs pick ONE hero price (same tabbed
164
- component the grid CarCard uses), then the fixed Preisbewertung meter and the CTA,
165
- anchored to the bottom (margin-top:auto) so the button sits flush at the foot. */}
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
  );