@jotul/jotul-widgets 1.2.6 → 2.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.
Files changed (60) hide show
  1. package/README.md +112 -28
  2. package/dist/JotulWidget.css +1 -1
  3. package/dist/JotulWidget.d.ts +1 -1
  4. package/dist/JotulWidget.js +356 -163
  5. package/dist/analytics/WidgetTrackingContext.d.ts +14 -0
  6. package/dist/analytics/WidgetTrackingContext.js +5 -0
  7. package/dist/analytics/gtm.d.ts +7 -0
  8. package/dist/analytics/gtm.js +17 -0
  9. package/dist/analytics/widgetTracking.d.ts +54 -0
  10. package/dist/analytics/widgetTracking.js +144 -0
  11. package/dist/api.d.ts +27 -1
  12. package/dist/api.js +74 -0
  13. package/dist/components/FindDealerDrawerWidget.d.ts +7 -4
  14. package/dist/components/FindDealerDrawerWidget.js +17 -14
  15. package/dist/components/InquiryField.d.ts +3 -1
  16. package/dist/components/InquiryField.js +19 -2
  17. package/dist/components/InquirySelectField.d.ts +13 -0
  18. package/dist/components/InquirySelectField.js +5 -0
  19. package/dist/components/ProductPageWidget.d.ts +7 -4
  20. package/dist/components/ProductPageWidget.js +12 -14
  21. package/dist/components/TurnstileField.d.ts +7 -0
  22. package/dist/components/TurnstileField.js +48 -0
  23. package/dist/components/WarrantyFormWidget.d.ts +12 -0
  24. package/dist/components/WarrantyFormWidget.js +98 -0
  25. package/dist/components/product-page/DealerList.d.ts +1 -1
  26. package/dist/components/product-page/DealerList.js +13 -5
  27. package/dist/components/product-page/InquiryForm.d.ts +6 -2
  28. package/dist/components/product-page/InquiryForm.js +21 -3
  29. package/dist/constants/turnstile.d.ts +8 -0
  30. package/dist/constants/turnstile.js +19 -0
  31. package/dist/hooks/useTurnstileSiteKey.d.ts +1 -0
  32. package/dist/hooks/useTurnstileSiteKey.js +38 -0
  33. package/dist/i18n/locales/cz.json +34 -1
  34. package/dist/i18n/locales/de.json +34 -1
  35. package/dist/i18n/locales/en.json +34 -1
  36. package/dist/i18n/locales/fi.json +34 -1
  37. package/dist/i18n/locales/fr.json +34 -1
  38. package/dist/i18n/locales/nl.json +34 -1
  39. package/dist/i18n/locales/no.json +34 -1
  40. package/dist/i18n/locales/pl.json +34 -1
  41. package/dist/i18n/locales/se.json +34 -1
  42. package/dist/i18n/widgetStrings.d.ts +33 -0
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.js +3 -0
  45. package/dist/turnstile.d.ts +18 -0
  46. package/dist/turnstile.js +31 -0
  47. package/dist/types.d.ts +56 -0
  48. package/dist/utils/inquiryCategories.d.ts +8 -0
  49. package/dist/utils/inquiryCategories.js +24 -0
  50. package/dist/utils/inquirySubmit.d.ts +24 -0
  51. package/dist/utils/inquirySubmit.js +35 -0
  52. package/dist/utils/urlDealerId.d.ts +2 -0
  53. package/dist/utils/urlDealerId.js +5 -0
  54. package/dist/utils/usMarket.d.ts +2 -0
  55. package/dist/utils/usMarket.js +9 -0
  56. package/dist/utils/warrantyForm.d.ts +38 -0
  57. package/dist/utils/warrantyForm.js +80 -0
  58. package/dist/utils.d.ts +10 -1
  59. package/dist/utils.js +46 -3
  60. package/package.json +5 -2
