@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.
@@ -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
+ };
@@ -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
- const phoneOk = phoneDigits.length >= 6;
2723
- // Round 13 (explicit spec): PHONE is always required (the * sits on the country-code field),
2724
- // e-mail is always optional (validated only when filled); names + consent always required.
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 ? `+49 ${areaCode.trim()} ${phoneNum.trim()}`.replace(/\s+/g, ' ').trim() : '';
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 always optional (round 13) — no required-*
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 (+49 fixed) · Vorwahl · Telefonnummer — outlined float-label row (Figma) */}
2864
+ {/* Land (country-code dropdown) · Vorwahl · Telefonnummer — outlined float-label row (Figma) */}
2854
2865
  <div className="enq-phone-row" style={{ marginTop: 16 }}>
2855
- {/* phone is always required — the * sits on the country-code field (round 13) */}
2856
- <FloatField label={t('autobahn:enquiry.form.countryLabel') + '*'}>
2857
- <span className="enq-country num">
2858
- +49
2859
- <Icon name="chevron-down" size={14} color="var(--md-on-surface-variant)" />
2860
- </span>
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')}