@jotul/jotul-widgets 1.0.5 → 1.2.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.
Files changed (44) hide show
  1. package/README.md +21 -1
  2. package/dist/JotulWidget.css +1 -1
  3. package/dist/JotulWidget.d.ts +2 -2
  4. package/dist/JotulWidget.js +166 -36
  5. package/dist/api.d.ts +2 -2
  6. package/dist/api.js +13 -2
  7. package/dist/components/FindDealerDrawerWidget.d.ts +39 -0
  8. package/dist/components/FindDealerDrawerWidget.js +318 -0
  9. package/dist/components/ProductPageWidget.d.ts +11 -2
  10. package/dist/components/ProductPageWidget.js +215 -41
  11. package/dist/components/product-page/CampaignStatus.d.ts +6 -0
  12. package/dist/components/product-page/CampaignStatus.js +48 -0
  13. package/dist/components/product-page/DealerList.d.ts +4 -2
  14. package/dist/components/product-page/DealerList.js +12 -4
  15. package/dist/components/product-page/StatusBanner.d.ts +1 -1
  16. package/dist/components/product-page/StatusBanner.js +3 -1
  17. package/dist/constants.d.ts +4 -0
  18. package/dist/constants.js +4 -0
  19. package/dist/i18n/locales/cz.json +5 -1
  20. package/dist/i18n/locales/de.json +5 -1
  21. package/dist/i18n/locales/en.json +5 -1
  22. package/dist/i18n/locales/fi.json +5 -1
  23. package/dist/i18n/locales/fr.json +5 -1
  24. package/dist/i18n/locales/nl.json +5 -1
  25. package/dist/i18n/locales/no.json +5 -1
  26. package/dist/i18n/locales/pl.json +5 -1
  27. package/dist/i18n/locales/se.json +5 -1
  28. package/dist/i18n/widgetStrings.d.ts +4 -0
  29. package/dist/images/dealer-pin-ildstedet.svg +31 -0
  30. package/dist/images/dealer-pin.svg +8 -3
  31. package/dist/images/ildstedet-dealer.svg +702 -0
  32. package/dist/index.d.ts +1 -1
  33. package/dist/types.d.ts +26 -5
  34. package/dist/utils/cssColor.d.ts +5 -0
  35. package/dist/utils/cssColor.js +31 -0
  36. package/dist/utils/dealerMapClustering.d.ts +28 -0
  37. package/dist/utils/dealerMapClustering.js +71 -0
  38. package/dist/utils/loadLeafletMarkerCluster.d.ts +5 -0
  39. package/dist/utils/loadLeafletMarkerCluster.js +20 -0
  40. package/dist/utils/markerClusterIconHtml.d.ts +2 -0
  41. package/dist/utils/markerClusterIconHtml.js +7 -0
  42. package/dist/utils.d.ts +3 -1
  43. package/dist/utils.js +10 -0
  44. package/package.json +5 -2
@@ -1,18 +1,38 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import 'leaflet/dist/leaflet.css';
3
- import { useEffect, useMemo, useState } from 'react';
4
- import { icon } from 'leaflet';
3
+ import 'leaflet.markercluster/dist/MarkerCluster.css';
4
+ import { useEffect, useMemo, useRef, useState } from 'react';
5
+ import { icon, latLng } from 'leaflet';
5
6
  import { MapContainer, Marker, TileLayer, Tooltip, useMap } from 'react-leaflet';
6
7
  import { DealerCardSkeleton } from './DealerCardSkeleton';
7
8
  import { DealerList } from './product-page/DealerList';
9
+ import { CampaignStatus } from './product-page/CampaignStatus';
8
10
  import { InquiryForm } from './product-page/InquiryForm';
9
11
  import { LocationSearch } from './product-page/LocationSearch';
10
12
  import { StatusBanner } from './product-page/StatusBanner';
11
13
  import { ArrowRightIcon } from '../icons/ArrowRightIcon';
12
14
  import dealerPin from '../images/dealer-pin.svg';
13
15
  import dealerPinExclusive from '../images/dealer-pin-exclusive.svg';
