@zappie/foundations 0.1.1 → 0.1.3
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/CHANGELOG.md +17 -0
- package/README.md +31 -0
- package/dist/account/index.js +49 -0
- package/dist/account/index.js.map +7 -0
- package/dist/branch-map/geometry.js +40 -0
- package/dist/branch-map/geometry.js.map +7 -0
- package/dist/branch-map/index.js +89 -0
- package/dist/branch-map/index.js.map +7 -0
- package/dist/branch-map/loader.js +39 -0
- package/dist/branch-map/loader.js.map +7 -0
- package/dist/branch-map/runtime.js +90 -0
- package/dist/branch-map/runtime.js.map +7 -0
- package/dist/cart/index.js +168 -0
- package/dist/cart/index.js.map +7 -0
- package/dist/fulfilment/address-picker.js +168 -0
- package/dist/fulfilment/address-picker.js.map +7 -0
- package/dist/fulfilment/branch-picker.js +54 -0
- package/dist/fulfilment/branch-picker.js.map +7 -0
- package/dist/fulfilment/schedule.js +146 -0
- package/dist/fulfilment/schedule.js.map +7 -0
- package/dist/fulfilment/switch.js +54 -0
- package/dist/fulfilment/switch.js.map +7 -0
- package/dist/index.d.ts +24 -4
- package/dist/index.js +50 -2114
- package/dist/index.js.map +7 -1
- package/dist/internal/combobox.js +131 -0
- package/dist/internal/combobox.js.map +7 -0
- package/dist/internal/data-attrs.js +8 -0
- package/dist/internal/data-attrs.js.map +7 -0
- package/dist/internal/ids.js +10 -0
- package/dist/internal/ids.js.map +7 -0
- package/dist/{chunk-D3J7S33L.js → internal/slot.js} +13 -6
- package/dist/internal/slot.js.map +7 -0
- package/dist/internal/use-controllable-state.js +29 -0
- package/dist/internal/use-controllable-state.js.map +7 -0
- package/dist/item-form/index.js +13 -0
- package/dist/item-form/index.js.map +7 -0
- package/dist/item-form/option-group.js +268 -0
- package/dist/item-form/option-group.js.map +7 -0
- package/dist/item-form/options.js +30 -0
- package/dist/item-form/options.js.map +7 -0
- package/dist/item-form/quantity.js +131 -0
- package/dist/item-form/quantity.js.map +7 -0
- package/dist/item-form/root.js +151 -0
- package/dist/item-form/root.js.map +7 -0
- package/dist/media/image.js +30 -0
- package/dist/media/image.js.map +7 -0
- package/dist/money.js +20 -0
- package/dist/money.js.map +7 -0
- package/dist/next.js +8 -6
- package/dist/next.js.map +7 -1
- package/dist/otp/input.js +110 -0
- package/dist/otp/input.js.map +7 -0
- package/dist/overlay/engine.js +16 -8
- package/dist/overlay/engine.js.map +7 -1
- package/dist/{chunk-BY2WQGLO.js → overlay/history.js} +9 -4
- package/dist/overlay/history.js.map +7 -0
- package/dist/overlay/react.js +261 -0
- package/dist/overlay/react.js.map +7 -0
- package/dist/payment/methods.js +103 -0
- package/dist/payment/methods.js.map +7 -0
- package/dist/rich-text/banners.js +9 -0
- package/dist/rich-text/banners.js.map +7 -0
- package/dist/rich-text/browser.js +3 -3
- package/dist/rich-text/browser.js.map +7 -1
- package/dist/rich-text/core.js +75 -0
- package/dist/rich-text/core.js.map +7 -0
- package/dist/rich-text/index.js +3 -79
- package/dist/rich-text/index.js.map +7 -1
- package/dist/search/engine.js +93 -0
- package/dist/search/engine.js.map +7 -0
- package/dist/search/index.js +175 -0
- package/dist/search/index.js.map +7 -0
- package/dist/status/hours.js +177 -0
- package/dist/status/hours.js.map +7 -0
- package/dist/status/open-status.js +45 -0
- package/dist/status/open-status.js.map +7 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +7 -0
- package/package.json +3 -3
- package/dist/chunk-BY2WQGLO.js.map +0 -1
- package/dist/chunk-D3J7S33L.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @zappie/foundations
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4bfb1b2: Add BranchMap with lazy Maps loading, themeable pins, controlled branch selection, shopper location and a fallback for branches without coordinates.
|
|
8
|
+
- Updated dependencies [4bfb1b2]
|
|
9
|
+
- Updated dependencies [666ba1c]
|
|
10
|
+
- @zappie/sdk@0.1.7
|
|
11
|
+
|
|
12
|
+
## 0.1.2
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- d3c5f98: Keep each runtime module separate so your app can split package code across routes. Check and verify accept compatible package updates without pinning you to one patch version.
|
|
17
|
+
- Updated dependencies [d3c5f98]
|
|
18
|
+
- @zappie/sdk@0.1.2
|
|
19
|
+
|
|
3
20
|
## 0.1.1
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -49,3 +49,34 @@ Foundations is at `0.x`. Its API can change in a minor release while it settles.
|
|
|
49
49
|
## License
|
|
50
50
|
|
|
51
51
|
MIT
|
|
52
|
+
|
|
53
|
+
## Branch map
|
|
54
|
+
|
|
55
|
+
`BranchMap` accepts SDK `Branch[]` data, `selectedBranchId`, `onSelect(branch)`,
|
|
56
|
+
`browserKey`, and optional `shopperLocation: { lat, lng }`. It also accepts
|
|
57
|
+
standard div attributes such as `className` and `style`.
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { BranchMap } from "@zappie/foundations";
|
|
61
|
+
|
|
62
|
+
<BranchMap
|
|
63
|
+
branches={branches}
|
|
64
|
+
selectedBranchId={selectedBranchId}
|
|
65
|
+
onSelect={(branch) => setSelectedBranchId(branch.id)}
|
|
66
|
+
browserKey={integrations.googleMaps?.browserKey}
|
|
67
|
+
shopperLocation={shopperLocation}
|
|
68
|
+
className="branch-map"
|
|
69
|
+
/>;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Set a height on `.branch-map`. Theme pins with `--branch-map-pin`,
|
|
73
|
+
`--branch-map-pin-selected`, `--branch-map-pin-border`, and
|
|
74
|
+
`--branch-map-pin-size`. Pins expose `data-selected` and `aria-pressed`.
|
|
75
|
+
The map fits branches with valid coordinates, including across the date line.
|
|
76
|
+
Branches without coordinates remain selectable with “Map location unavailable”.
|
|
77
|
+
|
|
78
|
+
The component renders nothing without a key. It loads its map runtime and the
|
|
79
|
+
Maps JavaScript API after mount, sharing one script across mounted maps.
|
|
80
|
+
Use one browser key per page. Loading failures show “Map location unavailable”.
|
|
81
|
+
The optional location button pans to coordinates supplied by your app. Your app
|
|
82
|
+
requests geolocation permission and supplies those coordinates.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/account/index.tsx
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { part } from "../internal/slot.js";
|
|
4
|
+
var AccountNotConfiguredError = class extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("Account authentication requires SDK auth (Open Storefronts PRD 13)");
|
|
7
|
+
this.code = "not_configured";
|
|
8
|
+
this.name = "AccountNotConfiguredError";
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
async function signIn(_options) {
|
|
12
|
+
throw new AccountNotConfiguredError();
|
|
13
|
+
}
|
|
14
|
+
async function signOut(_options) {
|
|
15
|
+
throw new AccountNotConfiguredError();
|
|
16
|
+
}
|
|
17
|
+
var SignIn = forwardRef(function SignIn2(props, ref) {
|
|
18
|
+
return part(
|
|
19
|
+
"div",
|
|
20
|
+
props,
|
|
21
|
+
{ signIn, status: "not_configured" },
|
|
22
|
+
{ "data-state": "invalid" },
|
|
23
|
+
ref
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
var SignOut = forwardRef(function SignOut2(props, ref) {
|
|
27
|
+
return part(
|
|
28
|
+
"div",
|
|
29
|
+
props,
|
|
30
|
+
{ signOut, status: "not_configured" },
|
|
31
|
+
{ "data-state": "invalid" },
|
|
32
|
+
ref
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
var CustomerPart = forwardRef(function Customer(props, ref) {
|
|
36
|
+
return part(
|
|
37
|
+
"div",
|
|
38
|
+
props,
|
|
39
|
+
{ customer: null, status: "not_configured" },
|
|
40
|
+
{ "data-state": "invalid" },
|
|
41
|
+
ref
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
var Account = { SignIn, SignOut, Customer: CustomerPart };
|
|
45
|
+
export {
|
|
46
|
+
Account,
|
|
47
|
+
AccountNotConfiguredError
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/account/index.tsx"],
|
|
4
|
+
"sourcesContent": ["import { forwardRef } from \"react\";\nimport { part, type PartProps } from \"../internal/slot\";\n/** Account stubs that do not read credentials or host authentication. */\nexport type Customer = { id: string; givenName?: string; phoneMasked: string };\n/**\n * Sign-in options reserved by the account contract.\n * The current Account sign-in stub rejects with AccountNotConfiguredError.\n */\nexport type SignInOptions = {\n returnTo?: string;\n prompt?: \"none\" | \"login\";\n display?: \"popup\" | \"redirect\" | \"auto\";\n scopes?: (\n | \"openid\"\n | \"phone\"\n | \"profile\"\n | \"addresses\"\n | \"orders:read\"\n | \"offline_access\"\n | \"cart\"\n )[];\n};\n/**\n * An error from Account sign-in and sign-out when authentication is not configured.\n */\nexport class AccountNotConfiguredError extends Error {\n readonly code = \"not_configured\";\n constructor() {\n super(\"Account authentication requires SDK auth (Open Storefronts PRD 13)\");\n this.name = \"AccountNotConfiguredError\";\n }\n}\nasync function signIn(_options?: SignInOptions): Promise<void> {\n throw new AccountNotConfiguredError();\n}\nasync function signOut(_options?: { everywhere?: boolean }): Promise<void> {\n throw new AccountNotConfiguredError();\n}\nconst SignIn = forwardRef<\n HTMLElement,\n PartProps<{ signIn: typeof signIn; status: \"not_configured\" }>\n>(function SignIn(props, ref) {\n return part(\n \"div\",\n props,\n { signIn, status: \"not_configured\" },\n { \"data-state\": \"invalid\" },\n ref,\n );\n});\nconst SignOut = forwardRef<\n HTMLElement,\n PartProps<{ signOut: typeof signOut; status: \"not_configured\" }>\n>(function SignOut(props, ref) {\n return part(\n \"div\",\n props,\n { signOut, status: \"not_configured\" },\n { \"data-state\": \"invalid\" },\n ref,\n );\n});\nconst CustomerPart = forwardRef<\n HTMLElement,\n PartProps<{ customer: Customer | null; status: \"not_configured\" }>\n>(function Customer(props, ref) {\n return part(\n \"div\",\n props,\n { customer: null, status: \"not_configured\" },\n { \"data-state\": \"invalid\" },\n ref,\n );\n});\n/**\n * Account UI stubs that expose a not_configured status and a null customer.\n * SignIn and SignOut reject with AccountNotConfiguredError. They do not authenticate.\n */\nexport const Account = { SignIn, SignOut, Customer: CustomerPart };\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAS,kBAAkB;AAC3B,SAAS,YAA4B;AAwB9B,IAAM,4BAAN,cAAwC,MAAM;AAAA,EAEnD,cAAc;AACZ,UAAM,oEAAoE;AAF5E,SAAS,OAAO;AAGd,SAAK,OAAO;AAAA,EACd;AACF;AACA,eAAe,OAAO,UAAyC;AAC7D,QAAM,IAAI,0BAA0B;AACtC;AACA,eAAe,QAAQ,UAAoD;AACzE,QAAM,IAAI,0BAA0B;AACtC;AACA,IAAM,SAAS,WAGb,SAASA,QAAO,OAAO,KAAK;AAC5B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,EAAE,QAAQ,QAAQ,iBAAiB;AAAA,IACnC,EAAE,cAAc,UAAU;AAAA,IAC1B;AAAA,EACF;AACF,CAAC;AACD,IAAM,UAAU,WAGd,SAASC,SAAQ,OAAO,KAAK;AAC7B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,EAAE,SAAS,QAAQ,iBAAiB;AAAA,IACpC,EAAE,cAAc,UAAU;AAAA,IAC1B;AAAA,EACF;AACF,CAAC;AACD,IAAM,eAAe,WAGnB,SAAS,SAAS,OAAO,KAAK;AAC9B,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,EAAE,UAAU,MAAM,QAAQ,iBAAiB;AAAA,IAC3C,EAAE,cAAc,UAAU;AAAA,IAC1B;AAAA,EACF;AACF,CAAC;AAKM,IAAM,UAAU,EAAE,QAAQ,SAAS,UAAU,aAAa;",
|
|
6
|
+
"names": ["SignIn", "SignOut"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/branch-map/geometry.ts
|
|
2
|
+
function validCoordinates(value) {
|
|
3
|
+
return !!value && Number.isFinite(value.lat) && Number.isFinite(value.lng) && Math.abs(value.lat) <= 90 && Math.abs(value.lng) <= 180;
|
|
4
|
+
}
|
|
5
|
+
function visibleBranches(branches) {
|
|
6
|
+
return branches.filter(
|
|
7
|
+
(branch) => validCoordinates(branch.coordinates)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
function branchBounds(branches) {
|
|
11
|
+
const points = visibleBranches(branches).map((branch) => branch.coordinates);
|
|
12
|
+
if (!points.length) {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
const longitudes = points.map((point) => (point.lng + 360) % 360).sort((a, b) => a - b);
|
|
16
|
+
let gap = -1;
|
|
17
|
+
let start = 0;
|
|
18
|
+
for (let index = 0; index < longitudes.length; index++) {
|
|
19
|
+
const next = longitudes[(index + 1) % longitudes.length] + (index === longitudes.length - 1 ? 360 : 0);
|
|
20
|
+
if (next - longitudes[index] > gap) {
|
|
21
|
+
gap = next - longitudes[index];
|
|
22
|
+
start = (index + 1) % longitudes.length;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const normalize = (lng) => lng > 180 ? lng - 360 : lng;
|
|
26
|
+
return {
|
|
27
|
+
north: Math.max(...points.map((point) => point.lat)),
|
|
28
|
+
south: Math.min(...points.map((point) => point.lat)),
|
|
29
|
+
west: normalize(longitudes[start]),
|
|
30
|
+
east: normalize(
|
|
31
|
+
longitudes[(start + longitudes.length - 1) % longitudes.length]
|
|
32
|
+
)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export {
|
|
36
|
+
branchBounds,
|
|
37
|
+
validCoordinates,
|
|
38
|
+
visibleBranches
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=geometry.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/geometry.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Branch } from \"@zappie/sdk\";\n\nexport type Coordinates = { lat: number; lng: number };\nexport function validCoordinates(value?: Coordinates): value is Coordinates {\n return (\n !!value &&\n Number.isFinite(value.lat) &&\n Number.isFinite(value.lng) &&\n Math.abs(value.lat) <= 90 &&\n Math.abs(value.lng) <= 180\n );\n}\nexport function visibleBranches(branches: Branch[]) {\n return branches.filter(\n (branch): branch is Branch & { coordinates: Coordinates } =>\n validCoordinates(branch.coordinates),\n );\n}\nexport function branchBounds(branches: Branch[]) {\n const points = visibleBranches(branches).map((branch) => branch.coordinates);\n if (!points.length) {\n return undefined;\n }\n // The largest longitude gap lies outside the bounds, including across the date line.\n const longitudes = points\n .map((point) => (point.lng + 360) % 360)\n .sort((a, b) => a - b);\n let gap = -1;\n let start = 0;\n for (let index = 0; index < longitudes.length; index++) {\n const next =\n longitudes[(index + 1) % longitudes.length]! +\n (index === longitudes.length - 1 ? 360 : 0);\n if (next - longitudes[index]! > gap) {\n gap = next - longitudes[index]!;\n start = (index + 1) % longitudes.length;\n }\n }\n const normalize = (lng: number) => (lng > 180 ? lng - 360 : lng);\n return {\n north: Math.max(...points.map((point) => point.lat)),\n south: Math.min(...points.map((point) => point.lat)),\n west: normalize(longitudes[start]!),\n east: normalize(\n longitudes[(start + longitudes.length - 1) % longitudes.length]!,\n ),\n };\n}\n"],
|
|
5
|
+
"mappings": ";AAGO,SAAS,iBAAiB,OAA2C;AAC1E,SACE,CAAC,CAAC,SACF,OAAO,SAAS,MAAM,GAAG,KACzB,OAAO,SAAS,MAAM,GAAG,KACzB,KAAK,IAAI,MAAM,GAAG,KAAK,MACvB,KAAK,IAAI,MAAM,GAAG,KAAK;AAE3B;AACO,SAAS,gBAAgB,UAAoB;AAClD,SAAO,SAAS;AAAA,IACd,CAAC,WACC,iBAAiB,OAAO,WAAW;AAAA,EACvC;AACF;AACO,SAAS,aAAa,UAAoB;AAC/C,QAAM,SAAS,gBAAgB,QAAQ,EAAE,IAAI,CAAC,WAAW,OAAO,WAAW;AAC3E,MAAI,CAAC,OAAO,QAAQ;AAClB,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,OAChB,IAAI,CAAC,WAAW,MAAM,MAAM,OAAO,GAAG,EACtC,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AACvB,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,WAAS,QAAQ,GAAG,QAAQ,WAAW,QAAQ,SAAS;AACtD,UAAM,OACJ,YAAY,QAAQ,KAAK,WAAW,MAAM,KACzC,UAAU,WAAW,SAAS,IAAI,MAAM;AAC3C,QAAI,OAAO,WAAW,KAAK,IAAK,KAAK;AACnC,YAAM,OAAO,WAAW,KAAK;AAC7B,eAAS,QAAQ,KAAK,WAAW;AAAA,IACnC;AAAA,EACF;AACA,QAAM,YAAY,CAAC,QAAiB,MAAM,MAAM,MAAM,MAAM;AAC5D,SAAO;AAAA,IACL,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,UAAU,MAAM,GAAG,CAAC;AAAA,IACnD,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,UAAU,MAAM,GAAG,CAAC;AAAA,IACnD,MAAM,UAAU,WAAW,KAAK,CAAE;AAAA,IAClC,MAAM;AAAA,MACJ,YAAY,QAAQ,WAAW,SAAS,KAAK,WAAW,MAAM;AAAA,IAChE;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/branch-map/index.tsx
|
|
4
|
+
import { useEffect, useRef, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
validCoordinates,
|
|
7
|
+
visibleBranches
|
|
8
|
+
} from "./geometry.js";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
function BranchMap({
|
|
11
|
+
branches,
|
|
12
|
+
selectedBranchId,
|
|
13
|
+
onSelect,
|
|
14
|
+
shopperLocation,
|
|
15
|
+
browserKey,
|
|
16
|
+
...props
|
|
17
|
+
}) {
|
|
18
|
+
const element = useRef(null);
|
|
19
|
+
const [runtime, setRuntime] = useState();
|
|
20
|
+
const [failed, setFailed] = useState(false);
|
|
21
|
+
const hasCoordinates = visibleBranches(branches).length > 0;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!browserKey || !element.current || !hasCoordinates) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let disposed = false;
|
|
27
|
+
const controller = new AbortController();
|
|
28
|
+
let instance;
|
|
29
|
+
const target = element.current;
|
|
30
|
+
setFailed(false);
|
|
31
|
+
void import("./runtime.js").then(({ createMap }) => createMap(target, browserKey, controller.signal)).then((map) => {
|
|
32
|
+
instance = map;
|
|
33
|
+
if (disposed) {
|
|
34
|
+
map.destroy();
|
|
35
|
+
} else {
|
|
36
|
+
setRuntime(map);
|
|
37
|
+
}
|
|
38
|
+
}).catch(() => {
|
|
39
|
+
if (!disposed) {
|
|
40
|
+
setFailed(true);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return () => {
|
|
44
|
+
disposed = true;
|
|
45
|
+
controller.abort();
|
|
46
|
+
instance?.destroy();
|
|
47
|
+
setRuntime(void 0);
|
|
48
|
+
};
|
|
49
|
+
}, [browserKey, hasCoordinates]);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
runtime?.update(branches, selectedBranchId, onSelect, shopperLocation);
|
|
52
|
+
}, [runtime, branches, selectedBranchId, onSelect, shopperLocation]);
|
|
53
|
+
if (!browserKey) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const unavailable = branches.filter(
|
|
57
|
+
(branch) => !validCoordinates(branch.coordinates)
|
|
58
|
+
);
|
|
59
|
+
return /* @__PURE__ */ jsxs("div", { ...props, "data-branch-map": "", children: [
|
|
60
|
+
/* @__PURE__ */ jsx(
|
|
61
|
+
"div",
|
|
62
|
+
{
|
|
63
|
+
hidden: failed || !hasCoordinates,
|
|
64
|
+
ref: element,
|
|
65
|
+
"data-branch-map-canvas": "",
|
|
66
|
+
style: { height: "100%" }
|
|
67
|
+
}
|
|
68
|
+
),
|
|
69
|
+
failed || !hasCoordinates ? /* @__PURE__ */ jsx("p", { role: "status", children: "Map location unavailable" }) : null,
|
|
70
|
+
unavailable.map((branch) => /* @__PURE__ */ jsxs(
|
|
71
|
+
"button",
|
|
72
|
+
{
|
|
73
|
+
type: "button",
|
|
74
|
+
"aria-pressed": branch.id === selectedBranchId,
|
|
75
|
+
onClick: () => onSelect(branch),
|
|
76
|
+
children: [
|
|
77
|
+
branch.name,
|
|
78
|
+
": Map location unavailable"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
branch.id
|
|
82
|
+
)),
|
|
83
|
+
runtime && validCoordinates(shopperLocation) ? /* @__PURE__ */ jsx("button", { type: "button", onClick: () => runtime.locate(shopperLocation), children: "Use my current location" }) : null
|
|
84
|
+
] });
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
BranchMap
|
|
88
|
+
};
|
|
89
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/index.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\nimport type { Branch } from \"@zappie/sdk\";\nimport { useEffect, useRef, useState, type HTMLAttributes } from \"react\";\nimport {\n validCoordinates,\n visibleBranches,\n type Coordinates,\n} from \"./geometry\";\nimport type { createMap } from \"./runtime\";\n\n/** Branches, selection and optional shopper coordinates for BranchMap. */\nexport type BranchMapProps = Omit<\n HTMLAttributes<HTMLDivElement>,\n \"onSelect\"\n> & {\n branches: Branch[];\n selectedBranchId?: string;\n onSelect: (branch: Branch) => void;\n shopperLocation?: Coordinates;\n browserKey?: string;\n};\n\n/**\n * Show branch locations. Maps loads after mount when you supply a browser key.\n * Set the container height with CSS. Style pins with --branch-map-pin,\n * --branch-map-pin-selected, --branch-map-pin-border and --branch-map-pin-size.\n */\nexport function BranchMap({\n branches,\n selectedBranchId,\n onSelect,\n shopperLocation,\n browserKey,\n ...props\n}: BranchMapProps) {\n const element = useRef<HTMLDivElement>(null);\n const [runtime, setRuntime] =\n useState<Awaited<ReturnType<typeof createMap>>>();\n const [failed, setFailed] = useState(false);\n const hasCoordinates = visibleBranches(branches).length > 0;\n useEffect(() => {\n if (!browserKey || !element.current || !hasCoordinates) {\n return;\n }\n let disposed = false;\n const controller = new AbortController();\n let instance: Awaited<ReturnType<typeof createMap>> | undefined;\n const target = element.current;\n setFailed(false);\n void import(\"./runtime\")\n .then(({ createMap }) => createMap(target, browserKey, controller.signal))\n .then((map) => {\n instance = map;\n if (disposed) {\n map.destroy();\n } else {\n setRuntime(map);\n }\n })\n .catch(() => {\n if (!disposed) {\n setFailed(true);\n }\n });\n return () => {\n disposed = true;\n controller.abort();\n instance?.destroy();\n setRuntime(undefined);\n };\n }, [browserKey, hasCoordinates]);\n useEffect(() => {\n runtime?.update(branches, selectedBranchId, onSelect, shopperLocation);\n }, [runtime, branches, selectedBranchId, onSelect, shopperLocation]);\n if (!browserKey) {\n return null;\n }\n const unavailable = branches.filter(\n (branch) => !validCoordinates(branch.coordinates),\n );\n return (\n <div {...props} data-branch-map=\"\">\n <div\n hidden={failed || !hasCoordinates}\n ref={element}\n data-branch-map-canvas=\"\"\n style={{ height: \"100%\" }}\n />\n {failed || !hasCoordinates ? (\n <p role=\"status\">Map location unavailable</p>\n ) : null}\n {unavailable.map((branch) => (\n <button\n key={branch.id}\n type=\"button\"\n aria-pressed={branch.id === selectedBranchId}\n onClick={() => onSelect(branch)}\n >\n {branch.name}: Map location unavailable\n </button>\n ))}\n {runtime && validCoordinates(shopperLocation) ? (\n <button type=\"button\" onClick={() => runtime.locate(shopperLocation)}>\n Use my current location\n </button>\n ) : null}\n </div>\n );\n}\n"],
|
|
5
|
+
"mappings": ";;;AAEA,SAAS,WAAW,QAAQ,gBAAqC;AACjE;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AA2ED,cAUE,YAVF;AAvDC,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAmB;AACjB,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,CAAC,SAAS,UAAU,IACxB,SAAgD;AAClD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,KAAK;AAC1C,QAAM,iBAAiB,gBAAgB,QAAQ,EAAE,SAAS;AAC1D,YAAU,MAAM;AACd,QAAI,CAAC,cAAc,CAAC,QAAQ,WAAW,CAAC,gBAAgB;AACtD;AAAA,IACF;AACA,QAAI,WAAW;AACf,UAAM,aAAa,IAAI,gBAAgB;AACvC,QAAI;AACJ,UAAM,SAAS,QAAQ;AACvB,cAAU,KAAK;AACf,SAAK,OAAO,cAAW,EACpB,KAAK,CAAC,EAAE,UAAU,MAAM,UAAU,QAAQ,YAAY,WAAW,MAAM,CAAC,EACxE,KAAK,CAAC,QAAQ;AACb,iBAAW;AACX,UAAI,UAAU;AACZ,YAAI,QAAQ;AAAA,MACd,OAAO;AACL,mBAAW,GAAG;AAAA,MAChB;AAAA,IACF,CAAC,EACA,MAAM,MAAM;AACX,UAAI,CAAC,UAAU;AACb,kBAAU,IAAI;AAAA,MAChB;AAAA,IACF,CAAC;AACH,WAAO,MAAM;AACX,iBAAW;AACX,iBAAW,MAAM;AACjB,gBAAU,QAAQ;AAClB,iBAAW,MAAS;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,YAAY,cAAc,CAAC;AAC/B,YAAU,MAAM;AACd,aAAS,OAAO,UAAU,kBAAkB,UAAU,eAAe;AAAA,EACvE,GAAG,CAAC,SAAS,UAAU,kBAAkB,UAAU,eAAe,CAAC;AACnE,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AACA,QAAM,cAAc,SAAS;AAAA,IAC3B,CAAC,WAAW,CAAC,iBAAiB,OAAO,WAAW;AAAA,EAClD;AACA,SACE,qBAAC,SAAK,GAAG,OAAO,mBAAgB,IAC9B;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,UAAU,CAAC;AAAA,QACnB,KAAK;AAAA,QACL,0BAAuB;AAAA,QACvB,OAAO,EAAE,QAAQ,OAAO;AAAA;AAAA,IAC1B;AAAA,IACC,UAAU,CAAC,iBACV,oBAAC,OAAE,MAAK,UAAS,sCAAwB,IACvC;AAAA,IACH,YAAY,IAAI,CAAC,WAChB;AAAA,MAAC;AAAA;AAAA,QAEC,MAAK;AAAA,QACL,gBAAc,OAAO,OAAO;AAAA,QAC5B,SAAS,MAAM,SAAS,MAAM;AAAA,QAE7B;AAAA,iBAAO;AAAA,UAAK;AAAA;AAAA;AAAA,MALR,OAAO;AAAA,IAMd,CACD;AAAA,IACA,WAAW,iBAAiB,eAAe,IAC1C,oBAAC,YAAO,MAAK,UAAS,SAAS,MAAM,QAAQ,OAAO,eAAe,GAAG,qCAEtE,IACE;AAAA,KACN;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/branch-map/loader.ts
|
|
2
|
+
var pending;
|
|
3
|
+
function loadMaps(key) {
|
|
4
|
+
if (globalThis.google?.maps?.Map) {
|
|
5
|
+
return Promise.resolve(google.maps);
|
|
6
|
+
}
|
|
7
|
+
if (pending) {
|
|
8
|
+
return pending;
|
|
9
|
+
}
|
|
10
|
+
pending = new Promise((resolve, reject) => {
|
|
11
|
+
const script = document.createElement("script");
|
|
12
|
+
const callback = `__zappieMaps${Date.now()}`;
|
|
13
|
+
const scope = globalThis;
|
|
14
|
+
const cleanup = () => {
|
|
15
|
+
clearTimeout(timeout);
|
|
16
|
+
delete scope[callback];
|
|
17
|
+
};
|
|
18
|
+
const fail = () => {
|
|
19
|
+
cleanup();
|
|
20
|
+
script.remove();
|
|
21
|
+
pending = void 0;
|
|
22
|
+
reject(new Error("Map location unavailable"));
|
|
23
|
+
};
|
|
24
|
+
const timeout = setTimeout(fail, 15e3);
|
|
25
|
+
scope[callback] = () => {
|
|
26
|
+
cleanup();
|
|
27
|
+
resolve(google.maps);
|
|
28
|
+
};
|
|
29
|
+
script.async = true;
|
|
30
|
+
script.src = `https://maps.googleapis.com/maps/api/js?${new URLSearchParams({ key, loading: "async", callback, v: "weekly" })}`;
|
|
31
|
+
script.onerror = fail;
|
|
32
|
+
document.head.appendChild(script);
|
|
33
|
+
});
|
|
34
|
+
return pending;
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
loadMaps
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/loader.ts"],
|
|
4
|
+
"sourcesContent": ["/// <reference types=\"google.maps\" />\nlet pending: Promise<typeof google.maps> | undefined;\n\nexport function loadMaps(key: string): Promise<typeof google.maps> {\n if (globalThis.google?.maps?.Map) {\n return Promise.resolve(google.maps);\n }\n if (pending) {\n return pending;\n }\n pending = new Promise((resolve, reject) => {\n const script = document.createElement(\"script\");\n const callback = `__zappieMaps${Date.now()}`;\n const scope = globalThis as unknown as Record<string, unknown>;\n const cleanup = () => {\n clearTimeout(timeout);\n delete scope[callback];\n };\n const fail = () => {\n cleanup();\n script.remove();\n pending = undefined;\n reject(new Error(\"Map location unavailable\"));\n };\n const timeout = setTimeout(fail, 15000);\n scope[callback] = () => {\n cleanup();\n resolve(google.maps);\n };\n script.async = true;\n script.src = `https://maps.googleapis.com/maps/api/js?${new URLSearchParams({ key, loading: \"async\", callback, v: \"weekly\" })}`;\n script.onerror = fail;\n document.head.appendChild(script);\n });\n return pending;\n}\n"],
|
|
5
|
+
"mappings": ";AACA,IAAI;AAEG,SAAS,SAAS,KAA0C;AACjE,MAAI,WAAW,QAAQ,MAAM,KAAK;AAChC,WAAO,QAAQ,QAAQ,OAAO,IAAI;AAAA,EACpC;AACA,MAAI,SAAS;AACX,WAAO;AAAA,EACT;AACA,YAAU,IAAI,QAAQ,CAAC,SAAS,WAAW;AACzC,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,UAAM,WAAW,eAAe,KAAK,IAAI,CAAC;AAC1C,UAAM,QAAQ;AACd,UAAM,UAAU,MAAM;AACpB,mBAAa,OAAO;AACpB,aAAO,MAAM,QAAQ;AAAA,IACvB;AACA,UAAM,OAAO,MAAM;AACjB,cAAQ;AACR,aAAO,OAAO;AACd,gBAAU;AACV,aAAO,IAAI,MAAM,0BAA0B,CAAC;AAAA,IAC9C;AACA,UAAM,UAAU,WAAW,MAAM,IAAK;AACtC,UAAM,QAAQ,IAAI,MAAM;AACtB,cAAQ;AACR,cAAQ,OAAO,IAAI;AAAA,IACrB;AACA,WAAO,QAAQ;AACf,WAAO,MAAM,2CAA2C,IAAI,gBAAgB,EAAE,KAAK,SAAS,SAAS,UAAU,GAAG,SAAS,CAAC,CAAC;AAC7H,WAAO,UAAU;AACjB,aAAS,KAAK,YAAY,MAAM;AAAA,EAClC,CAAC;AACD,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/branch-map/runtime.ts
|
|
2
|
+
import {
|
|
3
|
+
branchBounds,
|
|
4
|
+
validCoordinates,
|
|
5
|
+
visibleBranches
|
|
6
|
+
} from "./geometry.js";
|
|
7
|
+
import { loadMaps } from "./loader.js";
|
|
8
|
+
async function createMap(element, key, signal) {
|
|
9
|
+
const maps = await loadMaps(key);
|
|
10
|
+
signal.throwIfAborted();
|
|
11
|
+
const map = new maps.Map(element, {
|
|
12
|
+
center: { lat: 0, lng: 0 },
|
|
13
|
+
zoom: 12,
|
|
14
|
+
maxZoom: 18,
|
|
15
|
+
mapTypeControl: false,
|
|
16
|
+
streetViewControl: false
|
|
17
|
+
});
|
|
18
|
+
let pins = [];
|
|
19
|
+
class Pin extends maps.OverlayView {
|
|
20
|
+
constructor(position, label, selected, onClick) {
|
|
21
|
+
super();
|
|
22
|
+
this.position = position;
|
|
23
|
+
this.button = document.createElement("button");
|
|
24
|
+
this.button.type = "button";
|
|
25
|
+
this.button.setAttribute("aria-label", label);
|
|
26
|
+
this.button.setAttribute("aria-pressed", String(selected));
|
|
27
|
+
this.button.dataset.selected = String(selected);
|
|
28
|
+
this.button.dataset.branchMapPin = "";
|
|
29
|
+
this.button.style.cssText = `position:absolute;transform:translate(-50%,-100%);width:var(--branch-map-pin-size,24px);height:var(--branch-map-pin-size,24px);border-radius:50%;border:2px solid var(--branch-map-pin-border,currentColor);background:var(${selected ? "--branch-map-pin-selected" : "--branch-map-pin"},currentColor);cursor:pointer`;
|
|
30
|
+
this.button.onclick = onClick ?? null;
|
|
31
|
+
}
|
|
32
|
+
onAdd() {
|
|
33
|
+
this.getPanes()?.overlayMouseTarget.appendChild(this.button);
|
|
34
|
+
maps.OverlayView.preventMapHitsAndGesturesFrom(this.button);
|
|
35
|
+
}
|
|
36
|
+
draw() {
|
|
37
|
+
const point = this.getProjection().fromLatLngToDivPixel(
|
|
38
|
+
new maps.LatLng(this.position)
|
|
39
|
+
);
|
|
40
|
+
if (point) {
|
|
41
|
+
this.button.style.left = `${point.x}px`;
|
|
42
|
+
this.button.style.top = `${point.y}px`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
onRemove() {
|
|
46
|
+
this.button.remove();
|
|
47
|
+
this.button.onclick = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const clear = () => {
|
|
51
|
+
pins.forEach((pin) => pin.setMap(null));
|
|
52
|
+
pins = [];
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
update(branches, selected, onSelect, location) {
|
|
56
|
+
clear();
|
|
57
|
+
for (const branch of visibleBranches(branches)) {
|
|
58
|
+
const pin = new Pin(
|
|
59
|
+
branch.coordinates,
|
|
60
|
+
branch.name,
|
|
61
|
+
branch.id === selected,
|
|
62
|
+
() => onSelect(branch)
|
|
63
|
+
);
|
|
64
|
+
pin.setMap(map);
|
|
65
|
+
pins.push(pin);
|
|
66
|
+
}
|
|
67
|
+
const bounds = branchBounds(branches);
|
|
68
|
+
if (bounds) {
|
|
69
|
+
map.fitBounds(bounds, 32);
|
|
70
|
+
}
|
|
71
|
+
if (validCoordinates(location)) {
|
|
72
|
+
const pin = new Pin(location, "Your location", false);
|
|
73
|
+
pin.setMap(map);
|
|
74
|
+
pins.push(pin);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
locate(location) {
|
|
78
|
+
map.panTo(location);
|
|
79
|
+
},
|
|
80
|
+
destroy() {
|
|
81
|
+
clear();
|
|
82
|
+
maps.event.clearInstanceListeners(map);
|
|
83
|
+
element.replaceChildren();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export {
|
|
88
|
+
createMap
|
|
89
|
+
};
|
|
90
|
+
//# sourceMappingURL=runtime.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/runtime.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Branch } from \"@zappie/sdk\";\nimport {\n branchBounds,\n validCoordinates,\n visibleBranches,\n type Coordinates,\n} from \"./geometry\";\nimport { loadMaps } from \"./loader\";\n\nexport async function createMap(\n element: HTMLElement,\n key: string,\n signal: AbortSignal,\n) {\n const maps = await loadMaps(key);\n signal.throwIfAborted();\n const map = new maps.Map(element, {\n center: { lat: 0, lng: 0 },\n zoom: 12,\n maxZoom: 18,\n mapTypeControl: false,\n streetViewControl: false,\n });\n let pins: google.maps.OverlayView[] = [];\n class Pin extends maps.OverlayView {\n private button: HTMLButtonElement;\n constructor(\n private position: Coordinates,\n label: string,\n selected: boolean,\n onClick?: () => void,\n ) {\n super();\n this.button = document.createElement(\"button\");\n this.button.type = \"button\";\n this.button.setAttribute(\"aria-label\", label);\n this.button.setAttribute(\"aria-pressed\", String(selected));\n this.button.dataset.selected = String(selected);\n this.button.dataset.branchMapPin = \"\";\n this.button.style.cssText = `position:absolute;transform:translate(-50%,-100%);width:var(--branch-map-pin-size,24px);height:var(--branch-map-pin-size,24px);border-radius:50%;border:2px solid var(--branch-map-pin-border,currentColor);background:var(${selected ? \"--branch-map-pin-selected\" : \"--branch-map-pin\"},currentColor);cursor:pointer`;\n this.button.onclick = onClick ?? null;\n }\n onAdd() {\n this.getPanes()?.overlayMouseTarget.appendChild(this.button);\n maps.OverlayView.preventMapHitsAndGesturesFrom(this.button);\n }\n draw() {\n const point = this.getProjection().fromLatLngToDivPixel(\n new maps.LatLng(this.position),\n );\n if (point) {\n this.button.style.left = `${point.x}px`;\n this.button.style.top = `${point.y}px`;\n }\n }\n onRemove() {\n this.button.remove();\n this.button.onclick = null;\n }\n }\n const clear = () => {\n pins.forEach((pin) => pin.setMap(null));\n pins = [];\n };\n return {\n update(\n branches: Branch[],\n selected: string | undefined,\n onSelect: (branch: Branch) => void,\n location?: Coordinates,\n ) {\n clear();\n for (const branch of visibleBranches(branches)) {\n const pin = new Pin(\n branch.coordinates,\n branch.name,\n branch.id === selected,\n () => onSelect(branch),\n );\n pin.setMap(map);\n pins.push(pin);\n }\n const bounds = branchBounds(branches);\n if (bounds) {\n map.fitBounds(bounds, 32);\n }\n if (validCoordinates(location)) {\n const pin = new Pin(location, \"Your location\", false);\n pin.setMap(map);\n pins.push(pin);\n }\n },\n locate(location: Coordinates) {\n map.panTo(location);\n },\n destroy() {\n clear();\n maps.event.clearInstanceListeners(map);\n element.replaceChildren();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": ";AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,gBAAgB;AAEzB,eAAsB,UACpB,SACA,KACA,QACA;AACA,QAAM,OAAO,MAAM,SAAS,GAAG;AAC/B,SAAO,eAAe;AACtB,QAAM,MAAM,IAAI,KAAK,IAAI,SAAS;AAAA,IAChC,QAAQ,EAAE,KAAK,GAAG,KAAK,EAAE;AAAA,IACzB,MAAM;AAAA,IACN,SAAS;AAAA,IACT,gBAAgB;AAAA,IAChB,mBAAmB;AAAA,EACrB,CAAC;AACD,MAAI,OAAkC,CAAC;AAAA,EACvC,MAAM,YAAY,KAAK,YAAY;AAAA,IAEjC,YACU,UACR,OACA,UACA,SACA;AACA,YAAM;AALE;AAMR,WAAK,SAAS,SAAS,cAAc,QAAQ;AAC7C,WAAK,OAAO,OAAO;AACnB,WAAK,OAAO,aAAa,cAAc,KAAK;AAC5C,WAAK,OAAO,aAAa,gBAAgB,OAAO,QAAQ,CAAC;AACzD,WAAK,OAAO,QAAQ,WAAW,OAAO,QAAQ;AAC9C,WAAK,OAAO,QAAQ,eAAe;AACnC,WAAK,OAAO,MAAM,UAAU,8NAA8N,WAAW,8BAA8B,kBAAkB;AACrT,WAAK,OAAO,UAAU,WAAW;AAAA,IACnC;AAAA,IACA,QAAQ;AACN,WAAK,SAAS,GAAG,mBAAmB,YAAY,KAAK,MAAM;AAC3D,WAAK,YAAY,8BAA8B,KAAK,MAAM;AAAA,IAC5D;AAAA,IACA,OAAO;AACL,YAAM,QAAQ,KAAK,cAAc,EAAE;AAAA,QACjC,IAAI,KAAK,OAAO,KAAK,QAAQ;AAAA,MAC/B;AACA,UAAI,OAAO;AACT,aAAK,OAAO,MAAM,OAAO,GAAG,MAAM,CAAC;AACnC,aAAK,OAAO,MAAM,MAAM,GAAG,MAAM,CAAC;AAAA,MACpC;AAAA,IACF;AAAA,IACA,WAAW;AACT,WAAK,OAAO,OAAO;AACnB,WAAK,OAAO,UAAU;AAAA,IACxB;AAAA,EACF;AACA,QAAM,QAAQ,MAAM;AAClB,SAAK,QAAQ,CAAC,QAAQ,IAAI,OAAO,IAAI,CAAC;AACtC,WAAO,CAAC;AAAA,EACV;AACA,SAAO;AAAA,IACL,OACE,UACA,UACA,UACA,UACA;AACA,YAAM;AACN,iBAAW,UAAU,gBAAgB,QAAQ,GAAG;AAC9C,cAAM,MAAM,IAAI;AAAA,UACd,OAAO;AAAA,UACP,OAAO;AAAA,UACP,OAAO,OAAO;AAAA,UACd,MAAM,SAAS,MAAM;AAAA,QACvB;AACA,YAAI,OAAO,GAAG;AACd,aAAK,KAAK,GAAG;AAAA,MACf;AACA,YAAM,SAAS,aAAa,QAAQ;AACpC,UAAI,QAAQ;AACV,YAAI,UAAU,QAAQ,EAAE;AAAA,MAC1B;AACA,UAAI,iBAAiB,QAAQ,GAAG;AAC9B,cAAM,MAAM,IAAI,IAAI,UAAU,iBAAiB,KAAK;AACpD,YAAI,OAAO,GAAG;AACd,aAAK,KAAK,GAAG;AAAA,MACf;AAAA,IACF;AAAA,IACA,OAAO,UAAuB;AAC5B,UAAI,MAAM,QAAQ;AAAA,IACpB;AAAA,IACA,UAAU;AACR,YAAM;AACN,WAAK,MAAM,uBAAuB,GAAG;AACrC,cAAQ,gBAAgB;AAAA,IAC1B;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/cart/index.tsx
|
|
4
|
+
import { useCart, useCartCatalogue, useZappie } from "@zappie/sdk/react";
|
|
5
|
+
import {
|
|
6
|
+
createContext,
|
|
7
|
+
forwardRef,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useState
|
|
11
|
+
} from "react";
|
|
12
|
+
import { part } from "../internal/slot.js";
|
|
13
|
+
import { Quantity } from "../item-form/quantity.js";
|
|
14
|
+
import { jsx } from "react/jsx-runtime";
|
|
15
|
+
var Context = createContext(null);
|
|
16
|
+
function useContextValue() {
|
|
17
|
+
const value = useContext(Context);
|
|
18
|
+
if (!value) {
|
|
19
|
+
throw new Error("Cart parts require Cart.Root");
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function Root(props) {
|
|
24
|
+
const state = useCart();
|
|
25
|
+
return /* @__PURE__ */ jsx(Context.Provider, { value: state, children: part("div", props, state, {
|
|
26
|
+
"data-state": !state.isHydrated ? "loading" : state.issues.some((issue) => issue.severity === "blocking") ? "invalid" : "valid"
|
|
27
|
+
}) });
|
|
28
|
+
}
|
|
29
|
+
var Line = forwardRef(function Line2({ line, ...props }, ref) {
|
|
30
|
+
const cart = useContextValue();
|
|
31
|
+
const item = useCartCatalogue()?.items[line.itemId];
|
|
32
|
+
const current = cart.lines.find((candidate) => candidate.id === line.id);
|
|
33
|
+
if (!current) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const context = {
|
|
37
|
+
line: current,
|
|
38
|
+
item,
|
|
39
|
+
edit: (patch) => cart.actions.update(line.id, patch),
|
|
40
|
+
remove: () => cart.actions.remove(line.id)
|
|
41
|
+
};
|
|
42
|
+
return part(
|
|
43
|
+
"div",
|
|
44
|
+
props,
|
|
45
|
+
context,
|
|
46
|
+
{
|
|
47
|
+
"data-zappie-cart-line": line.id,
|
|
48
|
+
"data-invalid": cart.issues.some(
|
|
49
|
+
(issue) => issue.lineId === line.id && issue.severity === "blocking"
|
|
50
|
+
) ? "" : void 0
|
|
51
|
+
},
|
|
52
|
+
ref
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
function CartQuantity({
|
|
56
|
+
lineId,
|
|
57
|
+
...props
|
|
58
|
+
}) {
|
|
59
|
+
const cart = useContextValue();
|
|
60
|
+
const line = cart.lines.find((candidate) => candidate.id === lineId);
|
|
61
|
+
const item = useCartCatalogue()?.items[line?.itemId ?? ""];
|
|
62
|
+
if (!line) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const step = item?.quantity.step ?? 1;
|
|
66
|
+
const min = Math.ceil((item?.quantity.min ?? 1) / step) * step;
|
|
67
|
+
return /* @__PURE__ */ jsx(
|
|
68
|
+
Quantity.Root,
|
|
69
|
+
{
|
|
70
|
+
...props,
|
|
71
|
+
value: line.quantity,
|
|
72
|
+
min,
|
|
73
|
+
max: Math.min(item?.quantity.max ?? 99, 99),
|
|
74
|
+
step,
|
|
75
|
+
onValueChange: (quantity) => cart.actions.update(lineId, { quantity })
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
var Remove = forwardRef(
|
|
80
|
+
function Remove2({ lineId, ...props }, ref) {
|
|
81
|
+
const cart = useContextValue();
|
|
82
|
+
return part(
|
|
83
|
+
"button",
|
|
84
|
+
props,
|
|
85
|
+
{},
|
|
86
|
+
{ type: "button", onClick: () => cart.actions.remove(lineId) },
|
|
87
|
+
ref
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
var Empty = forwardRef(function Empty2(props, ref) {
|
|
92
|
+
const cart = useContextValue();
|
|
93
|
+
return cart.isHydrated && !cart.lines.length ? part("div", props, {}, {}, ref) : null;
|
|
94
|
+
});
|
|
95
|
+
var Issues = forwardRef(function Issues2(props, ref) {
|
|
96
|
+
const cart = useContextValue();
|
|
97
|
+
return part(
|
|
98
|
+
"div",
|
|
99
|
+
props,
|
|
100
|
+
{ issues: cart.issues, applyFix: cart.actions.applyFix },
|
|
101
|
+
{
|
|
102
|
+
"aria-live": "polite",
|
|
103
|
+
"data-state": cart.issues.length ? "invalid" : "valid"
|
|
104
|
+
},
|
|
105
|
+
ref
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
var Cart = {
|
|
109
|
+
Root,
|
|
110
|
+
Line,
|
|
111
|
+
Quantity: CartQuantity,
|
|
112
|
+
Remove,
|
|
113
|
+
Empty,
|
|
114
|
+
Issues
|
|
115
|
+
};
|
|
116
|
+
var Totals = forwardRef(
|
|
117
|
+
function Totals2(props, ref) {
|
|
118
|
+
const cart = useCart();
|
|
119
|
+
const client = useZappie();
|
|
120
|
+
const [failure, setFailure] = useState(null);
|
|
121
|
+
const [expired, setExpired] = useState(false);
|
|
122
|
+
const quote = "quote" in cart.state ? cart.state.quote : void 0;
|
|
123
|
+
useEffect(
|
|
124
|
+
() => client.on(
|
|
125
|
+
"quote_failed",
|
|
126
|
+
({ code }) => setFailure(
|
|
127
|
+
code === "network_error" || code === "timeout" ? "offline" : "stale"
|
|
128
|
+
)
|
|
129
|
+
),
|
|
130
|
+
[client]
|
|
131
|
+
);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (cart.state.status === "quoting" || quote) {
|
|
134
|
+
setFailure(null);
|
|
135
|
+
}
|
|
136
|
+
setExpired(Boolean(quote && Date.parse(quote.expiresAt) <= Date.now()));
|
|
137
|
+
if (!quote) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const timeout = setTimeout(
|
|
141
|
+
() => setExpired(true),
|
|
142
|
+
Math.max(
|
|
143
|
+
0,
|
|
144
|
+
Math.min(2147483647, Date.parse(quote.expiresAt) - Date.now())
|
|
145
|
+
)
|
|
146
|
+
);
|
|
147
|
+
return () => clearTimeout(timeout);
|
|
148
|
+
}, [quote, cart.state.status]);
|
|
149
|
+
const pending = cart.state.status === "ready" && !quote && cart.lines.length > 0;
|
|
150
|
+
const status = failure === "offline" ? "offline" : cart.state.status === "quoting" || !cart.isHydrated ? "loading" : failure === "stale" || expired ? "stale" : pending ? "loading" : !quote ? "stale" : "ready";
|
|
151
|
+
return part(
|
|
152
|
+
"div",
|
|
153
|
+
props,
|
|
154
|
+
{ quote, status },
|
|
155
|
+
{
|
|
156
|
+
"aria-live": "polite",
|
|
157
|
+
"aria-busy": status === "loading",
|
|
158
|
+
"data-state": status === "ready" ? "valid" : status === "offline" ? "stale" : status
|
|
159
|
+
},
|
|
160
|
+
ref
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
export {
|
|
165
|
+
Cart,
|
|
166
|
+
Totals
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/cart/index.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\nimport type {\n CartFix,\n CartIssue,\n CartLine,\n CartLineInput,\n Quote,\n} from \"@zappie/sdk\";\nimport { useCart, useCartCatalogue, useZappie } from \"@zappie/sdk/react\";\nimport {\n createContext,\n forwardRef,\n useContext,\n useEffect,\n useState,\n} from \"react\";\nimport { part, type PartProps } from \"../internal/slot\";\nimport { Quantity, type QuantityProps } from \"../item-form/quantity\";\ntype CartContext = ReturnType<typeof useCart>;\nconst Context = createContext<CartContext | null>(null);\nfunction useContextValue() {\n const value = useContext(Context);\n if (!value) {\n throw new Error(\"Cart parts require Cart.Root\");\n }\n return value;\n}\nfunction Root(props: PartProps<CartContext>) {\n const state = useCart();\n return (\n <Context.Provider value={state}>\n {part(\"div\", props, state, {\n \"data-state\": !state.isHydrated\n ? \"loading\"\n : state.issues.some((issue) => issue.severity === \"blocking\")\n ? \"invalid\"\n : \"valid\",\n })}\n </Context.Provider>\n );\n}\ntype LineContext = {\n line: CartLine;\n item: import(\"@zappie/sdk\").Item | undefined;\n edit: (patch: Partial<Omit<CartLineInput, \"itemId\">>) => CartLine;\n remove: () => void;\n};\nconst Line = forwardRef<\n HTMLElement,\n PartProps<LineContext> & { line: CartLine }\n>(function Line({ line, ...props }, ref) {\n const cart = useContextValue();\n const item = useCartCatalogue()?.items[line.itemId];\n const current = cart.lines.find((candidate) => candidate.id === line.id);\n if (!current) {\n return null;\n }\n const context = {\n line: current,\n item,\n edit: (patch: Partial<Omit<CartLineInput, \"itemId\">>) =>\n cart.actions.update(line.id, patch),\n remove: () => cart.actions.remove(line.id),\n };\n return part(\n \"div\",\n props,\n context,\n {\n \"data-zappie-cart-line\": line.id,\n \"data-invalid\": cart.issues.some(\n (issue) => issue.lineId === line.id && issue.severity === \"blocking\",\n )\n ? \"\"\n : undefined,\n },\n ref,\n );\n});\nfunction CartQuantity({\n lineId,\n ...props\n}: Omit<QuantityProps, \"value\" | \"defaultValue\" | \"onValueChange\"> & {\n lineId: string;\n}) {\n const cart = useContextValue();\n const line = cart.lines.find((candidate) => candidate.id === lineId);\n const item = useCartCatalogue()?.items[line?.itemId ?? \"\"];\n if (!line) {\n return null;\n }\n const step = item?.quantity.step ?? 1;\n const min = Math.ceil((item?.quantity.min ?? 1) / step) * step;\n return (\n <Quantity.Root\n {...props}\n value={line.quantity}\n min={min}\n max={Math.min(item?.quantity.max ?? 99, 99)}\n step={step}\n onValueChange={(quantity) => cart.actions.update(lineId, { quantity })}\n />\n );\n}\nconst Remove = forwardRef<HTMLElement, PartProps & { lineId: string }>(\n function Remove({ lineId, ...props }, ref) {\n const cart = useContextValue();\n return part(\n \"button\",\n props,\n {},\n { type: \"button\", onClick: () => cart.actions.remove(lineId) },\n ref,\n );\n },\n);\nconst Empty = forwardRef<HTMLElement, PartProps>(function Empty(props, ref) {\n const cart = useContextValue();\n return cart.isHydrated && !cart.lines.length\n ? part(\"div\", props, {}, {}, ref)\n : null;\n});\nconst Issues = forwardRef<\n HTMLElement,\n PartProps<{\n issues: readonly CartIssue[];\n applyFix: (issue: CartIssue, fix?: CartFix) => void;\n }>\n>(function Issues(props, ref) {\n const cart = useContextValue();\n return part(\n \"div\",\n props,\n { issues: cart.issues, applyFix: cart.actions.applyFix },\n {\n \"aria-live\": \"polite\",\n \"data-state\": cart.issues.length ? \"invalid\" : \"valid\",\n },\n ref,\n );\n});\n/**\n * Composable cart parts for lines, quantity changes, removal, empty state and validation issues.\n * Wrap the parts in Cart.Root inside your ZappieProvider.\n */\nexport const Cart = {\n Root,\n Line,\n Quantity: CartQuantity,\n Remove,\n Empty,\n Issues,\n};\n/**\n * The quote and its display status: loading, stale, ready or offline.\n */\nexport type TotalsState = {\n quote?: Quote | undefined;\n status: \"loading\" | \"stale\" | \"ready\" | \"offline\";\n};\n/**\n * Expose the cart quote and its freshness or network status to your render function.\n * Subscribes to the cart and quote failures through your ZappieProvider.\n */\nexport const Totals = forwardRef<HTMLElement, PartProps<TotalsState>>(\n function Totals(props, ref) {\n const cart = useCart();\n const client = useZappie();\n const [failure, setFailure] = useState<\"offline\" | \"stale\" | null>(null);\n const [expired, setExpired] = useState(false);\n const quote = \"quote\" in cart.state ? cart.state.quote : undefined;\n useEffect(\n () =>\n client.on(\"quote_failed\", ({ code }) =>\n setFailure(\n code === \"network_error\" || code === \"timeout\"\n ? \"offline\"\n : \"stale\",\n ),\n ),\n [client],\n );\n useEffect(() => {\n if (cart.state.status === \"quoting\" || quote) {\n setFailure(null);\n }\n setExpired(Boolean(quote && Date.parse(quote.expiresAt) <= Date.now()));\n if (!quote) {\n return;\n }\n const timeout = setTimeout(\n () => setExpired(true),\n Math.max(\n 0,\n Math.min(2147483647, Date.parse(quote.expiresAt) - Date.now()),\n ),\n );\n return () => clearTimeout(timeout);\n }, [quote, cart.state.status]);\n // The SDK re-quotes shortly after every change, so a cart with lines and\n // no quote yet is waiting for one rather than stale.\n const pending =\n cart.state.status === \"ready\" && !quote && cart.lines.length > 0;\n const status: TotalsState[\"status\"] =\n failure === \"offline\"\n ? \"offline\"\n : cart.state.status === \"quoting\" || !cart.isHydrated\n ? \"loading\"\n : failure === \"stale\" || expired\n ? \"stale\"\n : pending\n ? \"loading\"\n : !quote\n ? \"stale\"\n : \"ready\";\n return part(\n \"div\",\n props,\n { quote, status },\n {\n \"aria-live\": \"polite\",\n \"aria-busy\": status === \"loading\",\n \"data-state\":\n status === \"ready\"\n ? \"valid\"\n : status === \"offline\"\n ? \"stale\"\n : status,\n },\n ref,\n );\n },\n);\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["Line", "Remove", "Empty", "Issues", "Totals"]
|
|
7
|
+
}
|