@bytebrand/fe-ui-core-autobahn 1.0.73 → 1.0.75
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 +37 -4
- package/cards/cardParts.tsx +47 -21
- package/chrome/Footer.tsx +23 -18
- package/constants.ts +31 -31
- package/locales/de/autobahn.json +29 -8
- package/locales/en/autobahn.json +27 -6
- package/package.json +1 -1
- package/theme/md3.css +107 -28
- package/utils/financeFormat.ts +62 -62
- package/utils/priceRating.ts +58 -58
- package/vdp/VdpModules.tsx +398 -109
package/theme/md3.css
CHANGED
|
@@ -4365,7 +4365,6 @@ line-height: 31px;
|
|
|
4365
4365
|
justify-content: center;
|
|
4366
4366
|
position: absolute;
|
|
4367
4367
|
top: -5px;
|
|
4368
|
-
right: -5px;
|
|
4369
4368
|
border-radius: 50%;
|
|
4370
4369
|
width: 12px;
|
|
4371
4370
|
height: 11px;
|
|
@@ -4627,17 +4626,20 @@ line-height: 31px;
|
|
|
4627
4626
|
border: 1px solid transparent;
|
|
4628
4627
|
}
|
|
4629
4628
|
.md3 .cc-card .deal-chip--drop {
|
|
4630
|
-
|
|
4629
|
+
/* solid equivalent of rgba(17, 110, 57, 0.2) over white — the badge now sits over an
|
|
4630
|
+
edge-to-edge photo (CardCarousel padding removed), so a translucent fill let the image
|
|
4631
|
+
show through instead of the intended pastel tint. */
|
|
4632
|
+
background: rgb(207, 226, 215) !important;
|
|
4631
4633
|
border-color: rgba(17, 110, 57, 0.05);
|
|
4632
4634
|
color: #116e39 !important;
|
|
4633
4635
|
}
|
|
4634
4636
|
.md3 .cc-card .deal-chip--percent {
|
|
4635
|
-
background:
|
|
4637
|
+
background: rgb(247, 213, 210) !important;
|
|
4636
4638
|
border-color: rgba(217, 45, 32, 0.05);
|
|
4637
4639
|
color: #d92d20 !important;
|
|
4638
4640
|
}
|
|
4639
4641
|
.md3 .cc-card .deal-chip--zerodown {
|
|
4640
|
-
background:
|
|
4642
|
+
background: rgb(204, 222, 255) !important;
|
|
4641
4643
|
border-color: rgba(0, 92, 255, 0.05);
|
|
4642
4644
|
color: #005cff !important;
|
|
4643
4645
|
}
|
|
@@ -4645,7 +4647,7 @@ line-height: 31px;
|
|
|
4645
4647
|
.md3 .cc-card .neu-badge {
|
|
4646
4648
|
height: 30px;
|
|
4647
4649
|
padding: 0 10px;
|
|
4648
|
-
background:
|
|
4650
|
+
background: rgb(255, 241, 204);
|
|
4649
4651
|
border: 1px solid rgba(255, 184, 0, 0.05);
|
|
4650
4652
|
color: #ffb800;
|
|
4651
4653
|
font-size: 13px;
|
|
@@ -4656,7 +4658,7 @@ line-height: 31px;
|
|
|
4656
4658
|
height: 30px;
|
|
4657
4659
|
padding: 0 10px;
|
|
4658
4660
|
border-radius: 10px;
|
|
4659
|
-
background:
|
|
4661
|
+
background: rgb(204, 204, 204);
|
|
4660
4662
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
4661
4663
|
color: #000;
|
|
4662
4664
|
font-size: 14px;
|
|
@@ -4668,12 +4670,34 @@ line-height: 31px;
|
|
|
4668
4670
|
}
|
|
4669
4671
|
.md3 .cc-price .fin-tabs { margin-bottom: 12px; }
|
|
4670
4672
|
.md3 .cc-price .fin-tab { font-size: 17px; font-weight: 700; padding: 10px 5px; }
|
|
4673
|
+
@media (max-width: 1024px) {
|
|
4674
|
+
/* mobile CarCard tabs: only the ACTIVE tab bold + blue, matching the min-width:1025px desktop
|
|
4675
|
+
override further below — :not(.on) matches its specificity so a later, equal-specificity
|
|
4676
|
+
rule (.md3 .fin-tab:not(.on), bold+#111820 for both states) can't win the cascade on
|
|
4677
|
+
source-order alone. */
|
|
4678
|
+
.md3 .cc-price .fin-tab:not(.on) {
|
|
4679
|
+
font-family: 'Arial';
|
|
4680
|
+
font-weight: 400;
|
|
4681
|
+
font-size: 14px;
|
|
4682
|
+
line-height: 16px;
|
|
4683
|
+
color: #0E2238;
|
|
4684
|
+
}
|
|
4685
|
+
.md3 .cc-price .fin-tab.on {
|
|
4686
|
+
font-family: 'Arial';
|
|
4687
|
+
font-weight: 700;
|
|
4688
|
+
font-size: 14px;
|
|
4689
|
+
line-height: 16px;
|
|
4690
|
+
color: #005CFF;
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4671
4693
|
/* Finanzierung tab price row (Figma "similar cars" card) — matches the VDP finance-calculator's
|
|
4672
4694
|
MOBILE .fin-price-grid treatment one-to-one (same rate/old-price/pill typography and colors),
|
|
4673
4695
|
by explicit spec. Previously gated to min-width:601px (left the pre-existing mobile .cc-card
|
|
4674
4696
|
price row alone); now applied at every width on request, so mobile CarCard matches too. */
|
|
4675
4697
|
.md3 .cc-price .pp-eyebrow,
|
|
4676
|
-
.md3 .fin-calc-card .pp-eyebrow
|
|
4698
|
+
.md3 .fin-calc-card .pp-eyebrow,
|
|
4699
|
+
.md3 .vdp-bottom-bar .pp-eyebrow,
|
|
4700
|
+
.md3 .fin-modal .pp-eyebrow {
|
|
4677
4701
|
position: relative;
|
|
4678
4702
|
font-family: Arial, Helvetica, sans-serif;
|
|
4679
4703
|
font-style: normal;
|
|
@@ -4689,7 +4713,9 @@ line-height: 31px;
|
|
|
4689
4713
|
corner — absolute against .pp-eyebrow's own position:relative above. Shared by the CarCard
|
|
4690
4714
|
hero (.cc-price) and the VDP FinanceCalculator's own rate example (.fin-calc-card). */
|
|
4691
4715
|
.md3 .cc-price .pp-eyebrow-badge,
|
|
4692
|
-
.md3 .fin-calc-card .pp-eyebrow-badge
|
|
4716
|
+
.md3 .fin-calc-card .pp-eyebrow-badge,
|
|
4717
|
+
.md3 .vdp-bottom-bar .pp-eyebrow-badge,
|
|
4718
|
+
.md3 .fin-modal .pp-eyebrow-badge {
|
|
4693
4719
|
display: inline-flex;
|
|
4694
4720
|
align-items: center;
|
|
4695
4721
|
justify-content: center;
|
|
@@ -4712,7 +4738,9 @@ line-height: 31px;
|
|
|
4712
4738
|
}
|
|
4713
4739
|
/* mobile-only nudge: same badge, tucked slightly closer to the eyebrow text on narrow cards. */
|
|
4714
4740
|
@media (max-width: 1024px) {
|
|
4715
|
-
.md3 .cc-price .pp-eyebrow-badge
|
|
4741
|
+
.md3 .cc-price .pp-eyebrow-badge,
|
|
4742
|
+
.md3 .vdp-bottom-bar .pp-eyebrow-badge,
|
|
4743
|
+
.md3 .fin-modal .pp-eyebrow-badge {
|
|
4716
4744
|
top: -5px;
|
|
4717
4745
|
right: -5px;
|
|
4718
4746
|
}
|
|
@@ -4720,13 +4748,17 @@ line-height: 31px;
|
|
|
4720
4748
|
/* "2" badge (Kaufen, nested in .pp-hero-old instead of .pp-eyebrow) sits closer to the price
|
|
4721
4749
|
figure than the "3" badge's own offsets above. */
|
|
4722
4750
|
.md3 .cc-price .pp-hero-old .pp-eyebrow-badge,
|
|
4723
|
-
.md3 .fin-calc-card .pp-hero-old .pp-eyebrow-badge
|
|
4751
|
+
.md3 .fin-calc-card .pp-hero-old .pp-eyebrow-badge,
|
|
4752
|
+
.md3 .vdp-bottom-bar .fin-rate-old-bb .pp-eyebrow-badge,
|
|
4753
|
+
.md3 .fin-modal .fin-rate-old-list .pp-eyebrow-badge {
|
|
4724
4754
|
top: -7px;
|
|
4725
4755
|
right: -12px;
|
|
4726
4756
|
}
|
|
4727
4757
|
/* "3" footnote popover — click the badge above to open, the × or an outside click to close. */
|
|
4728
4758
|
.md3 .cc-price .pp-uvp-tooltip,
|
|
4729
|
-
.md3 .fin-calc-card .pp-uvp-tooltip
|
|
4759
|
+
.md3 .fin-calc-card .pp-uvp-tooltip,
|
|
4760
|
+
.md3 .vdp-bottom-bar .pp-uvp-tooltip,
|
|
4761
|
+
.md3 .fin-modal .pp-uvp-tooltip {
|
|
4730
4762
|
position: absolute;
|
|
4731
4763
|
top: 6px;
|
|
4732
4764
|
right: -84px;
|
|
@@ -4739,10 +4771,18 @@ line-height: 31px;
|
|
|
4739
4771
|
white-space: normal;
|
|
4740
4772
|
cursor: default;
|
|
4741
4773
|
}
|
|
4774
|
+
/* the bottom bar spans nearly the full phone width, so the shared -84px offset (tuned for
|
|
4775
|
+
the narrower .cc-price card / .fin-calc-card sheet) leaves the popover sitting too far left. */
|
|
4776
|
+
.md3 .vdp-bottom-bar .pp-uvp-tooltip {
|
|
4777
|
+
top: -20px;
|
|
4778
|
+
right: -110px;
|
|
4779
|
+
}
|
|
4742
4780
|
/* floating circular close button — overlaps the tooltip's top-right corner from outside,
|
|
4743
4781
|
same "glass circle" convention as VdpViewer's close/back/next overlay buttons. */
|
|
4744
4782
|
.md3 .cc-price .pp-uvp-tooltip-close,
|
|
4745
|
-
.md3 .fin-calc-card .pp-uvp-tooltip-close
|
|
4783
|
+
.md3 .fin-calc-card .pp-uvp-tooltip-close,
|
|
4784
|
+
.md3 .vdp-bottom-bar .pp-uvp-tooltip-close,
|
|
4785
|
+
.md3 .fin-modal .pp-uvp-tooltip-close {
|
|
4746
4786
|
position: absolute;
|
|
4747
4787
|
top: -38px;
|
|
4748
4788
|
right: -35px;
|
|
@@ -4758,7 +4798,9 @@ line-height: 31px;
|
|
|
4758
4798
|
cursor: pointer;
|
|
4759
4799
|
}
|
|
4760
4800
|
.md3 .cc-price .pp-uvp-tooltip-body,
|
|
4761
|
-
.md3 .fin-calc-card .pp-uvp-tooltip-body
|
|
4801
|
+
.md3 .fin-calc-card .pp-uvp-tooltip-body,
|
|
4802
|
+
.md3 .vdp-bottom-bar .pp-uvp-tooltip-body,
|
|
4803
|
+
.md3 .fin-modal .pp-uvp-tooltip-body {
|
|
4762
4804
|
font-family: Arial, Helvetica, sans-serif;
|
|
4763
4805
|
font-weight: 400;
|
|
4764
4806
|
font-size: 11px;
|
|
@@ -4846,6 +4888,25 @@ line-height: 31px;
|
|
|
4846
4888
|
order: -1;
|
|
4847
4889
|
align-self: flex-end;
|
|
4848
4890
|
}
|
|
4891
|
+
/* Kaufen hero price on mobile — was falling through to the generic (rate-sized) .pp-hero-val
|
|
4892
|
+
above (65px, shrinking only via the rate-digit len4/5/6 classes cardParts.tsx never applies
|
|
4893
|
+
in Kaufen mode), so real purchase prices had no digit-based shrink at all here and could
|
|
4894
|
+
overflow. Mirrors the existing min-width:1025px desktop rule exactly. */
|
|
4895
|
+
.md3 .cc-price .pp-hero--buy .pp-hero-val {
|
|
4896
|
+
font-family: 'Arial';
|
|
4897
|
+
font-weight: 700;
|
|
4898
|
+
font-size: 52px;
|
|
4899
|
+
line-height: 58px;
|
|
4900
|
+
color: #005CFF;
|
|
4901
|
+
}
|
|
4902
|
+
.md3 .cc-price .pp-hero--buy .pp-hero-val.pp-hero-val--buy-len6 {
|
|
4903
|
+
font-size: 48px;
|
|
4904
|
+
line-height: 54px;
|
|
4905
|
+
}
|
|
4906
|
+
.md3 .cc-price .pp-hero--buy .pp-hero-val.pp-hero-val--buy-len7 {
|
|
4907
|
+
font-size: 43px;
|
|
4908
|
+
line-height: 47px;
|
|
4909
|
+
}
|
|
4849
4910
|
/* Preisbewertung pill + segmented tier bar, grouped in one column exactly like the VDP
|
|
4850
4911
|
finance-calculator's MOBILE .fin-eval-col (FinanceCalculator.tsx) — replaces the old
|
|
4851
4912
|
StatChip pill + full-width gradient .pp-bar (see PriceEvalCol in cardParts.tsx). No
|
|
@@ -4877,9 +4938,11 @@ line-height: 31px;
|
|
|
4877
4938
|
/* footnote 4 ("Top-Preis" claim) — .fin-eval-pill is a flex container, so vertical-align has
|
|
4878
4939
|
no effect on the sup (it's a flex item, not inline content); position:relative/top raises
|
|
4879
4940
|
it instead, same technique as the hero price's own 1/2/3 footnote markers. Shared by the
|
|
4880
|
-
CarCard hero (.cc-price)
|
|
4941
|
+
CarCard hero (.cc-price), the VDP FinanceCalculator's own pill (.fin-calc-card), and the
|
|
4942
|
+
FinanceDetailsModal's embedded mobile rate sheet (.fin-modal). */
|
|
4881
4943
|
.md3 .cc-price .fin-eval-pill sup,
|
|
4882
|
-
.md3 .fin-calc-card .fin-eval-pill sup
|
|
4944
|
+
.md3 .fin-calc-card .fin-eval-pill sup,
|
|
4945
|
+
.md3 .fin-modal .fin-eval-pill sup {
|
|
4883
4946
|
position: relative;
|
|
4884
4947
|
top: 0px;
|
|
4885
4948
|
left: -4px;
|
|
@@ -4909,11 +4972,12 @@ line-height: 31px;
|
|
|
4909
4972
|
(PricePaths); the link resets button chrome and reuses the primary color so it reads as tappable. */
|
|
4910
4973
|
.md3 .cc-terms {
|
|
4911
4974
|
margin-top: 9px;
|
|
4975
|
+
margin-bottom: 10px;
|
|
4912
4976
|
font-family: Arial, Helvetica, sans-serif;
|
|
4913
4977
|
font-style: normal;
|
|
4914
4978
|
font-weight: 400;
|
|
4915
|
-
font-size:
|
|
4916
|
-
line-height:
|
|
4979
|
+
font-size: 12px;
|
|
4980
|
+
line-height: 14px;
|
|
4917
4981
|
color: #0e2238;
|
|
4918
4982
|
}
|
|
4919
4983
|
.md3 .cc-terms-dot {
|
|
@@ -4936,6 +5000,7 @@ line-height: 31px;
|
|
|
4936
5000
|
line-height: 15px;
|
|
4937
5001
|
letter-spacing: -0.03em;
|
|
4938
5002
|
color: #0e2238;
|
|
5003
|
+
text-decoration: underline;
|
|
4939
5004
|
cursor: pointer;
|
|
4940
5005
|
}
|
|
4941
5006
|
/* card CTA row (Angebot anfragen / phone / Rate anpassen) — the shared .mbtn padding (0 24px)
|
|
@@ -4995,6 +5060,12 @@ line-height: 31px;
|
|
|
4995
5060
|
border-bottom: 1px solid var(--md-outline-variant);
|
|
4996
5061
|
margin-bottom: 18px;
|
|
4997
5062
|
}
|
|
5063
|
+
/* VDP FinanceCalculator card only — scoped so it doesn't touch .vdp-bottom-bar / .fin-modal-body /
|
|
5064
|
+
.vdp-aside's own .fin-tabs margins (desktop base + mobile override above are both 1-class, so
|
|
5065
|
+
this 2-class selector wins purely on specificity regardless of viewport or source order). */
|
|
5066
|
+
.md3 .fin-calc-card .fin-tabs {
|
|
5067
|
+
margin-bottom: 10px;
|
|
5068
|
+
}
|
|
4998
5069
|
.md3 .fin-tab {
|
|
4999
5070
|
flex: 1;
|
|
5000
5071
|
background: none;
|
|
@@ -5035,7 +5106,7 @@ line-height: 31px;
|
|
|
5035
5106
|
grid-template-areas: "rate pill" "cond bar";
|
|
5036
5107
|
gap: 12px 16px;
|
|
5037
5108
|
align-items: center;
|
|
5038
|
-
margin-top:
|
|
5109
|
+
margin-top: 0;
|
|
5039
5110
|
}
|
|
5040
5111
|
.md3 .fin-rate-main {
|
|
5041
5112
|
grid-area: rate;
|
|
@@ -5488,7 +5559,24 @@ line-height: 31px;
|
|
|
5488
5559
|
}
|
|
5489
5560
|
.md3 .bb-tabs .fin-tab {
|
|
5490
5561
|
padding: 15px 14px 10px 14px;
|
|
5491
|
-
|
|
5562
|
+
}
|
|
5563
|
+
/* :not(.on) — a later, equal-specificity mobile rule (.md3 .fin-tab:not(.on), Figma "dark +
|
|
5564
|
+
bold" spec for the desktop-style tabs) would otherwise win on source order alone; matching
|
|
5565
|
+
its :not(.on) here brings this to 4 classes vs its 3 so bb-tabs always wins regardless of
|
|
5566
|
+
where either rule sits in the file. */
|
|
5567
|
+
.md3 .bb-tabs .fin-tab:not(.on) {
|
|
5568
|
+
font-family: 'Arial';
|
|
5569
|
+
font-weight: 400;
|
|
5570
|
+
font-size: 14px;
|
|
5571
|
+
line-height: 16px;
|
|
5572
|
+
color: #0E2238;
|
|
5573
|
+
}
|
|
5574
|
+
.md3 .bb-tabs .fin-tab.on {
|
|
5575
|
+
font-family: 'Arial';
|
|
5576
|
+
font-weight: 700;
|
|
5577
|
+
font-size: 14px;
|
|
5578
|
+
line-height: 16px;
|
|
5579
|
+
color: #005CFF;
|
|
5492
5580
|
}
|
|
5493
5581
|
.md3 .bb-price-row .num.num--len5 {
|
|
5494
5582
|
font-size: 42px;
|
|
@@ -7994,15 +8082,6 @@ color: #005CFF;
|
|
|
7994
8082
|
line-height: 9px;
|
|
7995
8083
|
color: #111820;
|
|
7996
8084
|
}
|
|
7997
|
-
/* "z.B. 499 €² / mtl.³" representative-example line under the hero rate */
|
|
7998
|
-
.md3 .fin-rate-main .fin-rate-example-label {
|
|
7999
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
8000
|
-
font-style: normal;
|
|
8001
|
-
font-weight: 700;
|
|
8002
|
-
font-size: 10px;
|
|
8003
|
-
line-height: 11px;
|
|
8004
|
-
color: #5b6b7f;
|
|
8005
|
-
}
|
|
8006
8085
|
.md3 .fin-rate-main .fin-rate-example-price {
|
|
8007
8086
|
position: relative;
|
|
8008
8087
|
display: inline-block;
|
package/utils/financeFormat.ts
CHANGED
|
@@ -1,62 +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
|
-
}
|
|
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/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
|
+
};
|