astro-tractstack 2.2.10 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-tractstack.js +2 -2
- package/dist/index.js +89 -8
- package/package.json +3 -1
- package/templates/custom/minimal/CodeHook.astro +14 -5
- package/templates/custom/shopify/CalDotComBooking.tsx +44 -0
- package/templates/custom/shopify/Cart.tsx +345 -0
- package/templates/custom/shopify/CartIcon.tsx +47 -0
- package/templates/custom/shopify/CartModal.tsx +63 -0
- package/templates/custom/shopify/CheckoutModal.tsx +187 -0
- package/templates/custom/shopify/ShopifyCartManager.tsx +145 -0
- package/templates/custom/shopify/ShopifyCheckout.tsx +167 -0
- package/templates/custom/shopify/ShopifyProductGrid.tsx +281 -0
- package/templates/custom/shopify/ShopifyServiceList.tsx +118 -0
- package/templates/custom/shopify/cart.astro +23 -0
- package/templates/custom/with-examples/CodeHook.astro +9 -1
- package/templates/custom/with-examples/ProductGrid.astro +1 -1
- package/templates/src/client/app.js +4 -2
- package/templates/src/components/Header.astro +37 -11
- package/templates/src/components/form/advanced/APIConfigSection.tsx +165 -38
- package/templates/src/components/storykeep/Dashboard.tsx +17 -3
- package/templates/src/components/storykeep/Dashboard_Advanced.tsx +1 -0
- package/templates/src/components/storykeep/Dashboard_Content.tsx +5 -96
- package/templates/src/components/storykeep/Dashboard_Shopify.tsx +525 -0
- package/templates/src/components/storykeep/StoryKeepBackdrop.astro +43 -23
- package/templates/src/components/storykeep/controls/content/ContentBrowser.tsx +0 -14
- package/templates/src/components/storykeep/controls/content/KnownResourceForm.tsx +36 -13
- package/templates/src/components/storykeep/controls/content/ManageContent.tsx +4 -11
- package/templates/src/components/storykeep/controls/content/ProductTable.tsx +254 -0
- package/templates/src/components/storykeep/controls/content/ResourceForm.tsx +108 -8
- package/templates/src/lib/resources.ts +11 -21
- package/templates/src/pages/api/shopify/createCart.ts +73 -0
- package/templates/src/pages/api/shopify/getProducts.ts +64 -0
- package/templates/src/pages/storykeep/login.astro +5 -10
- package/templates/src/pages/storykeep/logout.astro +1 -10
- package/templates/src/pages/storykeep/manage.astro +69 -0
- package/templates/src/pages/storykeep/{content.astro → pages.astro} +4 -8
- package/templates/src/pages/storykeep/shopify.astro +101 -0
- package/templates/src/stores/navigation.ts +3 -42
- package/templates/src/stores/nodes.ts +3 -1
- package/templates/src/stores/resources.ts +7 -10
- package/templates/src/stores/shopify.ts +210 -0
- package/templates/src/types/tractstack.ts +21 -0
- package/templates/src/utils/api/advancedConfig.ts +5 -1
- package/templates/src/utils/api/advancedHelpers.ts +48 -5
- package/templates/src/utils/api/brandHelpers.ts +4 -0
- package/templates/src/utils/api/resourceConfig.ts +13 -5
- package/templates/src/utils/customHelpers.ts +70 -0
- package/templates/src/utils/helpers.ts +59 -0
- package/utils/inject-files.ts +83 -2
package/bin/create-tractstack.js
CHANGED
|
@@ -348,7 +348,7 @@ PUBLIC_ENABLE_BUNNY="${finalResponses.enableBunny ? 'true' : 'false'}"
|
|
|
348
348
|
|
|
349
349
|
// Install UI components
|
|
350
350
|
execSync(
|
|
351
|
-
`${addCommand} @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date
|
|
351
|
+
`${addCommand} @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@3.10.0 @calcom/embed-react@^1.4.1`,
|
|
352
352
|
{
|
|
353
353
|
stdio: 'inherit',
|
|
354
354
|
}
|
|
@@ -382,7 +382,7 @@ PUBLIC_ENABLE_BUNNY="${finalResponses.enableBunny ? 'true' : 'false'}"
|
|
|
382
382
|
console.log('Please run manually:');
|
|
383
383
|
console.log(
|
|
384
384
|
kleur.cyan(
|
|
385
|
-
`${addCommand} react@^19.0.0 react-dom@^19.0.0 @astrojs/react@^4.4.2 @astrojs/node@^9.4.3 @nanostores/react@^1.0.0 nanostores@^1.0.1 @nanostores/persistent ulid@^3.0.1 @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date
|
|
385
|
+
`${addCommand} react@^19.0.0 react-dom@^19.0.0 @astrojs/react@^4.4.2 @astrojs/node@^9.4.3 @nanostores/react@^1.0.0 nanostores@^1.0.1 @nanostores/persistent ulid@^3.0.1 @ark-ui/react@^5.30.0 @heroicons/react@^2.1.1 @internationalized/date@3.10.0 @calcom/embed-react@^1.4.1 d3@^7.9.0 d3-sankey@^0.12.3 recharts@^3.1.2 player.js@^0.1.0 tinycolor2@1.6.0 html-to-image@^1.11.13 path-to-regexp@^8.0.0 postcss postcss-selector-parser`
|
|
386
386
|
)
|
|
387
387
|
);
|
|
388
388
|
console.log(
|
package/dist/index.js
CHANGED
|
@@ -21,7 +21,7 @@ function g(t, e) {
|
|
|
21
21
|
}
|
|
22
22
|
return r ? /^[a-zA-Z0-9_-]+$/.test(r) ? e.info(`Tenant ID validated: ${r}`) : e.error(`PUBLIC_TENANTID contains invalid characters: ${r}`) : e.warn("PUBLIC_TENANTID not set - this will be required at runtime"), t;
|
|
23
23
|
}
|
|
24
|
-
async function
|
|
24
|
+
async function y(t, e, c) {
|
|
25
25
|
e.info("TractStack: Injecting template files");
|
|
26
26
|
const r = [
|
|
27
27
|
// Core Configuration
|
|
@@ -586,6 +586,10 @@ async function w(t, e, c) {
|
|
|
586
586
|
src: t("../templates/src/stores/resources.ts"),
|
|
587
587
|
dest: "src/stores/resources.ts"
|
|
588
588
|
},
|
|
589
|
+
{
|
|
590
|
+
src: t("../templates/src/stores/shopify.ts"),
|
|
591
|
+
dest: "src/stores/shopify.ts"
|
|
592
|
+
},
|
|
589
593
|
// Compositor stores
|
|
590
594
|
{
|
|
591
595
|
src: t("../templates/src/stores/nodes.ts"),
|
|
@@ -856,8 +860,12 @@ async function w(t, e, c) {
|
|
|
856
860
|
dest: "src/pages/storykeep.astro"
|
|
857
861
|
},
|
|
858
862
|
{
|
|
859
|
-
src: t("../templates/src/pages/storykeep/
|
|
860
|
-
dest: "src/pages/storykeep/
|
|
863
|
+
src: t("../templates/src/pages/storykeep/pages.astro"),
|
|
864
|
+
dest: "src/pages/storykeep/pages.astro"
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
src: t("../templates/src/pages/storykeep/manage.astro"),
|
|
868
|
+
dest: "src/pages/storykeep/manage.astro"
|
|
861
869
|
},
|
|
862
870
|
{
|
|
863
871
|
src: t("../templates/src/pages/storykeep/branding.astro"),
|
|
@@ -867,10 +875,18 @@ async function w(t, e, c) {
|
|
|
867
875
|
src: t("../templates/src/pages/storykeep/advanced.astro"),
|
|
868
876
|
dest: "src/pages/storykeep/advanced.astro"
|
|
869
877
|
},
|
|
878
|
+
{
|
|
879
|
+
src: t("../templates/src/pages/storykeep/shopify.astro"),
|
|
880
|
+
dest: "src/pages/storykeep/shopify.astro"
|
|
881
|
+
},
|
|
870
882
|
{
|
|
871
883
|
src: t("../templates/src/pages/maint.astro"),
|
|
872
884
|
dest: "src/pages/maint.astro"
|
|
873
885
|
},
|
|
886
|
+
{
|
|
887
|
+
src: t("../templates/custom/shopify/cart.astro"),
|
|
888
|
+
dest: "src/pages/cart.astro"
|
|
889
|
+
},
|
|
874
890
|
{
|
|
875
891
|
src: t("../templates/src/pages/404.astro"),
|
|
876
892
|
dest: "src/pages/404.astro"
|
|
@@ -887,6 +903,14 @@ async function w(t, e, c) {
|
|
|
887
903
|
src: t("../templates/src/pages/sitemap.xml.ts"),
|
|
888
904
|
dest: "src/pages/sitemap.xml.ts"
|
|
889
905
|
},
|
|
906
|
+
{
|
|
907
|
+
src: t("../templates/src/pages/api/shopify/getProducts.ts"),
|
|
908
|
+
dest: "src/pages/api/shopify/getProducts.ts"
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
src: t("../templates/src/pages/api/shopify/createCart.ts"),
|
|
912
|
+
dest: "src/pages/api/shopify/createCart.ts"
|
|
913
|
+
},
|
|
890
914
|
{
|
|
891
915
|
src: t("../templates/src/pages/api/tailwind.ts"),
|
|
892
916
|
dest: "src/pages/api/tailwind.ts"
|
|
@@ -1140,6 +1164,12 @@ async function w(t, e, c) {
|
|
|
1140
1164
|
),
|
|
1141
1165
|
dest: "src/components/storykeep/Dashboard_Advanced.tsx"
|
|
1142
1166
|
},
|
|
1167
|
+
{
|
|
1168
|
+
src: t(
|
|
1169
|
+
"../templates/src/components/storykeep/Dashboard_Shopify.tsx"
|
|
1170
|
+
),
|
|
1171
|
+
dest: "src/components/storykeep/Dashboard_Shopify.tsx"
|
|
1172
|
+
},
|
|
1143
1173
|
{
|
|
1144
1174
|
src: t(
|
|
1145
1175
|
"../templates/src/components/storykeep/Dashboard_Analytics.tsx"
|
|
@@ -1195,6 +1225,12 @@ async function w(t, e, c) {
|
|
|
1195
1225
|
),
|
|
1196
1226
|
dest: "src/components/storykeep/controls/content/PaneTable.tsx"
|
|
1197
1227
|
},
|
|
1228
|
+
{
|
|
1229
|
+
src: t(
|
|
1230
|
+
"../templates/src/components/storykeep/controls/content/ProductTable.tsx"
|
|
1231
|
+
),
|
|
1232
|
+
dest: "src/components/storykeep/controls/content/ProductTable.tsx"
|
|
1233
|
+
},
|
|
1198
1234
|
{
|
|
1199
1235
|
src: t(
|
|
1200
1236
|
"../templates/src/components/storykeep/controls/content/ContentBrowser.tsx"
|
|
@@ -2159,6 +2195,51 @@ async function w(t, e, c) {
|
|
|
2159
2195
|
dest: "src/utils/customHelpers.ts",
|
|
2160
2196
|
protected: !0
|
|
2161
2197
|
},
|
|
2198
|
+
{
|
|
2199
|
+
src: t("../templates/custom/shopify/ShopifyProductGrid.tsx"),
|
|
2200
|
+
dest: "src/custom/shopify/ShopifyProductGrid.tsx",
|
|
2201
|
+
protected: !0
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
src: t("../templates/custom/shopify/ShopifyServiceList.tsx"),
|
|
2205
|
+
dest: "src/custom/shopify/ShopifyServiceList.tsx",
|
|
2206
|
+
protected: !0
|
|
2207
|
+
},
|
|
2208
|
+
{
|
|
2209
|
+
src: t("../templates/custom/shopify/CartIcon.tsx"),
|
|
2210
|
+
dest: "src/custom/shopify/CartIcon.tsx",
|
|
2211
|
+
protected: !0
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
src: t("../templates/custom/shopify/ShopifyCartManager.tsx"),
|
|
2215
|
+
dest: "src/custom/shopify/ShopifyCartManager.tsx",
|
|
2216
|
+
protected: !0
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
src: t("../templates/custom/shopify/CartModal.tsx"),
|
|
2220
|
+
dest: "src/custom/shopify/CartModal.tsx",
|
|
2221
|
+
protected: !0
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
src: t("../templates/custom/shopify/CheckoutModal.tsx"),
|
|
2225
|
+
dest: "src/custom/shopify/CheckoutModal.tsx",
|
|
2226
|
+
protected: !0
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
src: t("../templates/custom/shopify/Cart.tsx"),
|
|
2230
|
+
dest: "src/custom/shopify/Cart.tsx",
|
|
2231
|
+
protected: !0
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
src: t("../templates/custom/shopify/CalDotComBooking.tsx"),
|
|
2235
|
+
dest: "src/custom/shopify/CalDotComBooking.tsx",
|
|
2236
|
+
protected: !0
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
src: t("../templates/custom/shopify/ShopifyCheckout.tsx"),
|
|
2240
|
+
dest: "src/custom/shopify/ShopifyCheckout.tsx",
|
|
2241
|
+
protected: !0
|
|
2242
|
+
},
|
|
2162
2243
|
// Example Components (Conditional)
|
|
2163
2244
|
...c?.includeExamples ? [
|
|
2164
2245
|
{
|
|
@@ -2213,7 +2294,7 @@ async function w(t, e, c) {
|
|
|
2213
2294
|
if (n(s.src))
|
|
2214
2295
|
k(s.src, s.dest), e.info(`Updated ${s.dest}`);
|
|
2215
2296
|
else {
|
|
2216
|
-
const m =
|
|
2297
|
+
const m = w(s.dest);
|
|
2217
2298
|
u(s.dest, m), e.info(`Created placeholder ${s.dest}`);
|
|
2218
2299
|
}
|
|
2219
2300
|
else s.protected ? e.info(`Protected: ${s.dest} (skipped overwrite)`) : e.info(`Skipped existing ${s.dest}`);
|
|
@@ -2222,7 +2303,7 @@ async function w(t, e, c) {
|
|
|
2222
2303
|
e.error(`Failed to create ${s.dest}: ${o}`);
|
|
2223
2304
|
}
|
|
2224
2305
|
}
|
|
2225
|
-
function
|
|
2306
|
+
function w(t) {
|
|
2226
2307
|
return t.endsWith(".astro") ? `---
|
|
2227
2308
|
// TractStack placeholder component
|
|
2228
2309
|
---
|
|
@@ -2233,7 +2314,7 @@ export default function Placeholder() {
|
|
|
2233
2314
|
}` : t.endsWith(".ts") ? `// TractStack placeholder utility
|
|
2234
2315
|
export const placeholder = "${t}";` : `# TractStack placeholder: ${t}`;
|
|
2235
2316
|
}
|
|
2236
|
-
function
|
|
2317
|
+
function h(t = {}) {
|
|
2237
2318
|
const { resolve: e } = b(import.meta.url);
|
|
2238
2319
|
return {
|
|
2239
2320
|
name: "astro-tractstack",
|
|
@@ -2243,7 +2324,7 @@ function C(t = {}) {
|
|
|
2243
2324
|
const p = t.enableMultiTenant || !1;
|
|
2244
2325
|
if (s.info(
|
|
2245
2326
|
`DEBUG: enableMultiTenant = ${p}, process.env.PUBLIC_ENABLE_MULTI_TENANT = ${process.env.PUBLIC_ENABLE_MULTI_TENANT}`
|
|
2246
|
-
), s.info("TractStack: Starting file injection..."), await
|
|
2327
|
+
), s.info("TractStack: Starting file injection..."), await y(e, s, {
|
|
2247
2328
|
includeExamples: t.includeExamples,
|
|
2248
2329
|
enableMultiTenant: p
|
|
2249
2330
|
}), s.info("TractStack: File injection complete."), c.output !== "server")
|
|
@@ -2315,5 +2396,5 @@ function C(t = {}) {
|
|
|
2315
2396
|
};
|
|
2316
2397
|
}
|
|
2317
2398
|
export {
|
|
2318
|
-
|
|
2399
|
+
h as default
|
|
2319
2400
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astro-tractstack",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Astro integration for TractStack - the free web press by At Risk Media",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"build": "vite build && tsc -p tsconfig.dts.json",
|
|
25
25
|
"format": "prettier --write .",
|
|
26
26
|
"dev": "vite build --watch",
|
|
27
|
+
"lint": "prettier --check .",
|
|
27
28
|
"prepublishOnly": "pnpm format && pnpm tsc && pnpm build"
|
|
28
29
|
},
|
|
29
30
|
"keywords": [
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
"react-dom": "^19.0.0"
|
|
51
52
|
},
|
|
52
53
|
"dependencies": {
|
|
54
|
+
"@calcom/embed-react": "^1.5.3",
|
|
53
55
|
"kleur": "^4.1.5",
|
|
54
56
|
"prompts": "^2.4.2"
|
|
55
57
|
},
|
|
@@ -4,9 +4,12 @@ import ListContent from '@/components/codehooks/ListContent.astro';
|
|
|
4
4
|
import SearchWidget from '@/components/codehooks/SearchWidget.tsx';
|
|
5
5
|
import BunnyVideoWrapper from '@/components/codehooks/BunnyVideoWrapper.astro';
|
|
6
6
|
import EpinetWrapper from '@/components/codehooks/EpinetWrapper';
|
|
7
|
+
import ShopifyProductGrid from '@/custom/shopify/ShopifyProductGrid';
|
|
8
|
+
import ShopifyServiceList from '@/custom/shopify/ShopifyServiceList';
|
|
7
9
|
import type { FullContentMapItem } from '@/types/tractstack';
|
|
8
10
|
import type { ResourceNode } from '@/types/compositorTypes';
|
|
9
|
-
// import CustomHero from './CustomHero.astro';
|
|
11
|
+
// import CustomHero from './CustomHero.astro';
|
|
12
|
+
// Uncomment to add custom components
|
|
10
13
|
|
|
11
14
|
export interface Props {
|
|
12
15
|
target: string;
|
|
@@ -20,7 +23,7 @@ export interface Props {
|
|
|
20
23
|
};
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
const { target, options, fullContentMap
|
|
26
|
+
const { target, options, fullContentMap, resourcesPayload = {} } = Astro.props;
|
|
24
27
|
|
|
25
28
|
export const components = {
|
|
26
29
|
'featured-article': true,
|
|
@@ -29,6 +32,8 @@ export const components = {
|
|
|
29
32
|
'search-widget': true,
|
|
30
33
|
'bunny-video': import.meta.env.PUBLIC_ENABLE_BUNNY === 'true',
|
|
31
34
|
epinet: true,
|
|
35
|
+
'shopify-product-grid': true,
|
|
36
|
+
'shopify-service-list': true,
|
|
32
37
|
// "custom-hero": true, // Uncomment when you create CustomHero.astro
|
|
33
38
|
};
|
|
34
39
|
---
|
|
@@ -43,11 +48,15 @@ export const components = {
|
|
|
43
48
|
) : target === 'bunny-video' && import.meta.env.PUBLIC_ENABLE_BUNNY ? (
|
|
44
49
|
<BunnyVideoWrapper options={options} />
|
|
45
50
|
) : target === 'epinet' ? (
|
|
46
|
-
<EpinetWrapper fullContentMap={fullContentMap} client:only="react" />
|
|
47
|
-
: target ===
|
|
51
|
+
<EpinetWrapper fullContentMap={fullContentMap} client:only="react" />
|
|
52
|
+
) : target === 'shopify-product-grid' ? (
|
|
53
|
+
<ShopifyProductGrid resources={resourcesPayload} client:only="react" />
|
|
54
|
+
) : target === 'shopify-service-list' ? (
|
|
55
|
+
<ShopifyServiceList resources={resourcesPayload} client:only="react" />
|
|
56
|
+
) : (
|
|
57
|
+
/* : target === "custom-hero" ? (
|
|
48
58
|
<CustomHero />
|
|
49
59
|
) */
|
|
50
|
-
) : (
|
|
51
60
|
<div class="rounded-lg bg-gray-50 p-8 text-center">
|
|
52
61
|
<p class="text-gray-600">CodeHook target "{target}" not found</p>
|
|
53
62
|
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import Cal, { getCalApi } from '@calcom/embed-react';
|
|
3
|
+
|
|
4
|
+
interface CalDotComBookingProps {
|
|
5
|
+
calSlug: string;
|
|
6
|
+
traceId: string;
|
|
7
|
+
name: string;
|
|
8
|
+
email: string;
|
|
9
|
+
onSuccess: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function CalDotComBooking({
|
|
13
|
+
calSlug,
|
|
14
|
+
traceId,
|
|
15
|
+
name,
|
|
16
|
+
email,
|
|
17
|
+
onSuccess,
|
|
18
|
+
}: CalDotComBookingProps) {
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
(async function () {
|
|
21
|
+
const cal = await getCalApi();
|
|
22
|
+
cal('on', {
|
|
23
|
+
action: 'bookingSuccessful',
|
|
24
|
+
callback: () => {
|
|
25
|
+
onSuccess();
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
})();
|
|
29
|
+
}, [onSuccess]);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Cal
|
|
33
|
+
calLink={calSlug}
|
|
34
|
+
style={{ width: '100%', height: '100%', overflow: 'hidden' }}
|
|
35
|
+
config={{
|
|
36
|
+
name,
|
|
37
|
+
email,
|
|
38
|
+
metadata: {
|
|
39
|
+
traceId,
|
|
40
|
+
},
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import {
|
|
4
|
+
addQueue,
|
|
5
|
+
cartStore,
|
|
6
|
+
cartState,
|
|
7
|
+
CART_STATES,
|
|
8
|
+
isShopifyHandoff,
|
|
9
|
+
type CartAction,
|
|
10
|
+
type CartItemState,
|
|
11
|
+
} from '@/stores/shopify';
|
|
12
|
+
import { getShopifyImage } from '@/utils/helpers';
|
|
13
|
+
import type { ResourceNode } from '@/types/compositorTypes';
|
|
14
|
+
|
|
15
|
+
interface CartProps {
|
|
16
|
+
resources: ResourceNode[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const getCleanVariantTitle = (variant: any) => {
|
|
20
|
+
if (variant?.selectedOptions) {
|
|
21
|
+
return variant.selectedOptions
|
|
22
|
+
.filter((o: any) => o.name !== 'Mode')
|
|
23
|
+
.map((o: any) => o.value)
|
|
24
|
+
.join(' / ');
|
|
25
|
+
}
|
|
26
|
+
return variant?.title || '';
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default function Cart({ resources = [] }: CartProps) {
|
|
30
|
+
const cart = useStore(cartStore);
|
|
31
|
+
const isHandoff = useStore(isShopifyHandoff);
|
|
32
|
+
const [pickupEnabled, setPickupEnabled] = useState(false);
|
|
33
|
+
|
|
34
|
+
const cartValues = Object.values(cart);
|
|
35
|
+
|
|
36
|
+
const boundServiceIds = new Set(
|
|
37
|
+
cartValues
|
|
38
|
+
.map((item) => item.boundResourceId)
|
|
39
|
+
.filter((id) => !!id) as string[]
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const hasServiceBoundProduct = boundServiceIds.size > 0;
|
|
43
|
+
|
|
44
|
+
const displayableItems = cartValues.filter(
|
|
45
|
+
(item) => !boundServiceIds.has(item.resourceId)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const groupedItems = displayableItems.reduce(
|
|
49
|
+
(acc, item) => {
|
|
50
|
+
if (!acc[item.resourceId]) {
|
|
51
|
+
acc[item.resourceId] = [];
|
|
52
|
+
}
|
|
53
|
+
acc[item.resourceId].push(item);
|
|
54
|
+
return acc;
|
|
55
|
+
},
|
|
56
|
+
{} as Record<string, CartItemState[]>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const hasService = cartValues.some((item) => {
|
|
60
|
+
const resource = resources.find((r) => r.id === item.resourceId);
|
|
61
|
+
return !!resource?.optionsPayload?.bookingLengthMinutes;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const hasPhysicalProductWithPickup = cartValues.some(
|
|
65
|
+
(item) => !!item.variantIdPickup
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const canPickup = hasService && hasPhysicalProductWithPickup;
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (hasServiceBoundProduct) {
|
|
72
|
+
setPickupEnabled(true);
|
|
73
|
+
} else if (canPickup) {
|
|
74
|
+
setPickupEnabled(true);
|
|
75
|
+
} else {
|
|
76
|
+
setPickupEnabled(false);
|
|
77
|
+
}
|
|
78
|
+
}, [canPickup, hasServiceBoundProduct]);
|
|
79
|
+
|
|
80
|
+
const isPickupMode = (canPickup || hasServiceBoundProduct) && pickupEnabled;
|
|
81
|
+
|
|
82
|
+
const dispatchDualAction = (
|
|
83
|
+
item: CartItemState,
|
|
84
|
+
action: 'add' | 'remove'
|
|
85
|
+
) => {
|
|
86
|
+
const queueUpdates: CartAction[] = [];
|
|
87
|
+
|
|
88
|
+
queueUpdates.push({
|
|
89
|
+
resourceId: item.resourceId,
|
|
90
|
+
action,
|
|
91
|
+
variantId: item.variantId,
|
|
92
|
+
variantIdShipped: item.variantIdShipped,
|
|
93
|
+
variantIdPickup: item.variantIdPickup,
|
|
94
|
+
boundResourceId: item.boundResourceId,
|
|
95
|
+
suppressModal: action === 'add' ? true : undefined,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (item.boundResourceId) {
|
|
99
|
+
queueUpdates.push({
|
|
100
|
+
resourceId: item.boundResourceId,
|
|
101
|
+
action,
|
|
102
|
+
suppressModal: action === 'add' ? true : undefined,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addQueue.set([...addQueue.get(), ...queueUpdates]);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (cartValues.length === 0) {
|
|
110
|
+
return (
|
|
111
|
+
<div className="relative">
|
|
112
|
+
{isHandoff && (
|
|
113
|
+
<div className="absolute inset-0 z-103 flex flex-col items-center justify-center rounded-lg bg-black bg-opacity-75 backdrop-blur-md">
|
|
114
|
+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-gray-200 border-t-black"></div>
|
|
115
|
+
<h3 className="mt-4 text-lg font-bold text-gray-900">
|
|
116
|
+
Finalizing Handoff...
|
|
117
|
+
</h3>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
<div className="rounded-lg border bg-gray-50 p-8 text-center">
|
|
121
|
+
<h2 className="text-xl font-bold">Your cart is empty</h2>
|
|
122
|
+
<p className="mt-2 text-gray-600">Add some items to get started.</p>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="rounded-lg bg-white shadow">
|
|
130
|
+
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
|
|
131
|
+
<h2 className="text-xl font-bold text-gray-800">Shopping Cart</h2>
|
|
132
|
+
{canPickup && (
|
|
133
|
+
<label className="flex items-center space-x-2 text-sm font-bold text-gray-900">
|
|
134
|
+
<input
|
|
135
|
+
type="checkbox"
|
|
136
|
+
checked={pickupEnabled}
|
|
137
|
+
onChange={(e) => setPickupEnabled(e.target.checked)}
|
|
138
|
+
className="h-4 w-4 rounded border-gray-300 text-black focus:ring-black"
|
|
139
|
+
/>
|
|
140
|
+
<span>Pick up at Store</span>
|
|
141
|
+
</label>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<ul className="divide-y divide-gray-200">
|
|
146
|
+
{Object.keys(groupedItems).map((resourceId) => {
|
|
147
|
+
const items = groupedItems[resourceId];
|
|
148
|
+
const resource = resources.find((r) => r.id === resourceId);
|
|
149
|
+
if (!resource || items.length === 0) return null;
|
|
150
|
+
|
|
151
|
+
const isService = !!resource.optionsPayload?.bookingLengthMinutes;
|
|
152
|
+
const serviceDuration = resource.optionsPayload?.bookingLengthMinutes;
|
|
153
|
+
|
|
154
|
+
const firstItem = items[0];
|
|
155
|
+
const boundServiceId = firstItem.boundResourceId;
|
|
156
|
+
const boundServiceResource = boundServiceId
|
|
157
|
+
? resources.find((r) => r.id === boundServiceId)
|
|
158
|
+
: null;
|
|
159
|
+
|
|
160
|
+
const activeVariantIdFirst = isPickupMode
|
|
161
|
+
? firstItem.variantIdPickup
|
|
162
|
+
: firstItem.variantIdShipped;
|
|
163
|
+
const displayIdFirst =
|
|
164
|
+
activeVariantIdFirst ||
|
|
165
|
+
firstItem.variantIdPickup ||
|
|
166
|
+
firstItem.variantId;
|
|
167
|
+
const { src, srcSet } = getShopifyImage(
|
|
168
|
+
resource,
|
|
169
|
+
'600',
|
|
170
|
+
displayIdFirst
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
let productData: any = {};
|
|
174
|
+
try {
|
|
175
|
+
if (resource.optionsPayload?.shopifyData) {
|
|
176
|
+
productData = JSON.parse(resource.optionsPayload.shopifyData);
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.error('Failed to parse Shopify data', resource.id);
|
|
180
|
+
}
|
|
181
|
+
const variants = productData?.variants || [];
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<li key={resourceId} className="p-6">
|
|
185
|
+
<div className="flex items-start">
|
|
186
|
+
{!isService && (
|
|
187
|
+
<div className="h-16 w-16 flex-shrink-0 overflow-hidden rounded-md border border-gray-200">
|
|
188
|
+
<img
|
|
189
|
+
src={src}
|
|
190
|
+
srcSet={srcSet}
|
|
191
|
+
alt={resource.title}
|
|
192
|
+
className="aspect-square h-full w-full object-cover object-center"
|
|
193
|
+
loading="lazy"
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
<div className="ml-4 flex-1">
|
|
198
|
+
<div className="flex justify-between">
|
|
199
|
+
<div>
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
<h3 className="text-base font-bold text-gray-900">
|
|
202
|
+
{resource.title}
|
|
203
|
+
</h3>
|
|
204
|
+
{isService && (
|
|
205
|
+
<span className="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-bold text-blue-700">
|
|
206
|
+
{serviceDuration} mins
|
|
207
|
+
</span>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
{boundServiceResource && (
|
|
211
|
+
<p className="flex items-center text-xs font-bold text-blue-600">
|
|
212
|
+
<span className="mr-1 inline-block h-2 w-2 rounded-full bg-blue-500"></span>
|
|
213
|
+
Includes Booking: {boundServiceResource.title}
|
|
214
|
+
</p>
|
|
215
|
+
)}
|
|
216
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
217
|
+
{resource.oneliner}
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className="mt-4 space-y-4 border-t border-gray-100 pt-4">
|
|
223
|
+
{items.map((item) => {
|
|
224
|
+
const activeVariantId = isPickupMode
|
|
225
|
+
? item.variantIdPickup
|
|
226
|
+
: item.variantIdShipped;
|
|
227
|
+
|
|
228
|
+
const displayId =
|
|
229
|
+
activeVariantId ||
|
|
230
|
+
item.variantIdPickup ||
|
|
231
|
+
item.variantId;
|
|
232
|
+
|
|
233
|
+
let price = '0.00';
|
|
234
|
+
let currency = 'USD';
|
|
235
|
+
let variantTitle = '';
|
|
236
|
+
|
|
237
|
+
const variant = variants.find(
|
|
238
|
+
(v: any) => v.id === displayId
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (variant) {
|
|
242
|
+
price = variant.price?.amount || '0.00';
|
|
243
|
+
currency = variant.price?.currencyCode || 'USD';
|
|
244
|
+
variantTitle = getCleanVariantTitle(variant);
|
|
245
|
+
} else if (variants.length > 0 && !variantTitle) {
|
|
246
|
+
const v = variants[0];
|
|
247
|
+
price = v.price?.amount || '0.00';
|
|
248
|
+
currency = v.price?.currencyCode || 'USD';
|
|
249
|
+
variantTitle = getCleanVariantTitle(v);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<div
|
|
254
|
+
key={`${item.resourceId}_${displayId}`}
|
|
255
|
+
className="flex items-center justify-between"
|
|
256
|
+
>
|
|
257
|
+
<div className="flex items-center gap-2">
|
|
258
|
+
<div className="text-sm font-bold text-gray-700">
|
|
259
|
+
{variantTitle &&
|
|
260
|
+
variantTitle !== 'Default Title' && (
|
|
261
|
+
<span>{variantTitle}</span>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
{isPickupMode && !isService && (
|
|
265
|
+
<span className="inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-xs font-bold text-gray-800">
|
|
266
|
+
Store Pickup
|
|
267
|
+
</span>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<div className="flex items-center">
|
|
272
|
+
<div className="mr-6 text-right">
|
|
273
|
+
<p className="text-sm font-bold text-gray-900">
|
|
274
|
+
{price && parseFloat(price) > 0
|
|
275
|
+
? `${(parseFloat(price) * item.quantity).toFixed(2)} ${currency}`
|
|
276
|
+
: 'No Charge'}
|
|
277
|
+
</p>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
{isService ? (
|
|
281
|
+
<button
|
|
282
|
+
onClick={() =>
|
|
283
|
+
addQueue.set([
|
|
284
|
+
...addQueue.get(),
|
|
285
|
+
{
|
|
286
|
+
resourceId: item.resourceId,
|
|
287
|
+
action: 'remove',
|
|
288
|
+
variantId: item.variantId,
|
|
289
|
+
},
|
|
290
|
+
])
|
|
291
|
+
}
|
|
292
|
+
className="rounded-md border border-gray-300 px-3 py-1 text-sm font-bold text-gray-600 hover:bg-gray-100"
|
|
293
|
+
>
|
|
294
|
+
Remove
|
|
295
|
+
</button>
|
|
296
|
+
) : (
|
|
297
|
+
<div className="flex items-center rounded-md border border-gray-300">
|
|
298
|
+
<button
|
|
299
|
+
onClick={() =>
|
|
300
|
+
dispatchDualAction(item, 'remove')
|
|
301
|
+
}
|
|
302
|
+
className="px-3 py-1 text-gray-600 hover:bg-gray-100"
|
|
303
|
+
>
|
|
304
|
+
-
|
|
305
|
+
</button>
|
|
306
|
+
<span className="border-l border-r border-gray-300 px-3 py-1 text-gray-900">
|
|
307
|
+
{item.quantity}
|
|
308
|
+
</span>
|
|
309
|
+
<button
|
|
310
|
+
onClick={() =>
|
|
311
|
+
dispatchDualAction(item, 'add')
|
|
312
|
+
}
|
|
313
|
+
className="px-3 py-1 text-gray-600 hover:bg-gray-100"
|
|
314
|
+
>
|
|
315
|
+
+
|
|
316
|
+
</button>
|
|
317
|
+
</div>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
})}
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</li>
|
|
327
|
+
);
|
|
328
|
+
})}
|
|
329
|
+
</ul>
|
|
330
|
+
|
|
331
|
+
<div className="rounded-b-lg border-t border-gray-200 bg-gray-50 px-6 py-6">
|
|
332
|
+
<div className="flex justify-end">
|
|
333
|
+
<button
|
|
334
|
+
className="rounded-lg bg-black px-6 py-3 font-bold text-white transition-colors hover:bg-gray-800"
|
|
335
|
+
onClick={() => {
|
|
336
|
+
cartState.set(CART_STATES.CHECKOUT);
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
Proceed to Checkout
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
);
|
|
345
|
+
}
|