@jotul/jotul-widgets 0.0.1

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/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # `@jotul/jotul-widgets`
2
+
3
+ React component package for Jotul widgets.
4
+
5
+ This package does **not** talk to `api.jotul.com` directly.
6
+ It calls a partner-local backend route, for example:
7
+
8
+ - browser -> `/api/jotul/widget`
9
+ - partner server route -> `@jotul/jotul-api`
10
+ - `@jotul/jotul-api` -> `https://api.jotul.com/api/v1/find-dealer`
11
+
12
+ ## Example
13
+
14
+ ```tsx
15
+ import { JotulWidget } from '@jotul/jotul-widgets'
16
+
17
+ export default function Page() {
18
+ return <JotulWidget type="productPage" />
19
+ }
20
+ ```
21
+
22
+ Custom route:
23
+
24
+ ```tsx
25
+ <JotulWidget
26
+ type="productPage"
27
+ endpoint="/api/custom/jotul/widget"
28
+ />
29
+ ```
30
+
31
+ Current behavior:
32
+
33
+ - invalid API key -> `Invalid API key`
34
+ - missing permission -> `Insufficient permissions`
35
+ - invalid widget type -> `Invalid widget type`
36
+ - `productPage` renders zipcode input + search button + dealer list
@@ -0,0 +1,35 @@
1
+ export type JotulWidgetType = 'productPage' | 'dealerFinder' | 'warrantyForm';
2
+ export type WidgetAuthClientResponse = {
3
+ apiVersion?: string;
4
+ ok: boolean;
5
+ authorized?: boolean;
6
+ permissions?: {
7
+ dealers?: boolean;
8
+ };
9
+ error?: string;
10
+ };
11
+ export type DealerSearchResponse = {
12
+ apiVersion?: string;
13
+ ok: boolean;
14
+ type?: 'postalCode';
15
+ origin?: {
16
+ postalCode?: string | null;
17
+ postalCodeNumber?: number | null;
18
+ market?: string;
19
+ };
20
+ total?: number;
21
+ dealers?: Array<Record<string, unknown>>;
22
+ error?: string;
23
+ };
24
+ export type CheckWidgetAuthorizationOptions = {
25
+ endpoint?: string;
26
+ fetcher?: typeof fetch;
27
+ };
28
+ export type JotulWidgetProps = {
29
+ type?: string;
30
+ endpoint?: string;
31
+ className?: string;
32
+ };
33
+ export declare function checkWidgetAuthorization(options?: CheckWidgetAuthorizationOptions): Promise<WidgetAuthClientResponse>;
34
+ export declare function searchDealersByPostalCode(postalCode: string, options?: CheckWidgetAuthorizationOptions): Promise<DealerSearchResponse>;
35
+ export declare function JotulWidget({ type, endpoint, className, }: JotulWidgetProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,131 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ const VALID_WIDGET_TYPES = [
5
+ 'productPage',
6
+ 'dealerFinder',
7
+ 'warrantyForm',
8
+ ];
9
+ export async function checkWidgetAuthorization(options) {
10
+ const endpoint = options?.endpoint ?? '/api/jotul/widget';
11
+ const fetcher = options?.fetcher ?? fetch;
12
+ let response;
13
+ try {
14
+ response = await fetcher(endpoint, {
15
+ method: 'GET',
16
+ headers: {
17
+ Accept: 'application/json',
18
+ },
19
+ cache: 'no-store',
20
+ });
21
+ }
22
+ catch (error) {
23
+ return {
24
+ ok: false,
25
+ error: error instanceof Error ? error.message : 'Failed to reach widget auth route',
26
+ };
27
+ }
28
+ try {
29
+ return (await response.json());
30
+ }
31
+ catch {
32
+ return {
33
+ ok: false,
34
+ error: 'Invalid API key',
35
+ };
36
+ }
37
+ }
38
+ export async function searchDealersByPostalCode(postalCode, options) {
39
+ const endpoint = options?.endpoint ?? '/api/jotul/widget';
40
+ const fetcher = options?.fetcher ?? fetch;
41
+ const params = new URLSearchParams({ postalCode: postalCode.trim() });
42
+ let response;
43
+ try {
44
+ response = await fetcher(`${endpoint}?${params.toString()}`, {
45
+ method: 'GET',
46
+ headers: {
47
+ Accept: 'application/json',
48
+ },
49
+ cache: 'no-store',
50
+ });
51
+ }
52
+ catch (error) {
53
+ return {
54
+ ok: false,
55
+ error: error instanceof Error ? error.message : 'Failed to reach widget route',
56
+ };
57
+ }
58
+ try {
59
+ return (await response.json());
60
+ }
61
+ catch {
62
+ return {
63
+ ok: false,
64
+ error: 'Widget route returned invalid JSON',
65
+ };
66
+ }
67
+ }
68
+ function isWidgetType(value) {
69
+ return value != null && VALID_WIDGET_TYPES.includes(value);
70
+ }
71
+ function renderReadyState(type) {
72
+ switch (type) {
73
+ case 'productPage':
74
+ return null;
75
+ case 'dealerFinder':
76
+ return 'JotulWidget ready: dealerFinder';
77
+ case 'warrantyForm':
78
+ return 'JotulWidget ready: warrantyForm';
79
+ }
80
+ }
81
+ export function JotulWidget({ type, endpoint = '/api/jotul/widget', className, }) {
82
+ const [auth, setAuth] = useState(null);
83
+ const [isLoading, setIsLoading] = useState(true);
84
+ const [postalCode, setPostalCode] = useState('');
85
+ const [searchResult, setSearchResult] = useState(null);
86
+ const [isSearching, setIsSearching] = useState(false);
87
+ useEffect(() => {
88
+ let cancelled = false;
89
+ async function run() {
90
+ setIsLoading(true);
91
+ const result = await checkWidgetAuthorization({ endpoint });
92
+ if (!cancelled) {
93
+ setAuth(result);
94
+ setIsLoading(false);
95
+ }
96
+ }
97
+ void run();
98
+ return () => {
99
+ cancelled = true;
100
+ };
101
+ }, [endpoint]);
102
+ const typeState = useMemo(() => {
103
+ if (type == null)
104
+ return 'typeMissing';
105
+ if (!isWidgetType(type))
106
+ return 'typeInvalid';
107
+ return 'typeReady';
108
+ }, [type]);
109
+ if (isLoading) {
110
+ return _jsx("div", { className: className, children: "Loading JotulWidget\u2026" });
111
+ }
112
+ if (!auth?.ok || !auth.authorized) {
113
+ return (_jsxs("div", { className: className, children: ["JotulWidget auth failed: ", auth?.error ?? 'Unknown authorization error'] }));
114
+ }
115
+ if (typeState === 'typeMissing') {
116
+ return _jsx("div", { className: className, children: "JotulWidget type missing" });
117
+ }
118
+ if (typeState === 'typeInvalid') {
119
+ return _jsx("div", { className: className, children: "Invalid widget type" });
120
+ }
121
+ if (type === 'productPage') {
122
+ const dealers = searchResult?.dealers ?? [];
123
+ return (_jsx("div", { className: className, children: _jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("input", { type: "text", value: postalCode, onChange: (event) => setPostalCode(event.target.value), placeholder: "Zipcode" }), _jsx("button", { type: "button", disabled: isSearching || postalCode.trim().length === 0, onClick: async () => {
124
+ setIsSearching(true);
125
+ const result = await searchDealersByPostalCode(postalCode, { endpoint });
126
+ setSearchResult(result);
127
+ setIsSearching(false);
128
+ }, children: isSearching ? 'Searching…' : 'Search' }), searchResult?.ok === false && (_jsx("div", { children: searchResult.error ?? 'Unknown widget error' })), searchResult?.ok && (_jsx("ul", { children: dealers.map((dealer, index) => (_jsx("li", { children: String(dealer.name ?? dealer.dealerId ?? 'Unknown dealer') }, String(dealer.dealerId ?? dealer.name ?? index)))) }))] }) }));
129
+ }
130
+ return _jsx("div", { className: className, children: renderReadyState(type) });
131
+ }
@@ -0,0 +1 @@
1
+ export { checkWidgetAuthorization, searchDealersByPostalCode, JotulWidget, type CheckWidgetAuthorizationOptions, type DealerSearchResponse, type JotulWidgetProps, type JotulWidgetType, type WidgetAuthClientResponse, } from './JotulWidget';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { checkWidgetAuthorization, searchDealersByPostalCode, JotulWidget, } from './JotulWidget';
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@jotul/jotul-widgets",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "license": "UNLICENSED",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.build.json",
21
+ "clean": "rm -rf dist",
22
+ "prepublishOnly": "npm run clean && npm run build"
23
+ },
24
+ "peerDependencies": {
25
+ "react": ">=18",
26
+ "react-dom": ">=18"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }