@wix/agent-skills 1.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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +29 -0
- package/skills/wix-app/SKILL.md +344 -0
- package/skills/wix-app/references/APP_IDENTIFIERS.md +35 -0
- package/skills/wix-app/references/APP_MARKET_REVIEW.md +311 -0
- package/skills/wix-app/references/APP_VALIDATION.md +171 -0
- package/skills/wix-app/references/BACKEND_API.md +233 -0
- package/skills/wix-app/references/BACKEND_EVENT.md +116 -0
- package/skills/wix-app/references/CODE_QUALITY.md +33 -0
- package/skills/wix-app/references/CUSTOM_ELEMENT_WIDGET.md +587 -0
- package/skills/wix-app/references/DASHBOARD_MENU_PLUGIN.md +157 -0
- package/skills/wix-app/references/DASHBOARD_MODAL.md +283 -0
- package/skills/wix-app/references/DASHBOARD_PAGE.md +403 -0
- package/skills/wix-app/references/DASHBOARD_PLUGIN.md +205 -0
- package/skills/wix-app/references/DATA_COLLECTION.md +451 -0
- package/skills/wix-app/references/DOCUMENTATION.md +42 -0
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +65 -0
- package/skills/wix-app/references/EMBEDDED_SCRIPT.md +468 -0
- package/skills/wix-app/references/EXTENSION_REGISTRATION.md +96 -0
- package/skills/wix-app/references/SERVICE_PLUGIN.md +235 -0
- package/skills/wix-app/references/SITE_PLUGIN.md +436 -0
- package/skills/wix-app/references/STORES_VERSIONING.md +334 -0
- package/skills/wix-app/references/backend-event/COMMON-EVENTS.md +84 -0
- package/skills/wix-app/references/custom-element-widget/SETTINGS_PANEL.md +345 -0
- package/skills/wix-app/references/dashboard-menu-plugin/blog-slots.md +71 -0
- package/skills/wix-app/references/dashboard-menu-plugin/bookings-slots.md +58 -0
- package/skills/wix-app/references/dashboard-menu-plugin/crm-slots.md +8 -0
- package/skills/wix-app/references/dashboard-menu-plugin/ecommerce-slots.md +17 -0
- package/skills/wix-app/references/dashboard-menu-plugin/events-slots.md +29 -0
- package/skills/wix-app/references/dashboard-menu-plugin/restaurants-slots.md +27 -0
- package/skills/wix-app/references/dashboard-menu-plugin/stores-slots.md +35 -0
- package/skills/wix-app/references/dashboard-page/API_SPEC.md +138 -0
- package/skills/wix-app/references/dashboard-page/DASHBOARD_API.md +573 -0
- package/skills/wix-app/references/dashboard-page/DYNAMIC_PARAMETERS.md +269 -0
- package/skills/wix-app/references/dashboard-page/ECOM_NAVIGATION.md +164 -0
- package/skills/wix-app/references/dashboard-plugin/SLOTS.md +294 -0
- package/skills/wix-app/references/data-collection/WIX_DATA.md +270 -0
- package/skills/wix-app/references/data-collection/extension-template.ts +107 -0
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +138 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +454 -0
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +165 -0
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +576 -0
- package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +170 -0
- package/skills/wix-app/references/editor-react-component/EDIT-FLOW.md +6 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +25 -0
- package/skills/wix-app/references/editor-react-component/PROPS-VS-CSS.md +203 -0
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +167 -0
- package/skills/wix-app/references/editor-react-component/REACT-PATTERNS.md +137 -0
- package/skills/wix-app/references/service-plugin/ADDITIONAL-FEES.md +112 -0
- package/skills/wix-app/references/service-plugin/BOOKINGS-STAFF-SORTING.md +118 -0
- package/skills/wix-app/references/service-plugin/DISCOUNT-TRIGGERS.md +87 -0
- package/skills/wix-app/references/service-plugin/GIFT-CARDS.md +86 -0
- package/skills/wix-app/references/service-plugin/PAYMENT-SETTINGS.md +135 -0
- package/skills/wix-app/references/service-plugin/SHIPPING-RATES.md +79 -0
- package/skills/wix-app/references/service-plugin/VALIDATIONS.md +68 -0
- package/skills/wix-app/references/site-plugin/EXAMPLES.md +311 -0
- package/skills/wix-app/references/site-plugin/SLOTS.md +263 -0
- package/skills/wix-design-system/SKILL.md +129 -0
- package/skills/wix-design-system/references/file-structure.md +56 -0
- package/skills/wix-design-system/scripts/wds.cjs +531 -0
- package/skills/wix-headless/SKILL.md +418 -0
- package/skills/wix-headless/references/DISCOVERY.md +332 -0
- package/skills/wix-headless/references/ORCHESTRATION.md +269 -0
- package/skills/wix-headless/references/SETUP.md +154 -0
- package/skills/wix-headless/references/blog/BLOG_CONTENT.md +152 -0
- package/skills/wix-headless/references/blog/BLOG_PAGES.md +299 -0
- package/skills/wix-headless/references/blog/BLOG_SETUP.md +218 -0
- package/skills/wix-headless/references/blog/INSTRUCTIONS.md +32 -0
- package/skills/wix-headless/references/cms/CMS_FOUNDATIONS.md +373 -0
- package/skills/wix-headless/references/cms/FAQ_KNOWLEDGE_BASE.md +229 -0
- package/skills/wix-headless/references/cms/INSTRUCTIONS.md +40 -0
- package/skills/wix-headless/references/cms/PORTFOLIO.md +454 -0
- package/skills/wix-headless/references/cms/RESOURCE_LIBRARY.md +356 -0
- package/skills/wix-headless/references/cms/TEAM_DIRECTORY.md +299 -0
- package/skills/wix-headless/references/commands/install-app.md +42 -0
- package/skills/wix-headless/references/commands/known-apps.json +5 -0
- package/skills/wix-headless/references/commands/mcp-bootstrap.md +28 -0
- package/skills/wix-headless/references/designer/INSTRUCTIONS.md +828 -0
- package/skills/wix-headless/references/ecom/CART_PAGES.md +120 -0
- package/skills/wix-headless/references/ecom/CART_WIRING.md +103 -0
- package/skills/wix-headless/references/ecom/COMPONENTS_CSS.md +132 -0
- package/skills/wix-headless/references/ecom/INSTRUCTIONS.md +59 -0
- package/skills/wix-headless/references/forms/CONTACT_FORM.md +477 -0
- package/skills/wix-headless/references/forms/INSTRUCTIONS.md +43 -0
- package/skills/wix-headless/references/gift-cards/COMPONENTS.md +119 -0
- package/skills/wix-headless/references/gift-cards/INSTRUCTIONS.md +51 -0
- package/skills/wix-headless/references/gift-cards/PAGES.md +129 -0
- package/skills/wix-headless/references/images/INSTRUCTIONS.md +359 -0
- package/skills/wix-headless/references/shared/BUILD_NOISE.md +8 -0
- package/skills/wix-headless/references/shared/IMAGE_GENERATION.md +185 -0
- package/skills/wix-headless/references/shared/IMPLEMENTER.md +155 -0
- package/skills/wix-headless/references/shared/MCP_PREFIX.md +105 -0
- package/skills/wix-headless/references/shared/PRODUCTION_SHARP_EDGES.md +97 -0
- package/skills/wix-headless/references/shared/RETURN_CONTRACT.md +423 -0
- package/skills/wix-headless/references/shared/STYLING.md +117 -0
- package/skills/wix-headless/references/stores/BACK_IN_STOCK.md +178 -0
- package/skills/wix-headless/references/stores/CATEGORY_PAGES.md +86 -0
- package/skills/wix-headless/references/stores/COMPONENTS_CSS.md +129 -0
- package/skills/wix-headless/references/stores/HOME_AND_NAV.md +227 -0
- package/skills/wix-headless/references/stores/INSTRUCTIONS.md +96 -0
- package/skills/wix-headless/references/stores/PRODUCT_CATALOG_DATA.md +249 -0
- package/skills/wix-headless/references/stores/PRODUCT_PAGES.md +213 -0
- package/skills/wix-headless/references/stores/SHARED_WIRING.md +164 -0
- package/skills/wix-headless/references/verticals/_schema.json +160 -0
- package/skills/wix-headless/references/verticals/_schema.md +185 -0
- package/skills/wix-headless/references/verticals/blog.md +125 -0
- package/skills/wix-headless/references/verticals/cms.md +121 -0
- package/skills/wix-headless/references/verticals/ecom.md +96 -0
- package/skills/wix-headless/references/verticals/forms.md +106 -0
- package/skills/wix-headless/references/verticals/gift-cards.md +121 -0
- package/skills/wix-headless/references/verticals/stores.md +167 -0
- package/skills/wix-headless/scripts/check-manifest.mjs +198 -0
- package/skills/wix-headless/scripts/copy-utility-templates.mjs +153 -0
- package/skills/wix-headless/scripts/emit-design-tokens.mjs +77 -0
- package/skills/wix-headless/scripts/finalize-run-json.mjs +141 -0
- package/skills/wix-headless/scripts/init-site-json.mjs +63 -0
- package/skills/wix-headless/scripts/patch-decorative-slots.mjs +124 -0
- package/skills/wix-headless/scripts/plan-entity-image-waves.mjs +190 -0
- package/skills/wix-headless/scripts/preview.sh +45 -0
- package/skills/wix-headless/scripts/release.sh +73 -0
- package/skills/wix-headless/scripts/scaffold.sh +53 -0
- package/skills/wix-headless/scripts/seed-utilities.sh +55 -0
- package/skills/wix-headless/shared-utilities/analytics.ts +40 -0
- package/skills/wix-headless/shared-utilities/ricos.ts +179 -0
- package/skills/wix-headless/shared-utilities/wix-image.ts +72 -0
- package/skills/wix-headless/templates/ecom/CartBadge.tsx +84 -0
- package/skills/wix-headless/templates/ecom/CartView.tsx +543 -0
- package/skills/wix-headless/templates/ecom/cart.astro +13 -0
- package/skills/wix-headless/templates/ecom/components-ecom.css +215 -0
- package/skills/wix-headless/templates/ecom/discounts.ts +71 -0
- package/skills/wix-headless/templates/ecom/thank-you.astro +24 -0
- package/skills/wix-headless/templates/gift-cards/GiftCardPurchase.tsx +172 -0
- package/skills/wix-headless/templates/gift-cards/_home-teaser-snippet.astro +42 -0
- package/skills/wix-headless/templates/gift-cards/_nav-snippet.astro +11 -0
- package/skills/wix-headless/templates/gift-cards/components-gift-cards.css +219 -0
- package/skills/wix-headless/templates/gift-cards/gift-cards.astro +51 -0
- package/skills/wix-headless/templates/gift-cards/gift-cards.ts +75 -0
- package/skills/wix-headless/templates/stores/AddToCartButton.tsx +77 -0
- package/skills/wix-headless/templates/stores/BackInStockForm.tsx +176 -0
- package/skills/wix-headless/templates/stores/CategoryRail.astro +69 -0
- package/skills/wix-headless/templates/stores/ProductCard.astro +79 -0
- package/skills/wix-headless/templates/stores/ProductPurchase.tsx +397 -0
- package/skills/wix-headless/templates/stores/SeoTags.astro +21 -0
- package/skills/wix-headless/templates/stores/back-in-stock.ts +67 -0
- package/skills/wix-headless/templates/stores/categories.ts +155 -0
- package/skills/wix-headless/templates/stores/category/[slug].astro +130 -0
- package/skills/wix-headless/templates/stores/components-stores.css +295 -0
- package/skills/wix-headless/templates/stores/products/[slug].astro +128 -0
- package/skills/wix-headless/templates/stores/products/index.astro +101 -0
- package/skills/wix-manage/SKILL.md +280 -0
- package/skills/wix-manage/references/app-installation/install-wix-apps.md +155 -0
- package/skills/wix-manage/references/app-installation/list-installed-apps.md +124 -0
- package/skills/wix-manage/references/blog/how-to-create-blog-posts.md +226 -0
- package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +95 -0
- package/skills/wix-manage/references/bookings/booking-system-integration-gaps.md +416 -0
- package/skills/wix-manage/references/bookings/bookings-staff-setup.md +255 -0
- package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +462 -0
- package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +278 -0
- package/skills/wix-manage/references/bookings/external-calendar-integration.md +258 -0
- package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +231 -0
- package/skills/wix-manage/references/calendar/configure-default-business-hours.md +226 -0
- package/skills/wix-manage/references/cms/cms-data-items-crud.md +422 -0
- package/skills/wix-manage/references/cms/cms-data-operations-extended.md +263 -0
- package/skills/wix-manage/references/cms/cms-ecommerce-catalog-integration.md +136 -0
- package/skills/wix-manage/references/cms/cms-references-and-relationships.md +160 -0
- package/skills/wix-manage/references/cms/cms-schema-management.md +143 -0
- package/skills/wix-manage/references/contacts/bulk-delete-contacts.md +73 -0
- package/skills/wix-manage/references/contacts/bulk-label-and-unlabel-contacts.md +67 -0
- package/skills/wix-manage/references/domains/domain-search-and-purchase.md +434 -0
- package/skills/wix-manage/references/ecommerce/api-recommendation-tracking.md +420 -0
- package/skills/wix-manage/references/ecommerce/flow-add-free-shipping.md +267 -0
- package/skills/wix-manage/references/ecommerce/flow-bundle-and-save.md +290 -0
- package/skills/wix-manage/references/ecommerce/flow-fix-coverage-gaps.md +300 -0
- package/skills/wix-manage/references/ecommerce/flow-optimize-shipping-rates.md +364 -0
- package/skills/wix-manage/references/ecommerce/flow-seasonal-promotion.md +315 -0
- package/skills/wix-manage/references/ecommerce/flow-stock-mover.md +308 -0
- package/skills/wix-manage/references/ecommerce/flow-upsell-boost.md +271 -0
- package/skills/wix-manage/references/ecommerce/goal-clear-inventory.md +118 -0
- package/skills/wix-manage/references/ecommerce/goal-drive-cross-sells.md +122 -0
- package/skills/wix-manage/references/ecommerce/goal-increase-aov.md +132 -0
- package/skills/wix-manage/references/ecommerce/goal-reduce-cart-abandonment.md +148 -0
- package/skills/wix-manage/references/ecommerce/goal-seasonal-revenue.md +146 -0
- package/skills/wix-manage/references/ecommerce/guardrail-discount-conflicts.md +118 -0
- package/skills/wix-manage/references/ecommerce/guardrail-margin-protection.md +88 -0
- package/skills/wix-manage/references/ecommerce/guardrail-rate-pricing-sanity.md +108 -0
- package/skills/wix-manage/references/ecommerce/guardrail-shipping-health.md +124 -0
- package/skills/wix-manage/references/ecommerce/recipe-apply-shipping-recommendations.md +299 -0
- package/skills/wix-manage/references/ecommerce/recommend-ecommerce-strategy.md +582 -0
- package/skills/wix-manage/references/ecommerce/setup-coupons.md +338 -0
- package/skills/wix-manage/references/ecommerce/setup-discount-rules.md +444 -0
- package/skills/wix-manage/references/ecommerce/setup-shipping-rates.md +86 -0
- package/skills/wix-manage/references/ecommerce/setup-shipping-regions.md +56 -0
- package/skills/wix-manage/references/ecommerce/setup-store-pickup-location.md +209 -0
- package/skills/wix-manage/references/ecommerce/skill-graph.md +139 -0
- package/skills/wix-manage/references/ecommerce/troubleshoot-checkout-delivery-dropoff.md +124 -0
- package/skills/wix-manage/references/ecommerce/troubleshoot-discount-not-applying.md +95 -0
- package/skills/wix-manage/references/events/list-events.md +203 -0
- package/skills/wix-manage/references/forms/create-form.md +260 -0
- package/skills/wix-manage/references/get-paid/create-payment-links.md +212 -0
- package/skills/wix-manage/references/get-paid/how-to-setup-wix-payments.md +57 -0
- package/skills/wix-manage/references/get-paid/payment-links-for-bookings.md +101 -0
- package/skills/wix-manage/references/media/upload-media-to-wix.md +216 -0
- package/skills/wix-manage/references/pricing-plans/create-and-update-pricing-plans.md +77 -0
- package/skills/wix-manage/references/pricing-plans/pricing-plans-bookings-integration.md +166 -0
- package/skills/wix-manage/references/restaurants/wix-restaurants-setup.md +336 -0
- package/skills/wix-manage/references/rich-content/ricos-converter-service.md +295 -0
- package/skills/wix-manage/references/site-properties/change-payment-currency-site-properties.md +59 -0
- package/skills/wix-manage/references/sites/create-site-from-template.md +183 -0
- package/skills/wix-manage/references/sites/query-sites.md +125 -0
- package/skills/wix-manage/references/stores/add-store-pages-to-site.md +57 -0
- package/skills/wix-manage/references/stores/bulk-create-products-with-options.md +1041 -0
- package/skills/wix-manage/references/stores/create-product-catalog-v1.md +125 -0
- package/skills/wix-manage/references/stores/create-product-from-image.md +824 -0
- package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +566 -0
- package/skills/wix-manage/references/stores/find-products-query-and-search-catalog-v3.md +194 -0
- package/skills/wix-manage/references/stores/query-products-catalog-v1.md +80 -0
- package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +99 -0
- package/skills/wix-manage/references/stores/update-product-pre-order.md +213 -0
- package/skills/wix-manage/references/stores/update-product-with-options.md +318 -0
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { currentCart } from "@wix/ecom";
|
|
3
|
+
import { redirects } from "@wix/redirects";
|
|
4
|
+
import { trackEvent } from "../utils/analytics";
|
|
5
|
+
|
|
6
|
+
// ── Types ──
|
|
7
|
+
|
|
8
|
+
interface DescriptionLine {
|
|
9
|
+
name?: { translated?: string };
|
|
10
|
+
plainText?: { translated?: string };
|
|
11
|
+
colorInfo?: { translated?: string; code?: string };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Availability {
|
|
15
|
+
status?: string; // "AVAILABLE" | "NOT_AVAILABLE" | "NOT_FOUND" | "PARTIALLY_AVAILABLE"
|
|
16
|
+
quantityAvailable?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Modifier {
|
|
20
|
+
label?: { translated?: string };
|
|
21
|
+
quantity?: number;
|
|
22
|
+
price?: { formattedConvertedAmount?: string };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ModifierGroup {
|
|
26
|
+
name?: { translated?: string };
|
|
27
|
+
modifiers?: Modifier[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Local structural type. The SDK's lineItem types use `string | null` for
|
|
31
|
+
// translated fields; we keep this local shape narrower for ergonomic
|
|
32
|
+
// destructuring and use `as unknown as LineItem[]` at SDK boundaries.
|
|
33
|
+
// Narrowing is safe because we null-check each field at the use site.
|
|
34
|
+
interface LineItem {
|
|
35
|
+
_id?: string | null;
|
|
36
|
+
productName?: { translated?: string };
|
|
37
|
+
quantity?: number;
|
|
38
|
+
price?: { amount?: string; formattedConvertedAmount?: string };
|
|
39
|
+
fullPrice?: { formattedConvertedAmount?: string };
|
|
40
|
+
lineItemPrice?: { amount?: string; formattedConvertedAmount?: string };
|
|
41
|
+
image?: string; // "wix:image://v1/<mediaId>/..." — always a string, NOT an object
|
|
42
|
+
catalogReference?: { catalogItemId?: string };
|
|
43
|
+
descriptionLines?: DescriptionLine[];
|
|
44
|
+
availability?: Availability;
|
|
45
|
+
modifierGroups?: ModifierGroup[];
|
|
46
|
+
// The SDK returns `url` as an absolute URL string (e.g.
|
|
47
|
+
// "https://<site>.wixsite.com/<name>/product-page/<slug>"); REST returns
|
|
48
|
+
// an object `{ relativePath, url }`. Handle both in `resolveProductHref`.
|
|
49
|
+
url?: string | { relativePath?: string; url?: string };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface CartSummary {
|
|
53
|
+
subtotal?: string;
|
|
54
|
+
discount?: string;
|
|
55
|
+
total?: string;
|
|
56
|
+
discountNames: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// No server-side props — cart is per-visitor and must not break SSR caching.
|
|
60
|
+
// Always fetched client-side on mount.
|
|
61
|
+
|
|
62
|
+
// ── Helpers ──
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract display text from a description line. Handles both plainText and
|
|
66
|
+
* colorInfo types. colorInfo lines carry a human-readable name (e.g. "Dark Blue")
|
|
67
|
+
* alongside the hex code — use the translated name, not the code.
|
|
68
|
+
* Renders as "Title: Value", or just the title/value if one is missing.
|
|
69
|
+
*/
|
|
70
|
+
function formatDescriptionLine(line: DescriptionLine): string {
|
|
71
|
+
const title = line.name?.translated;
|
|
72
|
+
const value = line.plainText?.translated ?? line.colorInfo?.translated;
|
|
73
|
+
if (title && value) return `${title}: ${value}`;
|
|
74
|
+
return title ?? value ?? "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a cart line item image to a renderable CDN URL.
|
|
79
|
+
* The ecom SDK returns lineItem.image as a plain string "wix:image://v1/<mediaId>/..."
|
|
80
|
+
* (NOT an object { url } despite what the REST docs say).
|
|
81
|
+
* Parse it to a Wix static CDN URL with resizing.
|
|
82
|
+
*/
|
|
83
|
+
function resolveCartImage(image: string | undefined, width: number, height: number): string | undefined {
|
|
84
|
+
if (!image) return undefined;
|
|
85
|
+
if (image.startsWith("wix:image://")) {
|
|
86
|
+
const match = image.match(/wix:image:\/\/v1\/([^/]+)/);
|
|
87
|
+
if (match) {
|
|
88
|
+
return `https://static.wixstatic.com/media/${match[1]}/v1/fill/w_${width},h_${height},al_c,q_80/${match[1]}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Direct CDN URL — use as-is
|
|
92
|
+
return image;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether a line item is unavailable (out of stock, deleted, or not found) */
|
|
96
|
+
function isItemUnavailable(item: LineItem): boolean {
|
|
97
|
+
const status = item.availability?.status;
|
|
98
|
+
return status === "NOT_AVAILABLE" || status === "NOT_FOUND";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build a same-site link to the product detail page for this line item.
|
|
103
|
+
*
|
|
104
|
+
* `item.url` shape varies by caller:
|
|
105
|
+
* - `@wix/ecom` currentCart SDK: absolute URL string like
|
|
106
|
+
* "https://<site>.wixsite.com/<name>/product-page/<slug>"
|
|
107
|
+
* - REST: object `{ relativePath: "/product-page/<slug>", url: "..." }`
|
|
108
|
+
*
|
|
109
|
+
* Extract the slug after `/product-page/` either way and rewrite to the
|
|
110
|
+
* headless `/products/<slug>` route. Returns undefined when the URL is
|
|
111
|
+
* absent or doesn't follow the Stores route pattern.
|
|
112
|
+
*/
|
|
113
|
+
function resolveProductHref(item: LineItem): string | undefined {
|
|
114
|
+
const url = item.url;
|
|
115
|
+
if (!url) return undefined;
|
|
116
|
+
const str = typeof url === "string" ? url : (url.relativePath ?? url.url);
|
|
117
|
+
if (!str) return undefined;
|
|
118
|
+
const match = str.match(/\/product-page\/([^/?#]+)/);
|
|
119
|
+
return match ? `/products/${match[1]}` : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Cached cart snapshot so re-navigation to /cart renders the previous state
|
|
123
|
+
// instantly, while the authoritative fetch runs in the background. Kept in
|
|
124
|
+
// sessionStorage (per-tab, cleared on close) so logout/device-sharing never
|
|
125
|
+
// shows a stale cart to a different identity.
|
|
126
|
+
const CART_CACHE_KEY = "cart:last-snapshot";
|
|
127
|
+
type CartSnapshot = { lineItems: LineItem[]; summary: CartSummary };
|
|
128
|
+
|
|
129
|
+
function readCartSnapshot(): CartSnapshot | null {
|
|
130
|
+
if (typeof window === "undefined") return null;
|
|
131
|
+
try {
|
|
132
|
+
const raw = window.sessionStorage.getItem(CART_CACHE_KEY);
|
|
133
|
+
if (!raw) return null;
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
if (!Array.isArray(parsed?.lineItems)) return null;
|
|
136
|
+
return parsed as CartSnapshot;
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function writeCartSnapshot(snapshot: CartSnapshot): void {
|
|
143
|
+
if (typeof window === "undefined") return;
|
|
144
|
+
try {
|
|
145
|
+
window.sessionStorage.setItem(CART_CACHE_KEY, JSON.stringify(snapshot));
|
|
146
|
+
} catch {
|
|
147
|
+
/* storage full / denied — ignore, fall back to fetch-only */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extract cart-level totals + applied discount names from a cart response.
|
|
153
|
+
*
|
|
154
|
+
* `priceSummary` exposes:
|
|
155
|
+
* - `subtotal` — sum of line items BEFORE cart-level discounts (each line already
|
|
156
|
+
* reflects per-line automatic discounts, so this is "what the items cost")
|
|
157
|
+
* - `discount` — cart-level discount amount (automatic discounts, promo codes)
|
|
158
|
+
* - `total` — subtotal − discount + shipping + tax (shipping + tax are
|
|
159
|
+
* typically zero before checkout is created)
|
|
160
|
+
*
|
|
161
|
+
* `appliedDiscounts[]` lists every discount Wix applied; `discountName` is the
|
|
162
|
+
* merchant-facing label (e.g. "Spring Sale 20% off").
|
|
163
|
+
*
|
|
164
|
+
* Fallback: if `priceSummary.subtotal` is missing (some early-stage carts return
|
|
165
|
+
* an empty summary until checkout is created), compute it client-side from
|
|
166
|
+
* `lineItemPrice.amount` × rows. Shows something instead of a blank slot.
|
|
167
|
+
*/
|
|
168
|
+
function extractSummary(
|
|
169
|
+
// `lineItems` is `any[]` — the SDK's LineItem shape (string | null on
|
|
170
|
+
// translated fields) is structurally incompatible with our narrower local
|
|
171
|
+
// LineItem. We only read `lineItemPrice.amount` here, which is loose anyway.
|
|
172
|
+
cart: { priceSummary?: any; appliedDiscounts?: any[]; lineItems?: any[]; currency?: string } | undefined,
|
|
173
|
+
): CartSummary {
|
|
174
|
+
const ps = cart?.priceSummary;
|
|
175
|
+
let subtotal = ps?.subtotal?.formattedConvertedAmount as string | undefined;
|
|
176
|
+
const discount = ps?.discount?.formattedConvertedAmount as string | undefined;
|
|
177
|
+
const total = ps?.total?.formattedConvertedAmount as string | undefined;
|
|
178
|
+
|
|
179
|
+
// Fallback subtotal from line items when priceSummary is empty.
|
|
180
|
+
if (!subtotal && cart?.lineItems?.length) {
|
|
181
|
+
const sum = cart.lineItems.reduce((acc, item) => {
|
|
182
|
+
const amt = item.lineItemPrice?.amount;
|
|
183
|
+
return amt ? acc + Number(amt) : acc;
|
|
184
|
+
}, 0);
|
|
185
|
+
if (sum > 0) {
|
|
186
|
+
try {
|
|
187
|
+
subtotal = new Intl.NumberFormat(undefined, {
|
|
188
|
+
style: "currency",
|
|
189
|
+
currency: cart.currency ?? "USD",
|
|
190
|
+
}).format(sum);
|
|
191
|
+
} catch {
|
|
192
|
+
subtotal = sum.toFixed(2);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const discountNames = (cart?.appliedDiscounts ?? [])
|
|
198
|
+
.map((d: any) => d?.discountName || d?.coupon?.name || d?.merchantDiscount?.discountName)
|
|
199
|
+
.filter(Boolean);
|
|
200
|
+
|
|
201
|
+
return { subtotal, discount, total, discountNames };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Component ──
|
|
205
|
+
|
|
206
|
+
export default function CartView() {
|
|
207
|
+
// Hydrate from the last cart snapshot so re-navigating to /cart renders the
|
|
208
|
+
// previous state immediately — no empty/loading flash. The authoritative
|
|
209
|
+
// fetch in useEffect below reconciles shortly after.
|
|
210
|
+
const cached = typeof window !== "undefined" ? readCartSnapshot() : null;
|
|
211
|
+
const [items, setItems] = useState<LineItem[]>(cached?.lineItems ?? []);
|
|
212
|
+
const [summary, setSummary] = useState<CartSummary>(
|
|
213
|
+
cached?.summary ?? { discountNames: [] },
|
|
214
|
+
);
|
|
215
|
+
// `loading` is only true when there's no cached snapshot to render from.
|
|
216
|
+
const [loading, setLoading] = useState(!cached);
|
|
217
|
+
const [checkingOut, setCheckingOut] = useState(false);
|
|
218
|
+
const qtyTimers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
|
|
219
|
+
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
loadCart();
|
|
222
|
+
}, []);
|
|
223
|
+
|
|
224
|
+
// getCurrentCart() returns the cart object directly — NOT wrapped in { cart }.
|
|
225
|
+
// Totals + applied discounts all live under priceSummary + appliedDiscounts.
|
|
226
|
+
const loadCart = async () => {
|
|
227
|
+
try {
|
|
228
|
+
const cart = await currentCart.getCurrentCart();
|
|
229
|
+
const lineItems = (cart.lineItems as unknown as LineItem[]) ?? [];
|
|
230
|
+
const nextSummary = extractSummary(cart);
|
|
231
|
+
setItems(lineItems);
|
|
232
|
+
setSummary(nextSummary);
|
|
233
|
+
writeCartSnapshot({ lineItems, summary: nextSummary });
|
|
234
|
+
} catch {
|
|
235
|
+
// Leave any cached state in place on transient errors.
|
|
236
|
+
if (!cached) {
|
|
237
|
+
setItems([]);
|
|
238
|
+
setSummary({ discountNames: [] });
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
setLoading(false);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const handleUpdateQuantity = (itemId: string, quantity: number) => {
|
|
246
|
+
// Optimistic: update local state immediately
|
|
247
|
+
setItems((prev) =>
|
|
248
|
+
prev.map((it) => (it._id === itemId ? { ...it, quantity } : it))
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Debounce API call — rapid clicks coalesce into one request
|
|
252
|
+
const prev = qtyTimers.current.get(itemId);
|
|
253
|
+
if (prev) clearTimeout(prev);
|
|
254
|
+
qtyTimers.current.set(
|
|
255
|
+
itemId,
|
|
256
|
+
setTimeout(async () => {
|
|
257
|
+
qtyTimers.current.delete(itemId);
|
|
258
|
+
try {
|
|
259
|
+
const { cart } = await currentCart.updateCurrentCartLineItemQuantity([
|
|
260
|
+
{ _id: itemId, quantity },
|
|
261
|
+
]);
|
|
262
|
+
if (!cart) return;
|
|
263
|
+
window.dispatchEvent(new CustomEvent("cart-updated", { detail: { cart } }));
|
|
264
|
+
const nextItems = (cart.lineItems as unknown as LineItem[]) ?? [];
|
|
265
|
+
const nextSummary = extractSummary(cart);
|
|
266
|
+
setItems(nextItems);
|
|
267
|
+
setSummary(nextSummary);
|
|
268
|
+
writeCartSnapshot({ lineItems: nextItems, summary: nextSummary });
|
|
269
|
+
} catch {
|
|
270
|
+
await loadCart();
|
|
271
|
+
}
|
|
272
|
+
}, 300)
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const handleRemoveItem = async (itemId: string) => {
|
|
277
|
+
const removed = items.find((item) => item._id === itemId);
|
|
278
|
+
if (removed) {
|
|
279
|
+
trackEvent("RemoveFromCart", {
|
|
280
|
+
id: removed.catalogReference?.catalogItemId ?? removed._id,
|
|
281
|
+
name: removed.productName?.translated,
|
|
282
|
+
price: removed.price?.amount ? Number(removed.price.amount) : undefined,
|
|
283
|
+
quantity: removed.quantity,
|
|
284
|
+
origin: "Cart",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Optimistic: remove from local state immediately
|
|
289
|
+
setItems((prev) => prev.filter((it) => it._id !== itemId));
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
const { cart } = await currentCart.removeLineItemsFromCurrentCart([itemId]);
|
|
293
|
+
if (!cart) return;
|
|
294
|
+
window.dispatchEvent(new CustomEvent("cart-updated", { detail: { cart } }));
|
|
295
|
+
const nextItems = (cart.lineItems as unknown as LineItem[]) ?? [];
|
|
296
|
+
const nextSummary = extractSummary(cart);
|
|
297
|
+
setItems(nextItems);
|
|
298
|
+
setSummary(nextSummary);
|
|
299
|
+
writeCartSnapshot({ lineItems: nextItems, summary: nextSummary });
|
|
300
|
+
} catch {
|
|
301
|
+
await loadCart();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const handleCheckout = async () => {
|
|
306
|
+
setCheckingOut(true);
|
|
307
|
+
try {
|
|
308
|
+
trackEvent("InitiateCheckout", {
|
|
309
|
+
contents: items.map((item) => ({
|
|
310
|
+
id: item.catalogReference?.catalogItemId ?? item._id,
|
|
311
|
+
name: item.productName?.translated,
|
|
312
|
+
price: item.price?.amount ? Number(item.price.amount) : undefined,
|
|
313
|
+
quantity: item.quantity,
|
|
314
|
+
})),
|
|
315
|
+
origin: "Cart",
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const { checkoutId } = await currentCart.createCheckoutFromCurrentCart({
|
|
319
|
+
channelType: currentCart.ChannelType.WEB,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const { redirectSession } = await redirects.createRedirectSession({
|
|
323
|
+
ecomCheckout: { checkoutId },
|
|
324
|
+
callbacks: {
|
|
325
|
+
postFlowUrl: window.location.origin,
|
|
326
|
+
thankYouPageUrl: `${window.location.origin}/thank-you`,
|
|
327
|
+
cartPageUrl: `${window.location.origin}/cart`,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (redirectSession?.fullUrl) {
|
|
332
|
+
window.location.href = redirectSession.fullUrl;
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
setCheckingOut(false);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// ── Render ──
|
|
340
|
+
|
|
341
|
+
if (loading) {
|
|
342
|
+
return <p className="cart-empty">Loading cart...</p>;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (items.length === 0) {
|
|
346
|
+
return (
|
|
347
|
+
<div className="cart-empty">
|
|
348
|
+
<p>Your cart is empty.</p>
|
|
349
|
+
<a href="/products" className="checkout-btn">Browse Products</a>
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const hasUnavailable = items.some(isItemUnavailable);
|
|
355
|
+
|
|
356
|
+
return (
|
|
357
|
+
<div className="grid grid-cols-1 md:grid-cols-[1fr_22rem] gap-2xl md:gap-3xl md:items-start">
|
|
358
|
+
{/* ── Left column: line items ── */}
|
|
359
|
+
<div className="flex flex-col gap-lg">
|
|
360
|
+
{items.map((item) => {
|
|
361
|
+
const unavailable = isItemUnavailable(item);
|
|
362
|
+
const maxQty = item.availability?.quantityAvailable ?? 99;
|
|
363
|
+
const hasDiscount =
|
|
364
|
+
item.fullPrice?.formattedConvertedAmount &&
|
|
365
|
+
item.fullPrice.formattedConvertedAmount !== item.price?.formattedConvertedAmount;
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<div
|
|
369
|
+
key={item._id}
|
|
370
|
+
className={`flex gap-lg pb-lg border-b border-rule last:border-0${unavailable ? " unavailable" : ""}`}
|
|
371
|
+
>
|
|
372
|
+
{/* Product image + name link back to the product detail page
|
|
373
|
+
for available items. Unavailable lines render as plain text. */}
|
|
374
|
+
{(() => {
|
|
375
|
+
const productHref = !unavailable ? resolveProductHref(item) : undefined;
|
|
376
|
+
const imgSrc = resolveCartImage(item.image, 160, 160);
|
|
377
|
+
const imageNode = imgSrc ? (
|
|
378
|
+
<img src={imgSrc} alt={item.productName?.translated ?? ""} className="cart-item-image" />
|
|
379
|
+
) : (
|
|
380
|
+
<div className="cart-item-image cart-item-image-placeholder" aria-hidden="true" />
|
|
381
|
+
);
|
|
382
|
+
return productHref ? (
|
|
383
|
+
<a href={productHref} className="cart-item-image-link" aria-label={item.productName?.translated ?? "Product"}>
|
|
384
|
+
{imageNode}
|
|
385
|
+
</a>
|
|
386
|
+
) : imageNode;
|
|
387
|
+
})()}
|
|
388
|
+
|
|
389
|
+
<div className="cart-item-info">
|
|
390
|
+
<h3 className="cart-item-name">
|
|
391
|
+
{(() => {
|
|
392
|
+
const productHref = !unavailable ? resolveProductHref(item) : undefined;
|
|
393
|
+
return productHref ? (
|
|
394
|
+
<a href={productHref} className="cart-item-name-link">
|
|
395
|
+
{item.productName?.translated}
|
|
396
|
+
</a>
|
|
397
|
+
) : item.productName?.translated;
|
|
398
|
+
})()}
|
|
399
|
+
</h3>
|
|
400
|
+
|
|
401
|
+
{/* Description lines — option selections (Size, Color, etc.)
|
|
402
|
+
Both plainText and colorInfo types are handled by formatDescriptionLine. */}
|
|
403
|
+
{(item.descriptionLines ?? []).map((line, i) => {
|
|
404
|
+
const text = formatDescriptionLine(line);
|
|
405
|
+
return text ? (
|
|
406
|
+
<p key={i} className="cart-item-option">{text}</p>
|
|
407
|
+
) : null;
|
|
408
|
+
})}
|
|
409
|
+
|
|
410
|
+
{/* Modifier groups — extras/add-ons */}
|
|
411
|
+
{(item.modifierGroups ?? []).map((group, gi) => (
|
|
412
|
+
<div key={gi} className="cart-item-modifiers">
|
|
413
|
+
{(group.modifiers ?? []).map((mod, mi) => (
|
|
414
|
+
<p key={mi} className="cart-item-option">
|
|
415
|
+
{mod.label?.translated}
|
|
416
|
+
{mod.quantity && mod.quantity > 1 ? ` ×${mod.quantity}` : ""}
|
|
417
|
+
{mod.price?.formattedConvertedAmount
|
|
418
|
+
? ` (+${mod.price.formattedConvertedAmount})`
|
|
419
|
+
: ""}
|
|
420
|
+
</p>
|
|
421
|
+
))}
|
|
422
|
+
</div>
|
|
423
|
+
))}
|
|
424
|
+
|
|
425
|
+
{/* Quantity selector or unavailable label */}
|
|
426
|
+
{unavailable ? (
|
|
427
|
+
<p className="cart-item-unavailable">
|
|
428
|
+
{item.availability?.status === "NOT_FOUND"
|
|
429
|
+
? "This item is no longer available"
|
|
430
|
+
: "Out of Stock"}
|
|
431
|
+
</p>
|
|
432
|
+
) : (
|
|
433
|
+
<div className="cart-item-qty">
|
|
434
|
+
<button
|
|
435
|
+
className="qty-btn"
|
|
436
|
+
disabled={!item.quantity || item.quantity <= 1}
|
|
437
|
+
onClick={() =>
|
|
438
|
+
item._id && handleUpdateQuantity(item._id, (item.quantity ?? 1) - 1)
|
|
439
|
+
}
|
|
440
|
+
>
|
|
441
|
+
−
|
|
442
|
+
</button>
|
|
443
|
+
<span className="qty-value">{item.quantity}</span>
|
|
444
|
+
<button
|
|
445
|
+
className="qty-btn"
|
|
446
|
+
disabled={(item.quantity ?? 0) >= maxQty}
|
|
447
|
+
onClick={() =>
|
|
448
|
+
item._id && handleUpdateQuantity(item._id, (item.quantity ?? 1) + 1)
|
|
449
|
+
}
|
|
450
|
+
>
|
|
451
|
+
+
|
|
452
|
+
</button>
|
|
453
|
+
</div>
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
{/* Price display — THREE fields, each with a different meaning:
|
|
458
|
+
- fullPrice: per-unit price BEFORE discount (strikethrough when discounted)
|
|
459
|
+
- price: per-unit price AFTER discount (the actual unit price)
|
|
460
|
+
- lineItemPrice: total for the line (price × quantity)
|
|
461
|
+
Show fullPrice and price side by side (both per-unit).
|
|
462
|
+
Show lineItemPrice below ONLY when qty > 1 (otherwise it equals price). */}
|
|
463
|
+
<div className="cart-item-actions">
|
|
464
|
+
<div className="cart-item-prices">
|
|
465
|
+
{hasDiscount && (
|
|
466
|
+
<span className="cart-item-full-price">{item.fullPrice!.formattedConvertedAmount}</span>
|
|
467
|
+
)}
|
|
468
|
+
<span className="cart-item-unit-price">{item.price?.formattedConvertedAmount}</span>
|
|
469
|
+
</div>
|
|
470
|
+
{(item.quantity ?? 1) > 1 && (
|
|
471
|
+
<p className="cart-item-line-total">
|
|
472
|
+
{item.lineItemPrice?.formattedConvertedAmount}
|
|
473
|
+
</p>
|
|
474
|
+
)}
|
|
475
|
+
<button
|
|
476
|
+
onClick={() => item._id && handleRemoveItem(item._id)}
|
|
477
|
+
className="cart-item-remove"
|
|
478
|
+
>
|
|
479
|
+
Remove
|
|
480
|
+
</button>
|
|
481
|
+
</div>
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
})}
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
{/* ── Right column: order summary ──
|
|
488
|
+
Always render `Subtotal`. Three ways discounts surface — all values
|
|
489
|
+
come from the API, we don't compute amounts client-side:
|
|
490
|
+
1. Cart-level discount with amount (coupons, cart-level promos) →
|
|
491
|
+
`priceSummary.discount` populated → render `cart-discount` row
|
|
492
|
+
with amount + optional name.
|
|
493
|
+
2. Applied-promotion name only (common for line-item automatic
|
|
494
|
+
discounts, where `priceSummary.discount` stays empty but
|
|
495
|
+
`appliedDiscounts[].discountName` carries the promo label) →
|
|
496
|
+
render `cart-applied-discounts` row: name only, no amount.
|
|
497
|
+
Per-line savings show inline via fullPrice/price strikethrough.
|
|
498
|
+
3. No discount → no row.
|
|
499
|
+
Render `Total` when it differs from subtotal.
|
|
500
|
+
See extractSummary() above. */}
|
|
501
|
+
<div className="cart-summary">
|
|
502
|
+
<div className="flex justify-between items-baseline">
|
|
503
|
+
<span>Subtotal</span>
|
|
504
|
+
<span>{summary.subtotal}</span>
|
|
505
|
+
</div>
|
|
506
|
+
{summary.discount ? (
|
|
507
|
+
<div className="cart-discount">
|
|
508
|
+
<span>
|
|
509
|
+
Discount
|
|
510
|
+
{summary.discountNames.length > 0 && (
|
|
511
|
+
<span className="cart-discount-name"> · {summary.discountNames.join(", ")}</span>
|
|
512
|
+
)}
|
|
513
|
+
</span>
|
|
514
|
+
<span className="cart-discount-amount">−{summary.discount}</span>
|
|
515
|
+
</div>
|
|
516
|
+
) : summary.discountNames.length > 0 ? (
|
|
517
|
+
<div className="cart-applied-discounts">
|
|
518
|
+
<span>Applied discount</span>
|
|
519
|
+
<span className="cart-applied-discounts-name">{summary.discountNames.join(", ")}</span>
|
|
520
|
+
</div>
|
|
521
|
+
) : null}
|
|
522
|
+
{summary.total && summary.total !== summary.subtotal && (
|
|
523
|
+
<div className="cart-total">
|
|
524
|
+
<span>Total</span>
|
|
525
|
+
<span>{summary.total}</span>
|
|
526
|
+
</div>
|
|
527
|
+
)}
|
|
528
|
+
{hasUnavailable && (
|
|
529
|
+
<p className="cart-item-unavailable">
|
|
530
|
+
Remove unavailable items before checking out.
|
|
531
|
+
</p>
|
|
532
|
+
)}
|
|
533
|
+
<button
|
|
534
|
+
onClick={handleCheckout}
|
|
535
|
+
disabled={checkingOut || hasUnavailable}
|
|
536
|
+
className="checkout-btn"
|
|
537
|
+
>
|
|
538
|
+
{checkingOut ? "Redirecting to checkout..." : "Proceed to Checkout"}
|
|
539
|
+
</button>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
);
|
|
543
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "../layouts/Layout.astro";
|
|
3
|
+
import CartView from "../components/CartView.tsx";
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<Layout title="Cart">
|
|
7
|
+
<main class="py-3xl">
|
|
8
|
+
<div class="max-w-wide mx-auto px-xl">
|
|
9
|
+
<h1 class="font-display text-4xl mb-2xl">Your Cart</h1>
|
|
10
|
+
<CartView client:load />
|
|
11
|
+
</div>
|
|
12
|
+
</main>
|
|
13
|
+
</Layout>
|