@@ -0,0 +1,38 @@
1
+ import type { WarrantyFormValues } from '../types';
2
+ export declare function createWarrantyFormValues(): WarrantyFormValues;
3
+ export type WarrantyValidationError = {
4
+ field: keyof WarrantyFormValues;
5
+ type: 'required';
6
+ } | {
7
+ field: 'email';
8
+ type: 'invalid_email';
9
+ } | {
10
+ field: 'consent';
11
+ type: 'required';
12
+ };
13
+ export declare function validateWarrantyFormValues(values: WarrantyFormValues, options?: {
14
+ market?: string;
15
+ locale?: string;
16
+ }): WarrantyValidationError | null;
17
+ export declare function warrantyFormValuesToApiPayload(values: WarrantyFormValues, options?: {
18
+ turnstileToken?: string | null;
19
+ market?: string;
20
+ domain?: string;
21
+ }): {
22
+ turnstileToken?: string | undefined;
23
+ domain?: string | undefined;
24
+ market?: string | undefined;
25
+ telephone: string;
26
+ email: string;
27
+ product: string;
28
+ dealer: string;
29
+ purchaseDate: string;
30
+ selfInstalled: boolean;
31
+ installerName: string | undefined;
32
+ source: "widget";
33
+ county?: string | undefined;
34
+ name: string;
35
+ address: string;
36
+ zipcode: string;
37
+ city: string;
38
+ };
@@ -0,0 +1,80 @@
1
+ import { isValidEmail } from '../utils';
2
+ import { isUsWarrantyMarket } from './usMarket';
3
+ export function createWarrantyFormValues() {
4
+ return {
5
+ firstName: '',
6
+ lastName: '',
7
+ address: '',
8
+ zipcode: '',
9
+ city: '',
10
+ county: '',
11
+ phone: '',
12
+ email: '',
13
+ productName: '',
14
+ purchaseDate: '',
15
+ purchasedFromDealer: '',
16
+ selfInstalled: '',
17
+ installerName: '',
18
+ consent: false,
19
+ };
20
+ }
21
+ export function validateWarrantyFormValues(values, options) {
22
+ const requiredFields = [
23
+ 'firstName',
24
+ 'lastName',
25
+ 'address',
26
+ 'zipcode',
27
+ 'city',
28
+ 'phone',
29
+ 'email',
30
+ 'productName',
31
+ 'purchaseDate',
32
+ 'purchasedFromDealer',
33
+ ];
34
+ if (isUsWarrantyMarket(options?.market, options?.locale)) {
35
+ requiredFields.push('county');
36
+ }
37
+ for (const field of requiredFields) {
38
+ const value = values[field];
39
+ if (typeof value !== 'string' || !value.trim()) {
40
+ return { field, type: 'required' };
41
+ }
42
+ }
43
+ if (!isValidEmail(values.email.trim())) {
44
+ return { field: 'email', type: 'invalid_email' };
45
+ }
46
+ if (values.selfInstalled !== 'yes' && values.selfInstalled !== 'no') {
47
+ return { field: 'selfInstalled', type: 'required' };
48
+ }
49
+ if (values.selfInstalled === 'no' && !values.installerName.trim()) {
50
+ return { field: 'installerName', type: 'required' };
51
+ }
52
+ if (!values.consent) {
53
+ return { field: 'consent', type: 'required' };
54
+ }
55
+ return null;
56
+ }
57
+ export function warrantyFormValuesToApiPayload(values, options) {
58
+ const market = options?.market?.trim().toUpperCase();
59
+ const county = values.county.trim();
60
+ return {
61
+ name: `${values.firstName.trim()} ${values.lastName.trim()}`.trim(),
62
+ address: values.address.trim(),
63
+ zipcode: values.zipcode.trim(),
64
+ city: values.city.trim(),
65
+ ...(county ? { county } : {}),
66
+ telephone: values.phone.trim(),
67
+ email: values.email.trim(),
68
+ product: values.productName.trim(),
69
+ dealer: values.purchasedFromDealer.trim(),
70
+ purchaseDate: values.purchaseDate.trim(),
71
+ selfInstalled: values.selfInstalled === 'yes',
72
+ installerName: values.selfInstalled === 'no' ? values.installerName.trim() : undefined,
73
+ source: 'widget',
74
+ ...(market ? { market } : {}),
75
+ ...(options?.domain?.trim() ? { domain: options.domain.trim() } : {}),
76
+ ...(options?.turnstileToken?.trim()
77
+ ? { turnstileToken: options.turnstileToken.trim() }
78
+ : {}),
79
+ };
80
+ }
package/dist/utils.d.ts CHANGED
@@ -5,12 +5,21 @@ export declare function isWidgetType(value: string | undefined): value is JotulW
5
5
  export declare function asText(value: unknown): string | null;
