@bytebrand/fe-ui-core-autobahn 1.0.0
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/assets/autode-logo.svg +7 -0
- package/cards/CarCard.tsx +174 -0
- package/cards/CarRow.tsx +182 -0
- package/cards/cardParts.tsx +489 -0
- package/cards/index.ts +35 -0
- package/cards/types.ts +114 -0
- package/chrome/Footer.tsx +189 -0
- package/chrome/TopBar.tsx +405 -0
- package/chrome/index.ts +20 -0
- package/chrome/svg.d.ts +11 -0
- package/compat/AccardionSection/AccardionSection.styl +45 -0
- package/compat/AccardionSection/AccardionSection.tsx +56 -0
- package/compat/AccordionWidget/AccordionWidget.styl +221 -0
- package/compat/AccordionWidget/AccordionWidget.tsx +257 -0
- package/compat/AccordionWrapper/AccordionItem/AccordionItem.styl +34 -0
- package/compat/AccordionWrapper/AccordionItem/AccordionItem.tsx +64 -0
- package/compat/AccordionWrapper/AccordionWrapper.tsx +34 -0
- package/compat/AdvantageItem/AdvantageItem.styl +47 -0
- package/compat/AdvantageItem/AdvantageItem.tsx +26 -0
- package/compat/Alternative/DealerInfo.styl +116 -0
- package/compat/Alternative/DealerInfo.tsx +171 -0
- package/compat/Badge/Badge.styl +44 -0
- package/compat/Badge/Badge.tsx +25 -0
- package/compat/BodyTypeIcons/Audi.tsx +9 -0
- package/compat/BodyTypeIcons/Bmw.tsx +860 -0
- package/compat/BodyTypeIcons/Ford.tsx +31 -0
- package/compat/BodyTypeIcons/IconsConfig.tsx +44 -0
- package/compat/BodyTypeIcons/IconsConfigCompact.tsx +30 -0
- package/compat/BodyTypeIcons/Mazda.tsx +44 -0
- package/compat/BodyTypeIcons/MercedesBenz.tsx +38 -0
- package/compat/BodyTypeIcons/Opel.tsx +19 -0
- package/compat/BodyTypeIcons/Search.tsx +19 -0
- package/compat/BodyTypeIcons/SelectorCategoryCombi.tsx +68 -0
- package/compat/BodyTypeIcons/SelectorCategoryCompact.tsx +20 -0
- package/compat/BodyTypeIcons/SelectorCategoryConvertible.tsx +68 -0
- package/compat/BodyTypeIcons/SelectorCategoryCoupe.tsx +60 -0
- package/compat/BodyTypeIcons/SelectorCategoryMinibus.tsx +64 -0
- package/compat/BodyTypeIcons/SelectorCategorySedan.tsx +69 -0
- package/compat/BodyTypeIcons/SelectorCategorySuv.tsx +77 -0
- package/compat/BodyTypeIcons/SelectorCategoryTransporter.tsx +68 -0
- package/compat/BodyTypeIcons/SelectorCategoryVan.tsx +72 -0
- package/compat/BodyTypeIcons/Volkswagen.tsx +330 -0
- package/compat/BodyTypeIcons/index.ts +17 -0
- package/compat/Button/Button.tsx +65 -0
- package/compat/ButtonOld/Button.styl +65 -0
- package/compat/ButtonOld/Button.tsx +84 -0
- package/compat/Checkbox/Checkbox.styl +120 -0
- package/compat/Checkbox/Checkbox.tsx +151 -0
- package/compat/Checkbox/FormCheckbox.styl +22 -0
- package/compat/Checkbox/FormCheckbox.tsx +86 -0
- package/compat/Co2Widget/CO2Efficiency.styl +219 -0
- package/compat/Co2Widget/CO2Efficiency.tsx +93 -0
- package/compat/ContactInfo/ContactInfo.styl +22 -0
- package/compat/ContactInfo/ContactInfo.tsx +161 -0
- package/compat/ContactInfo/TemporaryTranslation.ts +11 -0
- package/compat/CookieModal/CookieModal.tsx +49 -0
- package/compat/CookieModal/Modal.tsx +43 -0
- package/compat/CookieModal/ModalsConfig.tsx +23 -0
- package/compat/CookieModal/modals/ManageCookieModal/ManageCookieModal.styl +87 -0
- package/compat/CookieModal/modals/ManageCookieModal/ManageCookieModal.tsx +184 -0
- package/compat/CookieModal/modals/PreviewCookieModal/PreviewCookieModal.styl +67 -0
- package/compat/CookieModal/modals/PreviewCookieModal/PreviewCookieModal.tsx +51 -0
- package/compat/CookieModal/modals/TestModal.tsx +7 -0
- package/compat/CookieModal/useModal.ts +13 -0
- package/compat/CustomerQuote/CustomerQuote.styl +80 -0
- package/compat/CustomerQuote/CustomerQuote.tsx +68 -0
- package/compat/CustomerQuotesSlider/CustomerQuotesSlider.styl +46 -0
- package/compat/CustomerQuotesSlider/CustomerQuotesSlider.tsx +79 -0
- package/compat/CustomerQuotesSlider/SliderArrow.tsx +20 -0
- package/compat/CustomerQuotesSlider/slider.global.styl +2 -0
- package/compat/ExpansionPanel/ExpansionPanel.tsx +68 -0
- package/compat/FirstInfoBlock/FirstInfoBlock.styl +33 -0
- package/compat/FirstInfoBlock/FirstInfoBlock.tsx +49 -0
- package/compat/FirstInfoBlock/FirstInfoBlockItem/FirstInfoBlockItem.styl +66 -0
- package/compat/FirstInfoBlock/FirstInfoBlockItem/FirstInfoBlockItem.tsx +21 -0
- package/compat/FormattedNumber/FormattedNumber.styl +2 -0
- package/compat/FormattedNumber/FormattedNumber.tsx +90 -0
- package/compat/IconContainer/IconContainer.styl +23 -0
- package/compat/IconContainer/IconContainer.tsx +34 -0
- package/compat/Image/Image.styl +109 -0
- package/compat/Image/Image.tsx +302 -0
- package/compat/ImageSettings/EditableOptionsTable/CustomCheckbox/CustomCheckbox.styl +7 -0
- package/compat/ImageSettings/EditableOptionsTable/CustomCheckbox/CustomCheckbox.tsx +30 -0
- package/compat/ImageSettings/EditableOptionsTable/EditableOptionsTable.styl +122 -0
- package/compat/ImageSettings/EditableOptionsTable/EditableOptionsTable.tsx +199 -0
- package/compat/ImageSettings/EditableOptionsTable/ImageIcon/ImageIcon.styl +3 -0
- package/compat/ImageSettings/EditableOptionsTable/ImageIcon/ImageIcon.tsx +15 -0
- package/compat/ImageSettings/OptionsTable/OptionsTable.styl +47 -0
- package/compat/ImageSettings/OptionsTable/OptionsTable.tsx +122 -0
- package/compat/ImageSettings/PricePlan/PricePlan.styl +63 -0
- package/compat/ImageSettings/PricePlan/PricePlan.tsx +32 -0
- package/compat/ImageSettings/index.ts +42 -0
- package/compat/ImagesSet/ImagesSet.styl +28 -0
- package/compat/ImagesSet/ImagesSet.tsx +76 -0
- package/compat/InfoBlockWrapper/InfoBlockWrapper.styl +41 -0
- package/compat/InfoBlockWrapper/InfoBlockWrapper.tsx +46 -0
- package/compat/Loader/Loader.styl +39 -0
- package/compat/Loader/Loader.tsx +32 -0
- package/compat/MagnifyGlass/MagnifyGlass.styl +33 -0
- package/compat/MagnifyGlass/MagnifyGlass.tsx +213 -0
- package/compat/MdAccordionGroup/MdAccordionGroup.tsx +53 -0
- package/compat/MdCheckbox/MdCheckbox.tsx +69 -0
- package/compat/MdField/MdField.tsx +100 -0
- package/compat/MdSelect/MdSelect.tsx +103 -0
- package/compat/MdSwitch/MdSwitch.tsx +29 -0
- package/compat/MdTabs/MdTabs.tsx +47 -0
- package/compat/MdTooltip/MdTooltip.tsx +31 -0
- package/compat/OfferDetailedSection/OfferDetailedSection.styl +31 -0
- package/compat/OfferDetailedSection/OfferDetailedSection.tsx +100 -0
- package/compat/OfferDetailedSection/partials/PanelConfig.styl +132 -0
- package/compat/OfferDetailedSection/partials/PanelConfig.tsx +121 -0
- package/compat/OfferPanel/AboAccordion/AboAccordion.styl +25 -0
- package/compat/OfferPanel/AboAccordion/AboAccordion.tsx +128 -0
- package/compat/OfferPanel/AboAccordion/AboColorOptions/AboColorOptions.styl +12 -0
- package/compat/OfferPanel/AboAccordion/AboColorOptions/AboColorOptions.tsx +35 -0
- package/compat/OfferPanel/AboAccordion/AboFaq/AboFaq.styl +21 -0
- package/compat/OfferPanel/AboAccordion/AboFaq/AboFaq.tsx +25 -0
- package/compat/OfferPanel/AboAccordion/AboIncludedServices/AboIncludedServices.styl +14 -0
- package/compat/OfferPanel/AboAccordion/AboIncludedServices/AboIncludedServices.tsx +39 -0
- package/compat/OfferPanel/MuiOfferPeriod/MuiOfferPeriod.tsx +42 -0
- package/compat/OfferPanel/OfferCheckboxGroup/CheckboxContainer.tsx +140 -0
- package/compat/OfferPanel/OfferCheckboxGroup/OfferCheckboxGroup.styl +144 -0
- package/compat/OfferPanel/OfferCheckboxGroup/OfferCheckboxGroup.tsx +23 -0
- package/compat/OfferPanel/OfferPanel.styl +194 -0
- package/compat/OfferPanel/OfferPanel.tsx +81 -0
- package/compat/OfferPanel/OfferPeriod/OfferPeriod.styl +25 -0
- package/compat/OfferPanel/OfferPeriod/OfferPeriod.tsx +45 -0
- package/compat/OfferPanel/RangeGroup/RangeGroup.tsx +39 -0
- package/compat/OfferRequestButtonWrapper/OfferRequestButtonWrapper.styl +120 -0
- package/compat/OfferRequestButtonWrapper/OfferRequestButtonWrapper.styled.tsx +33 -0
- package/compat/OfferRequestButtonWrapper/OfferRequestButtonWrapper.tsx +122 -0
- package/compat/PriceRating/PriceRating.styl +47 -0
- package/compat/PriceRating/PriceRating.tsx +64 -0
- package/compat/PriceRatingDetailed/PriceRatingDetailed.styl +120 -0
- package/compat/PriceRatingDetailed/PriceRatingDetailed.tsx +191 -0
- package/compat/PricingToolBlock/PricingTool.styl +7 -0
- package/compat/PricingToolBlock/PricingTool.tsx +29 -0
- package/compat/Radio/Radio.styl +115 -0
- package/compat/Radio/Radio.tsx +141 -0
- package/compat/Radio/RadioGroup.styl +5 -0
- package/compat/Radio/RadioGroup.tsx +65 -0
- package/compat/Range/Range.tsx +79 -0
- package/compat/RateSearchValue/RateSearchValue.styl +22 -0
- package/compat/RateSearchValue/RateSearchValue.tsx +55 -0
- package/compat/RateSearchWidget/RateSearchWidget.styl +107 -0
- package/compat/RateSearchWidget/RateSearchWidget.tsx +163 -0
- package/compat/ServiceBlockItem/ServiceBlockItem.styl +48 -0
- package/compat/ServiceBlockItem/ServiceBlockItem.tsx +25 -0
- package/compat/SkeletonItem/SkeletonItem.styl +81 -0
- package/compat/SkeletonItem/SkeletonItem.tsx +80 -0
- package/compat/StarButton/StarButton.styl +39 -0
- package/compat/StarButton/StarButton.tsx +31 -0
- package/compat/TabPanel/TabPanel.tsx +29 -0
- package/compat/TimePicker/TimePicker.styl +23 -0
- package/compat/TimePicker/TimePicker.tsx +156 -0
- package/compat/Tooltip/Tooltip.styl +117 -0
- package/compat/Tooltip/Tooltip.tsx +63 -0
- package/compat/Usp/Usp.styl +78 -0
- package/compat/Usp/Usp.tsx +30 -0
- package/compat/Usp/uspTranslate.ts +12 -0
- package/compat/VehicleDetailedSidebar/VehicleDetailedSidebar.styl +37 -0
- package/compat/VehicleDetailedSidebar/VehicleDetailedSidebar.tsx +178 -0
- package/compat/VehicleDetailedSidebar/partials/Price.styl +27 -0
- package/compat/VehicleDetailedSidebar/partials/Price.tsx +41 -0
- package/compat/VehicleDetailedSidebar/partials/PriceContent.styl +124 -0
- package/compat/VehicleDetailedSidebar/partials/PriceContent.tsx +128 -0
- package/compat/VehicleDetailedSidebar/partials/Properties.styl +58 -0
- package/compat/VehicleDetailedSidebar/partials/Properties.tsx +155 -0
- package/compat/VehicleDetailedSidebar/partials/Title.styl +35 -0
- package/compat/VehicleDetailedSidebar/partials/Title.tsx +33 -0
- package/compat/VehicleDetailedSlider/VehicleDetailedSlider.styl +147 -0
- package/compat/VehicleDetailedSlider/VehicleDetailedSlider.tsx +382 -0
- package/compat/VehicleDetailedSlider/partials/MobileStast.tsx +61 -0
- package/compat/VehicleDetailedSlider/partials/MobileStats.styl +71 -0
- package/compat/VehicleDetailedSlider/partials/PriceData.styl +51 -0
- package/compat/VehicleDetailedSlider/partials/PriceData.tsx +67 -0
- package/compat/VehicleDetailedSlider/partials/Stats.styl +80 -0
- package/compat/VehicleDetailedSlider/partials/Stats.tsx +62 -0
- package/compat/VehicleDetailedSlider/partials/Title.styl +45 -0
- package/compat/VehicleDetailedSlider/partials/Title.tsx +36 -0
- package/compat/VehicleFormattedPrice/VehicleFormattedPrice.styl +149 -0
- package/compat/VehicleFormattedPrice/VehicleFormattedPrice.tsx +163 -0
- package/compat/VehicleSlider/VehicleSlider.styl +63 -0
- package/compat/VehicleSlider/VehicleSlider.tsx +125 -0
- package/compat/VehicleSlider/VehicleSliderForSRL.tsx +175 -0
- package/compat/VehicleSmallCard/SeoText/SeoText.styl +8 -0
- package/compat/VehicleSmallCard/SeoText/SeoText.tsx +16 -0
- package/compat/VehicleSmallCard/VehicleData/VechiclePriceItem/VechiclePriceItem.styl +204 -0
- package/compat/VehicleSmallCard/VehicleData/VechiclePriceItem/VechiclePriceItem.tsx +199 -0
- package/compat/VehicleSmallCard/VehicleData/VehicleInfo/VehicleInfo.styl +273 -0
- package/compat/VehicleSmallCard/VehicleData/VehicleInfo/VehicleInfo.tsx +179 -0
- package/compat/VehicleSmallCard/VehicleData/VehiclePrice/VehiclePrice.styl +165 -0
- package/compat/VehicleSmallCard/VehicleData/VehiclePrice/VehiclePrice.tsx +299 -0
- package/compat/VehicleSmallCard/VehicleData/VehicleProperty/VehicleProperty.styl +33 -0
- package/compat/VehicleSmallCard/VehicleData/VehicleProperty/VehicleProperty.tsx +35 -0
- package/compat/VehicleSmallCard/VehicleData/VehicleTitle/VehicleTitle.styl +136 -0
- package/compat/VehicleSmallCard/VehicleData/VehicleTitle/VehicleTitle.tsx +91 -0
- package/compat/VehicleSmallCard/VehicleSmallCard.styl +310 -0
- package/compat/VehicleSmallCard/VehicleSmallCard.tsx +448 -0
- package/compat/index.ts +77 -0
- package/compat/legacy-theme/mixins.styl +93 -0
- package/compat/legacy-theme/priceReviewColors.styl +12 -0
- package/compat/legacy-theme/slider.global.styl +5 -0
- package/compat/legacy-theme/theme.styl +136 -0
- package/compat/withLabel/withLabel.styl +33 -0
- package/compat/withLabel/withLabel.tsx +44 -0
- package/compat/withStats/withStats.styl +174 -0
- package/compat/withStats/withStats.tsx +238 -0
- package/constants/RateSearchWidget.ts +15 -0
- package/constants/Search.ts +290 -0
- package/constants/SearchWidget.ts +108 -0
- package/constants/app.ts +223 -0
- package/constants/common.ts +127 -0
- package/constants/core.ts +131 -0
- package/constants/highlights.ts +165 -0
- package/constants/price.ts +9 -0
- package/constants/selectors.ts +38 -0
- package/constants.ts +91 -0
- package/data/DataTransformers.ts +88 -0
- package/data/fieldMapping.ts +342 -0
- package/data/toAutobahnCar.ts +442 -0
- package/iconsvg/IconSVG.tsx +30 -0
- package/iconsvg/IconSVGConfig.tsx +768 -0
- package/iconsvg/SVG/AboPartner.tsx +16 -0
- package/iconsvg/SVG/ArrowQuotes.tsx +22 -0
- package/iconsvg/SVG/CloseIcon.tsx +17 -0
- package/iconsvg/SVG/CloseIconSelect.tsx +18 -0
- package/iconsvg/SVG/ClosedLockIcon.tsx +18 -0
- package/iconsvg/SVG/DealerCheckIcon.tsx +18 -0
- package/iconsvg/SVG/DealerPopoverMail.tsx +21 -0
- package/iconsvg/SVG/DealerPopoverPhone.tsx +21 -0
- package/iconsvg/SVG/GrayPhoneIcon.tsx +22 -0
- package/iconsvg/SVG/InternetIcon.tsx +18 -0
- package/iconsvg/SVG/WhiteEnvelopeIcon.tsx +21 -0
- package/iconsvg/SVG/bodyType/Cabrio.tsx +23 -0
- package/iconsvg/SVG/bodyType/Coupe.tsx +22 -0
- package/iconsvg/SVG/bodyType/Minibus.tsx +22 -0
- package/iconsvg/SVG/bodyType/Other.tsx +25 -0
- package/iconsvg/SVG/bodyType/Sedan.tsx +22 -0
- package/iconsvg/SVG/bodyType/SelectorCategoryCombi.tsx +68 -0
- package/iconsvg/SVG/bodyType/SelectorCategoryCompact.tsx +20 -0
- package/iconsvg/SVG/bodyType/SelectorCategoryConvertible.tsx +68 -0
- package/iconsvg/SVG/bodyType/SelectorCategoryCoupe.tsx +60 -0
- package/iconsvg/SVG/bodyType/SelectorCategoryMinibus.tsx +64 -0
- package/iconsvg/SVG/bodyType/SelectorCategoryOther.tsx +60 -0
- package/iconsvg/SVG/bodyType/SelectorCategorySedan.tsx +69 -0
- package/iconsvg/SVG/bodyType/SelectorCategorySuv.tsx +77 -0
- package/iconsvg/SVG/bodyType/SelectorCategoryTransporter.tsx +68 -0
- package/iconsvg/SVG/bodyType/SelectorCategoryVan.tsx +72 -0
- package/iconsvg/SVG/bodyType/Small.tsx +23 -0
- package/iconsvg/SVG/bodyType/Suv.tsx +23 -0
- package/iconsvg/SVG/bodyType/Van.tsx +23 -0
- package/iconsvg/SVG/bodyType/Wagon.tsx +23 -0
- package/iconsvg/SVG/common/AddIcon.tsx +16 -0
- package/iconsvg/SVG/common/AddPlus.tsx +16 -0
- package/iconsvg/SVG/common/AddressDashboard.tsx +29 -0
- package/iconsvg/SVG/common/AddressIcon.tsx +20 -0
- package/iconsvg/SVG/common/ArrowDetailedAccordionBlue.tsx +26 -0
- package/iconsvg/SVG/common/ArrowDownBlue.tsx +21 -0
- package/iconsvg/SVG/common/ArrowSelect.tsx +23 -0
- package/iconsvg/SVG/common/ArrowStepDown.tsx +18 -0
- package/iconsvg/SVG/common/Autocheck.tsx +19 -0
- package/iconsvg/SVG/common/AutodeLogo.tsx +36 -0
- package/iconsvg/SVG/common/Avatar.tsx +17 -0
- package/iconsvg/SVG/common/BellGray.tsx +19 -0
- package/iconsvg/SVG/common/BigInfoTransparent.tsx +13 -0
- package/iconsvg/SVG/common/Browse.tsx +20 -0
- package/iconsvg/SVG/common/Calendar.tsx +24 -0
- package/iconsvg/SVG/common/CallbackDashboard.tsx +30 -0
- package/iconsvg/SVG/common/Camera.tsx +17 -0
- package/iconsvg/SVG/common/Car.tsx +16 -0
- package/iconsvg/SVG/common/CarBody.tsx +19 -0
- package/iconsvg/SVG/common/CarFront.tsx +43 -0
- package/iconsvg/SVG/common/CarInspectionCheck.tsx +21 -0
- package/iconsvg/SVG/common/CarInspectionScheme0.tsx +32 -0
- package/iconsvg/SVG/common/CarInspectionScheme1.tsx +53 -0
- package/iconsvg/SVG/common/CarInspectionScheme2.tsx +52 -0
- package/iconsvg/SVG/common/CarInspectionScheme3.tsx +32 -0
- package/iconsvg/SVG/common/CarInspectionScheme4.tsx +32 -0
- package/iconsvg/SVG/common/CarInspectionScheme5.tsx +32 -0
- package/iconsvg/SVG/common/CarInspectionScheme6.tsx +32 -0
- package/iconsvg/SVG/common/CarInspectionSchemeGeneral copy.tsx +24 -0
- package/iconsvg/SVG/common/CarInspectionSchemeGeneral.tsx +24 -0
- package/iconsvg/SVG/common/CheckBlue.tsx +18 -0
- package/iconsvg/SVG/common/CheckGreen.tsx +22 -0
- package/iconsvg/SVG/common/CheckMark.tsx +16 -0
- package/iconsvg/SVG/common/CheckWhite.tsx +19 -0
- package/iconsvg/SVG/common/Chevron.tsx +17 -0
- package/iconsvg/SVG/common/CircledCheckmark.tsx +25 -0
- package/iconsvg/SVG/common/Clock.tsx +18 -0
- package/iconsvg/SVG/common/Close.tsx +19 -0
- package/iconsvg/SVG/common/CloseGrey.tsx +22 -0
- package/iconsvg/SVG/common/ContactEmail.tsx +21 -0
- package/iconsvg/SVG/common/ContactPhone.tsx +28 -0
- package/iconsvg/SVG/common/CopyIcon copy.tsx +22 -0
- package/iconsvg/SVG/common/CopyIcon.tsx +22 -0
- package/iconsvg/SVG/common/CopyIconForDealer.tsx +21 -0
- package/iconsvg/SVG/common/DashboardIcon.tsx +20 -0
- package/iconsvg/SVG/common/DashboardQuestionMark.tsx +20 -0
- package/iconsvg/SVG/common/DealersIcon.tsx +20 -0
- package/iconsvg/SVG/common/DeleteCircle.tsx +31 -0
- package/iconsvg/SVG/common/DeleteV1Icon.tsx +18 -0
- package/iconsvg/SVG/common/DeliveryDashboard.tsx +25 -0
- package/iconsvg/SVG/common/DeliveryMail.tsx +24 -0
- package/iconsvg/SVG/common/Discount100De.tsx +18 -0
- package/iconsvg/SVG/common/Discount100En.tsx +19 -0
- package/iconsvg/SVG/common/Discount66De.tsx +28 -0
- package/iconsvg/SVG/common/Discount66En.tsx +20 -0
- package/iconsvg/SVG/common/DoneIcon.tsx +18 -0
- package/iconsvg/SVG/common/EditCircle.tsx +32 -0
- package/iconsvg/SVG/common/EditPriceIcon.tsx +20 -0
- package/iconsvg/SVG/common/EuroCurrency.tsx +18 -0
- package/iconsvg/SVG/common/Export.tsx +20 -0
- package/iconsvg/SVG/common/Eye.tsx +17 -0
- package/iconsvg/SVG/common/EyeCatch.tsx +19 -0
- package/iconsvg/SVG/common/FavoritesIcon.tsx +20 -0
- package/iconsvg/SVG/common/FilterRange.tsx +16 -0
- package/iconsvg/SVG/common/FilterToolBlack.tsx +23 -0
- package/iconsvg/SVG/common/FlagDe.tsx +21 -0
- package/iconsvg/SVG/common/FlagEn.tsx +43 -0
- package/iconsvg/SVG/common/GrayEnvelopeIcon.tsx +19 -0
- package/iconsvg/SVG/common/HamburgerMenuIcon.tsx +18 -0
- package/iconsvg/SVG/common/HandingOverDashboard.tsx +32 -0
- package/iconsvg/SVG/common/ImageSettingsIcon.tsx +20 -0
- package/iconsvg/SVG/common/Info.tsx +16 -0
- package/iconsvg/SVG/common/InfoIcon.tsx +17 -0
- package/iconsvg/SVG/common/InfoTransparent.tsx +18 -0
- package/iconsvg/SVG/common/LogoutIcon.tsx +20 -0
- package/iconsvg/SVG/common/MagnifyingGlass.tsx +31 -0
- package/iconsvg/SVG/common/MenuArrow.tsx +16 -0
- package/iconsvg/SVG/common/MenuGrey.tsx +22 -0
- package/iconsvg/SVG/common/MobileDeLogo.tsx +108 -0
- package/iconsvg/SVG/common/MyOrderIcon.tsx +20 -0
- package/iconsvg/SVG/common/MyVehiclesIcon.tsx +20 -0
- package/iconsvg/SVG/common/NewFlagDe.tsx +25 -0
- package/iconsvg/SVG/common/NewFlagEn.tsx +34 -0
- package/iconsvg/SVG/common/OrderReceivedDashboard.tsx +27 -0
- package/iconsvg/SVG/common/Package.tsx +30 -0
- package/iconsvg/SVG/common/Phone.tsx +18 -0
- package/iconsvg/SVG/common/PhoneGrey.tsx +22 -0
- package/iconsvg/SVG/common/PickupDashboard.tsx +30 -0
- package/iconsvg/SVG/common/Picture.tsx +24 -0
- package/iconsvg/SVG/common/Pictures.tsx +32 -0
- package/iconsvg/SVG/common/Plus.tsx +16 -0
- package/iconsvg/SVG/common/PlusNew.tsx +16 -0
- package/iconsvg/SVG/common/PreparationDashboard.tsx +29 -0
- package/iconsvg/SVG/common/ProfileDashboard.tsx +24 -0
- package/iconsvg/SVG/common/ProfileIcon.tsx +17 -0
- package/iconsvg/SVG/common/Question.tsx +19 -0
- package/iconsvg/SVG/common/QuestionBold.tsx +25 -0
- package/iconsvg/SVG/common/RegistrationDashboard.tsx +31 -0
- package/iconsvg/SVG/common/SavedSearchsIcon.tsx +20 -0
- package/iconsvg/SVG/common/Search.tsx +19 -0
- package/iconsvg/SVG/common/SearchGrey.tsx +22 -0
- package/iconsvg/SVG/common/SettingPenIcon.tsx +20 -0
- package/iconsvg/SVG/common/Share.tsx +16 -0
- package/iconsvg/SVG/common/ShoppingCart.tsx +18 -0
- package/iconsvg/SVG/common/SliderArrow.tsx +21 -0
- package/iconsvg/SVG/common/SliderEye.tsx +17 -0
- package/iconsvg/SVG/common/SortUpDownArrows.tsx +18 -0
- package/iconsvg/SVG/common/Speedometer.tsx +23 -0
- package/iconsvg/SVG/common/Star.tsx +20 -0
- package/iconsvg/SVG/common/StarNew.tsx +16 -0
- package/iconsvg/SVG/common/StarOutline.tsx +20 -0
- package/iconsvg/SVG/common/StarRoundedCorners.tsx +18 -0
- package/iconsvg/SVG/common/Stats.tsx +17 -0
- package/iconsvg/SVG/common/Step1.tsx +21 -0
- package/iconsvg/SVG/common/Step2.tsx +20 -0
- package/iconsvg/SVG/common/Step3.tsx +21 -0
- package/iconsvg/SVG/common/SupportCallbackIcon.tsx +20 -0
- package/iconsvg/SVG/common/Telephone.tsx +22 -0
- package/iconsvg/SVG/common/Time.tsx +17 -0
- package/iconsvg/SVG/common/Timetable.tsx +18 -0
- package/iconsvg/SVG/common/Trash.tsx +22 -0
- package/iconsvg/SVG/common/TrashRed.tsx +23 -0
- package/iconsvg/SVG/common/Upload.tsx +17 -0
- package/iconsvg/SVG/common/UserGrey.tsx +22 -0
- package/iconsvg/SVG/common/UserProfileIcon.tsx +20 -0
- package/iconsvg/SVG/common/ValidateFailure.tsx +21 -0
- package/iconsvg/SVG/common/ValidateSuccess.tsx +21 -0
- package/iconsvg/SVG/common/WarningTriangle.tsx +20 -0
- package/iconsvg/SVG/common/WhiteArrowUp.tsx +21 -0
- package/iconsvg/SVG/common/WhiteKey.tsx +21 -0
- package/iconsvg/SVG/common/WhitePeople.tsx +23 -0
- package/iconsvg/SVG/common/WhiteTick.tsx +19 -0
- package/iconsvg/SVG/flags/AT.tsx +19 -0
- package/iconsvg/SVG/flags/BE.tsx +20 -0
- package/iconsvg/SVG/flags/BG.tsx +20 -0
- package/iconsvg/SVG/flags/CH.tsx +22 -0
- package/iconsvg/SVG/flags/CZ.tsx +18 -0
- package/iconsvg/SVG/flags/DE.tsx +18 -0
- package/iconsvg/SVG/flags/DK.tsx +18 -0
- package/iconsvg/SVG/flags/ES.tsx +2585 -0
- package/iconsvg/SVG/flags/FI.tsx +18 -0
- package/iconsvg/SVG/flags/FR.tsx +20 -0
- package/iconsvg/SVG/flags/GB.tsx +26 -0
- package/iconsvg/SVG/flags/HR.tsx +135 -0
- package/iconsvg/SVG/flags/HU.tsx +20 -0
- package/iconsvg/SVG/flags/IT.tsx +20 -0
- package/iconsvg/SVG/flags/LI.tsx +106 -0
- package/iconsvg/SVG/flags/LU.tsx +18 -0
- package/iconsvg/SVG/flags/NL.tsx +18 -0
- package/iconsvg/SVG/flags/NO.tsx +20 -0
- package/iconsvg/SVG/flags/PL.tsx +19 -0
- package/iconsvg/SVG/flags/RO.tsx +20 -0
- package/iconsvg/SVG/flags/RU.tsx +20 -0
- package/iconsvg/SVG/flags/SE.tsx +20 -0
- package/iconsvg/SVG/flags/SI.tsx +54 -0
- package/iconsvg/SVG/flags/SK.tsx +34 -0
- package/iconsvg/SVG/flags/UA.tsx +19 -0
- package/iconsvg/SVG/infoBlocks/14DaysReturnPolicy.tsx +18 -0
- package/iconsvg/SVG/infoBlocks/AlertInfo.tsx +19 -0
- package/iconsvg/SVG/infoBlocks/Autocheck.tsx +19 -0
- package/iconsvg/SVG/infoBlocks/CheckTheStory.tsx +22 -0
- package/iconsvg/SVG/infoBlocks/CustomerServiceWhite.tsx +16 -0
- package/iconsvg/SVG/infoBlocks/Guarantee.tsx +16 -0
- package/iconsvg/SVG/infoBlocks/OrderWhite.tsx +13 -0
- package/iconsvg/SVG/infoBlocks/PriceGuarantee.tsx +19 -0
- package/iconsvg/SVG/infoBlocks/PriceGuaranteeWhite.tsx +17 -0
- package/iconsvg/SVG/infoBlocks/TransportAndDelivery.tsx +21 -0
- package/iconsvg/SVG/infoBlocks/Warranty.tsx +23 -0
- package/iconsvg/SVG/popower/Mail.tsx +18 -0
- package/iconsvg/SVG/popower/Phone.tsx +19 -0
- package/iconsvg/SVG/popower/Schedule.tsx +18 -0
- package/iconsvg/SVG/searchWidgets/drive/AAllWheelDrive.tsx +26 -0
- package/iconsvg/SVG/searchWidgets/drive/AllWheelDrive.tsx +26 -0
- package/iconsvg/SVG/searchWidgets/drive/FrontWheelDrive.tsx +25 -0
- package/iconsvg/SVG/searchWidgets/drive/OtherWheelDrive.tsx +26 -0
- package/iconsvg/SVG/searchWidgets/drive/RearWheelDrive.tsx +25 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelDiesel.tsx +18 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelElectrical.tsx +19 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelElectricalDiesel.tsx +34 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelElectricalPetrol.tsx +20 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelEthanol.tsx +53 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelHydrogen.tsx +24 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelLpg.tsx +24 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelNaturalGas.tsx +23 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelOther.tsx +28 -0
- package/iconsvg/SVG/searchWidgets/fuel/FuelPetrol.tsx +18 -0
- package/iconsvg/SVG/settings/Account.tsx +17 -0
- package/iconsvg/SVG/settings/DataUsage.tsx +17 -0
- package/iconsvg/SVG/settings/MarketResearch.tsx +17 -0
- package/iconsvg/SVG/settings/Newsletter.tsx +17 -0
- package/iconsvg/SVG/settings/RemoveEye.tsx +17 -0
- package/iconsvg/SVG/settings/RemoveEyeOff.tsx +17 -0
- package/iconsvg/SVG/settings/Security.tsx +17 -0
- package/iconsvg/SVG/settings/Warning.tsx +17 -0
- package/iconsvg/SVG/slider/360.tsx +23 -0
- package/iconsvg/SVG/slider/360New.tsx +20 -0
- package/iconsvg/SVG/slider/Checked.tsx +19 -0
- package/iconsvg/SVG/slider/DiscountRibbon.tsx +51 -0
- package/iconsvg/SVG/slider/DiscountRibbonMobile.tsx +47 -0
- package/iconsvg/SVG/slider/DiscountRibbonNew.tsx +29 -0
- package/iconsvg/SVG/slider/Easy.tsx +19 -0
- package/iconsvg/SVG/slider/Fullscreen.tsx +16 -0
- package/iconsvg/SVG/slider/FullscreenWindow.tsx +23 -0
- package/iconsvg/SVG/slider/Safe.tsx +19 -0
- package/iconsvg/SVG/slider/ToBuy.tsx +19 -0
- package/iconsvg/SVG/slider/YoutubeButton.tsx +26 -0
- package/iconsvg/SVG/slider/ZoomIn.tsx +21 -0
- package/iconsvg/SVG/slider/ZoomOut.tsx +21 -0
- package/iconsvg/SVG/social/AndroidLogo.tsx +18 -0
- package/iconsvg/SVG/social/Apple.tsx +19 -0
- package/iconsvg/SVG/social/FacebookF.tsx +19 -0
- package/iconsvg/SVG/social/GooglePlusG.tsx +19 -0
- package/iconsvg/SVG/social/Instagram.tsx +17 -0
- package/iconsvg/SVG/social/Pinterest.tsx +19 -0
- package/iconsvg/SVG/social/SharingInterface.tsx +21 -0
- package/iconsvg/SVG/social/Twitter.tsx +19 -0
- package/iconsvg/SVG/social/Youtube.tsx +19 -0
- package/iconsvg/SVG/vehicle/AboIcon.tsx +11 -0
- package/iconsvg/SVG/vehicle/Abs.tsx +24 -0
- package/iconsvg/SVG/vehicle/AirCond.tsx +34 -0
- package/iconsvg/SVG/vehicle/Airbag.tsx +21 -0
- package/iconsvg/SVG/vehicle/AuxiliaryHeating.tsx +23 -0
- package/iconsvg/SVG/vehicle/Bluetooth.tsx +22 -0
- package/iconsvg/SVG/vehicle/BuyIcon.tsx +15 -0
- package/iconsvg/SVG/vehicle/CarAvailability.tsx +22 -0
- package/iconsvg/SVG/vehicle/CarInspectionCheck.tsx +21 -0
- package/iconsvg/SVG/vehicle/CarInspectionCross.tsx +21 -0
- package/iconsvg/SVG/vehicle/CheckoutChecked.tsx +15 -0
- package/iconsvg/SVG/vehicle/Co2.tsx +24 -0
- package/iconsvg/SVG/vehicle/CruiseControl.tsx +25 -0
- package/iconsvg/SVG/vehicle/Date.tsx +21 -0
- package/iconsvg/SVG/vehicle/Delivery.tsx +12 -0
- package/iconsvg/SVG/vehicle/Diesel.tsx +28 -0
- package/iconsvg/SVG/vehicle/Display.tsx +25 -0
- package/iconsvg/SVG/vehicle/Done.tsx +11 -0
- package/iconsvg/SVG/vehicle/Door.tsx +19 -0
- package/iconsvg/SVG/vehicle/DoubleDone.tsx +18 -0
- package/iconsvg/SVG/vehicle/EditIcon.tsx +11 -0
- package/iconsvg/SVG/vehicle/EmergencyBrakeAssistant.tsx +27 -0
- package/iconsvg/SVG/vehicle/Eps.tsx +24 -0
- package/iconsvg/SVG/vehicle/Esp.tsx +24 -0
- package/iconsvg/SVG/vehicle/FinancingIcon.tsx +20 -0
- package/iconsvg/SVG/vehicle/FuelConsumption.tsx +20 -0
- package/iconsvg/SVG/vehicle/Fueling.tsx +22 -0
- package/iconsvg/SVG/vehicle/FullDrive.tsx +24 -0
- package/iconsvg/SVG/vehicle/GasElectric.tsx +28 -0
- package/iconsvg/SVG/vehicle/HandsFreePhoneSystem.tsx +24 -0
- package/iconsvg/SVG/vehicle/Kw.tsx +23 -0
- package/iconsvg/SVG/vehicle/LastCrawledAt.tsx +22 -0
- package/iconsvg/SVG/vehicle/LeasingIcon.tsx +14 -0
- package/iconsvg/SVG/vehicle/Led.tsx +24 -0
- package/iconsvg/SVG/vehicle/LightSensor.tsx +22 -0
- package/iconsvg/SVG/vehicle/LinkAlternative.tsx +15 -0
- package/iconsvg/SVG/vehicle/Location.tsx +17 -0
- package/iconsvg/SVG/vehicle/MinusIcon.tsx +11 -0
- package/iconsvg/SVG/vehicle/Mirror.tsx +19 -0
- package/iconsvg/SVG/vehicle/Navigation.tsx +27 -0
- package/iconsvg/SVG/vehicle/NewCar.tsx +21 -0
- package/iconsvg/SVG/vehicle/NoImage.tsx +17 -0
- package/iconsvg/SVG/vehicle/Owner.tsx +18 -0
- package/iconsvg/SVG/vehicle/PanoramicRoof.tsx +23 -0
- package/iconsvg/SVG/vehicle/ParkingAssistants.tsx +24 -0
- package/iconsvg/SVG/vehicle/PickYourself.tsx +18 -0
- package/iconsvg/SVG/vehicle/PlusIcon.tsx +11 -0
- package/iconsvg/SVG/vehicle/Power.tsx +21 -0
- package/iconsvg/SVG/vehicle/Ps.tsx +19 -0
- package/iconsvg/SVG/vehicle/Question.tsx +24 -0
- package/iconsvg/SVG/vehicle/Seats.tsx +21 -0
- package/iconsvg/SVG/vehicle/SeatsHeating.tsx +26 -0
- package/iconsvg/SVG/vehicle/SelectorGearboxAutomatic.tsx +24 -0
- package/iconsvg/SVG/vehicle/SelectorGearboxManualShift.tsx +19 -0
- package/iconsvg/SVG/vehicle/SelectorGearboxOther.tsx +24 -0
- package/iconsvg/SVG/vehicle/SelectorGearboxSemiAutomatic.tsx +30 -0
- package/iconsvg/SVG/vehicle/SelectorInteriorTypeAlcantara.tsx +225 -0
- package/iconsvg/SVG/vehicle/SelectorInteriorTypeFabric.tsx +29 -0
- package/iconsvg/SVG/vehicle/SelectorInteriorTypeFullLeather.tsx +29 -0
- package/iconsvg/SVG/vehicle/SelectorInteriorTypeOther.tsx +26 -0
- package/iconsvg/SVG/vehicle/SelectorInteriorTypePartLeather.tsx +142 -0
- package/iconsvg/SVG/vehicle/SelectorInteriorTypeSyntheticLeather.tsx +27 -0
- package/iconsvg/SVG/vehicle/SelectorInteriorTypeVelours.tsx +39 -0
- package/iconsvg/SVG/vehicle/Shield.tsx +39 -0
- package/iconsvg/SVG/vehicle/Speedometer.tsx +17 -0
- package/iconsvg/SVG/vehicle/StartStopSystem.tsx +30 -0
- package/iconsvg/SVG/vehicle/Stop.tsx +25 -0
- package/iconsvg/SVG/vehicle/TickMark.tsx +21 -0
- package/iconsvg/SVG/vehicle/TrafficSignRecognition.tsx +29 -0
- package/iconsvg/SVG/vehicle/TrailerCoupling.tsx +24 -0
- package/iconsvg/SVG/vehicle/Transmission.tsx +27 -0
- package/iconsvg/SVG/vehicle/Wheel.tsx +43 -0
- package/iconsvg/SVG/vehicle/Wipers.tsx +22 -0
- package/iconsvg/SVG/vehicle/Wrench.tsx +17 -0
- package/iconsvg/index.ts +7 -0
- package/index.ts +18 -0
- package/locales/de/autobahn.json +671 -0
- package/locales/en/autobahn.json +671 -0
- package/locales/legacy/SearchDetailedPage.ts +205 -0
- package/locales/legacy/cbd.ts +719 -0
- package/locales/legacy/common.ts +109 -0
- package/locales/legacy/data.ts +1118 -0
- package/locales/legacy/priceRating.ts +10 -0
- package/locales/legacy/sortingOptions.ts +21 -0
- package/media/images/question.svg +1 -0
- package/media/images/raiting-stars-min.svg +7 -0
- package/media/images/refresh-button.svg +49 -0
- package/media/images/searchWidget/selector_emissionSticker_green.png +0 -0
- package/media/images/searchWidget/selector_emissionSticker_red.png +0 -0
- package/media/images/searchWidget/selector_emissionSticker_yellow.png +0 -0
- package/media/images/slider-arrow-new.svg +1 -0
- package/media/images/user-icon.svg +16 -0
- package/media/svg/common/eye.svg +1 -0
- package/media/svg/common/star.svg +1 -0
- package/media/svg/infoBlocks/whyAutodeIconSet-F.svg +9 -0
- package/media/svg/infoBlocks/whyAutodeIconSet-G.svg +8 -0
- package/media/svg/infoBlocks/whyAutodeIconSet-Q.svg +6 -0
- package/media/svg/infoBlocks/whyAutodeIconSet-R.svg +9 -0
- package/media/svg/usp/icon1.svg +16 -0
- package/media/svg/usp/icon2.svg +13 -0
- package/media/svg/usp/icon3.svg +10 -0
- package/media/svg/usp/icon4.svg +14 -0
- package/package.json +30 -0
- package/schemes/AggregationScheme.ts +73 -0
- package/searchFilters/filterOptions.ts +119 -0
- package/searchFilters/filterProps.ts +35 -0
- package/searchFilters/index.ts +15 -0
- package/theme/autobahn.css +25 -0
- package/theme/colors_and_type.css +113 -0
- package/theme/md3.css +2034 -0
- package/types/types.ts +374 -0
- package/types.ts +8 -0
- package/ui/Accordion.tsx +134 -0
- package/ui/Button.tsx +62 -0
- package/ui/CardCarousel.tsx +133 -0
- package/ui/Chip.tsx +29 -0
- package/ui/DatePicker.tsx +145 -0
- package/ui/Dropdown.tsx +205 -0
- package/ui/FileUpload.tsx +154 -0
- package/ui/FormCheckbox.tsx +79 -0
- package/ui/FormDropdown.tsx +327 -0
- package/ui/FormField.tsx +88 -0
- package/ui/FormRadio.tsx +93 -0
- package/ui/FormRadioGroup.tsx +106 -0
- package/ui/Icon.tsx +391 -0
- package/ui/IconButton.tsx +44 -0
- package/ui/Modal.tsx +187 -0
- package/ui/Pagination.tsx +140 -0
- package/ui/Price.tsx +25 -0
- package/ui/PriceEvalBar.tsx +48 -0
- package/ui/SaveChip.tsx +34 -0
- package/ui/Segmented.tsx +48 -0
- package/ui/Skeleton.tsx +75 -0
- package/ui/Spec.tsx +24 -0
- package/ui/Stars.tsx +29 -0
- package/ui/StatChip.tsx +21 -0
- package/ui/Stepper.tsx +106 -0
- package/ui/Switch.tsx +65 -0
- package/ui/Table.tsx +156 -0
- package/ui/Textarea.tsx +58 -0
- package/ui/index.ts +88 -0
- package/utils/CommonUtils.ts +649 -0
- package/utils/DateUtils.ts +101 -0
- package/utils/index.ts +74 -0
- package/utils/power.ts +11 -0
- package/utils/query.ts +21 -0
- package/vdp/FinanceCalculator.tsx +465 -0
- package/vdp/VdpModules.tsx +1490 -0
- package/vdp/VdpViewer.tsx +139 -0
- package/vdp/index.ts +75 -0
- package/vdp/types.ts +197 -0
- package/vehiclesProps/cbdSelectors.ts +117 -0
- package/vehiclesProps/decoratedLightProps.tsx +572 -0
- package/vehiclesProps/decoratedProps.tsx +1746 -0
- package/vehiclesProps/equipment.ts +129 -0
- package/vehiclesProps/vehicleDetails.ts +428 -0
- package/widgets/SearchBox.tsx +256 -0
- package/widgets/index.ts +5 -0
|
@@ -0,0 +1,1490 @@
|
|
|
1
|
+
// Autobahn (auto.de redesign) — Vehicle-Detail (VDP) presentational modules.
|
|
2
|
+
//
|
|
3
|
+
// Typed React 18 + TS port of the remaining VDP pieces from the design kit
|
|
4
|
+
// (docs/redesign/design-source/ui_kits/website/CdpModules.jsx + ScreenVDP.jsx).
|
|
5
|
+
// Each is a pure / presentational ES named export with an explicit props
|
|
6
|
+
// interface; none touches a store, the API or `window`. The page wiring that
|
|
7
|
+
// feeds these from the real stores is a SEPARATE later step (W3b).
|
|
8
|
+
//
|
|
9
|
+
// Replaces the design kit's globals:
|
|
10
|
+
// - `window.CO2_CLASSES` / `window.CO2_COLORS` → ../constants
|
|
11
|
+
// - `equipFor(id)` / `car.priceHistory` / market band → typed props on VehicleDetail
|
|
12
|
+
// - `assetUrl(...)` → pre-resolved image URLs (caller's job)
|
|
13
|
+
// - `data-lucide` icons → the ui kit's <Icon />
|
|
14
|
+
// The DealerBlock from CdpModules is intentionally NOT ported (auto.de is the
|
|
15
|
+
// seller, not a dealer marketplace).
|
|
16
|
+
//
|
|
17
|
+
// NEEDS-DATA discipline (the point of the ticket): wherever the design showed a
|
|
18
|
+
// figure we have no real source for, these render the NEEDS_DATA_LABEL
|
|
19
|
+
// ('Daten folgt') marker rather than inventing a value — see PriceHistory (whole
|
|
20
|
+
// card) and its market band.
|
|
21
|
+
//
|
|
22
|
+
// All components render correctly only inside a `<div className="md3">` subtree.
|
|
23
|
+
|
|
24
|
+
import React, { useState } from 'react';
|
|
25
|
+
import { TranslationFunction } from '../types';
|
|
26
|
+
|
|
27
|
+
import { Button, Icon, Spec, StatChip, Stars } from '../ui';
|
|
28
|
+
import { CO2_CLASSES, CO2_COLORS } from '../constants';
|
|
29
|
+
import { defaultVdpSpecs, EquipmentGroups, VehicleDetail, VdpSpec, VdpReview } from './types';
|
|
30
|
+
import { Slider, TermChips } from './FinanceCalculator';
|
|
31
|
+
|
|
32
|
+
const eur = (n: number): string => n.toLocaleString('de-DE');
|
|
33
|
+
|
|
34
|
+
/** mileage → 'X.XXX km' (de-DE grouping). */
|
|
35
|
+
function fmtKm(n: number): string {
|
|
36
|
+
return `${n.toLocaleString('de-DE')} km`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** strip a trailing parenthetical, e.g. '110 kW (150 PS)' → '110 kW'. */
|
|
40
|
+
function stripParen(s: string): string {
|
|
41
|
+
return s.replace(/\s*\(.*\)/, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Header card — title, fact line, highlight chips, condition tiles
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export interface VdpHeaderCardProps {
|
|
49
|
+
vehicle: VehicleDetail;
|
|
50
|
+
/** i18n function (drilled from the page). */
|
|
51
|
+
t: TranslationFunction;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* One container holding the entire vehicle identity: headline + trim/power, the
|
|
56
|
+
* dot-separated fact line, the hand-picked highlight chips, and the condition
|
|
57
|
+
* (zustand) tiles footer. The gallery itself is composed by the page (W3b) — this
|
|
58
|
+
* is just the textual header card that sits beneath it.
|
|
59
|
+
*/
|
|
60
|
+
export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
|
|
61
|
+
const { condition } = vehicle;
|
|
62
|
+
const factLine: ReadonlyArray<readonly [string, string]> = [
|
|
63
|
+
['gauge', fmtKm(vehicle.km)],
|
|
64
|
+
['calendar', vehicle.reg],
|
|
65
|
+
['cog', vehicle.gear],
|
|
66
|
+
['fuel', stripParen(vehicle.fuel)],
|
|
67
|
+
['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
|
|
68
|
+
];
|
|
69
|
+
// condition.hu === 'Neu' compares the RAW backend data value, not a display string — left as-is.
|
|
70
|
+
const tiles: ReadonlyArray<readonly [string, string, string]> = [
|
|
71
|
+
['wrench', t('autobahn:header.huLabel'), condition.hu === 'Neu' ? t('autobahn:header.huNew') : t('autobahn:header.huUntil', { date: condition.hu })],
|
|
72
|
+
['shield-check', t('autobahn:header.accidentFree'), condition.unfallfrei ? t('autobahn:header.yes') : t('autobahn:header.no')],
|
|
73
|
+
['clipboard-check', t('autobahn:header.serviceHistory'), condition.scheckheft ? t('autobahn:header.yes') : t('autobahn:header.no')],
|
|
74
|
+
['award', t('autobahn:header.warranty'), condition.garantie],
|
|
75
|
+
];
|
|
76
|
+
return (
|
|
77
|
+
<div className="mcard" style={{ marginTop: 16, padding: '22px 24px 18px' }}>
|
|
78
|
+
<div className="vdp-headrow">
|
|
79
|
+
<div style={{ minWidth: 0 }}>
|
|
80
|
+
<h1 className="md-headline" style={{ margin: '0 0 4px' }}>
|
|
81
|
+
{vehicle.make} {vehicle.model}
|
|
82
|
+
</h1>
|
|
83
|
+
<p className="md-body" style={{ margin: 0, fontSize: 15 }}>
|
|
84
|
+
{vehicle.trim} · {vehicle.power}
|
|
85
|
+
</p>
|
|
86
|
+
{/* auto.de-ID lives in the offer card (next to Finanzierungsdetails), not here */}
|
|
87
|
+
</div>
|
|
88
|
+
{/* BL-08: no chip at all when there's no real rating (empty/broken price) — never a
|
|
89
|
+
fabricated "Top Preis"-shaped badge for `rating === ''`. */}
|
|
90
|
+
{vehicle.rating && (
|
|
91
|
+
<StatChip
|
|
92
|
+
tone={vehicle.rating === 'fair' ? 'warn' : 'good'}
|
|
93
|
+
icon={vehicle.rating === 'fair' ? 'minus' : 'check'}
|
|
94
|
+
>
|
|
95
|
+
{t('autobahn:tier.' + vehicle.rating)}
|
|
96
|
+
</StatChip>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* fact line — plain text with dot separators, not pills */}
|
|
101
|
+
<div className="vdp-meta">
|
|
102
|
+
{factLine.map(([ic, v]) => (
|
|
103
|
+
<span key={ic}>
|
|
104
|
+
<Icon name={ic} size={16} color="var(--md-on-surface-variant)" stroke={1.8} />
|
|
105
|
+
{v}
|
|
106
|
+
</span>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* hand-picked highlights — the single accent row of the header */}
|
|
111
|
+
<div
|
|
112
|
+
className="vdp-highlights"
|
|
113
|
+
style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginTop: 14 }}
|
|
114
|
+
>
|
|
115
|
+
{vehicle.highlights.slice(0, 6).map((f) => (
|
|
116
|
+
<span key={f} className="chip chip--filter chip--selected" style={{ cursor: 'default' }}>
|
|
117
|
+
<Icon name="check" size={14} stroke={2.4} />
|
|
118
|
+
{f}
|
|
119
|
+
</span>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* condition — footer row of the header card, divided by a hairline */}
|
|
124
|
+
<div className="zustand-grid">
|
|
125
|
+
{tiles.map(([ic, l, v]) => (
|
|
126
|
+
<div key={l} className="zustand-tile">
|
|
127
|
+
<Icon name={ic} size={19} color="var(--md-primary)" stroke={1.9} style={{ flex: 'none' }} />
|
|
128
|
+
<span style={{ minWidth: 0 }}>
|
|
129
|
+
<span
|
|
130
|
+
className="md-label-sm"
|
|
131
|
+
style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
|
|
132
|
+
>
|
|
133
|
+
{l}
|
|
134
|
+
</span>
|
|
135
|
+
<span className="md-title-sm" style={{ display: 'block' }}>
|
|
136
|
+
{v}
|
|
137
|
+
</span>
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Fahrzeugdaten spec table (mobile clamp + "Mehr anzeigen")
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
export interface VdpSpecTableProps {
|
|
151
|
+
vehicle: VehicleDetail;
|
|
152
|
+
/** i18n function (drilled from the page). */
|
|
153
|
+
t: TranslationFunction;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Fahrzeugdaten grid of Spec items. Uses the vehicle's explicit `specs` when
|
|
158
|
+
* present, else derives the design's twelve-row layout via `defaultVdpSpecs`. On
|
|
159
|
+
* phones the table clamps to a few rows (`vdp-specs--clamp`) with a "Mehr
|
|
160
|
+
* anzeigen" toggle (pure view state).
|
|
161
|
+
*/
|
|
162
|
+
export const VdpSpecTable: React.FunctionComponent<VdpSpecTableProps> = ({ vehicle, t }) => {
|
|
163
|
+
const [allSpecs, setAllSpecs] = useState(false);
|
|
164
|
+
const specs: VdpSpec[] = vehicle.specs && vehicle.specs.length ? vehicle.specs : defaultVdpSpecs(vehicle);
|
|
165
|
+
return (
|
|
166
|
+
<div className="mcard" id="vdp-daten" style={{ padding: 22 }}>
|
|
167
|
+
<h2 className="md-headline-sm" style={{ marginBottom: 16 }}>
|
|
168
|
+
{t('autobahn:specTable.title')}
|
|
169
|
+
</h2>
|
|
170
|
+
<div className={'vdp-specs' + (allSpecs ? '' : ' vdp-specs--clamp')}>
|
|
171
|
+
{specs.map(([ic, l, v]) => (
|
|
172
|
+
<Spec key={l} icon={ic} label={l}>
|
|
173
|
+
{v}
|
|
174
|
+
</Spec>
|
|
175
|
+
))}
|
|
176
|
+
</div>
|
|
177
|
+
{/* phone only: clamp button only meaningful when there are more than 6 specs */}
|
|
178
|
+
{specs.length > 6 && (
|
|
179
|
+
<button
|
|
180
|
+
className="mbtn mbtn--text show-mobile"
|
|
181
|
+
style={{ marginTop: 12, padding: 0 }}
|
|
182
|
+
onClick={() => setAllSpecs((a) => !a)}
|
|
183
|
+
>
|
|
184
|
+
{allSpecs ? t('autobahn:vdp.specs.showLess') : t('autobahn:specTable.showAll', { count: specs.length })}
|
|
185
|
+
<Icon name={allSpecs ? 'chevron-up' : 'chevron-down'} size={16} />
|
|
186
|
+
</button>
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Equipment list (grouped + searchable)
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
export interface EquipmentListProps {
|
|
197
|
+
/** grouped equipment, e.g. { Komfort: [...], Sicherheit: [...] }. */
|
|
198
|
+
groups: EquipmentGroups;
|
|
199
|
+
/** i18n function (drilled from the page). */
|
|
200
|
+
t: TranslationFunction;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Grouped + searchable equipment list (Autohero / AutoScout pattern). */
|
|
204
|
+
export const EquipmentList: React.FunctionComponent<EquipmentListProps> = ({ groups, t }) => {
|
|
205
|
+
const [q, setQ] = useState('');
|
|
206
|
+
const [openAll, setOpenAll] = useState(false);
|
|
207
|
+
const ql = q.trim().toLowerCase();
|
|
208
|
+
const entries = Object.entries(groups)
|
|
209
|
+
.map(([g, items]): [string, string[]] => [g, items.filter((it) => !ql || it.toLowerCase().includes(ql))])
|
|
210
|
+
.filter(([, items]) => items.length);
|
|
211
|
+
const total = Object.values(groups).reduce((s, a) => s + a.length, 0);
|
|
212
|
+
return (
|
|
213
|
+
<div className="mcard" id="vdp-ausstattung" style={{ padding: 22 }}>
|
|
214
|
+
<div
|
|
215
|
+
style={{
|
|
216
|
+
display: 'flex',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
justifyContent: 'space-between',
|
|
219
|
+
gap: 12,
|
|
220
|
+
marginBottom: 14,
|
|
221
|
+
flexWrap: 'wrap',
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
<h2 className="md-headline-sm">
|
|
225
|
+
{t('autobahn:equipment.title')}{' '}
|
|
226
|
+
<span style={{ color: 'var(--md-on-surface-variant)', fontWeight: 600 }}>({total})</span>
|
|
227
|
+
</h2>
|
|
228
|
+
<label className="mfield" style={{ height: 40, maxWidth: 240, flex: 1, minWidth: 170 }}>
|
|
229
|
+
<Icon name="search" size={18} />
|
|
230
|
+
<input placeholder={t('autobahn:equipment.search.placeholder')} value={q} onChange={(e) => setQ(e.target.value)} />
|
|
231
|
+
{q && (
|
|
232
|
+
<button className="icon-btn" style={{ width: 28, height: 28 }} onClick={() => setQ('')}>
|
|
233
|
+
<Icon name="x" size={15} />
|
|
234
|
+
</button>
|
|
235
|
+
)}
|
|
236
|
+
</label>
|
|
237
|
+
</div>
|
|
238
|
+
<div className="eq-cols">
|
|
239
|
+
{entries.map(([g, items]) => (
|
|
240
|
+
<div key={g} style={{ breakInside: 'avoid', marginBottom: 18 }}>
|
|
241
|
+
<div className="md-title-sm" style={{ color: 'var(--md-primary)', marginBottom: 8 }}>
|
|
242
|
+
{g}
|
|
243
|
+
</div>
|
|
244
|
+
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 7 }}>
|
|
245
|
+
{(openAll || ql ? items : items.slice(0, 4)).map((it) => (
|
|
246
|
+
<li
|
|
247
|
+
key={it}
|
|
248
|
+
style={{
|
|
249
|
+
display: 'flex',
|
|
250
|
+
gap: 9,
|
|
251
|
+
alignItems: 'flex-start',
|
|
252
|
+
fontSize: 13.5,
|
|
253
|
+
color: 'var(--md-on-surface-variant)',
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<Icon name="check" size={16} color="var(--md-good)" stroke={2.4} style={{ flex: 'none', marginTop: 1 }} />
|
|
257
|
+
{it}
|
|
258
|
+
</li>
|
|
259
|
+
))}
|
|
260
|
+
</ul>
|
|
261
|
+
</div>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
{!ql && !openAll && (
|
|
265
|
+
<button className="mbtn mbtn--text" style={{ marginTop: 4 }} onClick={() => setOpenAll(true)}>
|
|
266
|
+
{t('autobahn:equipment.showAll', { total })}
|
|
267
|
+
<Icon name="chevron-down" size={16} />
|
|
268
|
+
</button>
|
|
269
|
+
)}
|
|
270
|
+
{entries.length === 0 && <p className="md-body-sm">{t('autobahn:equipment.noResults', { q })}</p>}
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// CO₂ / energy-efficiency label
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
export interface Co2LabelProps {
|
|
280
|
+
vehicle: VehicleDetail;
|
|
281
|
+
/** i18n function (drilled from the page). */
|
|
282
|
+
t: TranslationFunction;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** CO₂ / energy-efficiency label (EU-style stepped colour band). Handles PHEV. */
|
|
286
|
+
export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t }) => {
|
|
287
|
+
const p = vehicle.phev;
|
|
288
|
+
const idx = Math.max(0, CO2_CLASSES.indexOf((p ? p.co2classW : vehicle.co2class) as (typeof CO2_CLASSES)[number]));
|
|
289
|
+
const stats: ReadonlyArray<readonly [string, string]> = p
|
|
290
|
+
? [
|
|
291
|
+
[t('autobahn:eco.stat.powerConsumptionWeighted'), p.consEl],
|
|
292
|
+
[t('autobahn:eco.stat.fuelConsumptionWeighted'), p.consFuel],
|
|
293
|
+
[t('autobahn:eco.stat.fuelConsumptionDischarged'), p.consDis],
|
|
294
|
+
[t('autobahn:eco.stat.co2WeightedCombined'), p.co2w],
|
|
295
|
+
]
|
|
296
|
+
: [
|
|
297
|
+
[t('autobahn:eco.stat.consumptionCombined'), vehicle.cons],
|
|
298
|
+
[t('autobahn:eco.stat.co2Emissions'), vehicle.co2],
|
|
299
|
+
[t('autobahn:eco.stat.co2Class'), vehicle.co2class],
|
|
300
|
+
];
|
|
301
|
+
return (
|
|
302
|
+
<div className="mcard" style={{ padding: 22 }}>
|
|
303
|
+
<h2 className="md-headline-sm" style={{ marginBottom: 14 }}>
|
|
304
|
+
{t('autobahn:eco.heading')}
|
|
305
|
+
</h2>
|
|
306
|
+
<div className="co2-stats">
|
|
307
|
+
{stats.map(([l, v]) => (
|
|
308
|
+
<div key={l}>
|
|
309
|
+
<div className="md-label-sm">{l}</div>
|
|
310
|
+
<div className="md-title-sm" style={{ fontSize: 16 }}>
|
|
311
|
+
{v}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
<div className="co2-scale">
|
|
317
|
+
{CO2_CLASSES.map((c, i) => (
|
|
318
|
+
<div key={c} className={'co2-seg' + (i === idx ? ' co2-seg--on' : '')} style={{ background: CO2_COLORS[i] }}>
|
|
319
|
+
{c}
|
|
320
|
+
</div>
|
|
321
|
+
))}
|
|
322
|
+
</div>
|
|
323
|
+
{p ? (
|
|
324
|
+
<p className="md-label-sm" style={{ margin: '10px 0 0' }}>
|
|
325
|
+
{t('autobahn:eco.co2ClassPrefix')} <b style={{ color: 'var(--md-on-surface)' }}>{p.co2classW}</b>{' '}
|
|
326
|
+
{t('autobahn:eco.weightedCombined')} · <b style={{ color: 'var(--md-on-surface)' }}>{p.co2classD}</b>{' '}
|
|
327
|
+
{t('autobahn:eco.dischargedCombined')} · {t('autobahn:eco.energyClassWltpSuffix')}
|
|
328
|
+
</p>
|
|
329
|
+
) : (
|
|
330
|
+
<p className="md-label-sm" style={{ margin: '10px 0 0' }}>
|
|
331
|
+
{t('autobahn:eco.energyClass')}
|
|
332
|
+
</p>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
// Free-text vehicle description (Fahrzeugbeschreibung)
|
|
340
|
+
// ---------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
export interface VehicleDescriptionProps {
|
|
343
|
+
vehicle: VehicleDetail;
|
|
344
|
+
/**
|
|
345
|
+
* Pre-composed description text. When omitted a sensible default is built from
|
|
346
|
+
* the vehicle's typed fields (matching the design's template sentence).
|
|
347
|
+
*/
|
|
348
|
+
text?: string;
|
|
349
|
+
/** i18n function (drilled from the page). */
|
|
350
|
+
t: TranslationFunction;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function defaultDescription(v: VehicleDetail, t: TranslationFunction): string {
|
|
354
|
+
return t('autobahn:description.defaultBody', {
|
|
355
|
+
make: v.make,
|
|
356
|
+
model: v.model,
|
|
357
|
+
trim: v.trim,
|
|
358
|
+
power: v.power,
|
|
359
|
+
gear: v.gear,
|
|
360
|
+
owners: v.owners,
|
|
361
|
+
reg: v.reg,
|
|
362
|
+
km: v.km.toLocaleString('de-DE'),
|
|
363
|
+
color: v.color,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Free-text vehicle description with a "Mehr anzeigen" clamp (pure view state). */
|
|
368
|
+
export const VehicleDescription: React.FunctionComponent<VehicleDescriptionProps> = ({ vehicle, text, t }) => {
|
|
369
|
+
const [open, setOpen] = useState(false);
|
|
370
|
+
const body = text ?? defaultDescription(vehicle, t);
|
|
371
|
+
return (
|
|
372
|
+
<div className="mcard" style={{ padding: 22 }}>
|
|
373
|
+
<h2 className="md-headline-sm" style={{ marginBottom: 12 }}>
|
|
374
|
+
{t('autobahn:description.heading')}
|
|
375
|
+
</h2>
|
|
376
|
+
<p
|
|
377
|
+
className="md-body"
|
|
378
|
+
style={{ margin: 0, maxHeight: open ? 'none' : 96, overflow: 'hidden', position: 'relative' }}
|
|
379
|
+
>
|
|
380
|
+
{body}
|
|
381
|
+
</p>
|
|
382
|
+
<button className="mbtn mbtn--text" style={{ marginTop: 8, padding: 0 }} onClick={() => setOpen((o) => !o)}>
|
|
383
|
+
{open ? t('autobahn:description.showLess') : t('autobahn:description.showMore')}
|
|
384
|
+
<Icon name={open ? 'chevron-up' : 'chevron-down'} size={16} />
|
|
385
|
+
</button>
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Trust strip
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
/** [kebab-case lucide icon, label]. */
|
|
395
|
+
export type TrustItem = readonly [icon: string, label: string];
|
|
396
|
+
|
|
397
|
+
export interface TrustStripProps {
|
|
398
|
+
/** trust badges; defaults to auto.de's four standard promises. */
|
|
399
|
+
items?: ReadonlyArray<TrustItem>;
|
|
400
|
+
/** i18n function (drilled from the page). */
|
|
401
|
+
t: TranslationFunction;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** auto.de's four standard promises — [icon, i18n label key]. */
|
|
405
|
+
const DEFAULT_TRUST: ReadonlyArray<TrustItem> = [
|
|
406
|
+
['shield-check', 'autobahn:trust.warranty'],
|
|
407
|
+
['undo-2', 'autobahn:trust.return'],
|
|
408
|
+
['truck', 'autobahn:trust.delivery'],
|
|
409
|
+
['badge-check', 'autobahn:trust.inspection'],
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
/** Horizontal trust-signal strip. */
|
|
413
|
+
export const TrustStrip: React.FunctionComponent<TrustStripProps> = ({ items, t }) => (
|
|
414
|
+
<div className="trust-strip">
|
|
415
|
+
{(items ?? DEFAULT_TRUST.map(([ic, key]) => [ic, t(key)] as TrustItem)).map(([ic, label]) => (
|
|
416
|
+
<div key={label}>
|
|
417
|
+
<Icon name={ic} size={17} color="var(--md-primary)" stroke={2} style={{ flex: 'none' }} />
|
|
418
|
+
{label}
|
|
419
|
+
</div>
|
|
420
|
+
))}
|
|
421
|
+
</div>
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Price history + market band (NEEDS-DATA by default)
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
export interface PriceHistoryProps {
|
|
429
|
+
vehicle: VehicleDetail;
|
|
430
|
+
/** i18n function (drilled from the page). */
|
|
431
|
+
t: TranslationFunction;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Price-history sparkline + market-value position bar.
|
|
436
|
+
*
|
|
437
|
+
* NEEDS-DATA: the sparkline needs `priceHistory` (with at least two points) and
|
|
438
|
+
* the position bar needs the full market band (`marketLow` / `marketAvg` /
|
|
439
|
+
* `marketHigh`). Either section that lacks real data renders the
|
|
440
|
+
* NEEDS_DATA_LABEL ('Daten folgt') marker — we never draw a fake trend or
|
|
441
|
+
* position. Both are needs-data by default, so until W3b supplies them this card
|
|
442
|
+
* shows the markers.
|
|
443
|
+
*/
|
|
444
|
+
export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehicle, t }) => {
|
|
445
|
+
const pts = vehicle.priceHistory;
|
|
446
|
+
const hasHistory = !!pts && pts.length >= 2;
|
|
447
|
+
const hasMarket =
|
|
448
|
+
vehicle.marketLow != null &&
|
|
449
|
+
vehicle.marketAvg != null &&
|
|
450
|
+
vehicle.marketHigh != null &&
|
|
451
|
+
vehicle.marketHigh > vehicle.marketLow;
|
|
452
|
+
|
|
453
|
+
// Nothing real to show yet — hide the whole card instead of rendering stubs.
|
|
454
|
+
if (!hasHistory && !hasMarket) return null;
|
|
455
|
+
|
|
456
|
+
// --- history sparkline (only when real points exist) ---
|
|
457
|
+
let historyBlock: React.ReactNode;
|
|
458
|
+
if (hasHistory) {
|
|
459
|
+
const prices = pts!.map((p) => p.price);
|
|
460
|
+
const lo = Math.min(...prices);
|
|
461
|
+
const hi = Math.max(...prices);
|
|
462
|
+
const W = 280;
|
|
463
|
+
const H = 70;
|
|
464
|
+
const x = (i: number) => (i / (pts!.length - 1)) * W;
|
|
465
|
+
const y = (p: number) => H - ((p - lo) / (hi - lo || 1)) * H;
|
|
466
|
+
const last = pts![pts!.length - 1];
|
|
467
|
+
const line = pts!.map((p, i) => `${i === 0 ? 'M' : 'L'} ${x(i).toFixed(1)} ${y(p.price).toFixed(1)}`).join(' ');
|
|
468
|
+
const area = `${line} L ${W} ${H} L 0 ${H} Z`;
|
|
469
|
+
const drop = vehicle.listPrice != null ? vehicle.listPrice - vehicle.price : null;
|
|
470
|
+
historyBlock = (
|
|
471
|
+
<div>
|
|
472
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
|
|
473
|
+
{drop != null && drop > 0 ? (
|
|
474
|
+
<>
|
|
475
|
+
<StatChip tone="good" icon="trending-down">
|
|
476
|
+
{t('autobahn:priceHistory.dropAmount', { amount: eur(drop) })}
|
|
477
|
+
</StatChip>
|
|
478
|
+
<span className="md-label-sm">{t('autobahn:priceHistory.sinceListing')}</span>
|
|
479
|
+
</>
|
|
480
|
+
) : (
|
|
481
|
+
<span className="md-label-sm">{t('autobahn:priceHistory.label')}</span>
|
|
482
|
+
)}
|
|
483
|
+
</div>
|
|
484
|
+
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="78" preserveAspectRatio="none" style={{ overflow: 'visible' }}>
|
|
485
|
+
<defs>
|
|
486
|
+
<linearGradient id="phg" x1="0" y1="0" x2="0" y2="1">
|
|
487
|
+
<stop offset="0%" stopColor="var(--md-primary)" stopOpacity=".22" />
|
|
488
|
+
<stop offset="100%" stopColor="var(--md-primary)" stopOpacity="0" />
|
|
489
|
+
</linearGradient>
|
|
490
|
+
</defs>
|
|
491
|
+
<path d={area} fill="url(#phg)" />
|
|
492
|
+
<path d={line} fill="none" stroke="var(--md-primary)" strokeWidth="2.5" strokeLinejoin="round" strokeLinecap="round" />
|
|
493
|
+
<circle cx={x(pts!.length - 1)} cy={y(last.price)} r="4.5" fill="var(--md-primary)" stroke="#fff" strokeWidth="2" />
|
|
494
|
+
</svg>
|
|
495
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
|
|
496
|
+
<span className="md-label-sm">{pts![0].label}</span>
|
|
497
|
+
<span className="md-label-sm">{last.label}</span>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
} else {
|
|
502
|
+
historyBlock = (
|
|
503
|
+
<div>
|
|
504
|
+
<div className="md-title-sm" style={{ marginBottom: 12 }}>
|
|
505
|
+
{t('autobahn:priceHistory.title')}
|
|
506
|
+
</div>
|
|
507
|
+
<p className="md-body-sm" style={{ margin: 0, fontStyle: 'italic', color: 'var(--md-on-surface-variant)' }}>
|
|
508
|
+
{t('autobahn:common.needsData')}
|
|
509
|
+
</p>
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// --- market-value position (only when the full band exists) ---
|
|
515
|
+
let marketBlock: React.ReactNode;
|
|
516
|
+
if (hasMarket) {
|
|
517
|
+
const mpos = Math.max(
|
|
518
|
+
0,
|
|
519
|
+
Math.min(1, (vehicle.price - vehicle.marketLow!) / (vehicle.marketHigh! - vehicle.marketLow!)),
|
|
520
|
+
);
|
|
521
|
+
const delta = vehicle.marketAvg! - vehicle.price;
|
|
522
|
+
marketBlock = (
|
|
523
|
+
<div>
|
|
524
|
+
<div className="md-title-sm" style={{ marginBottom: 12 }}>
|
|
525
|
+
{t('autobahn:marketValue.heading')}
|
|
526
|
+
</div>
|
|
527
|
+
<div
|
|
528
|
+
style={{
|
|
529
|
+
position: 'relative',
|
|
530
|
+
height: 10,
|
|
531
|
+
borderRadius: 'var(--md-r-full)',
|
|
532
|
+
background: 'linear-gradient(90deg,#0F7B43,#C7B53A 55%,#D4502B)',
|
|
533
|
+
}}
|
|
534
|
+
>
|
|
535
|
+
<span
|
|
536
|
+
style={{
|
|
537
|
+
position: 'absolute',
|
|
538
|
+
top: '50%',
|
|
539
|
+
left: `calc(${mpos * 100}% - 9px)`,
|
|
540
|
+
transform: 'translateY(-50%)',
|
|
541
|
+
width: 18,
|
|
542
|
+
height: 18,
|
|
543
|
+
borderRadius: '50%',
|
|
544
|
+
background: '#fff',
|
|
545
|
+
boxShadow: 'var(--md-e2), 0 0 0 2px var(--md-primary)',
|
|
546
|
+
}}
|
|
547
|
+
/>
|
|
548
|
+
</div>
|
|
549
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8 }}>
|
|
550
|
+
<span className="md-label-sm">{eur(vehicle.marketLow!)} €</span>
|
|
551
|
+
<span className="md-label-sm">{eur(vehicle.marketHigh!)} €</span>
|
|
552
|
+
</div>
|
|
553
|
+
{delta > 0 && (
|
|
554
|
+
<div
|
|
555
|
+
style={{
|
|
556
|
+
display: 'flex',
|
|
557
|
+
alignItems: 'center',
|
|
558
|
+
gap: 8,
|
|
559
|
+
marginTop: 12,
|
|
560
|
+
padding: '10px 12px',
|
|
561
|
+
borderRadius: 'var(--md-r-sm)',
|
|
562
|
+
background: 'var(--md-good-container)',
|
|
563
|
+
}}
|
|
564
|
+
>
|
|
565
|
+
<Icon name="badge-check" size={18} color="var(--md-on-good-container)" />
|
|
566
|
+
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--md-on-good-container)' }}>
|
|
567
|
+
{t('autobahn:marketValue.belowAverage', { rating: t('autobahn:tier.' + vehicle.rating), amount: eur(delta) })}
|
|
568
|
+
</span>
|
|
569
|
+
</div>
|
|
570
|
+
)}
|
|
571
|
+
</div>
|
|
572
|
+
);
|
|
573
|
+
} else {
|
|
574
|
+
marketBlock = (
|
|
575
|
+
<div>
|
|
576
|
+
<div className="md-title-sm" style={{ marginBottom: 12 }}>
|
|
577
|
+
{t('autobahn:marketValue.heading')}
|
|
578
|
+
</div>
|
|
579
|
+
<p className="md-body-sm" style={{ margin: 0, fontStyle: 'italic', color: 'var(--md-on-surface-variant)' }}>
|
|
580
|
+
{t('autobahn:common.needsData')}
|
|
581
|
+
</p>
|
|
582
|
+
</div>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return (
|
|
587
|
+
<div className="mcard" id="vdp-preis" style={{ padding: 22 }}>
|
|
588
|
+
<h2 className="md-headline-sm" style={{ marginBottom: 4 }}>
|
|
589
|
+
{t('autobahn:priceMarket.heading')}
|
|
590
|
+
</h2>
|
|
591
|
+
<p className="md-body-sm" style={{ marginBottom: 18 }}>
|
|
592
|
+
{t('autobahn:priceMarket.description')}
|
|
593
|
+
</p>
|
|
594
|
+
<div className="pmv-grid">
|
|
595
|
+
{historyBlock}
|
|
596
|
+
{marketBlock}
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// Protection products (CNP Santander upsell) — optional, non-functional UI
|
|
604
|
+
// ---------------------------------------------------------------------------
|
|
605
|
+
|
|
606
|
+
/** [kebab-case lucide icon, name, description, monthly price EUR]. */
|
|
607
|
+
export type ProtectionProduct = readonly [icon: string, name: string, desc: string, price: number];
|
|
608
|
+
|
|
609
|
+
export interface ProtectionProductsProps {
|
|
610
|
+
/** the upsell products; defaults to the design's four CNP Santander packages. */
|
|
611
|
+
items?: ReadonlyArray<ProtectionProduct>;
|
|
612
|
+
/** i18n function (drilled from the page). */
|
|
613
|
+
t: TranslationFunction;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* The design's four CNP Santander packages — [icon, product name (proper noun,
|
|
618
|
+
* also the selection key), i18n description key, monthly example price EUR].
|
|
619
|
+
*/
|
|
620
|
+
const DEFAULT_PROTECTION: ReadonlyArray<ProtectionProduct> = [
|
|
621
|
+
['shield-check', 'Safe Plus', 'autobahn:protection.items.safePlus.desc', 9],
|
|
622
|
+
['life-buoy', 'Drive Pro', 'autobahn:protection.items.drivePro.desc', 7],
|
|
623
|
+
['wrench', 'Smart Repair', 'autobahn:protection.items.smartRepair.desc', 12],
|
|
624
|
+
['rotate-ccw', 'Smart Return', 'autobahn:protection.items.smartReturn.desc', 10],
|
|
625
|
+
];
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Partner attach products (CNP Santander) sold alongside the financing. Kept as a
|
|
629
|
+
* presentational upsell demo: the selectable rows are pure VIEW state (no store /
|
|
630
|
+
* order side effects, the prices are example values), and the catalogue is a prop
|
|
631
|
+
* with sane defaults. Functional ordering is out of scope for W3a.
|
|
632
|
+
*/
|
|
633
|
+
export const ProtectionProducts: React.FunctionComponent<ProtectionProductsProps> = ({ items, t }) => {
|
|
634
|
+
const products: ReadonlyArray<ProtectionProduct> =
|
|
635
|
+
items ?? DEFAULT_PROTECTION.map(([ic, name, descKey, price]) => [ic, name, t(descKey), price] as ProtectionProduct);
|
|
636
|
+
const [sel, setSel] = useState<Record<string, boolean>>({});
|
|
637
|
+
const chosen = products.filter(([, n]) => sel[n]);
|
|
638
|
+
const sum = chosen.reduce((a, [, , , p]) => a + p, 0);
|
|
639
|
+
return (
|
|
640
|
+
<div className="mcard" style={{ padding: 20 }}>
|
|
641
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
|
|
642
|
+
<span
|
|
643
|
+
style={{
|
|
644
|
+
width: 34,
|
|
645
|
+
height: 34,
|
|
646
|
+
borderRadius: 'var(--md-r-full)',
|
|
647
|
+
background: 'var(--md-sc)',
|
|
648
|
+
display: 'flex',
|
|
649
|
+
alignItems: 'center',
|
|
650
|
+
justifyContent: 'center',
|
|
651
|
+
flex: 'none',
|
|
652
|
+
}}
|
|
653
|
+
>
|
|
654
|
+
<Icon name="umbrella" size={18} color="var(--md-on-surface-variant)" />
|
|
655
|
+
</span>
|
|
656
|
+
<div>
|
|
657
|
+
<div className="md-title-sm">{t('autobahn:protection.heading')}</div>
|
|
658
|
+
<div className="md-label-sm">{t('autobahn:protection.subtitle')}</div>
|
|
659
|
+
</div>
|
|
660
|
+
</div>
|
|
661
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
662
|
+
{products.map(([ic, name, desc, price]) => (
|
|
663
|
+
<button
|
|
664
|
+
key={name}
|
|
665
|
+
onClick={() => setSel((s) => ({ ...s, [name]: !s[name] }))}
|
|
666
|
+
style={{
|
|
667
|
+
display: 'flex',
|
|
668
|
+
alignItems: 'center',
|
|
669
|
+
gap: 12,
|
|
670
|
+
width: '100%',
|
|
671
|
+
textAlign: 'left',
|
|
672
|
+
cursor: 'pointer',
|
|
673
|
+
padding: '10px 12px',
|
|
674
|
+
borderRadius: 'var(--md-r-md)',
|
|
675
|
+
background: sel[name] ? 'var(--md-primary-container)' : 'var(--md-sc-low)',
|
|
676
|
+
border: '1px solid ' + (sel[name] ? 'var(--md-primary)' : 'transparent'),
|
|
677
|
+
transition: 'background var(--dur-1), border-color var(--dur-1)',
|
|
678
|
+
}}
|
|
679
|
+
>
|
|
680
|
+
<Icon
|
|
681
|
+
name={ic}
|
|
682
|
+
size={19}
|
|
683
|
+
stroke={1.8}
|
|
684
|
+
color={sel[name] ? 'var(--md-on-primary-container)' : 'var(--md-on-surface-variant)'}
|
|
685
|
+
style={{ flex: 'none' }}
|
|
686
|
+
/>
|
|
687
|
+
<span style={{ flex: 1, minWidth: 0 }}>
|
|
688
|
+
<span className="md-title-sm" style={{ display: 'block', fontSize: 13.5 }}>
|
|
689
|
+
{name}
|
|
690
|
+
</span>
|
|
691
|
+
<span className="md-label-sm" style={{ display: 'block', fontSize: 11.5 }}>
|
|
692
|
+
{desc}
|
|
693
|
+
</span>
|
|
694
|
+
</span>
|
|
695
|
+
<span style={{ flex: 'none', display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
696
|
+
<span className="num md-label-sm" style={{ fontWeight: 600 }}>
|
|
697
|
+
+{price} €/mtl.
|
|
698
|
+
</span>
|
|
699
|
+
<span
|
|
700
|
+
style={{
|
|
701
|
+
width: 20,
|
|
702
|
+
height: 20,
|
|
703
|
+
borderRadius: '50%',
|
|
704
|
+
display: 'flex',
|
|
705
|
+
alignItems: 'center',
|
|
706
|
+
justifyContent: 'center',
|
|
707
|
+
background: sel[name] ? 'var(--md-primary)' : 'transparent',
|
|
708
|
+
border: sel[name] ? 0 : '1.5px solid var(--md-outline)',
|
|
709
|
+
}}
|
|
710
|
+
>
|
|
711
|
+
{sel[name] && <Icon name="check" size={13} stroke={3} color="#fff" />}
|
|
712
|
+
</span>
|
|
713
|
+
</span>
|
|
714
|
+
</button>
|
|
715
|
+
))}
|
|
716
|
+
</div>
|
|
717
|
+
{sum > 0 && (
|
|
718
|
+
<div
|
|
719
|
+
style={{
|
|
720
|
+
display: 'flex',
|
|
721
|
+
justifyContent: 'space-between',
|
|
722
|
+
alignItems: 'baseline',
|
|
723
|
+
marginTop: 12,
|
|
724
|
+
paddingTop: 10,
|
|
725
|
+
borderTop: '1px solid var(--md-outline-variant)',
|
|
726
|
+
}}
|
|
727
|
+
>
|
|
728
|
+
<span className="md-label-sm">
|
|
729
|
+
{t('autobahn:protection.packagesSelected', { count: chosen.length })}
|
|
730
|
+
</span>
|
|
731
|
+
<span className="num" style={{ fontWeight: 700, color: 'var(--md-primary)' }}>
|
|
732
|
+
+{sum} €/mtl.
|
|
733
|
+
</span>
|
|
734
|
+
</div>
|
|
735
|
+
)}
|
|
736
|
+
<div className="md-label-sm" style={{ marginTop: 10, fontSize: 11.5 }}>
|
|
737
|
+
{t('autobahn:protection.exampleValues', { partner: 'CNP Santander Insurance' })}
|
|
738
|
+
</div>
|
|
739
|
+
</div>
|
|
740
|
+
);
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
// In-page anchor nav (mobile)
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
/** [target element id, label]. */
|
|
748
|
+
export type VdpAnchor = readonly [id: string, label: string];
|
|
749
|
+
|
|
750
|
+
export interface VdpAnchorsProps {
|
|
751
|
+
/** anchor chips; defaults to the four VDP sections. */
|
|
752
|
+
items?: ReadonlyArray<VdpAnchor>;
|
|
753
|
+
/**
|
|
754
|
+
* Optional navigation handler. When provided it OWNS the scroll (e.g. the page
|
|
755
|
+
* in W3b). When omitted the component falls back to scrolling the page's own
|
|
756
|
+
* element with that id into view — intrinsic anchor-nav behaviour on the
|
|
757
|
+
* document, not a data source.
|
|
758
|
+
*/
|
|
759
|
+
onNavigate?: (id: string) => void;
|
|
760
|
+
/** i18n function (drilled from the page). */
|
|
761
|
+
t: TranslationFunction;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** the four VDP sections — [target element id, i18n label key]. */
|
|
765
|
+
const DEFAULT_ANCHORS: ReadonlyArray<VdpAnchor> = [
|
|
766
|
+
['vdp-daten', 'autobahn:anchors.data'],
|
|
767
|
+
['vdp-ausstattung', 'autobahn:anchors.equipment'],
|
|
768
|
+
['vdp-preis', 'autobahn:anchors.priceMarket'],
|
|
769
|
+
['vdp-check', 'autobahn:anchors.inspection'],
|
|
770
|
+
];
|
|
771
|
+
|
|
772
|
+
/** Sticky in-page anchor nav (mobile) — quiet chips. */
|
|
773
|
+
export const VdpAnchors: React.FunctionComponent<VdpAnchorsProps> = ({ items, onNavigate, t }) => {
|
|
774
|
+
const anchors: ReadonlyArray<VdpAnchor> =
|
|
775
|
+
items ?? DEFAULT_ANCHORS.map(([id, key]) => [id, t(key)] as VdpAnchor);
|
|
776
|
+
const go = (id: string) => {
|
|
777
|
+
if (onNavigate) {
|
|
778
|
+
onNavigate(id);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (typeof document === 'undefined') return;
|
|
782
|
+
const el = document.getElementById(id);
|
|
783
|
+
if (!el) return;
|
|
784
|
+
let r = el.getBoundingClientRect();
|
|
785
|
+
if (!r.height && el.firstElementChild) r = el.firstElementChild.getBoundingClientRect();
|
|
786
|
+
window.scrollTo({ top: r.top + window.scrollY - 60, behavior: 'smooth' });
|
|
787
|
+
};
|
|
788
|
+
return (
|
|
789
|
+
<div className="vdp-anchors show-mobile" style={{ background: 'var(--md-sc-low)' }}>
|
|
790
|
+
{anchors.map(([id, l]) => (
|
|
791
|
+
<button key={id} className="chip" onClick={() => go(id)} style={{ flex: 'none' }}>
|
|
792
|
+
{l}
|
|
793
|
+
</button>
|
|
794
|
+
))}
|
|
795
|
+
</div>
|
|
796
|
+
);
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
// ---------------------------------------------------------------------------
|
|
800
|
+
// Desktop sticky summary bar
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
|
|
803
|
+
export interface VdpStickyBarProps {
|
|
804
|
+
vehicle: VehicleDetail;
|
|
805
|
+
/** thumbnail image URL (already resolved) for the docked bar. */
|
|
806
|
+
image: string;
|
|
807
|
+
/**
|
|
808
|
+
* Whether the bar is docked/visible. Controlled by the page (W3b owns the
|
|
809
|
+
* scroll position → show toggle), keeping this component pure — no window
|
|
810
|
+
* scroll listener here.
|
|
811
|
+
*/
|
|
812
|
+
show: boolean;
|
|
813
|
+
/** primary CTA click. */
|
|
814
|
+
onSecure?: () => void;
|
|
815
|
+
/** i18n function (drilled from the page). */
|
|
816
|
+
t: TranslationFunction;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Desktop sticky summary bar (AS24 pattern) — docks to top, driven by `show`. */
|
|
820
|
+
export const VdpStickyBar: React.FunctionComponent<VdpStickyBarProps> = ({ vehicle, image, show, onSecure, t }) => (
|
|
821
|
+
<div className={'vdp-stickytop hide-mobile' + (show ? ' vdp-stickytop--on' : '')}>
|
|
822
|
+
<div className="container" style={{ display: 'flex', alignItems: 'center', gap: 18, padding: '10px 24px' }}>
|
|
823
|
+
<img
|
|
824
|
+
src={image}
|
|
825
|
+
alt=""
|
|
826
|
+
style={{
|
|
827
|
+
width: 64,
|
|
828
|
+
height: 44,
|
|
829
|
+
objectFit: 'cover',
|
|
830
|
+
borderRadius: 'var(--md-r-sm)',
|
|
831
|
+
background: '#fff',
|
|
832
|
+
border: '1px solid var(--md-outline-variant)',
|
|
833
|
+
flex: 'none',
|
|
834
|
+
}}
|
|
835
|
+
/>
|
|
836
|
+
<div style={{ minWidth: 0, flex: 1 }}>
|
|
837
|
+
<div className="md-title-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
838
|
+
{vehicle.make} {vehicle.model} · {vehicle.trim}
|
|
839
|
+
</div>
|
|
840
|
+
<div className="md-label-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
841
|
+
{fmtKm(vehicle.km)} · {vehicle.reg} · {vehicle.gear}
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
<div style={{ textAlign: 'right', flex: 'none' }}>
|
|
845
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 4, justifyContent: 'flex-end' }}>
|
|
846
|
+
<span className="md-label-sm">{t('autobahn:sticky.from')}</span>
|
|
847
|
+
<span className="num" style={{ fontWeight: 700, fontSize: 20, color: 'var(--md-primary)', letterSpacing: '-.4px' }}>
|
|
848
|
+
{vehicle.monthly} €
|
|
849
|
+
</span>
|
|
850
|
+
<span className="md-label-sm">{t('autobahn:sticky.perMonth')}</span>
|
|
851
|
+
</div>
|
|
852
|
+
<div className="md-label-sm">{t('autobahn:sticky.purchasePrice', { price: eur(vehicle.price) })}</div>
|
|
853
|
+
</div>
|
|
854
|
+
<Button variant="filled" icon="shield-check" style={{ flex: 'none' }} onClick={onSecure}>
|
|
855
|
+
{t('autobahn:sticky.secureOffer')}
|
|
856
|
+
</Button>
|
|
857
|
+
</div>
|
|
858
|
+
</div>
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
// ---------------------------------------------------------------------------
|
|
862
|
+
// Mobile sticky CTA bar
|
|
863
|
+
// ---------------------------------------------------------------------------
|
|
864
|
+
|
|
865
|
+
export interface VdpBottomBarProps {
|
|
866
|
+
vehicle: VehicleDetail;
|
|
867
|
+
/** primary CTA click. */
|
|
868
|
+
onSecure?: () => void;
|
|
869
|
+
/** i18n function (drilled from the page). */
|
|
870
|
+
t: TranslationFunction;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** Mobile sticky bottom CTA bar — rate + Kaufpreis + "Angebot sichern". */
|
|
874
|
+
export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({ vehicle, onSecure, t }) => (
|
|
875
|
+
<div className="vdp-bottom-bar show-mobile">
|
|
876
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
877
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 5, whiteSpace: 'nowrap' }}>
|
|
878
|
+
<span className="md-label-sm">{t('autobahn:bottom.from')}</span>
|
|
879
|
+
<span className="num" style={{ fontWeight: 700, fontSize: 22, color: 'var(--md-primary)', letterSpacing: '-.5px' }}>
|
|
880
|
+
{vehicle.monthly} €
|
|
881
|
+
</span>
|
|
882
|
+
<span className="md-label-sm">{t('autobahn:bottom.perMonth')}</span>
|
|
883
|
+
</div>
|
|
884
|
+
<div className="md-label-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
885
|
+
{t('autobahn:bottom.purchasePrice', { price: eur(vehicle.price) })}
|
|
886
|
+
</div>
|
|
887
|
+
</div>
|
|
888
|
+
<Button variant="filled" icon="shield-check" style={{ flex: 'none' }} onClick={onSecure}>
|
|
889
|
+
{t('autobahn:bottom.secureOffer')}
|
|
890
|
+
</Button>
|
|
891
|
+
</div>
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
// 130-Punkte-Check block
|
|
896
|
+
// ---------------------------------------------------------------------------
|
|
897
|
+
|
|
898
|
+
export interface InspectionBlockProps {
|
|
899
|
+
/** pass categories; e.g. ['Karosserie & Lack', 'Motor & Antrieb', …]. */
|
|
900
|
+
categories: string[];
|
|
901
|
+
/** i18n function (drilled from the page). */
|
|
902
|
+
t: TranslationFunction;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** 130-Punkte-Check block — the auto.de inspection promise, all categories pass. */
|
|
906
|
+
export const InspectionBlock: React.FunctionComponent<InspectionBlockProps> = ({ categories, t }) => (
|
|
907
|
+
<div className="mcard mcard--filled" id="vdp-check" style={{ padding: 22 }}>
|
|
908
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
|
|
909
|
+
<Icon name="badge-check" size={24} color="var(--md-primary)" />
|
|
910
|
+
<h2 className="md-headline-sm">{t('autobahn:inspection.passed')}</h2>
|
|
911
|
+
</div>
|
|
912
|
+
<div className="eq-cols">
|
|
913
|
+
{categories.map((c) => (
|
|
914
|
+
<div
|
|
915
|
+
key={c}
|
|
916
|
+
style={{
|
|
917
|
+
display: 'flex',
|
|
918
|
+
alignItems: 'center',
|
|
919
|
+
gap: 9,
|
|
920
|
+
fontSize: 13.5,
|
|
921
|
+
color: 'var(--md-on-surface-variant)',
|
|
922
|
+
marginBottom: 10,
|
|
923
|
+
}}
|
|
924
|
+
>
|
|
925
|
+
<Icon name="check-circle" size={18} color="var(--md-good)" stroke={2} style={{ flex: 'none' }} />
|
|
926
|
+
{c}
|
|
927
|
+
</div>
|
|
928
|
+
))}
|
|
929
|
+
</div>
|
|
930
|
+
</div>
|
|
931
|
+
);
|
|
932
|
+
|
|
933
|
+
// ---------------------------------------------------------------------------
|
|
934
|
+
// Finanzierungs-/Leasingdetails popup (desktop modal / mobile bottom-sheet)
|
|
935
|
+
// ---------------------------------------------------------------------------
|
|
936
|
+
|
|
937
|
+
export interface FinanceDetailsModalProps {
|
|
938
|
+
/** open flag (controlled by the page). */
|
|
939
|
+
open: boolean;
|
|
940
|
+
/** active payment mode — drives labels + which legal example shows. */
|
|
941
|
+
mode: 'fin' | 'leasing' | 'cash';
|
|
942
|
+
price: number;
|
|
943
|
+
monthly: number;
|
|
944
|
+
apr: number;
|
|
945
|
+
down: number;
|
|
946
|
+
term: number;
|
|
947
|
+
/** leasing annual mileage (km/yr), shown only in leasing mode. */
|
|
948
|
+
annualMileage?: number;
|
|
949
|
+
/** REAL figures from the financing config (NOT computed — balloon/Schlussrate makes monthly·term wrong). */
|
|
950
|
+
netLoanAmount?: number;
|
|
951
|
+
/** Schlussrate (balloon final installment); shown only when > 0. */
|
|
952
|
+
finalInstallment?: number;
|
|
953
|
+
/** Gesamt(rückzahlungs)betrag from the config; shown only when > 0 (never the wrong monthly·term). */
|
|
954
|
+
grossAmount?: number;
|
|
955
|
+
/** Sollzinssatz p.a. (%); shown only when > 0. */
|
|
956
|
+
nominalRate?: number;
|
|
957
|
+
/** slider bounds — when the change handlers are supplied, the modal shows adjustment sliders. */
|
|
958
|
+
downMin?: number;
|
|
959
|
+
downMax?: number;
|
|
960
|
+
downStep?: number;
|
|
961
|
+
termMin?: number;
|
|
962
|
+
termMax?: number;
|
|
963
|
+
termStep?: number;
|
|
964
|
+
/** discrete selectable terms (months) for the Laufzeit chips. */
|
|
965
|
+
termOptions?: number[];
|
|
966
|
+
annualMileageMin?: number;
|
|
967
|
+
annualMileageMax?: number;
|
|
968
|
+
annualMileageStep?: number;
|
|
969
|
+
/** slider change handlers (same as the calculator); they recompute the rate via the page. */
|
|
970
|
+
onDownChange?: (down: number) => void;
|
|
971
|
+
onTermChange?: (term: number) => void;
|
|
972
|
+
onAnnualMileageChange?: (km: number) => void;
|
|
973
|
+
onClose: () => void;
|
|
974
|
+
/** i18n function (drilled from the page). */
|
|
975
|
+
t: TranslationFunction;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Full payment breakdown + legal example, opened from the calculator's
|
|
980
|
+
* "Finanzierungsdetails" / "Leasingdetails" link (matches production, which uses a popup).
|
|
981
|
+
* Desktop = centered modal, mobile = bottom-sheet (CSS). All figures are derived from the
|
|
982
|
+
* live calculator props (no fabricated Sollzins/Schlussrate — only what we can compute).
|
|
983
|
+
*/
|
|
984
|
+
export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalProps> = ({
|
|
985
|
+
open,
|
|
986
|
+
mode,
|
|
987
|
+
price,
|
|
988
|
+
monthly,
|
|
989
|
+
apr,
|
|
990
|
+
down,
|
|
991
|
+
term,
|
|
992
|
+
annualMileage,
|
|
993
|
+
netLoanAmount,
|
|
994
|
+
finalInstallment,
|
|
995
|
+
grossAmount,
|
|
996
|
+
nominalRate,
|
|
997
|
+
downMin,
|
|
998
|
+
downMax,
|
|
999
|
+
downStep = 100,
|
|
1000
|
+
termMin,
|
|
1001
|
+
termMax,
|
|
1002
|
+
termStep = 6,
|
|
1003
|
+
termOptions,
|
|
1004
|
+
annualMileageMin = 5000,
|
|
1005
|
+
annualMileageMax = 60000,
|
|
1006
|
+
annualMileageStep = 5000,
|
|
1007
|
+
onDownChange,
|
|
1008
|
+
onTermChange,
|
|
1009
|
+
onAnnualMileageChange,
|
|
1010
|
+
onClose,
|
|
1011
|
+
t,
|
|
1012
|
+
}) => {
|
|
1013
|
+
if (!open) return null;
|
|
1014
|
+
const isLease = mode === 'leasing';
|
|
1015
|
+
const title = isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails');
|
|
1016
|
+
// adjustable only for fin/leasing, and only when the page supplies the change handlers + bounds.
|
|
1017
|
+
const adjustable = mode !== 'cash' && !!onDownChange && downMin != null && downMax != null && termMin != null && termMax != null;
|
|
1018
|
+
// discrete term chip values (supplied periods, else derived from the bounds).
|
|
1019
|
+
const termVals =
|
|
1020
|
+
termOptions && termOptions.length
|
|
1021
|
+
? termOptions
|
|
1022
|
+
: (() => {
|
|
1023
|
+
const out: number[] = [];
|
|
1024
|
+
for (let v = termMin as number; v <= (termMax as number); v += termStep || 12) out.push(v);
|
|
1025
|
+
return out.length ? out : [term];
|
|
1026
|
+
})();
|
|
1027
|
+
const pct = (n: number) => `${n.toFixed(2).replace('.', ',')} %`;
|
|
1028
|
+
// Only REAL figures (from the financing config). Balloon financing means monthly·term is NOT the
|
|
1029
|
+
// total, so Schlussrate + Gesamtbetrag come from the config and are omitted when unavailable —
|
|
1030
|
+
// never a fabricated/misleading total.
|
|
1031
|
+
const netto = netLoanAmount != null ? netLoanAmount : Math.max(0, price - down);
|
|
1032
|
+
const rows: Array<[string, string]> = [
|
|
1033
|
+
[t('autobahn:modal.vehiclePrice'), `${eur(price)} €`],
|
|
1034
|
+
[isLease ? t('autobahn:modal.leasingAmount') : t('autobahn:modal.netLoanAmount'), `${eur(netto)} €`],
|
|
1035
|
+
[t('autobahn:modal.downPayment'), `${eur(down)} €`],
|
|
1036
|
+
...(isLease && annualMileage != null ? ([[t('autobahn:modal.annualMileage'), `${eur(annualMileage)} km`]] as Array<[string, string]>) : []),
|
|
1037
|
+
[t('autobahn:modal.term'), t('autobahn:modal.months', { months: term, count: term })],
|
|
1038
|
+
...(nominalRate != null && nominalRate > 0 ? ([[t('autobahn:modal.nominalRate'), pct(nominalRate)]] as Array<[string, string]>) : []),
|
|
1039
|
+
[t('autobahn:modal.effectiveRate'), pct(apr)],
|
|
1040
|
+
...(finalInstallment != null && finalInstallment > 0 ? ([[t('autobahn:modal.finalInstallment'), `${eur(finalInstallment)} €`]] as Array<[string, string]>) : []),
|
|
1041
|
+
[isLease ? t('autobahn:modal.leasingRate') : t('autobahn:modal.monthlyRate'), t('autobahn:modal.ratePerMonth', { amount: eur(monthly) })],
|
|
1042
|
+
...(grossAmount != null && grossAmount > 0 ? ([[isLease ? t('autobahn:modal.totalAmount') : t('autobahn:modal.totalRepayment'), `${eur(grossAmount)} €`]] as Array<[string, string]>) : []),
|
|
1043
|
+
];
|
|
1044
|
+
return (
|
|
1045
|
+
<div className="fin-modal-scrim" onClick={onClose}>
|
|
1046
|
+
<div className="fin-modal" role="dialog" aria-modal="true" aria-label={title} onClick={(e) => e.stopPropagation()}>
|
|
1047
|
+
<div className="fin-modal-head">
|
|
1048
|
+
<h2 className="md-headline-sm" style={{ margin: 0 }}>
|
|
1049
|
+
{title}
|
|
1050
|
+
</h2>
|
|
1051
|
+
<button className="icon-btn" onClick={onClose} aria-label={t('autobahn:modal.close')}>
|
|
1052
|
+
<Icon name="x" size={20} />
|
|
1053
|
+
</button>
|
|
1054
|
+
</div>
|
|
1055
|
+
<div className="fin-modal-body">
|
|
1056
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 14 }}>
|
|
1057
|
+
<span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
|
|
1058
|
+
{eur(monthly)} €
|
|
1059
|
+
</span>
|
|
1060
|
+
<span className="md-label-sm">{t('autobahn:finance.perMonth')}</span>
|
|
1061
|
+
</div>
|
|
1062
|
+
{/* adjustment sliders — change a value and every figure below recomputes live (via the page) */}
|
|
1063
|
+
{adjustable && (
|
|
1064
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 18 }}>
|
|
1065
|
+
<div>
|
|
1066
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
1067
|
+
<span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
|
|
1068
|
+
<span className="num" style={{ fontWeight: 700 }}>
|
|
1069
|
+
{eur(down)} €
|
|
1070
|
+
</span>
|
|
1071
|
+
</div>
|
|
1072
|
+
<Slider
|
|
1073
|
+
min={downMin as number}
|
|
1074
|
+
max={downMax as number}
|
|
1075
|
+
step={downStep}
|
|
1076
|
+
value={down}
|
|
1077
|
+
onChange={onDownChange as (n: number) => void}
|
|
1078
|
+
fmt={(v) => `${eur(Number(v))} €`}
|
|
1079
|
+
/>
|
|
1080
|
+
</div>
|
|
1081
|
+
{isLease && annualMileage != null && onAnnualMileageChange && (
|
|
1082
|
+
<div>
|
|
1083
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
|
1084
|
+
<span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
|
|
1085
|
+
<span className="num" style={{ fontWeight: 700 }}>
|
|
1086
|
+
{eur(annualMileage)} km
|
|
1087
|
+
</span>
|
|
1088
|
+
</div>
|
|
1089
|
+
<Slider
|
|
1090
|
+
min={annualMileageMin}
|
|
1091
|
+
max={annualMileageMax}
|
|
1092
|
+
step={annualMileageStep}
|
|
1093
|
+
value={annualMileage}
|
|
1094
|
+
onChange={onAnnualMileageChange}
|
|
1095
|
+
fmt={(v) => `${eur(Number(v))} km`}
|
|
1096
|
+
/>
|
|
1097
|
+
</div>
|
|
1098
|
+
)}
|
|
1099
|
+
<div>
|
|
1100
|
+
<div className="md-title-sm" style={{ marginBottom: 8 }}>
|
|
1101
|
+
{t('autobahn:finance.termSelection')}
|
|
1102
|
+
</div>
|
|
1103
|
+
<TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
|
|
1104
|
+
</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
)}
|
|
1107
|
+
{rows.map(([l, v], i) => (
|
|
1108
|
+
<div
|
|
1109
|
+
key={l}
|
|
1110
|
+
style={{
|
|
1111
|
+
display: 'flex',
|
|
1112
|
+
justifyContent: 'space-between',
|
|
1113
|
+
gap: 12,
|
|
1114
|
+
padding: '9px 0',
|
|
1115
|
+
borderBottom: i < rows.length - 1 ? '1px solid var(--md-outline-variant)' : 0,
|
|
1116
|
+
}}
|
|
1117
|
+
>
|
|
1118
|
+
<span className="md-label-sm">{l}</span>
|
|
1119
|
+
<span className="num" style={{ fontSize: 13.5, fontWeight: 600 }}>
|
|
1120
|
+
{v}
|
|
1121
|
+
</span>
|
|
1122
|
+
</div>
|
|
1123
|
+
))}
|
|
1124
|
+
<p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
|
|
1125
|
+
{isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
|
|
1126
|
+
</p>
|
|
1127
|
+
</div>
|
|
1128
|
+
</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
);
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
// ---------------------------------------------------------------------------
|
|
1134
|
+
// Customer ratings (auto.de Google reviews) — REAL data, loaded by the page
|
|
1135
|
+
// ---------------------------------------------------------------------------
|
|
1136
|
+
|
|
1137
|
+
export interface CustomerRatingsProps {
|
|
1138
|
+
/** the loaded reviews (REAL — from the WP json-quotes source). */
|
|
1139
|
+
reviews: VdpReview[];
|
|
1140
|
+
/** i18n function (drilled from the page). */
|
|
1141
|
+
t: TranslationFunction;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Customer-rating section (the legacy VDP's "Kundenmeinungen", rebuilt on the
|
|
1146
|
+
* Autobahn card). Shows the auto.de average (DERIVED from the loaded reviews — not
|
|
1147
|
+
* invented) + a grid of review cards (avatar, name, stars, quote). The reviews are
|
|
1148
|
+
* the SAME real source the legacy CustomerQuotes uses (auto.de Google reviews via
|
|
1149
|
+
* the WordPress json-quotes feed); the page loads them and passes them in. When no
|
|
1150
|
+
* reviews load, the page renders nothing (no fabricated content).
|
|
1151
|
+
*/
|
|
1152
|
+
export const CustomerRatings: React.FunctionComponent<CustomerRatingsProps> = ({ reviews, t }) => {
|
|
1153
|
+
const [showAll, setShowAll] = useState(false);
|
|
1154
|
+
if (!reviews || !reviews.length) return null;
|
|
1155
|
+
const count = reviews.length;
|
|
1156
|
+
const avg = reviews.reduce((s, r) => s + (r.rating || 0), 0) / count;
|
|
1157
|
+
const shown = showAll ? reviews : reviews.slice(0, 4);
|
|
1158
|
+
return (
|
|
1159
|
+
<section className="mcard" id="vdp-bewertungen" style={{ padding: 22, marginTop: 44 }}>
|
|
1160
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap', marginBottom: 18 }}>
|
|
1161
|
+
<h2 className="md-headline-sm" style={{ margin: 0 }}>
|
|
1162
|
+
{t('autobahn:reviews.title')}
|
|
1163
|
+
</h2>
|
|
1164
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginLeft: 'auto' }}>
|
|
1165
|
+
<span className="num" style={{ fontWeight: 800, fontSize: 26, color: 'var(--md-on-surface)', letterSpacing: '-.5px' }}>
|
|
1166
|
+
{avg.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}
|
|
1167
|
+
</span>
|
|
1168
|
+
<span>
|
|
1169
|
+
<Stars value={avg} size={16} />
|
|
1170
|
+
<span className="md-label-sm" style={{ display: 'block' }}>
|
|
1171
|
+
{t('autobahn:reviews.ratingCount', { count })} · via Google
|
|
1172
|
+
</span>
|
|
1173
|
+
</span>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
<div className="eq-cols">
|
|
1177
|
+
{shown.map((r) => (
|
|
1178
|
+
<div
|
|
1179
|
+
key={r.id}
|
|
1180
|
+
className="mcard mcard--filled"
|
|
1181
|
+
style={{ breakInside: 'avoid', marginBottom: 16, padding: 16 }}
|
|
1182
|
+
>
|
|
1183
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10 }}>
|
|
1184
|
+
{r.image ? (
|
|
1185
|
+
<img
|
|
1186
|
+
src={r.image}
|
|
1187
|
+
alt=""
|
|
1188
|
+
loading="lazy"
|
|
1189
|
+
style={{ width: 40, height: 40, borderRadius: '50%', objectFit: 'cover', flex: 'none', background: 'var(--md-sc)' }}
|
|
1190
|
+
/>
|
|
1191
|
+
) : (
|
|
1192
|
+
<span
|
|
1193
|
+
style={{
|
|
1194
|
+
width: 40,
|
|
1195
|
+
height: 40,
|
|
1196
|
+
borderRadius: '50%',
|
|
1197
|
+
flex: 'none',
|
|
1198
|
+
background: 'var(--md-primary-container)',
|
|
1199
|
+
color: 'var(--md-on-primary-container)',
|
|
1200
|
+
display: 'flex',
|
|
1201
|
+
alignItems: 'center',
|
|
1202
|
+
justifyContent: 'center',
|
|
1203
|
+
fontWeight: 700,
|
|
1204
|
+
}}
|
|
1205
|
+
>
|
|
1206
|
+
{(r.name || '?').charAt(0).toUpperCase()}
|
|
1207
|
+
</span>
|
|
1208
|
+
)}
|
|
1209
|
+
<div style={{ minWidth: 0 }}>
|
|
1210
|
+
<div className="md-title-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
1211
|
+
{r.name}
|
|
1212
|
+
</div>
|
|
1213
|
+
<Stars value={r.rating} />
|
|
1214
|
+
</div>
|
|
1215
|
+
</div>
|
|
1216
|
+
<p className="md-body-sm" style={{ margin: 0, color: 'var(--md-on-surface-variant)' }}>
|
|
1217
|
+
{r.review}
|
|
1218
|
+
</p>
|
|
1219
|
+
</div>
|
|
1220
|
+
))}
|
|
1221
|
+
</div>
|
|
1222
|
+
{reviews.length > 4 && (
|
|
1223
|
+
<button className="mbtn mbtn--text" style={{ marginTop: 4 }} onClick={() => setShowAll((a) => !a)}>
|
|
1224
|
+
{showAll ? t('autobahn:ratings.showLess') : t('autobahn:ratings.showMore', { count })}
|
|
1225
|
+
<Icon name={showAll ? 'chevron-up' : 'chevron-down'} size={16} />
|
|
1226
|
+
</button>
|
|
1227
|
+
)}
|
|
1228
|
+
</section>
|
|
1229
|
+
);
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
// ---------------------------------------------------------------------------
|
|
1233
|
+
// Enquiry block — low-friction "Frage zum Fahrzeug" lead form
|
|
1234
|
+
// ---------------------------------------------------------------------------
|
|
1235
|
+
|
|
1236
|
+
export interface VdpEnquiryProps {
|
|
1237
|
+
/** car title for the prefilled message + heading context. */
|
|
1238
|
+
carTitle: string;
|
|
1239
|
+
/** auto.de-ID reference, woven into the prefilled message when present. */
|
|
1240
|
+
autoDeId?: string;
|
|
1241
|
+
/** sales hotline shown in the aside (tap-to-call). */
|
|
1242
|
+
salesPhone?: string;
|
|
1243
|
+
/**
|
|
1244
|
+
* Submit handler — the page wires this to the REAL contact channel
|
|
1245
|
+
* (carStore.submitContactForm). Returns a promise; the form shows a thank-you on
|
|
1246
|
+
* resolve and an inline error on reject.
|
|
1247
|
+
*/
|
|
1248
|
+
onSubmit: (data: { name: string; email: string; phone: string; message: string }) => Promise<void> | void;
|
|
1249
|
+
/** i18n function (drilled from the page). */
|
|
1250
|
+
t: TranslationFunction;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const enquiryInput: React.CSSProperties = {
|
|
1254
|
+
width: '100%',
|
|
1255
|
+
height: 46,
|
|
1256
|
+
padding: '0 14px',
|
|
1257
|
+
borderRadius: 'var(--md-r-sm)',
|
|
1258
|
+
border: '1px solid var(--md-outline-variant)',
|
|
1259
|
+
background: 'var(--md-sc-lowest)',
|
|
1260
|
+
color: 'var(--md-on-surface)',
|
|
1261
|
+
font: 'inherit',
|
|
1262
|
+
fontSize: 14,
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
/** field with a small floating-style label above the input. */
|
|
1266
|
+
const EnquiryField: React.FunctionComponent<{
|
|
1267
|
+
label: string;
|
|
1268
|
+
children: React.ReactNode;
|
|
1269
|
+
}> = ({ label, children }) => (
|
|
1270
|
+
<label style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
|
1271
|
+
<span className="md-label-sm" style={{ fontWeight: 600 }}>
|
|
1272
|
+
{label}
|
|
1273
|
+
</span>
|
|
1274
|
+
{children}
|
|
1275
|
+
</label>
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
/** [icon, i18n label key] — resolved against the drilled t in the component. */
|
|
1279
|
+
const ENQUIRY_BENEFITS: ReadonlyArray<[string, string]> = [
|
|
1280
|
+
['clock', 'autobahn:enquiry.benefits.fastResponse'],
|
|
1281
|
+
['shield-check', 'autobahn:enquiry.benefits.freeAndNonBinding'],
|
|
1282
|
+
['hand-coins', 'autobahn:enquiry.benefits.financingAdvice'],
|
|
1283
|
+
];
|
|
1284
|
+
|
|
1285
|
+
/**
|
|
1286
|
+
* Vehicle enquiry — an enriched two-pane lead block: a left trust/benefits aside (persönliche
|
|
1287
|
+
* Beratung, response time, hotline) and a right structured form (preferred-contact toggle, named
|
|
1288
|
+
* fields, editable prefilled message), wired to the REAL submit (carStore.submitContactForm).
|
|
1289
|
+
*/
|
|
1290
|
+
export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
1291
|
+
carTitle,
|
|
1292
|
+
autoDeId,
|
|
1293
|
+
salesPhone = '0800 40 30 182',
|
|
1294
|
+
onSubmit,
|
|
1295
|
+
t,
|
|
1296
|
+
}) => {
|
|
1297
|
+
const [name, setName] = useState('');
|
|
1298
|
+
const [email, setEmail] = useState('');
|
|
1299
|
+
const [phone, setPhone] = useState('');
|
|
1300
|
+
const [pref, setPref] = useState<'email' | 'phone'>('email');
|
|
1301
|
+
const [message, setMessage] = useState(
|
|
1302
|
+
autoDeId
|
|
1303
|
+
? t('autobahn:enquiry.prefilledMessageWithId', { carTitle, autoDeId })
|
|
1304
|
+
: t('autobahn:enquiry.prefilledMessage', { carTitle })
|
|
1305
|
+
);
|
|
1306
|
+
const [sending, setSending] = useState(false);
|
|
1307
|
+
const [sent, setSent] = useState(false);
|
|
1308
|
+
const [error, setError] = useState('');
|
|
1309
|
+
|
|
1310
|
+
const emailOk = /.+@.+\..+/.test(email);
|
|
1311
|
+
const phoneOk = phone.replace(/\D/g, '').length >= 6;
|
|
1312
|
+
// the preferred channel is required; the other stays optional.
|
|
1313
|
+
const valid = name.trim().length > 1 && (pref === 'email' ? emailOk : phoneOk);
|
|
1314
|
+
|
|
1315
|
+
const submit = async (): Promise<void> => {
|
|
1316
|
+
if (!valid || sending) return;
|
|
1317
|
+
setSending(true);
|
|
1318
|
+
setError('');
|
|
1319
|
+
const note = t('autobahn:enquiry.preferredContact', {
|
|
1320
|
+
channel: pref === 'email' ? t('autobahn:enquiry.contactMethod.email') : t('autobahn:enquiry.contactMethod.phone'),
|
|
1321
|
+
});
|
|
1322
|
+
try {
|
|
1323
|
+
await onSubmit({ name: name.trim(), email: email.trim(), phone: phone.trim(), message: message.trim() + note });
|
|
1324
|
+
setSent(true);
|
|
1325
|
+
} catch (e) {
|
|
1326
|
+
setError(t('autobahn:enquiry.form.sendError'));
|
|
1327
|
+
} finally {
|
|
1328
|
+
setSending(false);
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
if (sent) {
|
|
1333
|
+
return (
|
|
1334
|
+
<section className="mcard mcard--filled" id="vdp-kontakt" style={{ padding: 22, marginTop: 24 }}>
|
|
1335
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
1336
|
+
<Icon name="check-circle" size={24} color="var(--md-good)" />
|
|
1337
|
+
<div>
|
|
1338
|
+
<div className="md-title-sm">{t('autobahn:enquiry.success.title')}</div>
|
|
1339
|
+
<div className="md-body-sm">{t('autobahn:enquiry.success.message')}</div>
|
|
1340
|
+
</div>
|
|
1341
|
+
</div>
|
|
1342
|
+
</section>
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
return (
|
|
1347
|
+
<section className="mcard" id="vdp-kontakt" style={{ padding: 0, overflow: 'hidden', marginTop: 24 }}>
|
|
1348
|
+
<div className="enquiry-grid">
|
|
1349
|
+
{/* trust / benefits aside */}
|
|
1350
|
+
<aside className="enquiry-aside">
|
|
1351
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
|
|
1352
|
+
<span className="enquiry-ico">
|
|
1353
|
+
<Icon name="headset" size={20} color="var(--md-on-primary-container)" />
|
|
1354
|
+
</span>
|
|
1355
|
+
<h2 className="md-title" style={{ margin: 0, fontSize: 19 }}>
|
|
1356
|
+
{t('autobahn:enquiry.personalConsultation')}
|
|
1357
|
+
</h2>
|
|
1358
|
+
</div>
|
|
1359
|
+
<p className="md-body-sm" style={{ marginBottom: 16 }}>
|
|
1360
|
+
{t('autobahn:enquiry.aside.description')}
|
|
1361
|
+
</p>
|
|
1362
|
+
<ul className="enquiry-benefits">
|
|
1363
|
+
{ENQUIRY_BENEFITS.map(([ic, key]) => (
|
|
1364
|
+
<li key={key}>
|
|
1365
|
+
<Icon name={ic} size={17} color="var(--md-primary)" stroke={2} />
|
|
1366
|
+
{t(key)}
|
|
1367
|
+
</li>
|
|
1368
|
+
))}
|
|
1369
|
+
</ul>
|
|
1370
|
+
<a className="enquiry-phone" href={`tel:${salesPhone.replace(/\s/g, '')}`}>
|
|
1371
|
+
<Icon name="phone" size={16} />
|
|
1372
|
+
{salesPhone}
|
|
1373
|
+
</a>
|
|
1374
|
+
</aside>
|
|
1375
|
+
|
|
1376
|
+
{/* form */}
|
|
1377
|
+
<div className="enquiry-form">
|
|
1378
|
+
<div style={{ marginBottom: 14 }}>
|
|
1379
|
+
<div className="md-label-sm" style={{ fontWeight: 600, marginBottom: 7 }}>
|
|
1380
|
+
{t('autobahn:enquiry.contactMethod.heading')}
|
|
1381
|
+
</div>
|
|
1382
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
1383
|
+
<button
|
|
1384
|
+
type="button"
|
|
1385
|
+
className={'chip' + (pref === 'email' ? ' chip--filter chip--selected' : '')}
|
|
1386
|
+
onClick={() => setPref('email')}
|
|
1387
|
+
>
|
|
1388
|
+
<Icon name="mail" size={15} />
|
|
1389
|
+
{t('autobahn:enquiry.contactMethod.email')}
|
|
1390
|
+
</button>
|
|
1391
|
+
<button
|
|
1392
|
+
type="button"
|
|
1393
|
+
className={'chip' + (pref === 'phone' ? ' chip--filter chip--selected' : '')}
|
|
1394
|
+
onClick={() => setPref('phone')}
|
|
1395
|
+
>
|
|
1396
|
+
<Icon name="phone" size={15} />
|
|
1397
|
+
{t('autobahn:enquiry.contactMethod.phone')}
|
|
1398
|
+
</button>
|
|
1399
|
+
</div>
|
|
1400
|
+
</div>
|
|
1401
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: 12 }}>
|
|
1402
|
+
<EnquiryField label={t('autobahn:enquiry.form.nameLabel')}>
|
|
1403
|
+
<input style={enquiryInput} placeholder={t('autobahn:enquiry.form.namePlaceholder')} value={name} onChange={(e) => setName(e.target.value)} aria-label={t('autobahn:enquiry.form.nameAria')} />
|
|
1404
|
+
</EnquiryField>
|
|
1405
|
+
<EnquiryField label={pref === 'email' ? t('autobahn:enquiry.form.emailLabelRequired') : t('autobahn:enquiry.form.emailLabel')}>
|
|
1406
|
+
<input style={enquiryInput} type="email" placeholder={t('autobahn:enquiry.form.emailPlaceholder')} value={email} onChange={(e) => setEmail(e.target.value)} aria-label={t('autobahn:enquiry.contactMethod.email')} />
|
|
1407
|
+
</EnquiryField>
|
|
1408
|
+
<EnquiryField label={pref === 'phone' ? t('autobahn:enquiry.form.phoneLabelRequired') : t('autobahn:enquiry.form.phoneLabel')}>
|
|
1409
|
+
<input style={enquiryInput} type="tel" placeholder={t('autobahn:enquiry.form.phonePlaceholder')} value={phone} onChange={(e) => setPhone(e.target.value)} aria-label={t('autobahn:enquiry.contactMethod.phone')} />
|
|
1410
|
+
</EnquiryField>
|
|
1411
|
+
</div>
|
|
1412
|
+
<div style={{ marginTop: 12 }}>
|
|
1413
|
+
<EnquiryField label={t('autobahn:enquiry.form.messageLabel')}>
|
|
1414
|
+
<textarea
|
|
1415
|
+
style={{ ...enquiryInput, height: 92, padding: '10px 14px', resize: 'vertical' }}
|
|
1416
|
+
value={message}
|
|
1417
|
+
onChange={(e) => setMessage(e.target.value)}
|
|
1418
|
+
aria-label={t('autobahn:enquiry.form.messageAria')}
|
|
1419
|
+
/>
|
|
1420
|
+
</EnquiryField>
|
|
1421
|
+
</div>
|
|
1422
|
+
{error && (
|
|
1423
|
+
<div className="md-label-sm" style={{ color: 'var(--md-error)', marginTop: 8 }}>
|
|
1424
|
+
{error}
|
|
1425
|
+
</div>
|
|
1426
|
+
)}
|
|
1427
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginTop: 16, flexWrap: 'wrap' }}>
|
|
1428
|
+
<Button variant="filled" size="lg" icon="send" disabled={!valid || sending} onClick={submit}>
|
|
1429
|
+
{sending ? t('autobahn:enquiry.form.sending') : t('autobahn:enquiry.sendButton')}
|
|
1430
|
+
</Button>
|
|
1431
|
+
<span className="md-label-sm" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
|
1432
|
+
<Icon name="lock" size={13} />
|
|
1433
|
+
{t('autobahn:enquiry.security')}
|
|
1434
|
+
</span>
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>
|
|
1438
|
+
</section>
|
|
1439
|
+
);
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
// ---------------------------------------------------------------------------
|
|
1443
|
+
// "So einfach geht's" steps
|
|
1444
|
+
// ---------------------------------------------------------------------------
|
|
1445
|
+
|
|
1446
|
+
/** [title, description]. */
|
|
1447
|
+
export type HowItWorksStep = readonly [title: string, desc: string];
|
|
1448
|
+
|
|
1449
|
+
export interface HowItWorksProps {
|
|
1450
|
+
/** the numbered steps; defaults to the design's four-step flow. */
|
|
1451
|
+
steps?: ReadonlyArray<HowItWorksStep>;
|
|
1452
|
+
/** i18n function (drilled from the page). */
|
|
1453
|
+
t: TranslationFunction;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** Lucide icon per step index: Angebot / Beratung / Bezahlung / Übergabe. */
|
|
1457
|
+
const STEP_ICONS = ['bookmark', 'message-circle', 'credit-card', 'truck'] as const;
|
|
1458
|
+
|
|
1459
|
+
/** the design's four-step flow — [title key, description key]. */
|
|
1460
|
+
const DEFAULT_STEPS: ReadonlyArray<HowItWorksStep> = [
|
|
1461
|
+
['autobahn:steps.step1.title', 'autobahn:steps.step1.description'],
|
|
1462
|
+
['autobahn:steps.step2.title', 'autobahn:steps.step2.description'],
|
|
1463
|
+
['autobahn:steps.step3.title', 'autobahn:steps.step3.description'],
|
|
1464
|
+
['autobahn:steps.step4.title', 'autobahn:steps.step4.description'],
|
|
1465
|
+
];
|
|
1466
|
+
|
|
1467
|
+
/** "So einfach geht's" — icon-step cards with connector line (professional redesign). */
|
|
1468
|
+
export const HowItWorks: React.FunctionComponent<HowItWorksProps> = ({ steps, t }) => {
|
|
1469
|
+
const items: ReadonlyArray<HowItWorksStep> =
|
|
1470
|
+
steps ?? DEFAULT_STEPS.map(([titleKey, descKey]) => [t(titleKey), t(descKey)] as HowItWorksStep);
|
|
1471
|
+
return (
|
|
1472
|
+
<section className="hiw-section" style={{ marginTop: 44 }}>
|
|
1473
|
+
<h2 className="md-headline-sm" style={{ marginBottom: 28, textAlign: 'center' }}>
|
|
1474
|
+
{t('autobahn:howItWorks.title')}
|
|
1475
|
+
</h2>
|
|
1476
|
+
<div className="steps-4 hiw-grid">
|
|
1477
|
+
{items.map(([title, desc], i) => (
|
|
1478
|
+
<div key={title} className="hiw-step">
|
|
1479
|
+
<div className="hiw-icon-wrap">
|
|
1480
|
+
<Icon name={STEP_ICONS[i] || 'star'} size={24} color="#fff" stroke={1.8} />
|
|
1481
|
+
<span className="hiw-num">{i + 1}</span>
|
|
1482
|
+
</div>
|
|
1483
|
+
<div className="hiw-title">{title}</div>
|
|
1484
|
+
<p className="hiw-desc">{desc}</p>
|
|
1485
|
+
</div>
|
|
1486
|
+
))}
|
|
1487
|
+
</div>
|
|
1488
|
+
</section>
|
|
1489
|
+
);
|
|
1490
|
+
};
|