@bytebrand/fe-ui-core-autobahn 1.0.51 → 1.0.54
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 +101 -77
- package/cards/CarRow.tsx +50 -24
- package/cards/cardParts.tsx +204 -33
- package/cards/index.ts +13 -0
- package/cards/types.d.ts +112 -0
- package/cards/types.js +14 -0
- package/cards/types.ts +8 -0
- package/constants/autobahn.d.ts +22 -0
- package/constants/autobahn.js +57 -0
- package/constants/autobahn.ts +68 -0
- package/constants/core.d.ts +152 -0
- package/constants/core.js +125 -0
- package/constants/core.ts +1 -1
- package/constants/price.d.ts +2 -0
- package/constants/price.js +11 -0
- package/constants/selectors.d.ts +11 -0
- package/constants/selectors.js +32 -0
- package/constants.ts +31 -91
- package/data/toAutobahnCar.d.ts +89 -0
- package/data/toAutobahnCar.js +421 -0
- package/data/toAutobahnCar.ts +27 -6
- package/locales/de/autobahn.json +3 -0
- package/locales/en/autobahn.json +3 -0
- package/package.json +5 -2
- package/theme/md3.css +25 -1
- package/tsconfig.node-safe.json +25 -0
- package/utils/CommonUtils.ts +5 -48
- package/utils/DateUtils.d.ts +21 -0
- package/utils/DateUtils.js +97 -0
- package/utils/DateUtils.ts +1 -1
- package/utils/financeFormat.d.ts +32 -0
- package/utils/financeFormat.js +42 -0
- package/utils/financeFormat.ts +62 -0
- package/utils/index.ts +11 -1
- package/utils/power.d.ts +1 -0
- package/utils/power.js +14 -0
- package/utils/priceRating.d.ts +3 -0
- package/utils/priceRating.js +55 -0
- package/utils/priceRating.ts +57 -0
- package/vdp/VdpModules.tsx +16 -14
package/constants.ts
CHANGED
|
@@ -1,91 +1,31 @@
|
|
|
1
|
-
// Autobahn (auto.de redesign) — shared lookup constants.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
] as const;
|
|
33
|
-
export type SortLabel = (typeof SORTS)[number];
|
|
34
|
-
|
|
35
|
-
/** Quick-filter chips for the srp-bar: [lucide icon, label, defaultActive?]. */
|
|
36
|
-
export type QuickFilter = readonly [icon: string, label: string, defaultActive?: boolean];
|
|
37
|
-
export const QUICK_FILTERS: readonly QuickFilter[] = [
|
|
38
|
-
['sliders-horizontal', 'Filter', true],
|
|
39
|
-
['fuel', 'Diesel'],
|
|
40
|
-
['cog', 'Automatik'],
|
|
41
|
-
['battery-charging', 'Hybrid'],
|
|
42
|
-
['gauge', 'bis 50.000 km'],
|
|
43
|
-
['wallet', 'bis 300 €/mtl.'],
|
|
44
|
-
['calendar', 'ab 2022'],
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
/** Brand shortlist for the Marke select (full list comes from appStore.manufacturers at runtime). */
|
|
48
|
-
export const BRANDS: readonly string[] = [
|
|
49
|
-
'VW', 'Mercedes-Benz', 'BMW', 'Audi', 'Opel', 'Škoda', 'Ford', 'Hyundai',
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
/** Body-type tiles: [label, lucide icon, placeholder count]. Counts are design placeholders (W2 → real aggregations). */
|
|
53
|
-
export type BodyType = readonly [label: string, icon: string, placeholderCount: string];
|
|
54
|
-
export const BODY_TYPES: readonly BodyType[] = [
|
|
55
|
-
['Kleinwagen', 'car', '4.812'],
|
|
56
|
-
['Limousine', 'car-front', '5.530'],
|
|
57
|
-
['Kombi', 'caravan', '6.118'],
|
|
58
|
-
['SUV', 'truck', '7.940'],
|
|
59
|
-
['Cabrio', 'wind', '1.270'],
|
|
60
|
-
['Van', 'bus', '2.691'],
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
/** Marker text rendered wherever the design shows a field we have no real data source for yet. */
|
|
64
|
-
export const NEEDS_DATA_LABEL = 'Daten folgt';
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Legacy shared constants (moved here from fe-ui-core).
|
|
68
|
-
//
|
|
69
|
-
// This block mirrors the public symbol set of the old
|
|
70
|
-
// `@bytebrand/fe-ui-core/constants` barrel so consumers can swap that
|
|
71
|
-
// specifier for `@bytebrand/fe-ui-core-autobahn/constants` 1:1. The
|
|
72
|
-
// implementations live under ./constants/ (core.ts, app.ts, price.ts,
|
|
73
|
-
// Search.ts, …); fe-ui-core re-exports them via shims until it is deleted.
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
|
|
76
|
-
export {
|
|
77
|
-
offers,
|
|
78
|
-
checkoutOffers,
|
|
79
|
-
checkoutWarranties,
|
|
80
|
-
checkoutSteps,
|
|
81
|
-
checkoutSubStep,
|
|
82
|
-
DELIVERY_PERIODS_EXTRA,
|
|
83
|
-
THREE,
|
|
84
|
-
WLTP_ENERGY_EFFICIENCY_CLASS
|
|
85
|
-
} from './constants/core';
|
|
86
|
-
|
|
87
|
-
export { CUSTOM_ERROR_MESSAGES, supportPhone, supportPhoneLink } from './constants/app';
|
|
88
|
-
|
|
89
|
-
export { priceRatings } from './constants/price';
|
|
90
|
-
|
|
91
|
-
export { THROTTLE_DELAY } from './constants/Search';
|
|
1
|
+
// Autobahn (auto.de redesign) — shared lookup constants barrel.
|
|
2
|
+
// Pure data re-exported from ./constants/autobahn (kept import-safe for plain Node consumers,
|
|
3
|
+
// see that file's header) plus the legacy fe-ui-core shim re-exports below.
|
|
4
|
+
export * from './constants/autobahn';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Legacy shared constants (moved here from fe-ui-core).
|
|
8
|
+
//
|
|
9
|
+
// This block mirrors the public symbol set of the old
|
|
10
|
+
// `@bytebrand/fe-ui-core/constants` barrel so consumers can swap that
|
|
11
|
+
// specifier for `@bytebrand/fe-ui-core-autobahn/constants` 1:1. The
|
|
12
|
+
// implementations live under ./constants/ (core.ts, app.ts, price.ts,
|
|
13
|
+
// Search.ts, …); fe-ui-core re-exports them via shims until it is deleted.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
offers,
|
|
18
|
+
checkoutOffers,
|
|
19
|
+
checkoutWarranties,
|
|
20
|
+
checkoutSteps,
|
|
21
|
+
checkoutSubStep,
|
|
22
|
+
DELIVERY_PERIODS_EXTRA,
|
|
23
|
+
THREE,
|
|
24
|
+
WLTP_ENERGY_EFFICIENCY_CLASS
|
|
25
|
+
} from './constants/core';
|
|
26
|
+
|
|
27
|
+
export { CUSTOM_ERROR_MESSAGES, supportPhone, supportPhoneLink } from './constants/app';
|
|
28
|
+
|
|
29
|
+
export { priceRatings } from './constants/price';
|
|
30
|
+
|
|
31
|
+
export { THROTTLE_DELAY } from './constants/Search';
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { AutobahnCar } from '../cards/types';
|
|
2
|
+
import { Co2Class } from '../constants/autobahn';
|
|
3
|
+
/**
|
|
4
|
+
* Last-resort placeholder shown in the carousel until the real images for a car
|
|
5
|
+
* are loaded. The SRP container SHOULD inject the app's real no-image asset via
|
|
6
|
+
* `deps.placeholder` (fe-main exposes PLACEHOLDER_IMAGE_LARGE_URL /
|
|
7
|
+
* PLACEHOLDER_IMAGE_SMALL_URL in framework/constants/app.ts, both derived from
|
|
8
|
+
* config.imagesAPI.imgUrlBase). We do NOT import that here to keep the mapper
|
|
9
|
+
* free of app-config coupling; this empty-data-URI default just guarantees a
|
|
10
|
+
* non-broken <img> in tests / before the container wires the real one.
|
|
11
|
+
*/
|
|
12
|
+
export declare const PLACEHOLDER_IMAGE = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";
|
|
13
|
+
/**
|
|
14
|
+
* Dependencies the SRP container injects. Everything here is what the mapper
|
|
15
|
+
* needs from the outside world — keeping the function itself pure.
|
|
16
|
+
*/
|
|
17
|
+
export interface ToAutobahnCarDeps {
|
|
18
|
+
/**
|
|
19
|
+
* Build a ready-to-use image URL for a car image.
|
|
20
|
+
* Pass `imagesStore.getSupportedImageFormat` straight through (it already
|
|
21
|
+
* picks webp/jpeg and the cached size). Signature matches ImagesStore:
|
|
22
|
+
* (vehicleID, imageIdentifier, size) => url
|
|
23
|
+
*/
|
|
24
|
+
getImageUrl?: (vehicleID: string, imageIdentifier: string, size: 'small' | 'medium' | 'large') => string;
|
|
25
|
+
/**
|
|
26
|
+
* Loaded gallery images for this car from `imagesStore.images.get(id)`
|
|
27
|
+
* (array of { imageUrlSmall, imageUrlMedium, imageUrlLarge, imageUrl, ... }).
|
|
28
|
+
* When present we use these real URLs; otherwise we fall back to the single
|
|
29
|
+
* main image via getImageUrl, then to the placeholder. Never faked.
|
|
30
|
+
*/
|
|
31
|
+
loadedImages?: Array<{
|
|
32
|
+
imageUrlSmall?: string;
|
|
33
|
+
imageUrlMedium?: string;
|
|
34
|
+
imageUrlLarge?: string;
|
|
35
|
+
imageUrl?: string;
|
|
36
|
+
}>;
|
|
37
|
+
/** image size used when building gallery URLs from the main image id. */
|
|
38
|
+
imageSize?: 'small' | 'medium' | 'large';
|
|
39
|
+
/** translator for cbd:* selector codes (fuel/gearbox). Falls back to a built-in de map, then the raw code. */
|
|
40
|
+
t?: (key: string, options?: object) => string;
|
|
41
|
+
/** active UI language ('de' | 'en' …). Drives de-DE vs en-IN number grouping for the EnVKV line. */
|
|
42
|
+
language?: string;
|
|
43
|
+
/** placeholder image URL override (defaults to PLACEHOLDER_IMAGE). */
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Buyer-activity stats for this car — UserActionsAPI 'stats/viewers' response, loaded async and
|
|
47
|
+
* keyed by carId (`searchStore.statsData.get(storeCar.id)`). Pass it straight through; absent
|
|
48
|
+
* (not loaded yet, or no stats for this car) → viewsCount/favoritesCount stay undefined and the
|
|
49
|
+
* gallery badge row simply omits those pills. Never fabricated.
|
|
50
|
+
*/
|
|
51
|
+
stats?: {
|
|
52
|
+
totalCarImpCount?: number;
|
|
53
|
+
totalFavCount?: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** translate a cbd:* selector code → human label. Exported: also used by the Meta AIA
|
|
57
|
+
* marketing-image renderer (plain Node backend, no i18next — relies on the built-in de fallback). */
|
|
58
|
+
export declare function cbd(code: any, t?: (key: string, options?: object) => string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Map the store's energy-efficiency-class selector code (or a bare letter) to a
|
|
61
|
+
* single CO2_CLASSES letter ('A'…'G'). A+/A++/A+++ collapse to 'A' so the card's
|
|
62
|
+
* colored InlineCls (which does CO2_CLASSES.indexOf) still resolves a band color.
|
|
63
|
+
* Returns '' when there is no class (caller decides what to render).
|
|
64
|
+
*/
|
|
65
|
+
export declare function co2ClassLetter(code: any): Co2Class | '';
|
|
66
|
+
export declare function mapRating(currentSalesPrice?: number, predicted?: number): {
|
|
67
|
+
rating: string;
|
|
68
|
+
ratingLabel: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Map the 5-tier rating to a 2-tone signal — only 'top'/'good' read as a genuinely good deal
|
|
72
|
+
* (green); 'fair'/'raised'/'high' all get the warn/peach tone (Figma: "Erhöhter-Preis" is
|
|
73
|
+
* peach, not green). Pure/non-React on purpose: shared by the web card's StatChip (via
|
|
74
|
+
* cards/cardParts.tsx's re-export) AND the Meta AIA marketing-image renderer (a plain Node
|
|
75
|
+
* service that can't import React components).
|
|
76
|
+
*/
|
|
77
|
+
export declare function ratingTone(rating: string): 'good' | 'warn';
|
|
78
|
+
/**
|
|
79
|
+
* Convert one fe-main store car → AutobahnCar.
|
|
80
|
+
*
|
|
81
|
+
* @param storeCar the object from SearchResultsStore.getCarsForPage()
|
|
82
|
+
* (shape = transformDataForVehicles output). `any` because the store car is
|
|
83
|
+
* itself untyped (`getCarsForPage: (page) => any[]`).
|
|
84
|
+
* @param deps injected services/locale (see ToAutobahnCarDeps). Optional so the
|
|
85
|
+
* mapper still runs in tests with zero wiring (gallery → placeholder, labels →
|
|
86
|
+
* built-in de fallback).
|
|
87
|
+
*/
|
|
88
|
+
export declare function toAutobahnCar(storeCar: any, deps?: ToAutobahnCarDeps): AutobahnCar;
|
|
89
|
+
export default toAutobahnCar;
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Autobahn (auto.de redesign) — store-car → AutobahnCar mapper (AB-P0-6).
|
|
3
|
+
//
|
|
4
|
+
// PURE adapter: converts the existing fe-main "store car" (the object
|
|
5
|
+
// `SearchResultsStore.getCarsForPage()` returns to the SRP, produced by
|
|
6
|
+
// `transformDataForVehicles` in framework/DataTransformers.ts) into the
|
|
7
|
+
// `AutobahnCar` view-model the new Autobahn cards consume
|
|
8
|
+
// (components/autobahn/cards/types.ts).
|
|
9
|
+
//
|
|
10
|
+
// WHY a pure function with injected deps:
|
|
11
|
+
// The Autobahn cards are pure/presentational and never touch the store. This
|
|
12
|
+
// mapper is the single seam that reads the real payload. It is kept pure (no
|
|
13
|
+
// store singletons imported here) so it is unit-testable and the SRP container
|
|
14
|
+
// injects the real stores/services (imagesStore.getSupportedImageFormat,
|
|
15
|
+
// appStore.language). Everything it needs arrives through `deps`.
|
|
16
|
+
//
|
|
17
|
+
// NEEDS-DATA discipline (the point of the ticket):
|
|
18
|
+
// Fields with NO real source on the search payload are left `undefined` so the
|
|
19
|
+
// cards render their 'Daten folgt' marker instead of a fabricated number. See
|
|
20
|
+
// the per-field rationale in ./fieldMapping.ts. We NEVER synthesize:
|
|
21
|
+
// drop, marketLow, marketAvg, marketHigh, watchers, views.
|
|
22
|
+
// listPrice / savings are emitted ONLY if a real strike/list price exists.
|
|
23
|
+
//
|
|
24
|
+
// Every field path below was confirmed against the real code (file:line in
|
|
25
|
+
// ./fieldMapping.ts), not guessed.
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.PLACEHOLDER_IMAGE = void 0;
|
|
28
|
+
exports.cbd = cbd;
|
|
29
|
+
exports.co2ClassLetter = co2ClassLetter;
|
|
30
|
+
exports.mapRating = mapRating;
|
|
31
|
+
exports.ratingTone = ratingTone;
|
|
32
|
+
exports.toAutobahnCar = toAutobahnCar;
|
|
33
|
+
const priceRating_1 = require("../utils/priceRating");
|
|
34
|
+
const DateUtils_1 = require("../utils/DateUtils");
|
|
35
|
+
const power_1 = require("../utils/power");
|
|
36
|
+
const autobahn_1 = require("../constants/autobahn");
|
|
37
|
+
/**
|
|
38
|
+
* Last-resort placeholder shown in the carousel until the real images for a car
|
|
39
|
+
* are loaded. The SRP container SHOULD inject the app's real no-image asset via
|
|
40
|
+
* `deps.placeholder` (fe-main exposes PLACEHOLDER_IMAGE_LARGE_URL /
|
|
41
|
+
* PLACEHOLDER_IMAGE_SMALL_URL in framework/constants/app.ts, both derived from
|
|
42
|
+
* config.imagesAPI.imgUrlBase). We do NOT import that here to keep the mapper
|
|
43
|
+
* free of app-config coupling; this empty-data-URI default just guarantees a
|
|
44
|
+
* non-broken <img> in tests / before the container wires the real one.
|
|
45
|
+
*/
|
|
46
|
+
exports.PLACEHOLDER_IMAGE = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// small pure helpers
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
const DEFAULT_LANG = 'de';
|
|
51
|
+
/** locale tag for Number#toLocaleString — de-DE (comma decimals) unless en. */
|
|
52
|
+
function localeTag(language) {
|
|
53
|
+
return language === 'en' ? 'en-IN' : 'de-DE';
|
|
54
|
+
}
|
|
55
|
+
/** finite-number coercion: accepts number | numeric string, else undefined. */
|
|
56
|
+
function num(v) {
|
|
57
|
+
if (v == null)
|
|
58
|
+
return undefined;
|
|
59
|
+
const n = typeof v === 'number' ? v : Number.parseFloat(String(v));
|
|
60
|
+
return Number.isFinite(n) ? n : undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Minimal de-DE fallback for the handful of cbd:* selector codes the cards show
|
|
64
|
+
* (fuel + gearbox). Used only when no `t` is injected (e.g. in unit tests).
|
|
65
|
+
* The full set lives in the cbd locale dictionary (@bytebrand/i18n-dictionaries).
|
|
66
|
+
*/
|
|
67
|
+
const CBD_DE_FALLBACK = {
|
|
68
|
+
selector_fuel_petrol: 'Benzin',
|
|
69
|
+
selector_fuel_diesel: 'Diesel',
|
|
70
|
+
selector_fuel_electrical: 'Elektro',
|
|
71
|
+
selector_fuel_ethanol: 'Ethanol',
|
|
72
|
+
selector_fuel_hybridElectricalDiesel: 'Hybrid (Elektro / Diesel)',
|
|
73
|
+
selector_fuel_hybridElectricalPetrol: 'Hybrid (Elektro / Benzin)',
|
|
74
|
+
selector_fuel_hydrogen: 'Wasserstoff',
|
|
75
|
+
selector_fuel_naturalGas: 'Erdgas',
|
|
76
|
+
selector_gearbox_automatic: 'Automatik',
|
|
77
|
+
selector_gearbox_manualShift: 'Manuell',
|
|
78
|
+
selector_gearbox_semiAutomatic: 'Halbautomatik',
|
|
79
|
+
// highlight-chip keys (DataTransformers highlights) — used when no `t` is injected
|
|
80
|
+
multimedia_navigationSystem: 'Navigationssystem',
|
|
81
|
+
lights_ledHeadlights: 'LED-Scheinwerfer',
|
|
82
|
+
lights_xenonHeadlights: 'Xenon-Scheinwerfer',
|
|
83
|
+
window_panoramicGlassRoof: 'Panoramadach',
|
|
84
|
+
assistants_headUpDisplay: 'Head-up-Display',
|
|
85
|
+
assistants_adaptiveCruiseControl: 'Abstandstempomat',
|
|
86
|
+
search_seatHeating: 'Sitzheizung',
|
|
87
|
+
selector_climatisation_automaticAirCondition: 'Klimaautomatik',
|
|
88
|
+
selector_climatisation_airCondition: 'Klimaanlage',
|
|
89
|
+
search_parkingAssistants: 'Einparkhilfe',
|
|
90
|
+
selector_driveType_allWheelDrive: 'Allradantrieb',
|
|
91
|
+
search_trailerHitch: 'Anhängerkupplung',
|
|
92
|
+
assistants_cruiseControl: 'Tempomat',
|
|
93
|
+
assistants_trafficSignRecognition: 'Verkehrszeichenerkennung',
|
|
94
|
+
assistants_collisionAvoidance: 'Notbremsassistent',
|
|
95
|
+
window_sunroof: 'Schiebedach',
|
|
96
|
+
seats_electricAdjustable: 'Elektr. Sitze',
|
|
97
|
+
exterior_metallic: 'Metallic-Lackierung',
|
|
98
|
+
multimedia_handsFreePhoneSystem: 'Freisprecheinrichtung',
|
|
99
|
+
heatingCooling_auxiliaryHeating: 'Standheizung',
|
|
100
|
+
multimedia_bluetooth: 'Bluetooth',
|
|
101
|
+
};
|
|
102
|
+
/** translate a cbd:* selector code → human label. Exported: also used by the Meta AIA
|
|
103
|
+
* marketing-image renderer (plain Node backend, no i18next — relies on the built-in de fallback). */
|
|
104
|
+
function cbd(code, t) {
|
|
105
|
+
if (code == null || code === 'N/A')
|
|
106
|
+
return '';
|
|
107
|
+
const key = String(code);
|
|
108
|
+
if (t) {
|
|
109
|
+
const out = t(`cbd:${key}`);
|
|
110
|
+
// i18next returns the key back when missing; treat that as "no translation".
|
|
111
|
+
if (out && out !== `cbd:${key}` && out !== key)
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
return CBD_DE_FALLBACK[key] || key;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Map the store's energy-efficiency-class selector code (or a bare letter) to a
|
|
118
|
+
* single CO2_CLASSES letter ('A'…'G'). A+/A++/A+++ collapse to 'A' so the card's
|
|
119
|
+
* colored InlineCls (which does CO2_CLASSES.indexOf) still resolves a band color.
|
|
120
|
+
* Returns '' when there is no class (caller decides what to render).
|
|
121
|
+
*/
|
|
122
|
+
function co2ClassLetter(code) {
|
|
123
|
+
if (code == null || code === 'N/A')
|
|
124
|
+
return '';
|
|
125
|
+
const raw = String(code);
|
|
126
|
+
// selector_energyEfficiencyClass_appp → 'a', _ap → 'a', _b → 'b', …
|
|
127
|
+
const m = raw.match(/selector_energyEfficiencyClass_(a|b|c|d|e|f|g)/i);
|
|
128
|
+
const letter = (m ? m[1] : raw.replace(/\+/g, '').trim().charAt(0)).toUpperCase();
|
|
129
|
+
return autobahn_1.CO2_CLASSES.includes(letter) ? letter : '';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Real Preisbewertung → AutobahnCar rating tone + German label.
|
|
133
|
+
*
|
|
134
|
+
* Source of truth (confirmed against the live SRP card,
|
|
135
|
+
* VehicleSmallCard/VehicleInfo.tsx:160): the legacy card renders
|
|
136
|
+
* getPriceRating(price.currentSalesPrice, price.mlCurrentSalesPricePredicted)
|
|
137
|
+
* which returns a numeric tier using priceRatings:
|
|
138
|
+
* 0 noRating · 1 highPrice · 2 increasedPrice · 3 fairPrice · 4 goodPrice · 5 topPrice
|
|
139
|
+
* (constants/price.ts in this package).
|
|
140
|
+
*
|
|
141
|
+
* We map that real number onto the Autobahn 5-tier scale (TIER_LABELS,
|
|
142
|
+
* components/autobahn/constants.ts), where index 0=Top … 4=Hoch:
|
|
143
|
+
* topPrice(5)→0 'Top Preis' · goodPrice(4)→1 'Guter Preis' ·
|
|
144
|
+
* fairPrice(3)→2 'Fairer Preis' · increasedPrice(2)→3 'Erhöht' ·
|
|
145
|
+
* highPrice(1)→4 'Hoher Preis'.
|
|
146
|
+
* noRating(0) has no real comparison → neutral 'Fairer Preis' (tier 2) WITHOUT a
|
|
147
|
+
* flattering label (documented in fieldMapping.ts). The card only special-cases
|
|
148
|
+
* the 'fair' tone (warn pill); every other tone is the good/green pill.
|
|
149
|
+
*
|
|
150
|
+
* AutobahnCar.rating is a free string; CarCard does `rating === 'fair' ? warn : good`.
|
|
151
|
+
* We emit the autobahn tier key ('top'|'good'|'fair'|'raised'|'high') so the
|
|
152
|
+
* neutral 'fair' maps to the warn pill exactly like the design intends.
|
|
153
|
+
*/
|
|
154
|
+
const LEGACY_TO_TIER = {
|
|
155
|
+
5: 0, // topPrice → Top Preis
|
|
156
|
+
4: 1, // goodPrice → Guter Preis
|
|
157
|
+
3: 2, // fairPrice → Fairer Preis
|
|
158
|
+
2: 3, // increasedPrice → Erhöht
|
|
159
|
+
1: 4, // highPrice → Hoher Preis
|
|
160
|
+
};
|
|
161
|
+
const TIER_TONE = {
|
|
162
|
+
0: 'top',
|
|
163
|
+
1: 'good',
|
|
164
|
+
2: 'fair',
|
|
165
|
+
3: 'raised',
|
|
166
|
+
4: 'high',
|
|
167
|
+
};
|
|
168
|
+
function mapRating(currentSalesPrice, predicted) {
|
|
169
|
+
var _a;
|
|
170
|
+
const legacy = (0, priceRating_1.getPriceRating)(currentSalesPrice, predicted); // 0..5
|
|
171
|
+
// 0 = noRating: keep neutral (tier 2) — never invent a flattering rating.
|
|
172
|
+
const tier = legacy === 0 ? 2 : (_a = LEGACY_TO_TIER[legacy]) !== null && _a !== void 0 ? _a : 2;
|
|
173
|
+
return { rating: TIER_TONE[tier], ratingLabel: autobahn_1.TIER_LABELS[tier] };
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Map the 5-tier rating to a 2-tone signal — only 'top'/'good' read as a genuinely good deal
|
|
177
|
+
* (green); 'fair'/'raised'/'high' all get the warn/peach tone (Figma: "Erhöhter-Preis" is
|
|
178
|
+
* peach, not green). Pure/non-React on purpose: shared by the web card's StatChip (via
|
|
179
|
+
* cards/cardParts.tsx's re-export) AND the Meta AIA marketing-image renderer (a plain Node
|
|
180
|
+
* service that can't import React components).
|
|
181
|
+
*/
|
|
182
|
+
function ratingTone(rating) {
|
|
183
|
+
return rating === 'top' || rating === 'good' ? 'good' : 'warn';
|
|
184
|
+
}
|
|
185
|
+
/** Is this car a plug-in hybrid? Determined by fuel type, mirroring ADV-88 in VehicleInfo.tsx. */
|
|
186
|
+
function isPluginHybrid(fuel) {
|
|
187
|
+
return fuel === 'selector_fuel_hybridElectricalDiesel' || fuel === 'selector_fuel_hybridElectricalPetrol';
|
|
188
|
+
}
|
|
189
|
+
/** Build the gallery URL list from real images, falling back to main image, then placeholder. */
|
|
190
|
+
function buildGallery(storeCar, deps) {
|
|
191
|
+
const placeholder = deps.placeholder || exports.PLACEHOLDER_IMAGE;
|
|
192
|
+
// Only 'small' and 'large' variants are pre-generated by image-generator; 'medium' has no cached file.
|
|
193
|
+
const size = deps.imageSize || 'small';
|
|
194
|
+
// 1) Real loaded images from imagesStore.images.get(id).
|
|
195
|
+
if (Array.isArray(deps.loadedImages) && deps.loadedImages.length) {
|
|
196
|
+
const urls = deps.loadedImages
|
|
197
|
+
.map((img) => size === 'large'
|
|
198
|
+
? img.imageUrlLarge || img.imageUrlMedium || img.imageUrl
|
|
199
|
+
: size === 'small'
|
|
200
|
+
? img.imageUrlSmall || img.imageUrlMedium || img.imageUrl
|
|
201
|
+
: img.imageUrlMedium || img.imageUrlLarge || img.imageUrl)
|
|
202
|
+
.filter((u) => !!u);
|
|
203
|
+
if (urls.length)
|
|
204
|
+
return urls;
|
|
205
|
+
}
|
|
206
|
+
// 2) Not loaded yet, but we have a main image id + a URL builder → single real image.
|
|
207
|
+
const id = storeCar.id;
|
|
208
|
+
const mainImageId = storeCar.mainImageId;
|
|
209
|
+
const hasImages = num(storeCar.imagesCount);
|
|
210
|
+
if (deps.getImageUrl && id && mainImageId && hasImages) {
|
|
211
|
+
const url = deps.getImageUrl(id, mainImageId, size);
|
|
212
|
+
if (url)
|
|
213
|
+
return [url];
|
|
214
|
+
}
|
|
215
|
+
// 3) Nothing real yet — single placeholder (never a fabricated photo).
|
|
216
|
+
return [placeholder];
|
|
217
|
+
}
|
|
218
|
+
/** Build the optional PHEV EnVKV sub-block when the car is a plug-in hybrid and weighted data exists. */
|
|
219
|
+
function buildPhev(storeCar, language) {
|
|
220
|
+
var _a, _b, _c;
|
|
221
|
+
const c = storeCar.consumption || {};
|
|
222
|
+
const e = storeCar.environmentEmissions || {};
|
|
223
|
+
if (!isPluginHybrid(c.fuel))
|
|
224
|
+
return undefined;
|
|
225
|
+
const consEl = (_a = num(c.wltpWeightedPowerCombined)) !== null && _a !== void 0 ? _a : num(c.consumptionPowerCombined);
|
|
226
|
+
const consFuel = num(c.wltpWeightedCombined);
|
|
227
|
+
const consDis = (_b = num(c.wltpCombined)) !== null && _b !== void 0 ? _b : num(c.consumptionCombined);
|
|
228
|
+
const co2w = (_c = num(e.wltpCo2)) !== null && _c !== void 0 ? _c : num(e.co2);
|
|
229
|
+
const clsW = co2ClassLetter(e.wltpEnergyEfficiencyClass);
|
|
230
|
+
const clsD = co2ClassLetter(e.wltpDischargedEnergyEfficiencyClass);
|
|
231
|
+
// Only emit the dual PHEV line when we actually have the weighted figures the
|
|
232
|
+
// EcoLine PHEV branch needs; otherwise let the normal single line render.
|
|
233
|
+
if (consEl == null || consFuel == null || co2w == null)
|
|
234
|
+
return undefined;
|
|
235
|
+
return {
|
|
236
|
+
consEl: `${consEl.toLocaleString(localeTag(language))} kWh/100 km`,
|
|
237
|
+
consFuel: `${consFuel.toLocaleString(localeTag(language))} l/100 km`,
|
|
238
|
+
consDis: consDis != null ? `${consDis.toLocaleString(localeTag(language))} l/100 km` : '–',
|
|
239
|
+
co2w: `${co2w.toLocaleString(localeTag(language))} g/km`,
|
|
240
|
+
co2classW: clsW || '–',
|
|
241
|
+
co2classD: clsD || clsW || '–',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// main mapper
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
/**
|
|
248
|
+
* Convert one fe-main store car → AutobahnCar.
|
|
249
|
+
*
|
|
250
|
+
* @param storeCar the object from SearchResultsStore.getCarsForPage()
|
|
251
|
+
* (shape = transformDataForVehicles output). `any` because the store car is
|
|
252
|
+
* itself untyped (`getCarsForPage: (page) => any[]`).
|
|
253
|
+
* @param deps injected services/locale (see ToAutobahnCarDeps). Optional so the
|
|
254
|
+
* mapper still runs in tests with zero wiring (gallery → placeholder, labels →
|
|
255
|
+
* built-in de fallback).
|
|
256
|
+
*/
|
|
257
|
+
function toAutobahnCar(storeCar, deps = {}) {
|
|
258
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
259
|
+
const language = deps.language || DEFAULT_LANG;
|
|
260
|
+
const t = deps.t;
|
|
261
|
+
const title = storeCar.title || {};
|
|
262
|
+
const info = storeCar.info || {};
|
|
263
|
+
const engineData = storeCar.engineData || {};
|
|
264
|
+
const consumption = storeCar.consumption || {};
|
|
265
|
+
const emissions = storeCar.environmentEmissions || {};
|
|
266
|
+
const price = storeCar.price || {};
|
|
267
|
+
// --- price / monthly (REQUIRED) ---
|
|
268
|
+
// monthly ← price.financing.monthlyInstallment. This is the financing rate
|
|
269
|
+
// produced by FinancingStore via getComputedFinancing(config).financing
|
|
270
|
+
// (getFinancing.ts:82 `monthlyInstallment`), spread into price.financing by
|
|
271
|
+
// transformDataForVehicles (DataTransformers.ts:345). It is the LEAD price.
|
|
272
|
+
const monthly = (_b = num((_a = price === null || price === void 0 ? void 0 : price.financing) === null || _a === void 0 ? void 0 : _a.monthlyInstallment)) !== null && _b !== void 0 ? _b : 0;
|
|
273
|
+
// financing terms the monthly rate was computed with — same merged per-car
|
|
274
|
+
// config (dealer rate + the user's saved localStorage adjustments, ADV-157).
|
|
275
|
+
// Emitted only when real; the cards fall back to the default-config terms.
|
|
276
|
+
// paybackPeriod FIRST: it carries the user's saved term, while rawPaybackPeriod
|
|
277
|
+
// is stamped from the dealer rate at normalize time and never reflects
|
|
278
|
+
// adjustments (normalizeRateObject.ts `rawPaybackPeriod: paybackPeriod`).
|
|
279
|
+
const finFirstInstallment = num((_c = price === null || price === void 0 ? void 0 : price.financing) === null || _c === void 0 ? void 0 : _c.firstInstallment);
|
|
280
|
+
const finMonths = (_e = num((_d = price === null || price === void 0 ? void 0 : price.financing) === null || _d === void 0 ? void 0 : _d.paybackPeriod)) !== null && _e !== void 0 ? _e : num((_f = price === null || price === void 0 ? void 0 : price.financing) === null || _f === void 0 ? void 0 : _f.rawPaybackPeriod);
|
|
281
|
+
// price ← price.currentSalesPrice (DataTransformers.ts:352).
|
|
282
|
+
const purchase = (_g = num(price === null || price === void 0 ? void 0 : price.currentSalesPrice)) !== null && _g !== void 0 ? _g : 0;
|
|
283
|
+
// --- rating (REQUIRED-WHEN-PRICED) ← real Preisbewertung ---
|
|
284
|
+
// BL-08 (P1, MONEY): getPriceRating(0, 0) mathematically resolves to tier 5
|
|
285
|
+
// ("Top Preis" — the BEST possible rating). Its internal min/max bands are all
|
|
286
|
+
// `predicted * (1 ± pct)`, which collapse to 0 when predicted is 0, and the
|
|
287
|
+
// topPrice branch checks `currentSalesPrice <= min` — 0 <= 0 is true. A
|
|
288
|
+
// missing/broken price payload must NEVER get dressed up as the best deal on
|
|
289
|
+
// the lot. Only compute/emit a rating when there is a real, positive sales
|
|
290
|
+
// price; otherwise rating/ratingLabel stay '' (falsy) — every consumer
|
|
291
|
+
// (CarCard.tsx, cardParts.tsx, FinanceCalculator.tsx) already treats a falsy
|
|
292
|
+
// rating as "no badge", so no type change is needed here.
|
|
293
|
+
const predicted = purchase > 0 ? (0, priceRating_1.getPredictablePrice)(purchase, num(price === null || price === void 0 ? void 0 : price.mlCurrentSalesPricePredicted)) : undefined;
|
|
294
|
+
const { rating, ratingLabel } = purchase > 0 ? mapRating(purchase, predicted) : { rating: '', ratingLabel: '' };
|
|
295
|
+
// --- key specs ---
|
|
296
|
+
const powerKW = num(engineData.powerKW);
|
|
297
|
+
const powerPS = num(engineData.powerPS);
|
|
298
|
+
const power = (0, power_1.getPowerLabel)(powerKW, powerPS); // "150 kW (204 PS)"
|
|
299
|
+
const regTs = num(info.regDate);
|
|
300
|
+
const reg = regTs ? (0, DateUtils_1.formatTimestamp)({ timestamp: regTs, format: 'MM/YYYY' }) : '';
|
|
301
|
+
const ownersN = num(info.numberOfPreviousOwners);
|
|
302
|
+
// --- EnVKV consumption line (REQUIRED on every German car ad) ---
|
|
303
|
+
// Prefer WLTP figures when present (the legacy cards do too), else NEDC.
|
|
304
|
+
const consVal = (_h = num(consumption.wltpCombined)) !== null && _h !== void 0 ? _h : num(consumption.consumptionCombined);
|
|
305
|
+
const co2Val = (_j = num(emissions.wltpCo2)) !== null && _j !== void 0 ? _j : num(emissions.co2);
|
|
306
|
+
const cons = consVal != null ? `${consVal.toLocaleString(localeTag(language))} l/100 km` : '';
|
|
307
|
+
const co2 = co2Val != null ? `${co2Val.toLocaleString(localeTag(language))} g/km` : '';
|
|
308
|
+
const co2class = co2ClassLetter(emissions.wltpEnergyEfficiencyClass);
|
|
309
|
+
const phev = buildPhev(storeCar, language);
|
|
310
|
+
// --- media ---
|
|
311
|
+
const gallery = buildGallery(storeCar, deps);
|
|
312
|
+
const photos = (_k = num(storeCar.imagesCount)) !== null && _k !== void 0 ? _k : 0;
|
|
313
|
+
// --- optional: leasing rate — ONLY when leasing is actually active for this car.
|
|
314
|
+
// SearchResultsStore.getCarsForPage stamps price.leasing.isActive via getLeasingActive
|
|
315
|
+
// (car age + mileage within the leasing limits). Legacy SRP shows "Leasing ---" when
|
|
316
|
+
// inactive; we mirror that by leaving `lease` undefined so PricePaths omits the row
|
|
317
|
+
// (instead of computing/showing a leasing rate the car isn't eligible for).
|
|
318
|
+
const leasingActive = !!(price && price.leasing && price.leasing.isActive);
|
|
319
|
+
const lease = leasingActive ? num((_l = price === null || price === void 0 ? void 0 : price.leasing) === null || _l === void 0 ? void 0 : _l.monthlyInstallment) : undefined;
|
|
320
|
+
// leasing terms the lease rate was computed with — same merged per-car config
|
|
321
|
+
// as the financing terms above (annualMileage/paybackPeriod carry the user's
|
|
322
|
+
// saved adjustments). Only meaningful alongside an active lease rate.
|
|
323
|
+
const leaseMileage = leasingActive ? num((_m = price === null || price === void 0 ? void 0 : price.leasing) === null || _m === void 0 ? void 0 : _m.annualMileage) : undefined;
|
|
324
|
+
const leaseMonths = leasingActive ? num((_o = price === null || price === void 0 ? void 0 : price.leasing) === null || _o === void 0 ? void 0 : _o.paybackPeriod) : undefined;
|
|
325
|
+
// --- optional: list price / savings — ONLY from a real strike price ---
|
|
326
|
+
// transformDataForVehicles exposes oldMonthlyInstallment / highestPrice via
|
|
327
|
+
// the financing spread, and price.historyPriceDifference. There is NO reliable
|
|
328
|
+
// purchase-price strike on the search payload today, so we leave listPrice /
|
|
329
|
+
// savings UNDEFINED unless a real highestPrice strike exists above the sale
|
|
330
|
+
// price. See fieldMapping.ts — flagged for backend confirmation.
|
|
331
|
+
const isStrikeShown = !!((_p = price === null || price === void 0 ? void 0 : price.financing) === null || _p === void 0 ? void 0 : _p.isStrikeShown);
|
|
332
|
+
const highest = (_r = num((_q = price === null || price === void 0 ? void 0 : price.financing) === null || _q === void 0 ? void 0 : _q.highestPrice)) !== null && _r !== void 0 ? _r : num(price === null || price === void 0 ? void 0 : price.highestPrice);
|
|
333
|
+
let listPrice;
|
|
334
|
+
let savings;
|
|
335
|
+
if (isStrikeShown && highest != null && purchase > 0 && highest > purchase) {
|
|
336
|
+
listPrice = highest;
|
|
337
|
+
savings = highest - purchase;
|
|
338
|
+
}
|
|
339
|
+
// --- struck-through old rate per payment path — same isStrikeShown gate as the Kaufen
|
|
340
|
+
// strike above. getFinancing.ts/getLeasing.ts each compute their own real
|
|
341
|
+
// oldMonthlyInstallment from the same common.highestPrice; only emitted when the tabbed
|
|
342
|
+
// hero price actually has something real to strike against.
|
|
343
|
+
const oldMonthly = isStrikeShown ? num((_s = price === null || price === void 0 ? void 0 : price.financing) === null || _s === void 0 ? void 0 : _s.oldMonthlyInstallment) : undefined;
|
|
344
|
+
const oldLease = isStrikeShown && leasingActive ? num((_t = price === null || price === void 0 ? void 0 : price.leasing) === null || _t === void 0 ? void 0 : _t.oldMonthlyInstallment) : undefined;
|
|
345
|
+
// --- price drop (REAL) — mirrors the legacy SRL green "−X €" badge: show the reduction only
|
|
346
|
+
// when price.historyPriceDifference is a meaningful amount (≥ 500 €, the legacy threshold).
|
|
347
|
+
const histDrop = num(price === null || price === void 0 ? void 0 : price.historyPriceDifference);
|
|
348
|
+
const drop = histDrop != null && histDrop >= 500 ? histDrop : undefined;
|
|
349
|
+
// percent reduction (historyPriceDifferencePerCent) — paired with `drop` for the −X% badge.
|
|
350
|
+
const histPct = num(price === null || price === void 0 ? void 0 : price.historyPriceDifferencePerCent);
|
|
351
|
+
const dropPct = drop != null && histPct != null && histPct > 0 ? Math.round(histPct) : undefined;
|
|
352
|
+
// --- NEU badge (REAL) — the legacy showNewLabel flag carried on the store car. ---
|
|
353
|
+
const isNew = !!storeCar.showNewLabel;
|
|
354
|
+
// --- availability (REAL) — offer.availabilityMode. 'always' → immediately available. ---
|
|
355
|
+
const offer = storeCar.offer && typeof storeCar.offer === 'object' ? storeCar.offer : {};
|
|
356
|
+
const availability = offer.availabilityMode === 'selector_availabilityMode_always'
|
|
357
|
+
? t
|
|
358
|
+
? t('autobahn:availability.immediately')
|
|
359
|
+
: 'Sofort verfügbar'
|
|
360
|
+
: undefined;
|
|
361
|
+
// 0 € Anzahlung available — transform sets offer.showDownPayment = (raw showDownPayment && zero first installment).
|
|
362
|
+
const zeroDown = !!offer.showDownPayment;
|
|
363
|
+
// --- gallery activity strip (REAL) — UserActionsAPI 'stats/viewers', injected via deps.stats. ---
|
|
364
|
+
const viewsCount = num((_u = deps.stats) === null || _u === void 0 ? void 0 : _u.totalCarImpCount);
|
|
365
|
+
const favoritesCount = num((_v = deps.stats) === null || _v === void 0 ? void 0 : _v.totalFavCount);
|
|
366
|
+
const carView = {
|
|
367
|
+
// identity / headline
|
|
368
|
+
make: String((_w = title.make) !== null && _w !== void 0 ? _w : ''),
|
|
369
|
+
model: String((_x = title.model) !== null && _x !== void 0 ? _x : ''),
|
|
370
|
+
trim: String((_y = title.subModel) !== null && _y !== void 0 ? _y : ''),
|
|
371
|
+
// key specs
|
|
372
|
+
km: (_z = num(info.mileage)) !== null && _z !== void 0 ? _z : 0,
|
|
373
|
+
reg,
|
|
374
|
+
power,
|
|
375
|
+
fuel: cbd(consumption.fuel, t),
|
|
376
|
+
gear: cbd(info.gearbox, t),
|
|
377
|
+
owners: ownersN !== null && ownersN !== void 0 ? ownersN : 0,
|
|
378
|
+
// price
|
|
379
|
+
monthly,
|
|
380
|
+
...(finFirstInstallment != null ? { finFirstInstallment } : {}),
|
|
381
|
+
...(finMonths != null ? { finMonths } : {}),
|
|
382
|
+
price: purchase,
|
|
383
|
+
// media
|
|
384
|
+
gallery,
|
|
385
|
+
photos,
|
|
386
|
+
// EnVKV
|
|
387
|
+
cons,
|
|
388
|
+
co2,
|
|
389
|
+
co2class: co2class || '',
|
|
390
|
+
...(phev ? { phev } : {}),
|
|
391
|
+
// trust / features — equipment highlight chips (Matrix-LED, Virtual Cockpit, AHK, ACC, …).
|
|
392
|
+
// transformDataForVehicles extracted the present highlight selector-keys via getActualHighlights
|
|
393
|
+
// (reading the raw equipment categories); translate them to labels here (cbd → t('cbd:<key>')).
|
|
394
|
+
// First 4, like the design's card. Empty when the car has no notable equipment.
|
|
395
|
+
features: (Array.isArray(storeCar.highlights) ? storeCar.highlights : [])
|
|
396
|
+
.map((k) => cbd(k, t))
|
|
397
|
+
.filter((s) => !!s)
|
|
398
|
+
.slice(0, 4),
|
|
399
|
+
rating,
|
|
400
|
+
ratingLabel,
|
|
401
|
+
// optional (only when real)
|
|
402
|
+
...(listPrice != null ? { listPrice } : {}),
|
|
403
|
+
...(savings != null ? { savings } : {}),
|
|
404
|
+
...(oldMonthly != null && oldMonthly > monthly ? { oldMonthly } : {}),
|
|
405
|
+
...(oldLease != null && lease != null && oldLease > lease ? { oldLease } : {}),
|
|
406
|
+
...(lease != null ? { lease } : {}),
|
|
407
|
+
...(leaseMileage != null ? { leaseMileage } : {}),
|
|
408
|
+
...(leaseMonths != null ? { leaseMonths } : {}),
|
|
409
|
+
...(drop != null ? { drop } : {}),
|
|
410
|
+
...(dropPct != null ? { dropPct } : {}),
|
|
411
|
+
...(zeroDown ? { zeroDown } : {}),
|
|
412
|
+
...(isNew ? { isNew } : {}),
|
|
413
|
+
...(availability ? { availability } : {}),
|
|
414
|
+
...(viewsCount != null ? { viewsCount } : {}),
|
|
415
|
+
...(favoritesCount != null ? { favoritesCount } : {}),
|
|
416
|
+
// NEEDS-DATA: deliberately omitted (marketLow, marketAvg, marketHigh, watchers, views).
|
|
417
|
+
// The cards render 'Daten folgt' for these until a real backend exists.
|
|
418
|
+
};
|
|
419
|
+
return carView;
|
|
420
|
+
}
|
|
421
|
+
exports.default = toAutobahnCar;
|