6
6
  export declare function formatDistance(dealer: DealerRecord): string | null;
7
7
  export declare function isExclusiveDealer(dealer: DealerRecord): boolean;
8
+ /** Normalize optional widget prop arrays: undefined or empty means no filter. */
9
+ export declare function normalizeWidgetFilterList(values: string[] | undefined, transform?: (value: string) => string): string[] | undefined;
10
+ export declare function getDealerId(dealer: DealerRecord): string | undefined;
8
11
  export declare function getDealerKey(dealer: DealerRecord, index: number): string;
9
12
  export declare function getDealerName(dealer: DealerRecord, unknownLabel: string): string;
13
+ export declare function getDealerEmail(dealer: DealerRecord): string | undefined;
14
+ export declare function getDealerSalesRep(dealer: DealerRecord): string | undefined;
10
15
  /** True when the tapped map pin matches a dealer already in the current successful search payload. */
11
16
  export declare function isDealerInSearchResult(dealerLabel: string, searchResult: DealerSearchResponse | null, unknownDealerLabel: string): boolean;
12
17
  export declare function getDealerAddressLines(dealer: DealerRecord): string[];
13
- export declare function createInquiryFormValues(productName: string | undefined, dealerName: string): InquiryFormValues;
18
+ export declare function createInquiryFormValues(productName: string | undefined, dealer: DealerRecord, unknownLabel: string, market?: string): InquiryFormValues;
19
+ export declare function validateInquiryFormValues(values: InquiryFormValues): {
20
+ field: 'name' | 'email' | 'phone';
21
+ type: 'required' | 'invalid_email';
22
+ } | null;
14
23
  export declare function isValidEmail(value: string): boolean;
15
24
  export declare function getSafeWidgetErrorMessage(error: unknown, t: WidgetStrings): string;
16
25
  export declare function renderReadyState(type: JotulWidgetType, t: WidgetStrings): string | null;
package/dist/utils.js CHANGED
@@ -36,12 +36,35 @@ export function isExclusiveDealer(dealer) {
36
36
  rawExclusive === 'y' ||
37
37
  rawExclusive === 'Y');
38
38
  }
39
+ /** Normalize optional widget prop arrays: undefined or empty means no filter. */
40
+ export function normalizeWidgetFilterList(values, transform) {
41
+ if (values == null || !Array.isArray(values))
42
+ return undefined;
43
+ const normalized = values
44
+ .map((value) => {
45
+ const trimmed = value?.trim();
46
+ if (!trimmed)
47
+ return '';
48
+ return transform ? transform(trimmed) : trimmed;
49
+ })
50
+ .filter((value) => value.length > 0);
51
+ return normalized.length > 0 ? normalized : undefined;
52
+ }
53
+ export function getDealerId(dealer) {
54
+ return asText(dealer.dealerId) ?? asText(dealer._id) ?? undefined;
55
+ }
39
56
  export function getDealerKey(dealer, index) {
40
57
  return String(dealer.dealerId ?? dealer.name ?? index);
41
58
  }
42
59
  export function getDealerName(dealer, unknownLabel) {
43
60
  return asText(dealer.name) ?? asText(dealer.dealerId) ?? unknownLabel;
44
61
  }
