@bytebrand/fe-ui-core-autobahn 1.0.56 → 1.0.58
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/chrome/TopBar.tsx +217 -10
- package/constants.ts +31 -31
- package/locales/de/autobahn.json +8 -1
- package/locales/en/autobahn.json +8 -1
- package/package.json +1 -1
- package/theme/md3.css +311 -17
- package/utils/financeFormat.ts +62 -62
- package/utils/priceRating.ts +58 -58
- package/vdp/VdpModules.tsx +32 -16
- package/vdp/VdpViewer.tsx +646 -56
package/utils/priceRating.ts
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
// Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
|
|
2
|
-
//
|
|
3
|
-
// Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
|
|
4
|
-
// `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
|
|
5
|
-
// This file only imports `../constants/price` (zero further imports) — a pure, testable leaf
|
|
6
|
-
// module. CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
|
|
7
|
-
// (image-generator's Meta AIA renderer vendors a copy of this math directly instead of depending
|
|
8
|
-
// on this package — see services/image-generator/src/services/metaAia/vendoredAutobahnMath.ts —
|
|
9
|
-
// so a plain-Node backend doesn't need to npm-install this whole design system's dependency tree.)
|
|
10
|
-
import { priceRatings, priceRatingConfig } from '../constants/price';
|
|
11
|
-
import { PREDICTABLE_PRICE_INDEX } from '../constants/core';
|
|
12
|
-
|
|
13
|
-
export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
|
|
14
|
-
const round = (value: number) => {
|
|
15
|
-
return Number.isFinite(pricePredicted)
|
|
16
|
-
? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
|
|
17
|
-
: '???';
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
if (currentPrice === 0) {
|
|
21
|
-
console.log('Empty current price');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
|
|
25
|
-
const { min, max } = priceRatingConfig[key];
|
|
26
|
-
return { ...acc, [key]: { min: round(min), max: round(max) } };
|
|
27
|
-
}, {}); // tslint:disable-line
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
|
|
31
|
-
if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
|
|
32
|
-
return predictablePrice;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
|
|
39
|
-
const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
|
|
40
|
-
// const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
|
|
41
|
-
|
|
42
|
-
if (!isCorrectCurrentSalesPrice) return 0;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
|
|
46
|
-
|
|
47
|
-
const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
|
|
48
|
-
|
|
49
|
-
for (const key of Object.keys(ratingConfig)) {
|
|
50
|
-
const { min, max } = ratingConfig[key];
|
|
51
|
-
const TOP_PRICE = 5;
|
|
52
|
-
if (key === 'highPrice' && currentSalesPrice === max) return 1;
|
|
53
|
-
if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
|
|
54
|
-
if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return 0;
|
|
58
|
-
};
|
|
1
|
+
// Autobahn (auto.de redesign) — Preisbewertung rating math, extracted from CommonUtils.ts.
|
|
2
|
+
//
|
|
3
|
+
// Pulled out on purpose: CommonUtils.ts transitively imports `../constants/Search`, which
|
|
4
|
+
// `require()`s image assets (webpack-only — breaks any plain Node `require`/`tsc` consumer).
|
|
5
|
+
// This file only imports `../constants/price` (zero further imports) — a pure, testable leaf
|
|
6
|
+
// module. CommonUtils.ts re-exports these for existing consumers — one implementation, not a fork.
|
|
7
|
+
// (image-generator's Meta AIA renderer vendors a copy of this math directly instead of depending
|
|
8
|
+
// on this package — see services/image-generator/src/services/metaAia/vendoredAutobahnMath.ts —
|
|
9
|
+
// so a plain-Node backend doesn't need to npm-install this whole design system's dependency tree.)
|
|
10
|
+
import { priceRatings, priceRatingConfig } from '../constants/price';
|
|
11
|
+
import { PREDICTABLE_PRICE_INDEX } from '../constants/core';
|
|
12
|
+
|
|
13
|
+
export const getPriceRatingConfig = (currentPrice: number, pricePredicted: number) => { // tslint:disable-line
|
|
14
|
+
const round = (value: number) => {
|
|
15
|
+
return Number.isFinite(pricePredicted)
|
|
16
|
+
? Math.round((pricePredicted * (1 + (value / 100))) / 100) * 100 // tslint:disable-line
|
|
17
|
+
: '???';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (currentPrice === 0) {
|
|
21
|
+
console.log('Empty current price');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return Object.keys(priceRatingConfig).reduce((acc: any, key: any) => {
|
|
25
|
+
const { min, max } = priceRatingConfig[key];
|
|
26
|
+
return { ...acc, [key]: { min: round(min), max: round(max) } };
|
|
27
|
+
}, {}); // tslint:disable-line
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const getPredictablePrice = (currentSalesPrice: number, predictablePrice: number) => {
|
|
31
|
+
if (Number.isFinite(Number.parseFloat(`${predictablePrice}`))) {
|
|
32
|
+
return predictablePrice;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return currentSalesPrice * PREDICTABLE_PRICE_INDEX;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const getPriceRating = (currentSalesPrice: number, predictablePrice: number) => {
|
|
39
|
+
const isCorrectCurrentSalesPrice = Number.isFinite(Number.parseFloat(`${currentSalesPrice}`));
|
|
40
|
+
// const isCorrectPredictablePrice = Number.isFinite(Number.parseFloat(`${predictablePrice}`));
|
|
41
|
+
|
|
42
|
+
if (!isCorrectCurrentSalesPrice) return 0;
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const validatedPredictablePrice = getPredictablePrice(currentSalesPrice, predictablePrice);
|
|
46
|
+
|
|
47
|
+
const ratingConfig = getPriceRatingConfig(currentSalesPrice, validatedPredictablePrice);
|
|
48
|
+
|
|
49
|
+
for (const key of Object.keys(ratingConfig)) {
|
|
50
|
+
const { min, max } = ratingConfig[key];
|
|
51
|
+
const TOP_PRICE = 5;
|
|
52
|
+
if (key === 'highPrice' && currentSalesPrice === max) return 1;
|
|
53
|
+
if (key === 'topPrice' && currentSalesPrice <= min) return TOP_PRICE;
|
|
54
|
+
if (currentSalesPrice >= min && currentSalesPrice < max) return priceRatings.indexOf(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 0;
|
|
58
|
+
};
|
package/vdp/VdpModules.tsx
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import React, { useState } from 'react';
|
|
25
25
|
import { TranslationFunction } from '../types';
|
|
26
26
|
|
|
27
|
-
import { Button, Icon, Spec, StatChip, Stars, Switch } from '../ui';
|
|
27
|
+
import { Button, Dropdown, DropdownOption, Icon, Spec, StatChip, Stars, Switch } from '../ui';
|
|
28
28
|
import { CO2_CLASSES, CO2_COLORS, TIER_LABELS, TIER_KEYS, TIER_COLORS, TIER_BACKGROUNDS } from '../constants';
|
|
29
29
|
import { defaultVdpSpecs, EquipmentGroups, PriceRatingBands, VehicleDetail, VdpSpec, VdpReview } from './types';
|
|
30
30
|
import { CalcRow, DocCheckIcon, EvalBar, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
|
|
@@ -2495,6 +2495,7 @@ export const ReviewsTeaser: React.FunctionComponent<ReviewsTeaserProps> = ({ cou
|
|
|
2495
2495
|
alignItems: 'center',
|
|
2496
2496
|
justifyContent: 'space-between',
|
|
2497
2497
|
gap: 12,
|
|
2498
|
+
border: '1px solid #D4DDE7',
|
|
2498
2499
|
// sits between the finance card (order:3) and the highlights card (order:4) in the
|
|
2499
2500
|
// mobile .vdp-grid flex flow — tie-broken to AFTER the aside by JSX/DOM source order.
|
|
2500
2501
|
order: 3,
|
|
@@ -2682,6 +2683,12 @@ const FloatField: React.FunctionComponent<{
|
|
|
2682
2683
|
</label>
|
|
2683
2684
|
);
|
|
2684
2685
|
|
|
2686
|
+
/** phone country-code options for the "Land" dropdown (Figma: DE/AT/CH pinned first, then A–Z). */
|
|
2687
|
+
const ENQUIRY_COUNTRY_CODES: DropdownOption[] = [
|
|
2688
|
+
'+49', '+43', '+41', '+7', '+31', '+32', '+33', '+34', '+36', '+39', '+40', '+44', '+45', '+46',
|
|
2689
|
+
'+47', '+48', '+352', '+358', '+359', '+380', '+385', '+386', '+420', '+421', '+423',
|
|
2690
|
+
].map((code) => ({ value: code, label: code }));
|
|
2691
|
+
|
|
2685
2692
|
/** [icon, i18n label key] — resolved against the drilled t in the component. */
|
|
2686
2693
|
const ENQUIRY_BENEFITS: ReadonlyArray<[string, string]> = [
|
|
2687
2694
|
['clock', 'autobahn:enquiry.benefits.fastResponse'],
|
|
@@ -2704,6 +2711,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2704
2711
|
const [firstName, setFirstName] = useState('');
|
|
2705
2712
|
const [lastName, setLastName] = useState('');
|
|
2706
2713
|
const [email, setEmail] = useState('');
|
|
2714
|
+
const [countryCode, setCountryCode] = useState('+49');
|
|
2707
2715
|
const [areaCode, setAreaCode] = useState('');
|
|
2708
2716
|
const [phoneNum, setPhoneNum] = useState('');
|
|
2709
2717
|
const [consent, setConsent] = useState(false);
|
|
@@ -2719,15 +2727,18 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2719
2727
|
|
|
2720
2728
|
const emailOk = /.+@.+\..+/.test(email);
|
|
2721
2729
|
const phoneDigits = (areaCode + phoneNum).replace(/\D/g, '');
|
|
2722
|
-
|
|
2723
|
-
//
|
|
2724
|
-
|
|
2730
|
+
// backend (contact-form-users) caps the concatenated phoneMain (countryCode + areaCode +
|
|
2731
|
+
// phoneNum) at 22 chars total — mirrored here so an over-length number is caught before submit.
|
|
2732
|
+
const phoneOk = phoneDigits.length >= 6 && countryCode.length + areaCode.trim().length + phoneNum.trim().length <= 22;
|
|
2733
|
+
// Round 14: which channel is required follows the E-Mail/Telefon toggle — the * moves
|
|
2734
|
+
// between the e-mail field and the country-code field accordingly; the other channel
|
|
2735
|
+
// stays optional (validated only when filled); names + consent always required.
|
|
2725
2736
|
const valid =
|
|
2726
2737
|
firstName.trim().length > 0 &&
|
|
2727
2738
|
lastName.trim().length > 0 &&
|
|
2728
2739
|
consent &&
|
|
2729
|
-
phoneOk &&
|
|
2730
|
-
(email.trim().length === 0 || emailOk);
|
|
2740
|
+
(pref === 'email' ? emailOk : phoneOk) &&
|
|
2741
|
+
(pref === 'email' ? phoneDigits.length === 0 || phoneOk : email.trim().length === 0 || emailOk);
|
|
2731
2742
|
|
|
2732
2743
|
const submit = async (): Promise<void> => {
|
|
2733
2744
|
if (!valid || sending) return;
|
|
@@ -2736,7 +2747,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2736
2747
|
const note = t('autobahn:enquiry.preferredContact', {
|
|
2737
2748
|
channel: pref === 'email' ? t('autobahn:enquiry.contactMethod.email') : t('autobahn:enquiry.contactMethod.phone'),
|
|
2738
2749
|
});
|
|
2739
|
-
const phone = phoneDigits ?
|
|
2750
|
+
const phone = phoneDigits ? `${countryCode}${areaCode.trim()}${phoneNum.trim()}`.replace(/\s+/g, '') : '';
|
|
2740
2751
|
try {
|
|
2741
2752
|
await onSubmit({
|
|
2742
2753
|
name: `${firstName.trim()} ${lastName.trim()}`,
|
|
@@ -2843,26 +2854,30 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2843
2854
|
<input
|
|
2844
2855
|
style={enquiryInput}
|
|
2845
2856
|
type="email"
|
|
2846
|
-
// e-mail is
|
|
2847
|
-
placeholder={t('autobahn:enquiry.form.emailPlaceholder').replace(/\*$/, '')}
|
|
2857
|
+
// asterisk shows only while e-mail is the selected contact channel (round 14)
|
|
2858
|
+
placeholder={pref === 'email' ? t('autobahn:enquiry.form.emailPlaceholder') : t('autobahn:enquiry.form.emailPlaceholder').replace(/\*$/, '')}
|
|
2848
2859
|
value={email}
|
|
2849
2860
|
onChange={(e) => setEmail(e.target.value)}
|
|
2850
2861
|
aria-label={t('autobahn:enquiry.contactMethod.email')}
|
|
2851
2862
|
/>
|
|
2852
2863
|
</div>
|
|
2853
|
-
{/* Land (
|
|
2864
|
+
{/* Land (country-code dropdown) · Vorwahl · Telefonnummer — outlined float-label row (Figma) */}
|
|
2854
2865
|
<div className="enq-phone-row" style={{ marginTop: 16 }}>
|
|
2855
|
-
{/*
|
|
2856
|
-
<FloatField label={t('autobahn:enquiry.form.countryLabel') + '*'}>
|
|
2857
|
-
<
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2866
|
+
{/* * sits on the country-code field only while Telefon is the selected channel (round 14) */}
|
|
2867
|
+
<FloatField label={t('autobahn:enquiry.form.countryLabel') + (pref === 'phone' ? '*' : '')}>
|
|
2868
|
+
<Dropdown
|
|
2869
|
+
value={countryCode}
|
|
2870
|
+
options={ENQUIRY_COUNTRY_CODES}
|
|
2871
|
+
onChange={setCountryCode}
|
|
2872
|
+
ariaLabel={t('autobahn:enquiry.form.countryLabel')}
|
|
2873
|
+
className="enq-country"
|
|
2874
|
+
/>
|
|
2861
2875
|
</FloatField>
|
|
2862
2876
|
<FloatField label={t('autobahn:enquiry.form.areaCodeLabel')}>
|
|
2863
2877
|
<input
|
|
2864
2878
|
type="tel"
|
|
2865
2879
|
placeholder="1638"
|
|
2880
|
+
maxLength={5}
|
|
2866
2881
|
value={areaCode}
|
|
2867
2882
|
onChange={(e) => setAreaCode(e.target.value)}
|
|
2868
2883
|
aria-label={t('autobahn:enquiry.form.areaCodeLabel')}
|
|
@@ -2872,6 +2887,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2872
2887
|
<input
|
|
2873
2888
|
type="tel"
|
|
2874
2889
|
placeholder="5857458"
|
|
2890
|
+
maxLength={13}
|
|
2875
2891
|
value={phoneNum}
|
|
2876
2892
|
onChange={(e) => setPhoneNum(e.target.value)}
|
|
2877
2893
|
aria-label={t('autobahn:enquiry.form.phoneNumberLabel')}
|