14
- import { R10 } from '../constants';
15
- const OSM_SMOOTH_TILE_URL = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
16
+ import dealerPinIldstedet from '../images/dealer-pin-ildstedet.svg';
17
+ import { MARKER_CLUSTER_LINK_KM, MARKER_CLUSTER_MIN_GROUP, partitionDealersForMarkerClusterPlugin, } from '../utils/dealerMapClustering';
18
+ import { loadLeafletMarkerCluster } from '../utils/loadLeafletMarkerCluster';
19
+ import { markerClusterCountIconHtml } from '../utils/markerClusterIconHtml';
20
+ import { JOTUL_BRAND_PRIMARY_HEX, R10 } from '../constants';
21
+ const OSM_MINIMAL_TILE_URL = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
22
+ function mapPointsBoundsKey(points, defaultCenter) {
23
+ const body = points.length === 0
24
+ ? ''
25
+ : [...points]
26
+ .sort((a, b) => a.latitude !== b.latitude
27
+ ? a.latitude - b.latitude
28
+ : a.longitude !== b.longitude
29
+ ? a.longitude - b.longitude
30
+ : a.dealerName.localeCompare(b.dealerName))
31
+ .map((p) => `${p.dealerName}\t${p.latitude}\t${p.longitude}`)
32
+ .join('\n');
33
+ const dc = defaultCenter != null ? `${defaultCenter[0]},${defaultCenter[1]}` : '';
34
+ return `${body}|${dc}`;
35
+ }
16
36
  function readNumber(value) {
17
37
  if (typeof value === 'number' && Number.isFinite(value))
18
38
  return value;
@@ -53,10 +73,11 @@ function getDealerMapPoint(dealer) {
53
73
  function toAssetSrc(value) {
54
74
  return typeof value === 'string' ? value : value.src;
55
75
  }
56
- function createDealerPinIcon(isExclusive, active) {
76
+ function createDealerPinIcon(isExclusive, active, market) {
57
77
  const size = active ? 48 : 42;
58
78
  const width = Math.round((16 / 20) * size);
59
- const pinUrl = isExclusive ? toAssetSrc(dealerPinExclusive) : toAssetSrc(dealerPin);
79
+ const exclusivePin = market === 'NO' ? dealerPinIldstedet : dealerPinExclusive;
80
+ const pinUrl = isExclusive ? toAssetSrc(exclusivePin) : toAssetSrc(dealerPin);
60
81
  return icon({
61
82
  iconUrl: pinUrl,
62
83
  iconRetinaUrl: pinUrl,
@@ -66,7 +87,7 @@ function createDealerPinIcon(isExclusive, active) {
66
87
  className: 'jwi-map-pin',
67
88
  });
68
89
  }
69
- function FitMapBounds({ points, defaultCenter, }) {
90
+ function FitMapBounds({ boundsKey, points, defaultCenter, }) {
70
91
  const map = useMap();
71
92
  useEffect(() => {
72
93
  const all = points.map((p) => [p.latitude, p.longitude]);
@@ -75,11 +96,13 @@ function FitMapBounds({ points, defaultCenter, }) {
75
96
  if (all.length === 0)
76
97
  return;
77
98
  if (all.length === 1) {
78
- map.setView(all[0], 12, { animate: true });
99
+ map.setView(all[0], 12, { animate: false });
79
100
  return;
80
101
  }
81
- map.fitBounds(all, { padding: [30, 30], animate: true, maxZoom: 14 });
82
- }, [defaultCenter, map, points]);
102
+ // No animation must complete before FocusActiveDealer centers the selected pin (see FindDealerDrawerWidget).
103
+ map.fitBounds(all, { padding: [50, 50], animate: false, maxZoom: 19 });
104
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- boundsKey encodes points + defaultCenter; avoid refit on array identity churn
105
+ }, [boundsKey, map]);
83
106
  return null;
84
107
  }
85
108
  function FocusActiveDealer({ point }) {
@@ -87,34 +110,149 @@ function FocusActiveDealer({ point }) {
87
110
  useEffect(() => {
88
111
  if (point == null)
89
112
  return;
90
- map.setView([point.latitude, point.longitude], Math.max(map.getZoom(), 13), {
91
- animate: true,
92
- });
113
+ const target = latLng(point.latitude, point.longitude);
114
+ map.setView(target, Math.max(map.getZoom(), 13), { animate: true });
93
115
  }, [map, point]);
94
116
  return null;
95
117
  }
96
- export function ProductPageWidget({ t, buttonStyling, isSearching, locationError, searchResult, inquiryValues, inquiryError, isInquirySubmitted, selectedDealerName, isManualSearchEnabled, query, suggestions, suggestionsOpen, isSuggestionsLoading, onQueryChange, onQuerySubmit, onSuggestionSelect, onDismissSuggestions, onInquiryClose, onInquirySubmit, onInquiryFieldChange, onStartInquiry, onClosePopup, }) {
118
+ function ClusteredMapMarkers({ points, pointsBoundsKey, clusterThemeKey, clusterBrandFill, clusterBrandLabel, market, activeDealerName, onSelectDealer, onUnavailable, }) {
119
+ const map = useMap();
120
+ const markersByDealerRef = useRef(new Map());
121
+ const pointsRef = useRef(points);
122
+ const activeNameRef = useRef(activeDealerName);
123
+ const onSelectRef = useRef(onSelectDealer);
124
+ const onUnavailableRef = useRef(onUnavailable);
125
+ pointsRef.current = points;
126
+ activeNameRef.current = activeDealerName;
127
+ onSelectRef.current = onSelectDealer;
128
+ onUnavailableRef.current = onUnavailable;
129
+ function applyPinsForSelection() {
130
+ const active = activeNameRef.current;
131
+ for (const p of pointsRef.current) {
132
+ const marker = markersByDealerRef.current.get(p.dealerName);
133
+ if (marker != null) {
134
+ marker.setIcon(createDealerPinIcon(p.isExclusive, active === p.dealerName, market));
135
+ }
136
+ }
137
+ }
138
+ useEffect(() => {
139
+ applyPinsForSelection();
140
+ }, [activeDealerName]);
141
+ useEffect(() => {
142
+ let disposed = false;
143
+ markersByDealerRef.current.clear();
144
+ let clusterGroup = null;
145
+ let plainGroup = null;
146
+ async function mountClusters() {
147
+ try {
148
+ const leafletModule = await import('leaflet');
149
+ if (disposed)
150
+ return;
151
+ const { clustered, standalone } = partitionDealersForMarkerClusterPlugin(pointsRef.current, {
152
+ linkKm: MARKER_CLUSTER_LINK_KM,
153
+ minGroupSize: MARKER_CLUSTER_MIN_GROUP,
154
+ });
155
+ const leafletMaybeDefault = leafletModule;
156
+ const leaf = (leafletMaybeDefault.default ??
157
+ leafletMaybeDefault);
158
+ if (typeof leaf.featureGroup !== 'function') {
159
+ onUnavailableRef.current?.();
160
+ return;
161
+ }
162
+ function makeMarker(point) {
163
+ const marker = leaf.marker([point.latitude, point.longitude], {
164
+ icon: createDealerPinIcon(point.isExclusive, activeNameRef.current === point.dealerName, market),
165
+ });
166
+ marker.on('click', () => {
167
+ onSelectRef.current?.({
168
+ dealerName: point.dealerName,
169
+ latitude: point.latitude,
170
+ longitude: point.longitude,
171
+ });
172
+ });
173
+ marker.bindTooltip(point.dealerName);
174
+ markersByDealerRef.current.set(point.dealerName, marker);
175
+ return marker;
176
+ }
177
+ if (standalone.length > 0) {
178
+ plainGroup = leaf.featureGroup();
179
+ for (const point of standalone) {
180
+ plainGroup.addLayer(makeMarker(point));
181
+ }
182
+ plainGroup.addTo(map);
183
+ }
184
+ if (clustered.length > 0) {
185
+ await loadLeafletMarkerCluster(leaf);
186
+ if (disposed)
187
+ return;
188
+ if (typeof leaf.markerClusterGroup !== 'function') {
189
+ onUnavailableRef.current?.();
190
+ return;
191
+ }
192
+ clusterGroup = leaf.markerClusterGroup({
193
+ showCoverageOnHover: false,
194
+ spiderfyOnMaxZoom: true,
195
+ spiderLegPolylineOptions: {
196
+ weight: 1.75,
197
+ color: clusterBrandFill,
198
+ opacity: 0.38,
199
+ },
200
+ iconCreateFunction: (cluster) => {
201
+ const n = cluster.getChildCount();
202
+ return leaf.divIcon({
203
+ html: markerClusterCountIconHtml(n, clusterBrandFill, clusterBrandLabel),
204
+ className: '',
205
+ iconSize: leaf.point(40, 40),
206
+ });
207
+ },
208
+ maxClusterRadius: (zoom) => zoom <= 6 ? 140 : zoom <= 8 ? 125 : zoom <= 10 ? 100 : zoom <= 11 ? 72 : 45,
209
+ disableClusteringAtZoom: 12,
210
+ chunkedLoading: true,
211
+ });
212
+ for (const point of clustered) {
213
+ clusterGroup.addLayer(makeMarker(point));
214
+ }
215
+ clusterGroup.addTo(map);
216
+ }
217
+ applyPinsForSelection();
218
+ }
219
+ catch {
220
+ onUnavailableRef.current?.();
221
+ }
222
+ }
223
+ void mountClusters();
224
+ return () => {
225
+ disposed = true;
226
+ if (clusterGroup != null) {
227
+ clusterGroup.clearLayers();
228
+ clusterGroup.removeFrom(map);
229
+ }
230
+ if (plainGroup != null) {
231
+ plainGroup.clearLayers();
232
+ plainGroup.removeFrom(map);
233
+ }
234
+ markersByDealerRef.current.clear();
235
+ };
236
+ }, [map, pointsBoundsKey, clusterThemeKey]);
237
+ return null;
238
+ }
239
+ export function ProductPageWidget({ t, buttonStyling, borderStyling, market, isSearching, locationError, searchResult, mapSearchResult, inquiryValues, inquiryError, isInquirySubmitted, selectedDealerName, isManualSearchEnabled, query, suggestions, suggestionsOpen, isSuggestionsLoading, onQueryChange, onQuerySubmit, onSuggestionSelect, onDismissSuggestions, onInquiryClose, onInquirySubmit, onInquiryFieldChange, onStartInquiry, onMapDealerSelect, onClosePopup, }) {
97
240
  const dealers = (searchResult?.dealers ?? []);
98
- const total = searchResult?.total ?? dealers.length;
241
+ const mapDealers = (mapSearchResult?.dealers ?? dealers);
242
+ const total = dealers.length;
99
243
  const inquiryFormOpen = inquiryValues != null;
100
- const [viewMode, setViewMode] = useState('list');
101
244
  const [activeDealerName, setActiveDealerName] = useState(null);
102
245
  const [mobileMapExpanded, setMobileMapExpanded] = useState(false);
103
246
  const [isMobileViewport, setIsMobileViewport] = useState(false);
247
+ const [visibleDealerCount, setVisibleDealerCount] = useState(10);
248
+ const [clusterUnavailable, setClusterUnavailable] = useState(false);
104
249
  useEffect(() => {
105
- if (viewMode !== 'map') {
106
- setMobileMapExpanded(false);
107
- }
108
- }, [viewMode]);
109
- useEffect(() => {
110
- if (viewMode !== 'map')
111
- return;
112
250
  const previous = document.body.style.overflow;
113
251
  document.body.style.overflow = 'hidden';
114
252
  return () => {
115
253
  document.body.style.overflow = previous;
116
254
  };
117
- }, [viewMode]);
255
+ }, []);
118
256
  useEffect(() => {
119
257
  if (typeof window === 'undefined')
120
258
  return;
@@ -124,7 +262,23 @@ export function ProductPageWidget({ t, buttonStyling, isSearching, locationError
124
262
  media.addEventListener('change', update);
125
263
  return () => media.removeEventListener('change', update);
126
264
  }, []);
127
- const mapPoints = useMemo(() => dealers.map((dealer) => getDealerMapPoint(dealer)).filter((v) => v != null), [dealers]);
265
+ useEffect(() => {
266
+ setVisibleDealerCount(10);
267
+ }, [dealers]);
268
+ const visibleDealers = useMemo(() => dealers.slice(0, Math.max(10, visibleDealerCount)), [dealers, visibleDealerCount]);
269
+ useEffect(() => {
270
+ if (activeDealerName == null)
271
+ return;
272
+ const activeIndex = dealers.findIndex((dealer) => getDealerName(dealer) === activeDealerName);
273
+ if (activeIndex < 0 || activeIndex < visibleDealerCount)
274
+ return;
275
+ const nextCount = Math.ceil((activeIndex + 1) / 10) * 10;
276
+ setVisibleDealerCount(nextCount);
277
+ }, [activeDealerName, dealers, visibleDealerCount]);
278
+ const mapPoints = useMemo(() => mapDealers.map((dealer) => getDealerMapPoint(dealer)).filter((v) => v != null), [mapDealers]);
279
+ useEffect(() => {
280
+ setClusterUnavailable(false);
281
+ }, [mapPoints]);
128
282
  const defaultCenter = useMemo(() => {
129
283
  const first = mapPoints[0];
130
284
  if (first != null)
@@ -135,22 +289,41 @@ export function ProductPageWidget({ t, buttonStyling, isSearching, locationError
135
289
  return null;
136
290
  return [latitude, longitude];
137
291
  }, [mapPoints, searchResult?.origin?.latitude, searchResult?.origin?.longitude]);
292
+ const mapBoundsKey = useMemo(() => mapPointsBoundsKey(mapPoints, defaultCenter), [mapPoints, defaultCenter]);
138
293
  useEffect(() => {
139
294
  setActiveDealerName(dealers.length > 0 ? getDealerName(dealers[0]) : null);
140
295
  }, [dealers]);
141
296
  const activeMapPoint = useMemo(() => mapPoints.find((p) => p.dealerName === activeDealerName) ?? null, [activeDealerName, mapPoints]);
142
- const listMapPills = (_jsxs("div", { className: "jwi-inline-flex jwi-items-center jwi-gap-1 jwi-rounded-full jwi-p-1", children: [_jsx("button", { type: "button", onClick: () => setViewMode('list'), className: `jwi-cursor-pointer jwi-rounded-full jwi-px-4 jwi-py-1.5 jwi-text-sm jwi-font-medium ${viewMode === 'list'
143
- ? 'jwi-bg-[#f0f0f0] jwi-text-[#000000]'
144
- : 'jwi-bg-transparent jwi-text-[#555555]'}`, children: t.listView }), _jsx("button", { type: "button", onClick: () => setViewMode('map'), className: `jwi-cursor-pointer jwi-rounded-full jwi-px-4 jwi-py-1.5 jwi-text-sm jwi-font-medium ${viewMode === 'map'
145
- ? 'jwi-bg-[#f0f0f0] jwi-text-[#000000]'
146
- : 'jwi-bg-transparent jwi-text-[#555555]'}`, children: t.mapView })] }));
147
- const mapCanvas = (_jsxs(MapContainer, { center: defaultCenter ?? [59.9139, 10.7522], zoom: 10, className: "jwi-h-full jwi-w-full", zoomControl: true, children: [_jsx(TileLayer, { attribution: '\u00A9 <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer">OpenStreetMap</a> contributors \u00A9 <a href="https://carto.com/attributions" target="_blank" rel="noreferrer">CARTO</a>', url: OSM_SMOOTH_TILE_URL }), _jsx(FitMapBounds, { points: mapPoints, defaultCenter: defaultCenter }), _jsx(FocusActiveDealer, { point: activeMapPoint }), mapPoints.map((point) => {
148
- const isActive = activeDealerName === point.dealerName;
149
- return (_jsx(Marker, { position: [point.latitude, point.longitude], icon: createDealerPinIcon(point.isExclusive, isActive), eventHandlers: {
150
- click: () => setActiveDealerName(point.dealerName),
151
- }, children: _jsx(Tooltip, { children: point.dealerName }) }, `${point.dealerName}-${point.latitude}-${point.longitude}`));
152
- })] }));
153
- const showInquiryInMapPopup = inquiryFormOpen && searchResult?.ok && viewMode === 'map';
297
+ const mapClusterTheme = useMemo(() => {
298
+ const fill = buttonStyling?.backgroundColor?.trim() || JOTUL_BRAND_PRIMARY_HEX;
299
+ const label = buttonStyling?.textColor?.trim() || '#ffffff';
300
+ return {
301
+ fill,
302
+ label,
303
+ key: `${fill}\0${label}`,
304
+ };
305
+ }, [buttonStyling?.backgroundColor, buttonStyling?.textColor]);
306
+ const mapCanvas = (_jsxs(MapContainer, { center: defaultCenter ?? [59.9139, 10.7522], zoom: 6, className: "jwi-h-full jwi-w-full", zoomControl: true, children: [_jsx(TileLayer, { attribution: '\u00A9 <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noreferrer">OpenStreetMap</a> contributors \u00A9 <a href="https://carto.com/attributions" target="_blank" rel="noreferrer">CARTO</a>', url: OSM_MINIMAL_TILE_URL, maxZoom: 19 }), _jsx(FitMapBounds, { boundsKey: mapBoundsKey, points: mapPoints, defaultCenter: defaultCenter }), _jsx(FocusActiveDealer, { point: activeMapPoint }), _jsx(ClusteredMapMarkers, { points: mapPoints, pointsBoundsKey: mapBoundsKey, clusterThemeKey: mapClusterTheme.key, clusterBrandFill: mapClusterTheme.fill, clusterBrandLabel: mapClusterTheme.label, market: market, activeDealerName: activeDealerName, onUnavailable: () => setClusterUnavailable(true), onSelectDealer: (dealer) => {
307
+ setActiveDealerName(dealer.dealerName);
308
+ onMapDealerSelect?.(dealer);
309
+ } }), clusterUnavailable &&
310
+ mapPoints.map((point) => {
311
+ const isActive = activeDealerName === point.dealerName;
312
+ return (_jsx(Marker, { position: [point.latitude, point.longitude], icon: createDealerPinIcon(point.isExclusive, isActive, market), eventHandlers: {
313
+ click: () => {
314
+ setActiveDealerName(point.dealerName);
315
+ onMapDealerSelect?.({
316
+ dealerName: point.dealerName,
317
+ latitude: point.latitude,
318
+ longitude: point.longitude,
319
+ });
320
+ },
321
+ }, children: _jsx(Tooltip, { children: point.dealerName }) }, `${point.dealerName}-${point.latitude}-${point.longitude}`));
322
+ })] }));
323
+ const showInquiryInMapPopup = inquiryFormOpen && searchResult?.ok;
324
+ const showInquirySuccessScreen = isInquirySubmitted && !inquiryFormOpen;
325
+ const canShowMore = visibleDealerCount < dealers.length;
326
+ const showMoreButton = canShowMore ? (_jsx("button", { type: "button", onClick: () => setVisibleDealerCount((count) => Math.min(count + 10, dealers.length)), className: "jwi-mt-3 jwi-inline-flex jwi-w-full jwi-cursor-pointer jwi-items-center jwi-justify-center jwi-rounded-md jwi-border jwi-border-[#d8d2c7] jwi-bg-white jwi-px-3 jwi-py-2 jwi-text-sm jwi-font-medium jwi-text-[#111111]", children: "Show more" })) : null;
154
327
  const handleDealerFocus = (dealerName) => {
155
328
  setActiveDealerName(dealerName);
156
329
  };
@@ -159,10 +332,11 @@ export function ProductPageWidget({ t, buttonStyling, isSearching, locationError
159
332
  onClosePopup();
160
333
  return;
161
334
  }
162
- setViewMode('list');
163
335
  };
164
- if (inquiryFormOpen && searchResult?.ok && viewMode !== 'map') {
165
- return (_jsx("div", { className: "jwi-flex jwi-w-full jwi-flex-col jwi-gap-3", children: _jsx(InquiryForm, { t: t, buttonStyling: buttonStyling, inquiryValues: inquiryValues, inquiryError: inquiryError, onInquiryClose: onInquiryClose, onInquirySubmit: onInquirySubmit, onInquiryFieldChange: onInquiryFieldChange }) }));
336
+ function renderSuccessPanel() {
337
+ return (_jsx("div", { className: "jwi-flex jwi-w-full jwi-items-center jwi-justify-center jwi-bg-white jwi-p-2", children: _jsxs("div", { className: `jwi-flex jwi-w-full jwi-max-w-[520px] jwi-flex-col jwi-items-center jwi-gap-4 ${R10} jwi-bg-white jwi-p-8 jwi-text-center`, children: [_jsx("div", { className: "jwi-text-2xl jwi-font-semibold jwi-text-[#111111]", children: t.inquiryThankYouTitle }), _jsx("div", { className: "jwi-text-sm jwi-leading-6 jwi-text-[#333333]", children: t.inquirySentSuccess }), _jsx("button", { type: "button", onClick: closeMapPopup, className: "jwi-mt-2 jwi-inline-flex jwi-cursor-pointer jwi-items-center jwi-justify-center jwi-rounded-md jwi-border jwi-border-[#d8d2c7] jwi-bg-white jwi-px-5 jwi-py-2.5 jwi-text-sm jwi-font-medium jwi-text-[#111111]", children: t.goBack })] }) }));
166
338
  }
167
- return (_jsxs(_Fragment, { children: [viewMode === 'list' && (_jsxs("div", { className: `jwi-flex jwi-w-full jwi-flex-col jwi-gap-3 ${R10} jwi-border jwi-border-[#e6e1d7] jwi-bg-white jwi-p-6`, children: [_jsx(LocationSearch, { t: t, isManualSearchEnabled: isManualSearchEnabled, query: query, suggestions: suggestions, suggestionsOpen: suggestionsOpen, isSuggestionsLoading: isSuggestionsLoading, onQueryChange: onQueryChange, onQuerySubmit: onQuerySubmit, onSuggestionSelect: onSuggestionSelect, onDismissSuggestions: onDismissSuggestions }), locationError != null && !isManualSearchEnabled && (_jsx(StatusBanner, { tone: "error", children: locationError })), searchResult?.ok === false && (_jsx(StatusBanner, { tone: "error", children: searchResult.error ?? '' })), isInquirySubmitted && _jsx(StatusBanner, { tone: "success", children: t.inquirySentSuccess }), isSearching && (_jsxs("div", { className: "jwi-flex jwi-flex-col", children: [_jsx("div", { className: "jwi-w-full jwi-flex-shrink-0 jwi-border-b jwi-border-[#e6e1d7] jwi-pb-3", children: _jsx("div", { className: "jwi-h-5 jwi-w-48 jwi-animate-pulse jwi-rounded-full jwi-bg-[#ece8df]" }) }), _jsxs("div", { className: "jwi-mt-3 jwi-mr-[-12px] jwi-flex jwi-max-h-[min(60vh,480px)] jwi-flex-col jwi-gap-4 jwi-overflow-y-auto jwi-pr-[12px]", children: [_jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {})] })] })), searchResult?.ok && !isSearching && viewMode === 'list' && (_jsx(DealerList, { dealers: dealers, total: total, selectedDealerName: selectedDealerName, headerRight: listMapPills, t: t, buttonStyling: buttonStyling, onStartInquiry: onStartInquiry }))] })), searchResult?.ok && viewMode === 'map' && (_jsxs(_Fragment, { children: [!isMobileViewport && (_jsx("div", { className: "jwi-fixed jwi-inset-0 jwi-z-[9999] jwi-flex jwi-items-center jwi-justify-center jwi-bg-black/45 jwi-p-4", onClick: () => setViewMode('list'), children: _jsxs("div", { className: `jwi-relative jwi-flex jwi-h-[min(85vh,860px)] jwi-w-[min(96vw,1200px)] jwi-scale-100 jwi-flex-col jwi-overflow-hidden ${R10} jwi-bg-white jwi-shadow-[0_20px_60px_rgba(0,0,0,0.25)] jwi-transition-all md:jwi-flex-row`, onClick: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", onClick: closeMapPopup, className: "jwi-absolute jwi-right-3 jwi-top-3 jwi-z-[1200] jwi-inline-flex jwi-h-9 jwi-w-9 jwi-cursor-pointer jwi-items-center jwi-justify-center jwi-rounded-full jwi-border jwi-border-[#d8d2c7] jwi-bg-white jwi-text-xl jwi-leading-none jwi-text-[#111111] jwi-shadow-[0_2px_8px_rgba(0,0,0,0.12)]", "aria-label": t.closeMap, children: "\u00D7" }), _jsx("div", { className: "jwi-flex jwi-h-full jwi-min-h-0 jwi-w-full jwi-flex-col jwi-overflow-hidden md:jwi-w-[48%] md:jwi-border-r md:jwi-border-[#ece8df]", children: _jsx("div", { className: "jwi-flex jwi-h-full jwi-min-h-0 jwi-w-full jwi-flex-col jwi-gap-3 jwi-overflow-hidden jwi-bg-white jwi-p-6", children: showInquiryInMapPopup ? (_jsx(InquiryForm, { t: t, buttonStyling: buttonStyling, inquiryValues: inquiryValues, inquiryError: inquiryError, embedded: true, onInquiryClose: onInquiryClose, onInquirySubmit: onInquirySubmit, onInquiryFieldChange: onInquiryFieldChange })) : (_jsxs(_Fragment, { children: [_jsx(LocationSearch, { t: t, isManualSearchEnabled: isManualSearchEnabled, query: query, suggestions: suggestions, suggestionsOpen: suggestionsOpen, isSuggestionsLoading: isSuggestionsLoading, onQueryChange: onQueryChange, onQuerySubmit: onQuerySubmit, onSuggestionSelect: onSuggestionSelect, onDismissSuggestions: onDismissSuggestions }), locationError != null && !isManualSearchEnabled && (_jsx(StatusBanner, { tone: "error", children: locationError })), isInquirySubmitted && (_jsx(StatusBanner, { tone: "success", children: t.inquirySentSuccess })), _jsx(DealerList, { dealers: dealers, total: total, selectedDealerName: selectedDealerName, activeDealerName: activeDealerName, fitAvailableHeight: true, autoScrollToActive: true, maxHeightClassName: "jwi-max-h-none", headerRight: listMapPills, t: t, buttonStyling: buttonStyling, onStartInquiry: onStartInquiry, onSelectDealer: handleDealerFocus })] })) }) }), _jsx("div", { className: "jwi-relative jwi-h-[45%] jwi-w-full md:jwi-h-full md:jwi-w-[52%]", children: mapCanvas })] }) })), isMobileViewport && (_jsx("div", { className: "jwi-fixed jwi-inset-0 jwi-z-[9999] jwi-bg-white", children: _jsxs("div", { className: "jwi-relative jwi-flex jwi-h-full jwi-flex-col", children: [_jsx("div", { className: "jwi-flex jwi-justify-end jwi-bg-white jwi-px-4 jwi-pt-3", children: _jsx("button", { type: "button", onClick: closeMapPopup, className: "jwi-inline-flex jwi-h-9 jwi-w-9 jwi-cursor-pointer jwi-items-center jwi-justify-center jwi-rounded-full jwi-border jwi-border-[#d8d2c7] jwi-bg-white jwi-text-xl jwi-leading-none jwi-text-[#111111]", "aria-label": t.closeMap, children: "\u00D7" }) }), _jsx("div", { className: "jwi-min-h-0 jwi-flex-1 jwi-overflow-y-auto jwi-bg-white jwi-p-4 jwi-pb-24", children: showInquiryInMapPopup ? (_jsx(InquiryForm, { t: t, buttonStyling: buttonStyling, inquiryValues: inquiryValues, inquiryError: inquiryError, embedded: true, onInquiryClose: onInquiryClose, onInquirySubmit: onInquirySubmit, onInquiryFieldChange: onInquiryFieldChange })) : (_jsxs(_Fragment, { children: [_jsx(LocationSearch, { t: t, isManualSearchEnabled: isManualSearchEnabled, query: query, suggestions: suggestions, suggestionsOpen: suggestionsOpen, isSuggestionsLoading: isSuggestionsLoading, onQueryChange: onQueryChange, onQuerySubmit: onQuerySubmit, onSuggestionSelect: onSuggestionSelect, onDismissSuggestions: onDismissSuggestions }), locationError != null && !isManualSearchEnabled && (_jsx(StatusBanner, { tone: "error", children: locationError })), isInquirySubmitted && (_jsx(StatusBanner, { tone: "success", children: t.inquirySentSuccess })), _jsx(DealerList, { dealers: dealers, total: total, selectedDealerName: selectedDealerName, activeDealerName: activeDealerName, maxHeightClassName: "jwi-max-h-none", autoScrollToActive: true, enableInternalScroll: false, headerRight: listMapPills, t: t, buttonStyling: buttonStyling, onStartInquiry: onStartInquiry, onSelectDealer: handleDealerFocus })] })) }), !showInquiryInMapPopup && !mobileMapExpanded && (_jsxs("button", { type: "button", onClick: () => setMobileMapExpanded((open) => !open), className: "jwi-absolute jwi-inset-x-0 jwi-bottom-0 jwi-z-20 jwi-flex jwi-h-14 jwi-items-center jwi-justify-center jwi-gap-2 jwi-rounded-t-[16px] jwi-border-t jwi-border-[#e6e1d7] jwi-bg-white jwi-text-sm jwi-font-semibold jwi-text-[#111111] jwi-shadow-[0_-6px_20px_rgba(0,0,0,0.12)]", children: [t.openMap, _jsx("span", { className: "jwi-inline-flex", style: { transform: 'rotate(-90deg)' }, children: _jsx(ArrowRightIcon, { className: "jwi-h-4 jwi-w-4 jwi-shrink-0" }) })] })), mobileMapExpanded && !showInquiryInMapPopup && (_jsxs("div", { className: "jwi-absolute jwi-inset-x-0 jwi-bottom-0 jwi-z-30 jwi-h-[78vh] jwi-overflow-hidden jwi-rounded-t-[16px] jwi-bg-white jwi-shadow-[0_-12px_36px_rgba(0,0,0,0.22)]", children: [_jsxs("button", { type: "button", onClick: () => setMobileMapExpanded(false), className: "jwi-flex jwi-h-12 jwi-w-full jwi-items-center jwi-justify-center jwi-gap-2 jwi-border-b jwi-border-[#e6e1d7] jwi-bg-white jwi-text-sm jwi-font-semibold jwi-text-[#111111]", children: [t.closeMapMobile, _jsx("span", { className: "jwi-inline-flex", style: { transform: 'rotate(90deg)' }, children: _jsx(ArrowRightIcon, { className: "jwi-h-4 jwi-w-4 jwi-shrink-0" }) })] }), _jsx("div", { className: "jwi-h-[calc(78vh-48px)] jwi-w-full", children: mapCanvas })] }))] }) }))] }))] }));
339
+ return (_jsxs(_Fragment, { children: [!isMobileViewport && (_jsx(_Fragment, { children: _jsx("div", { className: "jwi-fixed jwi-inset-0 jwi-z-[9999] jwi-flex jwi-items-center jwi-justify-center jwi-bg-black/45 jwi-p-4", onClick: closeMapPopup, children: _jsxs("div", { className: `jwi-relative jwi-flex jwi-scale-100 jwi-flex-col jwi-overflow-hidden ${R10} jwi-bg-white jwi-shadow-[0_20px_60px_rgba(0,0,0,0.25)] jwi-transition-all ${showInquirySuccessScreen
340
+ ? 'jwi-h-auto jwi-w-[min(92vw,620px)]'
341
+ : 'jwi-h-[min(85vh,860px)] jwi-w-[min(96vw,1200px)] md:jwi-flex-row'}`, onClick: (event) => event.stopPropagation(), children: [_jsx("button", { type: "button", onClick: closeMapPopup, className: "jwi-absolute jwi-right-3 jwi-top-3 jwi-z-[1200] jwi-inline-flex jwi-h-9 jwi-w-9 jwi-cursor-pointer jwi-items-center jwi-justify-center jwi-rounded-full jwi-border jwi-border-[#d8d2c7] jwi-bg-white jwi-text-xl jwi-leading-none jwi-text-[#111111] jwi-shadow-[0_2px_8px_rgba(0,0,0,0.12)]", "aria-label": t.closeMap, children: "\u00D7" }), showInquirySuccessScreen ? (renderSuccessPanel()) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "jwi-flex jwi-h-full jwi-min-h-0 jwi-w-full jwi-flex-col jwi-overflow-hidden md:jwi-w-[48%] md:jwi-border-r md:jwi-border-[#ece8df]", children: _jsx("div", { className: "jwi-flex jwi-h-full jwi-min-h-0 jwi-w-full jwi-flex-col jwi-gap-3 jwi-overflow-hidden jwi-bg-white jwi-p-6", children: showInquiryInMapPopup ? (_jsx(InquiryForm, { t: t, buttonStyling: buttonStyling, inquiryValues: inquiryValues, inquiryError: inquiryError, embedded: true, onInquiryClose: onInquiryClose, onInquirySubmit: onInquirySubmit, onInquiryFieldChange: onInquiryFieldChange })) : (_jsxs(_Fragment, { children: [_jsx(LocationSearch, { t: t, isManualSearchEnabled: isManualSearchEnabled, query: query, suggestions: suggestions, suggestionsOpen: suggestionsOpen, isSuggestionsLoading: isSuggestionsLoading, onQueryChange: onQueryChange, onQuerySubmit: onQuerySubmit, onSuggestionSelect: onSuggestionSelect, onDismissSuggestions: onDismissSuggestions }), _jsx(CampaignStatus, { campaign: searchResult?.campaign, t: t }), locationError != null && !isManualSearchEnabled && (_jsx(StatusBanner, { tone: "error", children: locationError })), searchResult?.ok === false && (_jsx(StatusBanner, { tone: "error", children: searchResult.error ?? '' })), isSearching ? (_jsxs("div", { className: "jwi-flex jwi-flex-col", children: [_jsx("div", { className: "jwi-w-full jwi-flex-shrink-0 jwi-border-b jwi-border-[#e6e1d7] jwi-pb-3", children: _jsx("div", { className: "jwi-h-5 jwi-w-48 jwi-animate-pulse jwi-rounded-full jwi-bg-[#ece8df]" }) }), _jsxs("div", { className: "jwi-mt-3 jwi-mr-[-12px] jwi-flex jwi-h-full jwi-flex-col jwi-gap-4 jwi-overflow-y-auto jwi-pr-[12px]", children: [_jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {})] })] })) : (_jsxs(_Fragment, { children: [searchResult?.ok && (_jsx(DealerList, { dealers: visibleDealers, total: total, selectedDealerName: selectedDealerName, activeDealerName: activeDealerName, fitAvailableHeight: true, autoScrollToActive: true, maxHeightClassName: "jwi-max-h-none", t: t, buttonStyling: buttonStyling, borderStyling: borderStyling, market: market, onStartInquiry: onStartInquiry, onSelectDealer: handleDealerFocus })), showMoreButton] }))] })) }) }), _jsx("div", { className: "jwi-relative jwi-h-[45%] jwi-w-full md:jwi-h-full md:jwi-w-[52%]", children: mapCanvas })] }))] }) }) })), isMobileViewport && (_jsx("div", { className: "jwi-fixed jwi-inset-0 jwi-z-[9999] jwi-bg-white", children: _jsxs("div", { className: "jwi-relative jwi-flex jwi-h-full jwi-flex-col", children: [_jsx("div", { className: "jwi-flex jwi-justify-end jwi-bg-white jwi-px-4 jwi-pt-3", children: _jsx("button", { type: "button", onClick: closeMapPopup, className: "jwi-inline-flex jwi-h-9 jwi-w-9 jwi-cursor-pointer jwi-items-center jwi-justify-center jwi-rounded-full jwi-border jwi-border-[#d8d2c7] jwi-bg-white jwi-text-xl jwi-leading-none jwi-text-[#111111]", "aria-label": t.closeMap, children: "\u00D7" }) }), _jsx("div", { className: "jwi-min-h-0 jwi-flex-1 jwi-overflow-y-auto jwi-bg-white jwi-p-4 jwi-pb-24", children: showInquirySuccessScreen ? (renderSuccessPanel()) : showInquiryInMapPopup ? (_jsx(InquiryForm, { t: t, buttonStyling: buttonStyling, inquiryValues: inquiryValues, inquiryError: inquiryError, embedded: true, onInquiryClose: onInquiryClose, onInquirySubmit: onInquirySubmit, onInquiryFieldChange: onInquiryFieldChange })) : (_jsxs(_Fragment, { children: [_jsx(LocationSearch, { t: t, isManualSearchEnabled: isManualSearchEnabled, query: query, suggestions: suggestions, suggestionsOpen: suggestionsOpen, isSuggestionsLoading: isSuggestionsLoading, onQueryChange: onQueryChange, onQuerySubmit: onQuerySubmit, onSuggestionSelect: onSuggestionSelect, onDismissSuggestions: onDismissSuggestions }), _jsx(CampaignStatus, { campaign: searchResult?.campaign, t: t }), locationError != null && !isManualSearchEnabled && (_jsx(StatusBanner, { tone: "error", children: locationError })), searchResult?.ok === false && (_jsx(StatusBanner, { tone: "error", children: searchResult.error ?? '' })), isSearching ? (_jsxs("div", { className: "jwi-flex jwi-flex-col", children: [_jsx("div", { className: "jwi-w-full jwi-flex-shrink-0 jwi-border-b jwi-border-[#e6e1d7] jwi-pb-3", children: _jsx("div", { className: "jwi-h-5 jwi-w-48 jwi-animate-pulse jwi-rounded-full jwi-bg-[#ece8df]" }) }), _jsxs("div", { className: "jwi-mt-3 jwi-flex jwi-flex-col jwi-gap-4", children: [_jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {}), _jsx(DealerCardSkeleton, {})] })] })) : (_jsxs(_Fragment, { children: [searchResult?.ok && (_jsx(DealerList, { dealers: visibleDealers, total: total, selectedDealerName: selectedDealerName, activeDealerName: activeDealerName, maxHeightClassName: "jwi-max-h-none", autoScrollToActive: true, enableInternalScroll: false, t: t, buttonStyling: buttonStyling, borderStyling: borderStyling, market: market, onStartInquiry: onStartInquiry, onSelectDealer: handleDealerFocus })), showMoreButton] }))] })) }), !showInquiryInMapPopup && !showInquirySuccessScreen && !mobileMapExpanded && (_jsxs("button", { type: "button", onClick: () => setMobileMapExpanded((open) => !open), className: "jwi-absolute jwi-inset-x-0 jwi-bottom-0 jwi-z-20 jwi-flex jwi-h-14 jwi-items-center jwi-justify-center jwi-gap-2 jwi-rounded-t-[16px] jwi-border-t jwi-border-[#e6e1d7] jwi-bg-white jwi-text-sm jwi-font-semibold jwi-text-[#111111] jwi-shadow-[0_-6px_20px_rgba(0,0,0,0.12)]", children: [t.openMap, _jsx("span", { className: "jwi-inline-flex", style: { transform: 'rotate(-90deg)' }, children: _jsx(ArrowRightIcon, { className: "jwi-h-4 jwi-w-4 jwi-shrink-0" }) })] })), mobileMapExpanded && !showInquiryInMapPopup && !showInquirySuccessScreen && (_jsxs("div", { className: "jwi-absolute jwi-inset-x-0 jwi-bottom-0 jwi-z-30 jwi-h-[78vh] jwi-overflow-hidden jwi-rounded-t-[16px] jwi-bg-white jwi-shadow-[0_-12px_36px_rgba(0,0,0,0.22)]", children: [_jsxs("button", { type: "button", onClick: () => setMobileMapExpanded(false), className: "jwi-flex jwi-h-12 jwi-w-full jwi-items-center jwi-justify-center jwi-gap-2 jwi-border-b jwi-border-[#e6e1d7] jwi-bg-white jwi-text-sm jwi-font-semibold jwi-text-[#111111]", children: [t.closeMapMobile, _jsx("span", { className: "jwi-inline-flex", style: { transform: 'rotate(90deg)' }, children: _jsx(ArrowRightIcon, { className: "jwi-h-4 jwi-w-4 jwi-shrink-0" }) })] }), _jsx("div", { className: "jwi-h-[calc(78vh-48px)] jwi-w-full", children: mapCanvas })] }))] }) }))] }));
168
342
  }
@@ -0,0 +1,6 @@
1
+ import type { WidgetStrings } from '../../i18n/widgetStrings';
2
+ import type { DealerSearchResponse } from '../../types';
3
+ export declare function CampaignStatus({ campaign, t, }: {
4
+ campaign: DealerSearchResponse['campaign'];
5
+ t: WidgetStrings;
6
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ function formatCountdown(milliseconds, t) {
4
+ const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
5
+ const days = Math.floor(totalSeconds / 86400);
6
+ const hours = Math.floor((totalSeconds % 86400) / 3600);
7
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
8
+ const seconds = totalSeconds % 60;
9
+ return t.campaignCountdownTime
10
+ .replace('{days}', String(days))
11
+ .replace('{hours}', String(hours))
12
+ .replace('{minutes}', String(minutes))
13
+ .replace('{seconds}', String(seconds));
14
+ }
15
+ export function CampaignStatus({ campaign, t, }) {
16
+ const [remaining, setRemaining] = useState(null);
17
+ const fromTime = campaign?.from ? Date.parse(campaign.from) : Number.NaN;
18
+ const toTime = campaign?.to ? Date.parse(campaign.to) : Number.NaN;
19
+ const hasCountdown = Number.isFinite(fromTime) && Number.isFinite(toTime);
20
+ const title = campaign?.name?.trim();
21
+ const salesMessage = campaign?.salesMessage?.trim();
22
+ const hasContent = Boolean(title || salesMessage);
23
+ const bannerClassName = 'jwi-w-full jwi-flex-shrink-0 jwi-rounded-[12px] jwi-border jwi-border-[#b8d8aa] jwi-bg-[#eff9e9] jwi-px-4 jwi-py-3 jwi-text-center jwi-text-[#16330f] jwi-shadow-[0_6px_16px_rgba(22,51,15,0.08)]';
24
+ const titleClassName = 'jwi-text-base jwi-font-semibold jwi-leading-tight';
25
+ const salesMessageClassName = title
26
+ ? 'jwi-mt-1 jwi-text-sm jwi-font-medium jwi-leading-snug'
27
+ : 'jwi-text-sm jwi-font-medium jwi-leading-snug';
28
+ useEffect(() => {
29
+ if (!hasCountdown)
30
+ return;
31
+ const update = () => setRemaining(toTime - Date.now());
32
+ const initial = window.setTimeout(update, 0);
33
+ const timer = window.setInterval(update, 1000);
34
+ return () => {
35
+ window.clearTimeout(initial);
36
+ window.clearInterval(timer);
37
+ };
38
+ }, [hasCountdown, toTime]);
39
+ if (!hasCountdown || remaining == null) {
40
+ if (!hasContent)
41
+ return null;
42
+ return (_jsxs("div", { className: bannerClassName, children: [title && _jsx("div", { className: titleClassName, children: title }), salesMessage && _jsx("div", { className: salesMessageClassName, children: salesMessage })] }));
43
+ }
44
+ if (remaining <= 0) {
45
+ return (_jsxs("div", { className: bannerClassName, children: [_jsx("div", { className: "jwi-text-xs jwi-font-semibold jwi-uppercase jwi-tracking-[0.06em]", children: t.campaignEnded }), title && _jsx("div", { className: `jwi-mt-1 ${titleClassName}`, children: title }), salesMessage && _jsx("div", { className: "jwi-mt-1 jwi-text-sm jwi-font-medium jwi-leading-snug", children: salesMessage })] }));
46
+ }
47
+ return (_jsxs("div", { className: bannerClassName, children: [title && _jsx("div", { className: titleClassName, children: title }), salesMessage && _jsx("div", { className: salesMessageClassName, children: salesMessage }), _jsx("div", { className: hasContent ? 'jwi-mt-2 jwi-text-xs jwi-font-medium md:jwi-text-sm' : 'jwi-text-xs jwi-font-medium md:jwi-text-sm', children: t.campaignEndsIn.replace('{time}', formatCountdown(remaining, t)) })] }));
48
+ }
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { WidgetStrings } from '../../i18n/widgetStrings';
3
- import type { DealerRecord, JotulWidgetButtonStyling } from '../../types';
3
+ import type { DealerRecord, JotulWidgetButtonStyling, JotulWidgetBorderStyling } from '../../types';
4
4
  type DealerListProps = {
5
5
  dealers: DealerRecord[];
6
6
  total: number;
@@ -13,8 +13,10 @@ type DealerListProps = {
13
13
  headerRight?: ReactNode;
14
14
  t: WidgetStrings;
15
15
  buttonStyling?: JotulWidgetButtonStyling;
16
+ borderStyling?: JotulWidgetBorderStyling;
17
+ market?: string;
16
18
  onStartInquiry: (dealerName: string) => void;
17
19
  onSelectDealer?: (dealerName: string) => void;
18
20
  };
19
- export declare function DealerList({ dealers, total, selectedDealerName, activeDealerName, maxHeightClassName, fitAvailableHeight, autoScrollToActive, enableInternalScroll, headerRight, t, buttonStyling, onStartInquiry, onSelectDealer, }: DealerListProps): import("react/jsx-runtime").JSX.Element;
21
+ export declare function DealerList({ dealers, total, selectedDealerName, activeDealerName, maxHeightClassName, fitAvailableHeight, autoScrollToActive, enableInternalScroll, headerRight, t, buttonStyling, borderStyling, market, onStartInquiry, onSelectDealer, }: DealerListProps): import("react/jsx-runtime").JSX.Element;
20
22
  export {};
@@ -5,8 +5,9 @@ import { ArrowRightIcon } from '../../icons/ArrowRightIcon';
5
5
  import { TelephoneIcon } from '../../icons/TelephoneIcon';
6
6
  import { getWidgetPrimaryButtonPresentation } from '../../utils/widgetPrimaryButtonPresentation';
7
7
  import exclusiveDealerBadge from '../../images/jotul-exclusive-dealer.png';
8
+ import ildstedetDealer from '../../images/ildstedet-dealer.svg';
8
9
  import { asText, formatDistance, getDealerAddressLines, getDealerKey, getDealerName, } from '../../utils';
9
- export function DealerList({ dealers, total, selectedDealerName, activeDealerName = null, maxHeightClassName = 'jwi-max-h-[min(60vh,480px)]', fitAvailableHeight = false, autoScrollToActive = false, enableInternalScroll = true, headerRight, t, buttonStyling, onStartInquiry, onSelectDealer, }) {
10
+ export function DealerList({ dealers, total, selectedDealerName, activeDealerName = null, maxHeightClassName = 'jwi-max-h-[min(60vh,480px)]', fitAvailableHeight = false, autoScrollToActive = false, enableInternalScroll = true, headerRight, t, buttonStyling, borderStyling, market, onStartInquiry, onSelectDealer, }) {
10
11
  const cardRefs = useRef({});
11
12
  useEffect(() => {
12
13
  if (!autoScrollToActive || !activeDealerName)
@@ -16,6 +17,7 @@ export function DealerList({ dealers, total, selectedDealerName, activeDealerNam
16
17
  return;
17
18
  card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
18
19
  }, [activeDealerName, autoScrollToActive]);
20
+ const defaultBorderColor = borderStyling?.borderColor ?? '#e6e1d7';
19
21
  const inquiryCta = getWidgetPrimaryButtonPresentation(buttonStyling, 'panel', {
20
22
  extraClassName: 'jwi-w-full jwi-max-w-full jwi-shrink-0 jwi-justify-between jwi-gap-2 md:jwi-max-w-[220px]',
21
23
  });
@@ -33,9 +35,15 @@ export function DealerList({ dealers, total, selectedDealerName, activeDealerNam
33
35
  dealer.exclusive === '1';
34
36
  return (_jsxs("div", { ref: (element) => {
35
37
  cardRefs.current[dealerName] = element;
36
- }, className: `jwi-w-full jwi-cursor-pointer ${R10} jwi-border jwi-bg-white jwi-p-4 jwi-shadow-[0_1px_2px_rgba(17,17,17,0.03)] ${isActiveDealer ? 'jwi-border-[#ef2b18]' : 'jwi-border-[#e6e1d7]'}`, onClick: () => onSelectDealer?.(dealerName), children: [_jsxs("div", { className: "jwi-flex jwi-items-start jwi-justify-between jwi-gap-3", children: [_jsx("div", { className: "jwi-min-w-0 jwi-max-w-[calc(100%-5rem)] jwi-pr-1", children: _jsxs("div", { className: "jwi-flex jwi-items-center jwi-gap-2", children: [isExclusive && (_jsx("img", { src: typeof exclusiveDealerBadge === 'string'
37
- ? exclusiveDealerBadge
38
- : exclusiveDealerBadge.src, alt: "Jotul exclusive dealer", className: "jwi-h-auto jwi-w-[40px] jwi-shrink-0 jwi-rounded-full jwi-m-0" })), _jsx("h3", { className: "jwi-m-0 jwi-text-sm md:jwi-text-base jwi-font-semibold jwi-leading-snug jwi-text-[#111111]", children: dealerName })] }) }), distance && (_jsx("div", { className: `jwi-shrink-0 jwi-whitespace-nowrap ${R10} jwi-bg-[#fbf3db] jwi-px-2.5 jwi-py-1 jwi-text-xs jwi-font-medium jwi-leading-none jwi-text-[#111111]`, children: distance }))] }), addressLines.length > 0 && (_jsx("div", { className: "jwi-mt-3 jwi-flex jwi-flex-col jwi-gap-0.5 jwi-text-sm jwi-leading-snug jwi-text-[#111111]", children: addressLines.map((line) => (_jsx("div", { children: line }, line))) })), _jsxs("div", { className: "jwi-mt-4 jwi-flex jwi-flex-col jwi-gap-3 md:jwi-flex-row md:jwi-items-center md:jwi-justify-between", children: [_jsxs("button", { type: "button", onClick: (event) => {
38
+ }, className: `jwi-w-full jwi-cursor-pointer ${R10} jwi-border jwi-bg-white jwi-p-4 jwi-shadow-[0_1px_2px_rgba(17,17,17,0.03)] ${isActiveDealer ? 'jwi-border-[#ef2b18]' : ''}`, style: !isActiveDealer && defaultBorderColor !== '#e6e1d7'
39
+ ? { borderColor: defaultBorderColor }
40
+ : undefined, onClick: () => onSelectDealer?.(dealerName), children: [_jsxs("div", { className: "jwi-flex jwi-items-start jwi-justify-between jwi-gap-3", children: [_jsx("div", { className: "jwi-min-w-0 jwi-max-w-[calc(100%-5rem)] jwi-pr-1", children: _jsxs("div", { className: "jwi-flex jwi-items-center jwi-gap-2", children: [isExclusive && (() => {
41
+ const badgeImage = market === 'NO' ? ildstedetDealer : exclusiveDealerBadge;
42
+ const badgeAlt = market === 'NO' ? 'Ildstedet dealer' : 'Jotul exclusive dealer';
43
+ return (_jsx("img", { src: typeof badgeImage === 'string'
44
+ ? badgeImage
45
+ : badgeImage.src, alt: badgeAlt, className: "jwi-h-auto jwi-w-[40px] jwi-shrink-0 jwi-rounded-full jwi-m-0" }));
46
+ })(), _jsx("h3", { className: "jwi-m-0 jwi-text-sm md:jwi-text-base jwi-font-semibold jwi-leading-snug jwi-text-[#111111]", children: dealerName })] }) }), distance && (_jsx("div", { className: `jwi-shrink-0 jwi-whitespace-nowrap ${R10} jwi-bg-[#fbf3db] jwi-px-2.5 jwi-py-1 jwi-text-xs jwi-font-medium jwi-leading-none jwi-text-[#111111]`, children: distance }))] }), addressLines.length > 0 && (_jsx("div", { className: "jwi-mt-3 jwi-flex jwi-flex-col jwi-gap-0.5 jwi-text-sm jwi-leading-snug jwi-text-[#111111]", children: addressLines.map((line) => (_jsx("div", { children: line }, line))) })), _jsxs("div", { className: "jwi-mt-4 jwi-flex jwi-flex-col jwi-gap-3 md:jwi-flex-row md:jwi-items-center md:jwi-justify-between", children: [_jsxs("button", { type: "button", onClick: (event) => {
39
47
  event.stopPropagation();
40
48
  onStartInquiry(dealerName);
41
49
  }, className: inquiryCta.className, style: inquiryCta.style, children: [_jsx("span", { children: isSelectedDealer ? t.sendInquiryEditing : t.sendInquiryCta }), _jsx(ArrowRightIcon, { className: "jwi-h-[18px] jwi-w-[18px] jwi-shrink-0" })] }), phone && (_jsxs("a", { href: `tel:${phone.replace(/\s+/g, '')}`, onClick: (event) => event.stopPropagation(), className: "jwi-inline-flex jwi-min-w-0 jwi-items-center jwi-gap-2 jwi-text-sm jwi-font-normal jwi-tabular-nums jwi-text-[#111111] hover:jwi-underline", children: [_jsx(TelephoneIcon, {}), _jsx("span", { className: "jwi-break-all", children: phone })] }))] })] }, getDealerKey(dealer, index)));
@@ -1,5 +1,5 @@
1
1
  type StatusBannerProps = {
2
- tone: 'error' | 'success';
2
+ tone: 'error' | 'success' | 'info';
3
3
  children: string;
4
4
  };
5
5
  export declare function StatusBanner({ tone, children }: StatusBannerProps): import("react/jsx-runtime").JSX.Element;
@@ -3,6 +3,8 @@ import { R10 } from '../../constants';
3
3
  export function StatusBanner({ tone, children }) {
4
4
  const toneClass = tone === 'success'
5
5
  ? 'jwi-border-[#b7e5c2] jwi-bg-[#eefbf2] jwi-text-[#1b5e20]'
6
- : 'jwi-border-[#f0c7c2] jwi-bg-[#fff3f1] jwi-text-[#8f2d21]';
6
+ : tone === 'info'
7
+ ? 'jwi-border-[#e6e1d7] jwi-bg-[#fbf3db] jwi-text-[#111111]'
8
+ : 'jwi-border-[#f0c7c2] jwi-bg-[#fff3f1] jwi-text-[#8f2d21]';
7
9
  return (_jsx("div", { className: `jwi-w-full jwi-flex-shrink-0 ${R10} jwi-border jwi-px-4 jwi-py-3 jwi-text-sm jwi-leading-[1.4] ${toneClass}`, children: children }));
8
10
  }
@@ -1,3 +1,7 @@
1
+ /** Primary Jotul brand red (CTAs, active list border, map clusters). */
2
+ export declare const JOTUL_BRAND_PRIMARY_HEX = "#ef2b18";
3
+ /** Slightly darker red for hover / cluster ring. */
4
+ export declare const JOTUL_BRAND_PRIMARY_DARK_HEX = "#d92817";
1
5
  /** 10px radius for widget chrome (matches design spec). */
2
6
  export declare const R10 = "jwi-rounded-[10px]";
3
7
  export declare const FIND_DEALER_BUTTON_CLASS = "jwi-inline-flex jwi-w-full jwi-min-h-[56px] jwi-cursor-pointer jwi-items-center jwi-justify-center jwi-rounded-[10px] jwi-border-0 jwi-bg-[#ef2b18] jwi-px-7 jwi-text-base jwi-font-medium jwi-text-white hover:jwi-bg-[#d92817] disabled:jwi-cursor-wait disabled:hover:jwi-bg-[#ef2b18]";
package/dist/constants.js CHANGED
@@ -1,3 +1,7 @@
1
+ /** Primary Jotul brand red (CTAs, active list border, map clusters). */
2
+ export const JOTUL_BRAND_PRIMARY_HEX = '#ef2b18';
3
+ /** Slightly darker red for hover / cluster ring. */
4
+ export const JOTUL_BRAND_PRIMARY_DARK_HEX = '#d92817';
1
5
  /** 10px radius for widget chrome (matches design spec). */
2
6
  export const R10 = 'jwi-rounded-[10px]';
3
7
  export const FIND_DEALER_BUTTON_CLASS = `jwi-inline-flex jwi-w-full jwi-min-h-[56px] jwi-cursor-pointer jwi-items-center jwi-justify-center ${R10} jwi-border-0 jwi-bg-[#ef2b18] jwi-px-7 jwi-text-base jwi-font-medium jwi-text-white hover:jwi-bg-[#d92817] disabled:jwi-cursor-wait disabled:hover:jwi-bg-[#ef2b18]`;
@@ -17,6 +17,7 @@
17
17
  "find": "Najít",
18
18
  "finding": "Vyhledávání …",
19
19
  "inquirySentSuccess": "Váš požadavek byl odeslán.",
20
+ "inquiryThankYouTitle": "Dekujeme za vas pozadavek",
20
21
  "goBack": "Zpět",
21
22
  "sendInquiryTitle": "Poptávka",
22
23
  "sendInquiryTitleWithProduct": "Poptávka na {product}",
@@ -37,5 +38,8 @@
37
38
  "mapView": "Mapa",
38
39
  "closeMap": "Zavřít mapu",
39
40
  "openMap": "Otevřít mapu",
40
- "closeMapMobile": "Zavřít mapu"
41
+ "closeMapMobile": "Zavřít mapu",
42
+ "campaignEndsIn": "Campaign ends in {time}",
43
+ "campaignCountdownTime": "{days} days {hours} hours {minutes} minutes and {seconds} seconds",
44
+ "campaignEnded": "Campaign ended"
41
45
  }
@@ -17,6 +17,7 @@
17
17
  "find": "Suchen",
18
18
  "finding": "Suche …",
19
19
  "inquirySentSuccess": "Ihre Anfrage wurde gesendet.",
20
+ "inquiryThankYouTitle": "Vielen Dank fur Ihre Anfrage",
20
21
  "goBack": "Zurück",
21
22
  "sendInquiryTitle": "Anfrage",
22
23
  "sendInquiryTitleWithProduct": "Anfrage zu {product}",
@@ -37,5 +38,8 @@
37
38
  "mapView": "Karte",
38
39
  "closeMap": "Karte schließen",
39
40
  "openMap": "Karte öffnen",
40
- "closeMapMobile": "Karte schließen"
41
+ "closeMapMobile": "Karte schließen",
42
+ "campaignEndsIn": "Kampagne endet in {time}",
43
+ "campaignCountdownTime": "{days} Tage {hours} Stunden {minutes} Minuten und {seconds} Sekunden",
44
+ "campaignEnded": "Kampagne beendet"
41
45
  }
@@ -17,6 +17,7 @@
17
17
  "find": "Locate",
18
18
  "finding": "Searching …",
19
19
  "inquirySentSuccess": "Your request has been sent.",
20
+ "inquiryThankYouTitle": "Thank you for your inquiry",
20
21
  "goBack": "Back",
21
22
  "sendInquiryTitle": "Request",
22
23
  "sendInquiryTitleWithProduct": "Request regarding {product}",
@@ -37,5 +38,8 @@
37
38
  "mapView": "Map",
38
39
  "closeMap": "Close map",
39
40
  "openMap": "Open map",
40
- "closeMapMobile": "Close map"
41
+ "closeMapMobile": "Close map",
42
+ "campaignEndsIn": "Campaign ends in {time}",
43
+ "campaignCountdownTime": "{days} days {hours} hours {minutes} minutes and {seconds} seconds",
44
+ "campaignEnded": "Campaign ended"
41
45
  }
@@ -17,6 +17,7 @@
17
17
  "find": "Etsi",
18
18
  "finding": "Haetaan …",
19
19
  "inquirySentSuccess": "Pyyntösi on lähetetty.",
20
+ "inquiryThankYouTitle": "Kiitos yhteydenotostasi",
20
21
  "goBack": "Takaisin",
21
22
  "sendInquiryTitle": "Pyyntö",
22
23
  "sendInquiryTitleWithProduct": "Pyyntö tuotteesta {product}",
@@ -37,5 +38,8 @@
37
38
  "mapView": "Kartta",
38
39
  "closeMap": "Sulje kartta",
39
40
  "openMap": "Avaa kartta",
40
- "closeMapMobile": "Sulje kartta"
41
+ "closeMapMobile": "Sulje kartta",
42
+ "campaignEndsIn": "Campaign ends in {time}",
43
+ "campaignCountdownTime": "{days} days {hours} hours {minutes} minutes and {seconds} seconds",
44
+ "campaignEnded": "Campaign ended"
41
45
  }