62
+ export function getDealerEmail(dealer) {
63
+ return asText(dealer.email) ?? undefined;
64
+ }
65
+ export function getDealerSalesRep(dealer) {
66
+ return asText(dealer.salesRep) ?? undefined;
67
+ }
45
68
  /** True when the tapped map pin matches a dealer already in the current successful search payload. */
46
69
  export function isDealerInSearchResult(dealerLabel, searchResult, unknownDealerLabel) {
47
70
  if (searchResult?.ok !== true || !Array.isArray(searchResult.dealers))
@@ -64,16 +87,36 @@ export function getDealerAddressLines(dealer) {
64
87
  .join(', ');
65
88
  return [address, locationLine || null].filter((value) => Boolean(value));
66
89
  }
67
- export function createInquiryFormValues(productName, dealerName) {
90
+ export function createInquiryFormValues(productName, dealer, unknownLabel, market) {
68
91
  return {
69
92
  productName: productName?.trim() ?? '',
70
- dealerName,
93
+ dealerName: getDealerName(dealer, unknownLabel),
94
+ dealerId: getDealerId(dealer),
95
+ dealerEmail: getDealerEmail(dealer),
96
+ salesRepresentative: getDealerSalesRep(dealer),
97
+ market: market?.trim().toUpperCase(),
98
+ requestCategory: 'price_quote',
71
99
  name: '',
72
100
  email: '',
73
101
  phone: '',
74
102
  comment: '',
75
103
  };
76
104
  }
105
+ export function validateInquiryFormValues(values) {
106
+ if (!values.name.trim()) {
107
+ return { field: 'name', type: 'required' };
108
+ }
109
+ if (!values.email.trim()) {
110
+ return { field: 'email', type: 'required' };
111
+ }
112
+ if (!isValidEmail(values.email.trim())) {
113
+ return { field: 'email', type: 'invalid_email' };
114
+ }
115
+ if (!values.phone.trim()) {
116
+ return { field: 'phone', type: 'required' };
117
+ }
118
+ return null;
119
+ }
77
120
  export function isValidEmail(value) {
78
121
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
79
122
  }
@@ -102,6 +145,6 @@ export function renderReadyState(type, t) {
102
145
  case 'dealerFinder':
103
146
  return t.readyDealerFinder;
104
147
  case 'warrantyForm':
105
- return t.readyWarrantyForm;
148
+ return null;
106
149
  }
107
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jotul/jotul-widgets",
3
- "version": "1.2.6",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "sideEffects": [
@@ -23,8 +23,10 @@
23
23
  "@types/leaflet": "^1.9.21",
24
24
  "@types/leaflet.markercluster": "^1.5.6",
25
25
  "autoprefixer": "^10.4.21",
26
+ "esbuild": "^0.25.12",
26
27
  "postcss": "^8.5.3",
27
- "tailwindcss": "^3.4.17"
28
+ "tailwindcss": "^3.4.17",
29
+ "xlsx": "^0.18.5"
28
30
  },
29
31
  "dependencies": {
30
32
  "leaflet": "^1.9.4",
@@ -35,6 +37,7 @@
35
37
  "build:css": "tailwindcss -i ./src/tw-entry.css -o ./dist/JotulWidget.css --minify",
36
38
  "build:assets": "mkdir -p ./dist/images ./dist/i18n/locales && cp -R ./src/images/. ./dist/images/ && cp -R ./src/i18n/locales/. ./dist/i18n/locales/",
37
39
  "build": "tsc -p tsconfig.build.json && npm run build:css && npm run build:assets",
40
+ "export-translations": "node scripts/exportWidgetStringsForTranslation.mjs",
38
41
  "clean": "rm -rf dist",
39
42
  "watch:css": "tailwindcss -i ./src/tw-entry.css -o ./dist/JotulWidget.css --watch",
40
43
  "watch:ts": "tsc -p tsconfig.build.json -w --preserveWatchOutput",