@bytebrand/fe-ui-core-autobahn 1.0.51 → 1.0.53
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 +124 -0
- 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/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/data/toAutobahnCar.ts
CHANGED
|
@@ -23,12 +23,12 @@
|
|
|
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 { getPriceRating, getPredictablePrice } from '../utils/
|
|
26
|
+
import { getPriceRating, getPredictablePrice } from '../utils/priceRating';
|
|
27
27
|
import { formatTimestamp } from '../utils/DateUtils';
|
|
28
28
|
import { getPowerLabel } from '../utils/power';
|
|
29
29
|
|
|
30
30
|
import { AutobahnCar, AutobahnPhev } from '../cards/types';
|
|
31
|
-
import { TIER_LABELS, RatingTier, CO2_CLASSES, Co2Class } from '../constants';
|
|
31
|
+
import { TIER_LABELS, RatingTier, CO2_CLASSES, Co2Class } from '../constants/autobahn';
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Last-resort placeholder shown in the carousel until the real images for a car
|
|
@@ -137,8 +137,9 @@ const CBD_DE_FALLBACK: Record<string, string> = {
|
|
|
137
137
|
multimedia_bluetooth: 'Bluetooth',
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
/** translate a cbd:* selector code → human label.
|
|
141
|
-
|
|
140
|
+
/** translate a cbd:* selector code → human label. Exported: also used by the Meta AIA
|
|
141
|
+
* marketing-image renderer (plain Node backend, no i18next — relies on the built-in de fallback). */
|
|
142
|
+
export function cbd(code: any, t?: (key: string, options?: object) => string): string {
|
|
142
143
|
if (code == null || code === 'N/A') return '';
|
|
143
144
|
const key = String(code);
|
|
144
145
|
if (t) {
|
|
@@ -155,7 +156,7 @@ function cbd(code: any, t?: (key: string, options?: object) => string): string {
|
|
|
155
156
|
* colored InlineCls (which does CO2_CLASSES.indexOf) still resolves a band color.
|
|
156
157
|
* Returns '' when there is no class (caller decides what to render).
|
|
157
158
|
*/
|
|
158
|
-
function co2ClassLetter(code: any): Co2Class | '' {
|
|
159
|
+
export function co2ClassLetter(code: any): Co2Class | '' {
|
|
159
160
|
if (code == null || code === 'N/A') return '';
|
|
160
161
|
const raw = String(code);
|
|
161
162
|
// selector_energyEfficiencyClass_appp → 'a', _ap → 'a', _b → 'b', …
|
|
@@ -202,13 +203,24 @@ const TIER_TONE: Record<RatingTier, string> = {
|
|
|
202
203
|
4: 'high',
|
|
203
204
|
};
|
|
204
205
|
|
|
205
|
-
function mapRating(currentSalesPrice?: number, predicted?: number): { rating: string; ratingLabel: string } {
|
|
206
|
+
export function mapRating(currentSalesPrice?: number, predicted?: number): { rating: string; ratingLabel: string } {
|
|
206
207
|
const legacy = getPriceRating(currentSalesPrice as number, predicted as number); // 0..5
|
|
207
208
|
// 0 = noRating: keep neutral (tier 2) — never invent a flattering rating.
|
|
208
209
|
const tier: RatingTier = legacy === 0 ? 2 : LEGACY_TO_TIER[legacy] ?? 2;
|
|
209
210
|
return { rating: TIER_TONE[tier], ratingLabel: TIER_LABELS[tier] };
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
/**
|
|
214
|
+
* Map the 5-tier rating to a 2-tone signal — only 'top'/'good' read as a genuinely good deal
|
|
215
|
+
* (green); 'fair'/'raised'/'high' all get the warn/peach tone (Figma: "Erhöhter-Preis" is
|
|
216
|
+
* peach, not green). Pure/non-React on purpose: shared by the web card's StatChip (via
|
|
217
|
+
* cards/cardParts.tsx's re-export) AND the Meta AIA marketing-image renderer (a plain Node
|
|
218
|
+
* service that can't import React components).
|
|
219
|
+
*/
|
|
220
|
+
export function ratingTone(rating: string): 'good' | 'warn' {
|
|
221
|
+
return rating === 'top' || rating === 'good' ? 'good' : 'warn';
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
/** Is this car a plug-in hybrid? Determined by fuel type, mirroring ADV-88 in VehicleInfo.tsx. */
|
|
213
225
|
function isPluginHybrid(fuel: any): boolean {
|
|
214
226
|
return fuel === 'selector_fuel_hybridElectricalDiesel' || fuel === 'selector_fuel_hybridElectricalPetrol';
|
|
@@ -381,6 +393,13 @@ export function toAutobahnCar(storeCar: any, deps: ToAutobahnCarDeps = {}): Auto
|
|
|
381
393
|
savings = highest - purchase;
|
|
382
394
|
}
|
|
383
395
|
|
|
396
|
+
// --- struck-through old rate per payment path — same isStrikeShown gate as the Kaufen
|
|
397
|
+
// strike above. getFinancing.ts/getLeasing.ts each compute their own real
|
|
398
|
+
// oldMonthlyInstallment from the same common.highestPrice; only emitted when the tabbed
|
|
399
|
+
// hero price actually has something real to strike against.
|
|
400
|
+
const oldMonthly = isStrikeShown ? num(price?.financing?.oldMonthlyInstallment) : undefined;
|
|
401
|
+
const oldLease = isStrikeShown && leasingActive ? num(price?.leasing?.oldMonthlyInstallment) : undefined;
|
|
402
|
+
|
|
384
403
|
// --- price drop (REAL) — mirrors the legacy SRL green "−X €" badge: show the reduction only
|
|
385
404
|
// when price.historyPriceDifference is a meaningful amount (≥ 500 €, the legacy threshold).
|
|
386
405
|
const histDrop = num(price?.historyPriceDifference);
|
|
@@ -451,6 +470,8 @@ export function toAutobahnCar(storeCar: any, deps: ToAutobahnCarDeps = {}): Auto
|
|
|
451
470
|
// optional (only when real)
|
|
452
471
|
...(listPrice != null ? { listPrice } : {}),
|
|
453
472
|
...(savings != null ? { savings } : {}),
|
|
473
|
+
...(oldMonthly != null && oldMonthly > monthly ? { oldMonthly } : {}),
|
|
474
|
+
...(oldLease != null && lease != null && oldLease > lease ? { oldLease } : {}),
|
|
454
475
|
...(lease != null ? { lease } : {}),
|
|
455
476
|
...(leaseMileage != null ? { leaseMileage } : {}),
|
|
456
477
|
...(leaseMonths != null ? { leaseMonths } : {}),
|
package/locales/de/autobahn.json
CHANGED
package/locales/en/autobahn.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/fe-ui-core-autobahn",
|
|
3
|
-
"version": "1.0.
|
|
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.",
|
|
3
|
+
"version": "1.0.53",
|
|
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. A small set of pure, asset-require-free leaf modules (see tsconfig.node-safe.json) additionally ship a compiled .js/.d.ts sibling next to their .ts source, so plain-Node backends (e.g. image-generator's Meta AIA renderer) can require() them directly without a bundler — frontend consumers are unaffected (webpack resolves .ts/.tsx before .js).",
|
|
5
5
|
"main": "index.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build:node-safe": "tsc -p tsconfig.node-safe.json"
|
|
8
|
+
},
|
|
6
9
|
"license": "UNLICENSED",
|
|
7
10
|
"peerDependencies": {
|
|
8
11
|
"react": "^18.0.0",
|
package/theme/md3.css
CHANGED
|
@@ -957,6 +957,9 @@
|
|
|
957
957
|
.md3 .srp-chips { flex: 1 1 260px; min-width: 0; }
|
|
958
958
|
/* "NEU" badge on car cards (legacy new-car label). */
|
|
959
959
|
.md3 .neu-badge { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: var(--md-r-full); background: var(--md-primary); color: #fff; font-size: 10.5px; font-weight: 700; letter-spacing: .3px; white-space: nowrap; box-shadow: var(--md-e1); }
|
|
960
|
+
/* "⚡ Angebot" badge — dark neutral pill above the gallery (card mockup), distinct from NEU. */
|
|
961
|
+
.md3 .offer-badge { display: inline-flex; align-items: center; gap: 4px; height: 24px; padding: 0 9px; border-radius: var(--md-r-full); background: var(--md-on-surface); color: #fff; font-size: 11px; font-weight: 600; white-space: nowrap; }
|
|
962
|
+
.md3 .offer-badge .lucide { width: 13px; height: 13px; }
|
|
960
963
|
@media (max-width: 820px) { .md3 .srp-chips { order: 3; flex-basis: 100%; } }
|
|
961
964
|
.md3 .vdp-grid { display: grid; grid-template-columns: 1fr 372px; gap: 28px; align-items: start; }
|
|
962
965
|
.md3 .vdp-gallery { grid-column: 1; grid-row: 1; }
|
|
@@ -1120,9 +1123,13 @@
|
|
|
1120
1123
|
/* financing hero (legacy-style): eyebrow → big blue rate → terms line (fills the upper gap) */
|
|
1121
1124
|
.md3 .pp-eyebrow { font-size: 11px; font-weight: 700; letter-spacing: .4px; text-transform: uppercase;
|
|
1122
1125
|
color: var(--md-on-surface-variant); margin-bottom: 4px; }
|
|
1123
|
-
.md3 .pp-hero-rate { display: flex; align-items: baseline; gap:
|
|
1126
|
+
.md3 .pp-hero-rate { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
|
|
1124
1127
|
.md3 .pp-hero-val { font-size: 32px; font-weight: 700; color: var(--md-primary); letter-spacing: -1px; line-height: 1; }
|
|
1125
1128
|
.md3 .pp-hero-unit { font-size: 13px; color: var(--md-on-surface-variant); }
|
|
1129
|
+
/* struck-through reference rate/price shown next to the tabbed hero price (Figma card) —
|
|
1130
|
+
same red strike as .pp-old, sized for the hero context rather than the compact list row. */
|
|
1131
|
+
.md3 .pp-hero-old { font-size: 15px; color: var(--md-error); text-decoration: line-through; white-space: nowrap; }
|
|
1132
|
+
.md3 .pp-hero-val sup, .md3 .pp-hero-old sup, .md3 .pp-hero-unit sup { font-size: .6em; }
|
|
1126
1133
|
.md3 .pp-hero-terms { margin-top: 9px; font-size: 11.5px; color: var(--md-on-surface-variant); }
|
|
1127
1134
|
/* separator line between the financing block and the leasing row (legacy-design parity) */
|
|
1128
1135
|
.md3 .pp-lease-row { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--md-outline-variant); }
|
|
@@ -1163,6 +1170,23 @@
|
|
|
1163
1170
|
background: linear-gradient(90deg, var(--md-good) 0%, #E8B500 58%, #E8601A 100%); }
|
|
1164
1171
|
.md3 .pp-bar-dot { position: absolute; top: 50%; width: 13px; height: 13px; border-radius: 50%; background: #fff;
|
|
1165
1172
|
border: 2.5px solid var(--md-on-surface); transform: translate(-50%, -50%); box-shadow: var(--md-e1); }
|
|
1173
|
+
/* grid Car Card price block (Figma "Component | Car Card | Desktop") — the hero price/tabs are
|
|
1174
|
+
much larger here than the shared .pp-hero-* sizing used to give them (those classes are only
|
|
1175
|
+
consumed inside .cc-price now, so bumping them here is safe). */
|
|
1176
|
+
.md3 .cc-price .fin-tabs { margin-bottom: 12px; }
|
|
1177
|
+
.md3 .cc-price .fin-tab { font-size: 17px; font-weight: 700; padding: 8px 4px; }
|
|
1178
|
+
.md3 .cc-price .pp-eyebrow { font-size: 13px; margin-bottom: 2px; }
|
|
1179
|
+
.md3 .cc-price .pp-hero-val { font-size: 44px; letter-spacing: -1.5px; }
|
|
1180
|
+
.md3 .cc-price .pp-hero-old { font-size: 20px; }
|
|
1181
|
+
.md3 .cc-price .pp-hero-unit { font-size: 16px; }
|
|
1182
|
+
.md3 .cc-price .pp-bar { height: 7px; }
|
|
1183
|
+
.md3 .cc-price .pp-bar-dot { width: 16px; height: 16px; }
|
|
1184
|
+
/* card CTA row (Angebot anfragen / phone / Rate anpassen) — the shared .mbtn padding (0 24px)
|
|
1185
|
+
and 14px font were sized for full-width VDP buttons; at a ~330-360px card/column width three
|
|
1186
|
+
buttons with real text at that size overflow their flex share. Scoped-down here only. */
|
|
1187
|
+
.md3 .card-cta-row .mbtn { font-size: 12.5px; padding: 0 10px; gap: 5px; min-width: 0; }
|
|
1188
|
+
.md3 .card-cta-row .mbtn span, .md3 .card-cta-row .mbtn { overflow: hidden; text-overflow: ellipsis; }
|
|
1189
|
+
.md3 .card-cta-row .mbtn .lucide { width: 15px; height: 15px; flex: none; }
|
|
1166
1190
|
/* muted buyer activity (#5) */
|
|
1167
1191
|
.md3 .pp-activity { display: flex; align-items: center; gap: 5px; margin-top: 12px; font-size: 11px;
|
|
1168
1192
|
color: var(--md-on-surface-variant); white-space: nowrap; }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"//": "Compiles ONLY the pure, asset-require-free leaf modules a plain-Node backend can safely import (currently: image-generator's Meta AIA renderer). Emits .js/.d.ts NEXT TO each .ts source (no outDir/rootDir — setting outDir to '.' makes tsc auto-exclude the whole package as its own output dir). Frontend (webpack) consumers are unaffected: resolve.extensions puts .ts/.tsx before .js. Run `npm run build:node-safe` before publishing. Do NOT add a file here unless you've confirmed its full transitive import closure never reaches a .tsx file or a webpack-only asset require() (see constants/autobahn.ts's header comment for why that matters).",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "es2019",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"moduleResolution": "node",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"strict": false,
|
|
11
|
+
"noEmitOnError": true
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"data/toAutobahnCar.ts",
|
|
15
|
+
"utils/priceRating.ts",
|
|
16
|
+
"utils/DateUtils.ts",
|
|
17
|
+
"utils/power.ts",
|
|
18
|
+
"utils/financeFormat.ts",
|
|
19
|
+
"cards/types.ts",
|
|
20
|
+
"constants/autobahn.ts",
|
|
21
|
+
"constants/price.ts",
|
|
22
|
+
"constants/core.ts",
|
|
23
|
+
"constants/selectors.ts"
|
|
24
|
+
]
|
|
25
|
+
}
|
package/utils/CommonUtils.ts
CHANGED
|
@@ -6,9 +6,8 @@ import {
|
|
|
6
6
|
FIRST_REGISTRATION, MANUFACTURER, MILEAGE, MODEL, BODY_TYPE, PRICE, RATE,
|
|
7
7
|
DROP_DOWN_GROUP, RATE_DEFAULT, MMS_GROUPS_KEY, MMS_GROUP_FIELDS
|
|
8
8
|
} from '../constants/SearchWidget';
|
|
9
|
-
import { priceRatings, priceRatingConfig } from '../constants/price';
|
|
10
9
|
import { HUNDRED } from '../constants/common';
|
|
11
|
-
import { offers
|
|
10
|
+
import { offers } from '../constants/core';
|
|
12
11
|
import { FILTERS_IN_TITLE, MANUFACTURER_KEY, MAX_FILTERS_IN_TITLE, MODEL_KEY } from '../constants/Search';
|
|
13
12
|
import { isArray } from 'lodash';
|
|
14
13
|
|
|
@@ -324,52 +323,10 @@ export const getFormattedPrice = (price: number, format = '$,.0f', currency = ''
|
|
|
324
323
|
};
|
|
325
324
|
|
|
326
325
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
: '???';
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
if (currentPrice === 0) {
|
|
335
|
-
console.log('Empty current price');
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
|
|
339
|
-
const { min, max } = priceRatingConfig[key];
|
|
340
|
-
return { ...acc, [key]: { min: round(min), max: round(max) } };
|
|
341
|
-
}, {}); // tslint:disable-line
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
|
|
345
|
-
if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
|
|
346
|
-
return predictablePrice;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
|
|
353
|
-
const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
|
|
354
|
-
// const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
|
|
355
|
-
|
|
356
|
-
if (!isCorrectCurrentSalesPrice) return 0;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
|
|
360
|
-
|
|
361
|
-
const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
|
|
362
|
-
|
|
363
|
-
for (const key of Object.keys(ratingConfig)) {
|
|
364
|
-
const { min, max } = ratingConfig[key];
|
|
365
|
-
const TOP_PRICE = 5;
|
|
366
|
-
if (key === 'highPrice' && currentSalesPrice === max) return 1;
|
|
367
|
-
if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
|
|
368
|
-
if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
return 0;
|
|
372
|
-
};
|
|
326
|
+
// Preisbewertung rating math moved to ./priceRating.ts (kept import-safe for plain Node
|
|
327
|
+
// consumers — this file pulls in ../constants/Search, which requires image assets). Re-exported
|
|
328
|
+
// here so existing CommonUtils consumers are unaffected.
|
|
329
|
+
export { getPriceRatingConfig, getPredictablePrice, getPriceRating } from './priceRating';
|
|
373
330
|
|
|
374
331
|
export const fixNumber = (num: number | string) => {
|
|
375
332
|
const isNum = typeof num === 'number' ? num : parseFloat(num);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MomentInput } from 'moment';
|
|
2
|
+
export interface IFormatTimestamp {
|
|
3
|
+
timestamp?: MomentInput;
|
|
4
|
+
format?: string;
|
|
5
|
+
parseFormat?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const timestampToDate: (timestamp: number) => string;
|
|
8
|
+
export declare const formatTimestamp: ({ timestamp, format, parseFormat }: IFormatTimestamp) => string;
|
|
9
|
+
export declare const getSearchYears: () => any[];
|
|
10
|
+
export declare const getCurrentYear: () => number;
|
|
11
|
+
export declare const getMonths: () => string[];
|
|
12
|
+
export declare function isDateInDaysPeriod(timestamp: number, days: number): boolean;
|
|
13
|
+
export declare const handleMaxDate: () => string;
|
|
14
|
+
export declare const handleMaxDateForPicker: () => string;
|
|
15
|
+
export declare const _minDate: Date;
|
|
16
|
+
export declare const сheckingMinDateIsCorrect: (value: any) => boolean;
|
|
17
|
+
export declare const сheckingMaxDateIsCorrect: (value: any) => boolean;
|
|
18
|
+
export declare const checkingWhenLess18Age: (value: any) => boolean;
|
|
19
|
+
export declare const getYears: (minYear?: number, maxYear?: number, reverse?: boolean) => string[];
|
|
20
|
+
export declare const createDateAsUTC: (date: Date) => Date;
|
|
21
|
+
export declare const fromUTCToDateString: (UTCDate: Date) => string;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.fromUTCToDateString = exports.createDateAsUTC = exports.getYears = exports.checkingWhenLess18Age = exports.сheckingMaxDateIsCorrect = exports.сheckingMinDateIsCorrect = exports._minDate = exports.handleMaxDateForPicker = exports.handleMaxDate = exports.getMonths = exports.getCurrentYear = exports.getSearchYears = exports.formatTimestamp = exports.timestampToDate = void 0;
|
|
7
|
+
exports.isDateInDaysPeriod = isDateInDaysPeriod;
|
|
8
|
+
const moment_1 = __importDefault(require("moment"));
|
|
9
|
+
const timestampToDate = (timestamp) => {
|
|
10
|
+
if (!timestamp)
|
|
11
|
+
return 'N/A';
|
|
12
|
+
const date = new Date(timestamp * 1000);
|
|
13
|
+
const year = date.getFullYear();
|
|
14
|
+
const month = date.getMonth() + 1;
|
|
15
|
+
const monthFormatted = month < 10 ? `0${month}` : month;
|
|
16
|
+
return (`${monthFormatted}/${year}`);
|
|
17
|
+
};
|
|
18
|
+
exports.timestampToDate = timestampToDate;
|
|
19
|
+
// accepts ISO 8601, RFC 2822 or Unix timestamp in seconds and converts it to the given format
|
|
20
|
+
// invocation without arguments creates a current UNIX timestamp in seconds
|
|
21
|
+
const formatTimestamp = ({ timestamp, format = 'X', parseFormat }) => {
|
|
22
|
+
// try to parse number timestamp as unix timestamp in case if parseFormat is not specified
|
|
23
|
+
// use undefined for string timestamps with no parseFormat in hope that it will either be
|
|
24
|
+
// ISO 8601 or RFC 2822 and moment will be able to figure it out
|
|
25
|
+
let parseAs = parseFormat;
|
|
26
|
+
if (!parseFormat && typeof timestamp === 'number')
|
|
27
|
+
parseAs = 'X';
|
|
28
|
+
return moment_1.default.utc(timestamp, parseAs).format(format);
|
|
29
|
+
};
|
|
30
|
+
exports.formatTimestamp = formatTimestamp;
|
|
31
|
+
const getSearchYears = () => {
|
|
32
|
+
const currentYear = (new Date()).getFullYear();
|
|
33
|
+
const years = [];
|
|
34
|
+
let nextYear = currentYear;
|
|
35
|
+
while (nextYear >= 2011) { // tslint:disable-line
|
|
36
|
+
years.push(nextYear);
|
|
37
|
+
nextYear = nextYear - 1;
|
|
38
|
+
}
|
|
39
|
+
return [...years];
|
|
40
|
+
};
|
|
41
|
+
exports.getSearchYears = getSearchYears;
|
|
42
|
+
const getCurrentYear = () => (new Date()).getFullYear();
|
|
43
|
+
exports.getCurrentYear = getCurrentYear;
|
|
44
|
+
const getMonths = () => {
|
|
45
|
+
const count = [...new Array(12)]; // tslint:disable-line
|
|
46
|
+
return count.map((_, index) => (String(1 + index)));
|
|
47
|
+
};
|
|
48
|
+
exports.getMonths = getMonths;
|
|
49
|
+
function isDateInDaysPeriod(timestamp, days) {
|
|
50
|
+
const today = new Date();
|
|
51
|
+
// tslint:disable-next-line: no-magic-numbers
|
|
52
|
+
const period = 1000 * 60 * 60 * 24 * days; // 1000 * 60 * 60 * 24 * days
|
|
53
|
+
const ms = 1000;
|
|
54
|
+
const result = !!timestamp && today.getTime() - timestamp * ms < period;
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
// for checkout datepicker
|
|
58
|
+
const handleMaxDate = () => {
|
|
59
|
+
const day = +String(new Date().getDate()).padStart(2, '0');
|
|
60
|
+
const month = String(new Date().getMonth() + 1).padStart(2, '0');
|
|
61
|
+
const year = new Date().getFullYear() - 18;
|
|
62
|
+
return `${month}/${day}/${year}`;
|
|
63
|
+
};
|
|
64
|
+
exports.handleMaxDate = handleMaxDate;
|
|
65
|
+
const handleMaxDateForPicker = () => {
|
|
66
|
+
const day = String(new Date().getDate()).padStart(2, '0');
|
|
67
|
+
const month = String(new Date().getMonth() + 1).padStart(2, '0');
|
|
68
|
+
const year = new Date().getFullYear() - 18;
|
|
69
|
+
return `${month}/${day}/${year}`;
|
|
70
|
+
};
|
|
71
|
+
exports.handleMaxDateForPicker = handleMaxDateForPicker;
|
|
72
|
+
exports._minDate = new Date('01/01/1900');
|
|
73
|
+
const сheckingMinDateIsCorrect = (value) => exports._minDate.getTime() < new Date(value).getTime();
|
|
74
|
+
exports.сheckingMinDateIsCorrect = сheckingMinDateIsCorrect;
|
|
75
|
+
const сheckingMaxDateIsCorrect = (value) => new Date((0, exports.handleMaxDate)()).getTime() > new Date(value).getTime();
|
|
76
|
+
exports.сheckingMaxDateIsCorrect = сheckingMaxDateIsCorrect;
|
|
77
|
+
const checkingWhenLess18Age = (value) => new Date().getTime() >= new Date(value).getTime();
|
|
78
|
+
exports.checkingWhenLess18Age = checkingWhenLess18Age;
|
|
79
|
+
const getYears = (minYear = 1980, maxYear = (0, moment_1.default)().year(), reverse = false) => {
|
|
80
|
+
const years = [];
|
|
81
|
+
for (let i = maxYear; i >= minYear; i -= 1) {
|
|
82
|
+
years.push(`${i}`);
|
|
83
|
+
}
|
|
84
|
+
return reverse ? years.sort() : years;
|
|
85
|
+
};
|
|
86
|
+
exports.getYears = getYears;
|
|
87
|
+
const createDateAsUTC = (date) => new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()));
|
|
88
|
+
exports.createDateAsUTC = createDateAsUTC;
|
|
89
|
+
// end for checkout datepicker
|
|
90
|
+
const fromUTCToDateString = (UTCDate) => {
|
|
91
|
+
const date = new Date(UTCDate);
|
|
92
|
+
const day = date.getUTCDate().toString().padStart(2, '0');
|
|
93
|
+
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
|
|
94
|
+
const year = date.getUTCFullYear();
|
|
95
|
+
return `${day}.${month}.${year}`;
|
|
96
|
+
};
|
|
97
|
+
exports.fromUTCToDateString = fromUTCToDateString;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
|
|
2
|
+
* toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
|
|
3
|
+
export declare function formatEur(n: number): string;
|
|
4
|
+
/** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
|
|
5
|
+
export declare function formatPct(n: number): string;
|
|
6
|
+
export interface FinanceExampleFields {
|
|
7
|
+
price: number;
|
|
8
|
+
apr: number;
|
|
9
|
+
down: number;
|
|
10
|
+
/** Real config figure — falls back to price-down ONLY when genuinely absent. */
|
|
11
|
+
netLoanAmount?: number;
|
|
12
|
+
nominalRate?: number;
|
|
13
|
+
finalInstallment?: number;
|
|
14
|
+
grossAmount?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface FinanceExampleLabels {
|
|
17
|
+
vehiclePrice: string;
|
|
18
|
+
priceSuffix: string;
|
|
19
|
+
apr: string;
|
|
20
|
+
nominal: string;
|
|
21
|
+
net: string;
|
|
22
|
+
down: string;
|
|
23
|
+
final: string;
|
|
24
|
+
gross: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
|
|
28
|
+
* included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
|
|
29
|
+
* derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
|
|
30
|
+
* `exampleLine`, byte-for-byte).
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker?: string): string;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Autobahn (auto.de redesign) — shared financing-number formatting.
|
|
3
|
+
//
|
|
4
|
+
// Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
|
|
5
|
+
// Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
|
|
6
|
+
// second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
|
|
7
|
+
// Node service.
|
|
8
|
+
//
|
|
9
|
+
// The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
|
|
10
|
+
// financing means monthly×term is NOT the total) and lives here once, not per-caller.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.formatEur = formatEur;
|
|
13
|
+
exports.formatPct = formatPct;
|
|
14
|
+
exports.buildFinanceExampleLine = buildFinanceExampleLine;
|
|
15
|
+
/** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
|
|
16
|
+
* toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
|
|
17
|
+
function formatEur(n) {
|
|
18
|
+
return n.toLocaleString('de-DE');
|
|
19
|
+
}
|
|
20
|
+
/** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
|
|
21
|
+
function formatPct(n) {
|
|
22
|
+
return `${n.toFixed(2).replace('.', ',')} %`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
|
|
26
|
+
* included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
|
|
27
|
+
* derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
|
|
28
|
+
* `exampleLine`, byte-for-byte).
|
|
29
|
+
*/
|
|
30
|
+
function buildFinanceExampleLine(fields, labels, footnoteMarker = '³') {
|
|
31
|
+
const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
|
|
32
|
+
const parts = [
|
|
33
|
+
`${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
|
|
34
|
+
...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
|
|
35
|
+
...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
|
|
36
|
+
`${labels.net}: ${formatEur(netto)} €`,
|
|
37
|
+
`${labels.down}: ${formatEur(fields.down)} €`,
|
|
38
|
+
...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
|
|
39
|
+
...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
|
|
40
|
+
];
|
|
41
|
+
return `${footnoteMarker} ${parts.join('; ')};`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Autobahn (auto.de redesign) — shared financing-number formatting.
|
|
2
|
+
//
|
|
3
|
+
// Extracted from vdp/VdpModules.tsx's FinanceDetailsModal so non-React consumers (e.g. the
|
|
4
|
+
// Meta AIA marketing-image renderer) format the SAME real numbers the same way, instead of a
|
|
5
|
+
// second copy drifting from the VDP's. Pure TS, no React/JSX — safe to import from a plain
|
|
6
|
+
// Node service.
|
|
7
|
+
//
|
|
8
|
+
// The "only real figures, never a fabricated total" rule is load-bearing (legal: balloon
|
|
9
|
+
// financing means monthly×term is NOT the total) and lives here once, not per-caller.
|
|
10
|
+
|
|
11
|
+
/** Monetary figure → de-DE grouped string (e.g. 47400 → "47.400"). Feed rounded integers —
|
|
12
|
+
* toLocaleString on a fractional value prints a bogus 3-decimal grouping. */
|
|
13
|
+
export function formatEur(n: number): string {
|
|
14
|
+
return n.toLocaleString('de-DE');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Rate → de-DE percent string (e.g. 6.49 → "6,49 %"). */
|
|
18
|
+
export function formatPct(n: number): string {
|
|
19
|
+
return `${n.toFixed(2).replace('.', ',')} %`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FinanceExampleFields {
|
|
23
|
+
price: number;
|
|
24
|
+
apr: number;
|
|
25
|
+
down: number;
|
|
26
|
+
/** Real config figure — falls back to price-down ONLY when genuinely absent. */
|
|
27
|
+
netLoanAmount?: number;
|
|
28
|
+
nominalRate?: number;
|
|
29
|
+
finalInstallment?: number;
|
|
30
|
+
grossAmount?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface FinanceExampleLabels {
|
|
34
|
+
vehiclePrice: string;
|
|
35
|
+
priceSuffix: string;
|
|
36
|
+
apr: string;
|
|
37
|
+
nominal: string;
|
|
38
|
+
net: string;
|
|
39
|
+
down: string;
|
|
40
|
+
final: string;
|
|
41
|
+
gross: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The "Repräsentatives Beispiel" disclaimer line (PAngV). Only fields with a genuine value are
|
|
46
|
+
* included — nominalRate/finalInstallment/grossAmount are optional real config figures, never
|
|
47
|
+
* derived (see fields doc). Matches production wording/format exactly (VdpModules.tsx's
|
|
48
|
+
* `exampleLine`, byte-for-byte).
|
|
49
|
+
*/
|
|
50
|
+
export function buildFinanceExampleLine(fields: FinanceExampleFields, labels: FinanceExampleLabels, footnoteMarker = '³'): string {
|
|
51
|
+
const netto = fields.netLoanAmount != null ? fields.netLoanAmount : Math.max(0, fields.price - fields.down);
|
|
52
|
+
const parts: string[] = [
|
|
53
|
+
`${labels.vehiclePrice}: ${formatEur(fields.price)} € ${labels.priceSuffix}`,
|
|
54
|
+
...(fields.apr > 0 ? [`${labels.apr}: ${formatPct(fields.apr)}`] : []),
|
|
55
|
+
...(fields.nominalRate != null && fields.nominalRate > 0 ? [`${labels.nominal}: ${formatPct(fields.nominalRate)}`] : []),
|
|
56
|
+
`${labels.net}: ${formatEur(netto)} €`,
|
|
57
|
+
`${labels.down}: ${formatEur(fields.down)} €`,
|
|
58
|
+
...(fields.finalInstallment != null && fields.finalInstallment > 0 ? [`${labels.final}: ${formatEur(fields.finalInstallment)} €`] : []),
|
|
59
|
+
...(fields.grossAmount != null && fields.grossAmount > 0 ? [`${labels.gross}: ${formatEur(fields.grossAmount)} €`] : []),
|
|
60
|
+
];
|
|
61
|
+
return `${footnoteMarker} ${parts.join('; ')};`;
|
|
62
|
+
}
|
package/utils/index.ts
CHANGED
|
@@ -20,9 +20,19 @@ export {
|
|
|
20
20
|
checkingWhenLess18Age,
|
|
21
21
|
getMonths,
|
|
22
22
|
getYears,
|
|
23
|
-
createDateAsUTC
|
|
23
|
+
createDateAsUTC,
|
|
24
|
+
fromUTCToDateString
|
|
24
25
|
} from './DateUtils';
|
|
25
26
|
|
|
27
|
+
// Finance number formatting (pure — no React; safe for non-React consumers)
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
formatEur,
|
|
31
|
+
formatPct,
|
|
32
|
+
buildFinanceExampleLine
|
|
33
|
+
} from './financeFormat';
|
|
34
|
+
export type { FinanceExampleFields, FinanceExampleLabels } from './financeFormat';
|
|
35
|
+
|
|
26
36
|
// Common utils
|
|
27
37
|
|
|
28
38
|
export {
|
package/utils/power.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getPowerLabel: (powerKW: number, powerPS: number) => string;
|
package/utils/power.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPowerLabel = void 0;
|
|
4
|
+
// Pure power-label formatter, extracted from fe-ui-core's
|
|
5
|
+
// source/framework/vehiclesProps/decoratedProps.tsx (which stays in fe-ui-core
|
|
6
|
+
// because it drags React UI). Exactly one implementation lives here;
|
|
7
|
+
// decoratedProps re-exports it for backwards compatibility.
|
|
8
|
+
const getPowerLabel = (powerKW, powerPS) => {
|
|
9
|
+
const powerKWlabel = !!powerKW ? `${powerKW} kW ` : '';
|
|
10
|
+
const powerPSlabel = !!powerPS ? `${!!powerKW ? '(' : ''}${powerPS} PS${!!powerKW ? ')' : ''}` : '';
|
|
11
|
+
const powerKWpowerPS = `${powerKWlabel}${powerPSlabel}`;
|
|
12
|
+
return powerKWpowerPS;
|
|
13
|
+
};
|
|
14
|
+
exports.getPowerLabel = getPowerLabel;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const getPriceRatingConfig: (currentPrice: number, pricePredicted: number) => any;
|
|
2
|
+
export declare const getPredictablePrice: (currentSalesPrice: number, predictablePrice: number) => number;
|
|
3
|
+
export declare const getPriceRating: (currentSalesPrice: number, predictablePrice: number) => number;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPriceRating = exports.getPredictablePrice = exports.getPriceRatingConfig = void 0;
|
|
4
|
+
// Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
|
|
5
|
+
//
|
|
6
|
+
// Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
|
|
7
|
+
// `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
|
|
8
|
+
// This file only imports `../constants/price` (zero further imports), so the Meta AIA
|
|
9
|
+
// marketing-image renderer (a plain Node backend, see ../data/toAutobahnCar.ts's `mapRating`)
|
|
10
|
+
// can import real Preisbewertung math without dragging in frontend-only asset requires.
|
|
11
|
+
// CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
|
|
12
|
+
const price_1 = require("../constants/price");
|
|
13
|
+
const core_1 = require("../constants/core");
|
|
14
|
+
const getPriceRatingConfig = (currentPrice, pricePredicted) => {
|
|
15
|
+
const round = (value) => {
|
|
16
|
+
return Number.isFinite(pricePredicted)
|
|
17
|
+
? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
|
|
18
|
+
: '???';
|
|
19
|
+
};
|
|
20
|
+
if (currentPrice === 0) {
|
|
21
|
+
console.log('Empty current price');
|
|
22
|
+
}
|
|
23
|
+
return Object.keys(price_1.priceRatingConfig).reduce((acc, key) => {
|
|
24
|
+
const { min, max } = price_1.priceRatingConfig[key];
|
|
25
|
+
return { ...acc, [key]: { min: round(min), max: round(max) } };
|
|
26
|
+
}, {}); // tslint:disable-line
|
|
27
|
+
};
|
|
28
|
+
exports.getPriceRatingConfig = getPriceRatingConfig;
|
|
29
|
+
const getPredictablePrice = (currentSalesPrice, predictablePrice) => {
|
|
30
|
+
if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
|
|
31
|
+
return predictablePrice;
|
|
32
|
+
}
|
|
33
|
+
return currentSalesPrice * core_1.PREDICTABLE_PRICE_INDEX;
|
|
34
|
+
};
|
|
35
|
+
exports.getPredictablePrice = getPredictablePrice;
|
|
36
|
+
const getPriceRating = (currentSalesPrice, predictablePrice) => {
|
|
37
|
+
const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
|
|
38
|
+
// const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
|
|
39
|
+
if (!isCorrectCurrentSalesPrice)
|
|
40
|
+
return 0;
|
|
41
|
+
const validatedPredictablePrice = (0, exports.getPredictablePrice)(currentSalesPrice, predictablePrice);
|
|
42
|
+
const ratingConfig = (0, exports.getPriceRatingConfig)(currentSalesPrice, validatedPredictablePrice);
|
|
43
|
+
for (const key of Object.keys(ratingConfig)) {
|
|
44
|
+
const { min, max } = ratingConfig[key];
|
|
45
|
+
const TOP_PRICE = 5;
|
|
46
|
+
if (key === 'highPrice' && currentSalesPrice === max)
|
|
47
|
+
return 1;
|
|
48
|
+
if (key === 'topPrice' && currentSalesPrice <= min)
|
|
49
|
+
return TOP_PRICE;
|
|
50
|
+
if (currentSalesPrice >= min && currentSalesPrice < max)
|
|
51
|
+
return price_1.priceRatings.indexOf(key);
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
};
|
|
55
|
+
exports.getPriceRating = getPriceRating;
|