ecommerce 2/3

This commit is contained in:
lovebird 2026-02-18 15:32:07 +01:00
parent 152bd98533
commit 1b34cb5922
24 changed files with 2256 additions and 1252 deletions

View File

@ -1,5 +1,6 @@
{
"private": true,
"type": "module",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",

View File

@ -1,10 +1,22 @@
import { CartState } from './types';
/**
* Global cart store.
* Global cart store persisted to localStorage.
*
* Usage:
* ```tsx
* const { items, addItem, subtotal } = useCartStore();
* ```
*/
export declare const useCartStore: import('zustand').UseBoundStore<import('zustand').StoreApi<CartState>>;
export declare const useCartStore: import('zustand').UseBoundStore<Omit<import('zustand').StoreApi<CartState>, "setState" | "persist"> & {
setState(partial: CartState | Partial<CartState> | ((state: CartState) => CartState | Partial<CartState>), replace?: false): unknown;
setState(state: CartState | ((state: CartState) => CartState), replace: true): unknown;
persist: {
setOptions: (options: Partial<import('zustand/middleware').PersistOptions<CartState, CartState, unknown>>) => void;
clearStorage: () => void;
rehydrate: () => Promise<void> | void;
hasHydrated: () => boolean;
onHydrate: (fn: (state: CartState) => void) => () => void;
onFinishHydration: (fn: (state: CartState) => void) => () => void;
getOptions: () => Partial<import('zustand/middleware').PersistOptions<CartState, CartState, unknown>>;
};
}>;

View File

@ -16,6 +16,8 @@ export interface CheckoutPageProps {
}) => void;
/** Called when user clicks "Back to Cart". */
onBackToCart?: () => void;
/** Pre-fill shipping form fields (e.g. from user profile). */
initialShipping?: Partial<ShippingAddress>;
/** Pre-filled tax amount, if known. */
tax?: number;
/** Pre-filled shipping cost. */
@ -27,4 +29,4 @@ export interface CheckoutPageProps {
* Checkout page two-column layout with shipping form + payment selector on
* the left and an OrderSummary on the right.
*/
export declare function CheckoutPage({ onPlaceOrder, onBackToCart, tax, shipping, className, }: CheckoutPageProps): import("react/jsx-runtime").JSX.Element;
export declare function CheckoutPage({ onPlaceOrder, onBackToCart, initialShipping, tax, shipping, className, }: CheckoutPageProps): import("react/jsx-runtime").JSX.Element;

View File

@ -8,3 +8,15 @@ export { OrderSummary } from './checkout/OrderSummary';
export type { OrderSummaryProps } from './checkout/OrderSummary';
export { CheckoutPage } from './checkout/CheckoutPage';
export type { CheckoutPageProps, ShippingAddress, PaymentMethod, } from './checkout/CheckoutPage';
export { PolicyPage } from './policies/PolicyPage';
export type { PolicyPageProps } from './policies/PolicyPage';
export { ShippingPage } from './policies/ShippingPage';
export type { ShippingPageProps, ShippingRate } from './policies/ShippingPage';
export { ReturnsPage } from './policies/ReturnsPage';
export type { ReturnsPageProps } from './policies/ReturnsPage';
export { PrivacyPolicyPage } from './policies/PrivacyPolicyPage';
export type { PrivacyPolicyPageProps } from './policies/PrivacyPolicyPage';
export { TermsPage } from './policies/TermsPage';
export type { TermsPageProps } from './policies/TermsPage';
export { PolicyLinks } from './policies/PolicyLinks';
export type { PolicyLinksProps, PolicyLink } from './policies/PolicyLinks';

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
export interface PolicyLink {
label: string;
href: string;
}
export interface PolicyLinksProps {
links?: PolicyLink[];
className?: string;
}
/**
* Small footer row of policy links for cart / checkout pages.
*/
export declare function PolicyLinks({ links, className }: PolicyLinksProps): import("react/jsx-runtime").JSX.Element;

View File

@ -0,0 +1,14 @@
import { default as React } from 'react';
export interface PolicyPageProps {
/** Page title override. */
title?: string;
/** Rich content to render inside the page body (JSX). */
children?: React.ReactNode;
/** Optional extra class names. */
className?: string;
}
/**
* Generic policy/info page shell centered card with title + prose body.
* Used by ShippingPage, ReturnsPage, PrivacyPolicyPage, TermsPage.
*/
export declare function PolicyPage({ title, children, className }: PolicyPageProps): import("react/jsx-runtime").JSX.Element;

View File

@ -0,0 +1,10 @@
export interface PrivacyPolicyPageProps {
/** Business / site name used in the policy text. */
siteName?: string;
/** Contact email for privacy inquiries. */
contactEmail?: string;
/** Optional extra class names. */
className?: string;
}
/** Public page with privacy / cookie information. */
export declare function PrivacyPolicyPage({ siteName, contactEmail, className, }: PrivacyPolicyPageProps): import("react/jsx-runtime").JSX.Element;

View File

@ -0,0 +1,8 @@
export interface ReturnsPageProps {
/** Number of days for return window. */
returnWindowDays?: number;
/** Optional extra class names. */
className?: string;
}
/** Public page with refund / return policy. */
export declare function ReturnsPage({ returnWindowDays, className }: ReturnsPageProps): import("react/jsx-runtime").JSX.Element;

View File

@ -0,0 +1,14 @@
export interface ShippingRate {
region: string;
method: string;
estimate: string;
price: string;
}
export interface ShippingPageProps {
/** Custom shipping rates to display. Falls back to placeholder content. */
rates?: ShippingRate[];
/** Optional extra class names. */
className?: string;
}
/** Public page with shipping info and rates. */
export declare function ShippingPage({ rates, className }: ShippingPageProps): import("react/jsx-runtime").JSX.Element;

View File

@ -0,0 +1,10 @@
export interface TermsPageProps {
/** Business / site name. */
siteName?: string;
/** Contact email for legal inquiries. */
contactEmail?: string;
/** Optional extra class names. */
className?: string;
}
/** Optional terms of service page. */
export declare function TermsPage({ siteName, contactEmail, className, }: TermsPageProps): import("react/jsx-runtime").JSX.Element;

View File

@ -7,6 +7,127 @@
"": {
"name": "@polymech/ecommerce",
"version": "0.0.1",
"dependencies": {
"@google/genai": "^1.34.0",
"@google/generative-ai": "^0.24.1",
"@hookform/resolvers": "^3.10.0",
"@mdxeditor/editor": "^3.47.0",
"@milkdown/core": "^7.16.0",
"@milkdown/crepe": "^7.16.0",
"@milkdown/utils": "^7.16.0",
"@playwright/test": "^1.55.1",
"@polymech/ui": "file:../ui",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.15",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.14",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@supabase-cache-helpers/postgrest-swr": "^2.0.3",
"@supabase/supabase-js": "^2.58.0",
"@tanstack/react-query": "^5.83.0",
"@types/dompurify": "^3.2.0",
"@types/file-saver": "^2.0.7",
"@uppy/tus": "^5.0.2",
"@vidstack/react": "^1.12.13",
"@xyflow/react": "^12.8.6",
"axios": "^1.12.2",
"buffer": "^6.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0",
"dompurify": "^3.2.7",
"dotenv": "^17.2.3",
"embla-carousel-react": "^8.6.0",
"exifreader": "^4.33.1",
"file-saver": "^2.0.5",
"hls.js": "^1.6.13",
"idb-keyval": "^6.2.2",
"input-otp": "^1.4.2",
"jszip": "^3.10.1",
"lucide-react": "^0.462.0",
"marked": "^16.3.0",
"next-themes": "^0.3.0",
"openai": "^6.0.0",
"playwright": "^1.55.1",
"prismjs": "^1.30.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.61.1",
"react-intersection-observer": "^10.0.0",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^2.1.9",
"react-router-dom": "^6.30.1",
"react-zoom-pan-pinch": "^3.7.0",
"recharts": "^2.15.4",
"replicate": "^1.2.0",
"rollup-plugin-visualizer": "^6.0.5",
"sonner": "^1.7.4",
"stream-browserify": "^3.0.0",
"swr": "^2.3.7",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^0.9.9",
"vite-bundle-analyzer": "^1.3.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-pwa": "^1.0.3",
"vue": "^3.5.22",
"workbox-core": "^7.4.0",
"zod": "^3.25.76",
"zod-to-json-schema": "^3.24.6",
"zustand": "^5.0.11"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.16.5",
"@types/react": "^18.3.23",
"@types/react-dom": "^18.3.7",
"@vitejs/plugin-react-swc": "^3.11.0",
"@vitejs/plugin-vue": "^6.0.1",
"autoprefixer": "^10.4.21",
"eslint": "^9.32.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^15.15.0",
"lovable-tagger": "^1.1.10",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.17",
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0",
"vite": "^5.4.19",
"vite-plugin-dts": "^4.5.4",
"workbox-precaching": "^7.4.0",
"workbox-routing": "^7.4.0",
"workbox-window": "^7.4.0"
}
},
"../ui": {
"name": "@polymech/ui",
"version": "0.0.1",
"dependencies": {
"@google/genai": "^1.34.0",
"@google/generative-ai": "^0.24.1",
@ -97,7 +218,7 @@
"workbox-core": "^7.4.0",
"zod": "^3.25.76",
"zod-to-json-schema": "^3.24.6",
"zustand": "^5.0.11"
"zustand": "^5.0.8"
},
"devDependencies": {
"@eslint/js": "^9.32.0",
@ -2872,9 +2993,9 @@
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
"integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
@ -3479,9 +3600,9 @@
}
},
"node_modules/@microsoft/api-extractor": {
"version": "7.55.2",
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.55.2.tgz",
"integrity": "sha512-1jlWO4qmgqYoVUcyh+oXYRztZde/pAi7cSVzBz/rc+S7CoVzDasy8QE13dx6sLG4VRo8SfkkLbFORR6tBw4uGQ==",
"version": "7.56.3",
"resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.56.3.tgz",
"integrity": "sha512-fRqok4aRNq5GpgGBv2fKlSSKbirPKTJ75vQefthB5x9dwt4Zz+AezUzdc1p/AG4wUBIgmhjcEwn/Rj+N4Wh4Mw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -3490,11 +3611,11 @@
"@microsoft/tsdoc-config": "~0.18.0",
"@rushstack/node-core-library": "5.19.1",
"@rushstack/rig-package": "0.6.0",
"@rushstack/terminal": "0.19.5",
"@rushstack/ts-command-line": "5.1.5",
"@rushstack/terminal": "0.21.0",
"@rushstack/ts-command-line": "5.2.0",
"diff": "~8.0.2",
"lodash": "~4.17.15",
"minimatch": "10.0.3",
"lodash": "~4.17.23",
"minimatch": "10.1.2",
"resolve": "~1.22.1",
"semver": "~7.5.4",
"source-map": "~0.6.1",
@ -3540,13 +3661,13 @@
}
},
"node_modules/@microsoft/api-extractor/node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz",
"integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==",
"dev": true,
"license": "ISC",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"@isaacs/brace-expansion": "^5.0.1"
},
"engines": {
"node": "20 || >=22"
@ -4040,6 +4161,10 @@
"node": ">=18"
}
},
"node_modules/@polymech/ui": {
"resolved": "../ui",
"link": true
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@ -6388,9 +6513,9 @@
}
},
"node_modules/@rushstack/terminal": {
"version": "0.19.5",
"resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.19.5.tgz",
"integrity": "sha512-6k5tpdB88G0K7QrH/3yfKO84HK9ggftfUZ51p7fePyCE7+RLLHkWZbID9OFWbXuna+eeCFE7AkKnRMHMxNbz7Q==",
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.21.0.tgz",
"integrity": "sha512-cLaI4HwCNYmknM5ns4G+drqdEB6q3dCPV423+d3TZeBusYSSm09+nR7CnhzJMjJqeRcdMAaLnrA4M/3xDz4R3w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -6424,13 +6549,13 @@
}
},
"node_modules/@rushstack/ts-command-line": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.1.5.tgz",
"integrity": "sha512-YmrFTFUdHXblYSa+Xc9OO9FsL/XFcckZy0ycQ6q7VSBsVs5P0uD9vcges5Q9vctGlVdu27w+Ct6IuJ458V0cTQ==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-5.2.0.tgz",
"integrity": "sha512-lYxCX0nDdkDtCkVpvF0m25ymf66SaMWuppbD6b7MdkIzvGXKBXNIVZlwBH/C0YfkanrupnICWf2n4z3AKSfaHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rushstack/terminal": "0.19.5",
"@rushstack/terminal": "0.21.0",
"@types/argparse": "1.0.38",
"argparse": "~1.0.9",
"string-argv": "~0.3.1"
@ -7129,17 +7254,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
"integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz",
"integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.53.1",
"@typescript-eslint/type-utils": "8.53.1",
"@typescript-eslint/utils": "8.53.1",
"@typescript-eslint/visitor-keys": "8.53.1",
"@typescript-eslint/scope-manager": "8.56.0",
"@typescript-eslint/type-utils": "8.56.0",
"@typescript-eslint/utils": "8.56.0",
"@typescript-eslint/visitor-keys": "8.56.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.4.0"
@ -7152,8 +7277,8 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.53.1",
"eslint": "^8.57.0 || ^9.0.0",
"@typescript-eslint/parser": "^8.56.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
@ -7168,17 +7293,17 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz",
"integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz",
"integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.53.1",
"@typescript-eslint/types": "8.53.1",
"@typescript-eslint/typescript-estree": "8.53.1",
"@typescript-eslint/visitor-keys": "8.53.1",
"@typescript-eslint/scope-manager": "8.56.0",
"@typescript-eslint/types": "8.56.0",
"@typescript-eslint/typescript-estree": "8.56.0",
"@typescript-eslint/visitor-keys": "8.56.0",
"debug": "^4.4.3"
},
"engines": {
@ -7189,19 +7314,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz",
"integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz",
"integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.53.1",
"@typescript-eslint/types": "^8.53.1",
"@typescript-eslint/tsconfig-utils": "^8.56.0",
"@typescript-eslint/types": "^8.56.0",
"debug": "^4.4.3"
},
"engines": {
@ -7216,14 +7341,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz",
"integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz",
"integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.53.1",
"@typescript-eslint/visitor-keys": "8.53.1"
"@typescript-eslint/types": "8.56.0",
"@typescript-eslint/visitor-keys": "8.56.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -7234,9 +7359,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz",
"integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz",
"integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==",
"dev": true,
"license": "MIT",
"engines": {
@ -7251,15 +7376,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz",
"integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz",
"integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.53.1",
"@typescript-eslint/typescript-estree": "8.53.1",
"@typescript-eslint/utils": "8.53.1",
"@typescript-eslint/types": "8.56.0",
"@typescript-eslint/typescript-estree": "8.56.0",
"@typescript-eslint/utils": "8.56.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.4.0"
},
@ -7271,14 +7396,14 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz",
"integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz",
"integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -7290,16 +7415,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz",
"integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz",
"integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.53.1",
"@typescript-eslint/tsconfig-utils": "8.53.1",
"@typescript-eslint/types": "8.53.1",
"@typescript-eslint/visitor-keys": "8.53.1",
"@typescript-eslint/project-service": "8.56.0",
"@typescript-eslint/tsconfig-utils": "8.56.0",
"@typescript-eslint/types": "8.56.0",
"@typescript-eslint/visitor-keys": "8.56.0",
"debug": "^4.4.3",
"minimatch": "^9.0.5",
"semver": "^7.7.3",
@ -7344,16 +7469,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz",
"integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz",
"integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.53.1",
"@typescript-eslint/types": "8.53.1",
"@typescript-eslint/typescript-estree": "8.53.1"
"@typescript-eslint/scope-manager": "8.56.0",
"@typescript-eslint/types": "8.56.0",
"@typescript-eslint/typescript-estree": "8.56.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -7363,19 +7488,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz",
"integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz",
"integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.53.1",
"eslint-visitor-keys": "^4.2.1"
"@typescript-eslint/types": "8.56.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -7385,6 +7510,19 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz",
"integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@ -7848,9 +7986,9 @@
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -8086,13 +8224,13 @@
}
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@ -9246,9 +9384,9 @@
"license": "Apache-2.0"
},
"node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz",
"integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
@ -11820,15 +11958,15 @@
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.22",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
"license": "MIT"
},
"node_modules/lodash._baseiteratee": {
@ -15374,9 +15512,9 @@
}
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"dev": true,
"license": "ISC",
"bin": {
@ -16424,16 +16562,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.53.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz",
"integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.0.tgz",
"integrity": "sha512-c7toRLrotJ9oixgdW7liukZpsnq5CZ7PuKztubGYlNppuTqhIoWfhgHo/7EU0v06gS2l/x0i2NEFK1qMIf0rIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.53.1",
"@typescript-eslint/parser": "8.53.1",
"@typescript-eslint/typescript-estree": "8.53.1",
"@typescript-eslint/utils": "8.53.1"
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/typescript-estree": "8.56.0",
"@typescript-eslint/utils": "8.56.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -16443,7 +16581,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
@ -17748,9 +17886,9 @@
"license": "MIT"
},
"node_modules/workbox-build/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"peer": true,
"dependencies": {

View File

@ -41,6 +41,7 @@
"@milkdown/crepe": "^7.16.0",
"@milkdown/utils": "^7.16.0",
"@playwright/test": "^1.55.1",
"@polymech/ui": "file:../ui",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
@ -148,4 +149,4 @@
"workbox-routing": "^7.4.0",
"workbox-window": "^7.4.0"
}
}
}

View File

@ -6,6 +6,7 @@ import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { useCartStore } from "./useCartStore";
import { CartItemRow } from "./CartItem";
import { PolicyLinks } from "@/policies/PolicyLinks";
export interface CartPageProps {
/** Called when user clicks "Proceed to Checkout". */
@ -81,6 +82,8 @@ export function CartPage({ onCheckout, className }: CartPageProps) {
</Button>
</CardFooter>
</Card>
<PolicyLinks className="pt-2" />
</div>
);
}

View File

@ -1,4 +1,5 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { CartItem, CartState } from "./types";
/** Recompute derived totals from items array. */
@ -10,51 +11,58 @@ function computeTotals(items: CartItem[]) {
}
/**
* Global cart store.
* Global cart store persisted to localStorage.
*
* Usage:
* ```tsx
* const { items, addItem, subtotal } = useCartStore();
* ```
*/
export const useCartStore = create<CartState>((set) => ({
items: [],
subtotal: 0,
itemCount: 0,
export const useCartStore = create<CartState>()(
persist(
(set) => ({
items: [],
subtotal: 0,
itemCount: 0,
addItem: (incoming) =>
set((state) => {
const existing = state.items.find((i) => i.id === incoming.id);
let items: CartItem[];
if (existing) {
items = state.items.map((i) =>
i.id === incoming.id
? { ...i, quantity: i.quantity + (incoming.quantity ?? 1) }
: i,
);
} else {
items = [...state.items, { ...incoming, quantity: incoming.quantity ?? 1 }];
}
return { items, ...computeTotals(items) };
addItem: (incoming) =>
set((state) => {
const existing = state.items.find((i) => i.id === incoming.id);
let items: CartItem[];
if (existing) {
items = state.items.map((i) =>
i.id === incoming.id
? { ...i, quantity: i.quantity + (incoming.quantity ?? 1) }
: i,
);
} else {
items = [...state.items, { ...incoming, quantity: incoming.quantity ?? 1 }];
}
return { items, ...computeTotals(items) };
}),
removeItem: (id) =>
set((state) => {
const items = state.items.filter((i) => i.id !== id);
return { items, ...computeTotals(items) };
}),
updateQuantity: (id, quantity) =>
set((state) => {
if (quantity <= 0) {
const items = state.items.filter((i) => i.id !== id);
return { items, ...computeTotals(items) };
}
const items = state.items.map((i) =>
i.id === id ? { ...i, quantity } : i,
);
return { items, ...computeTotals(items) };
}),
clearCart: () => set({ items: [], subtotal: 0, itemCount: 0 }),
}),
removeItem: (id) =>
set((state) => {
const items = state.items.filter((i) => i.id !== id);
return { items, ...computeTotals(items) };
}),
updateQuantity: (id, quantity) =>
set((state) => {
if (quantity <= 0) {
const items = state.items.filter((i) => i.id !== id);
return { items, ...computeTotals(items) };
}
const items = state.items.map((i) =>
i.id === id ? { ...i, quantity } : i,
);
return { items, ...computeTotals(items) };
}),
clearCart: () => set({ items: [], subtotal: 0, itemCount: 0 }),
}));
{
name: "pm-ecommerce-cart",
},
),
);

View File

@ -8,6 +8,7 @@ import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
import { useCartStore } from "@/cart/useCartStore";
import { OrderSummary } from "./OrderSummary";
import { PolicyLinks } from "@/policies/PolicyLinks";
/** Shipping address fields collected at checkout. */
export interface ShippingAddress {
@ -29,6 +30,8 @@ export interface CheckoutPageProps {
}) => void;
/** Called when user clicks "Back to Cart". */
onBackToCart?: () => void;
/** Pre-fill shipping form fields (e.g. from user profile). */
initialShipping?: Partial<ShippingAddress>;
/** Pre-filled tax amount, if known. */
tax?: number;
/** Pre-filled shipping cost. */
@ -44,6 +47,7 @@ export interface CheckoutPageProps {
export function CheckoutPage({
onPlaceOrder,
onBackToCart,
initialShipping,
tax,
shipping,
className,
@ -52,12 +56,12 @@ export function CheckoutPage({
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("shopify");
const [form, setForm] = useState<ShippingAddress>({
fullName: "",
email: "",
address: "",
city: "",
zip: "",
country: "",
fullName: initialShipping?.fullName ?? "",
email: initialShipping?.email ?? "",
address: initialShipping?.address ?? "",
city: initialShipping?.city ?? "",
zip: initialShipping?.zip ?? "",
country: initialShipping?.country ?? "",
});
const field = (key: keyof ShippingAddress, value: string) =>
@ -232,6 +236,8 @@ export function CheckoutPage({
Back to Cart
</Button>
)}
<PolicyLinks className="pt-4" />
</div>
</form>
);

View File

@ -19,3 +19,22 @@ export type {
ShippingAddress,
PaymentMethod,
} from "./checkout/CheckoutPage";
// === Policies ===
export { PolicyPage } from "./policies/PolicyPage";
export type { PolicyPageProps } from "./policies/PolicyPage";
export { ShippingPage } from "./policies/ShippingPage";
export type { ShippingPageProps, ShippingRate } from "./policies/ShippingPage";
export { ReturnsPage } from "./policies/ReturnsPage";
export type { ReturnsPageProps } from "./policies/ReturnsPage";
export { PrivacyPolicyPage } from "./policies/PrivacyPolicyPage";
export type { PrivacyPolicyPageProps } from "./policies/PrivacyPolicyPage";
export { TermsPage } from "./policies/TermsPage";
export type { TermsPageProps } from "./policies/TermsPage";
export { PolicyLinks } from "./policies/PolicyLinks";
export type { PolicyLinksProps, PolicyLink } from "./policies/PolicyLinks";

View File

@ -0,0 +1,37 @@
import React from "react";
import { cn } from "@/lib/utils";
export interface PolicyLink {
label: string;
href: string;
}
const defaultLinks: PolicyLink[] = [
{ label: "Returns & Refunds", href: "/returns" },
{ label: "Shipping", href: "/shipping" },
{ label: "Privacy Policy", href: "/privacy" },
{ label: "Terms of Service", href: "/terms" },
];
export interface PolicyLinksProps {
links?: PolicyLink[];
className?: string;
}
/**
* Small footer row of policy links for cart / checkout pages.
*/
export function PolicyLinks({ links = defaultLinks, className }: PolicyLinksProps) {
return (
<nav className={cn("flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted-foreground", className)}>
{links.map((l, i) => (
<React.Fragment key={l.href}>
{i > 0 && <span className="hidden sm:inline" aria-hidden>·</span>}
<a href={l.href} className="hover:text-foreground hover:underline transition-colors">
{l.label}
</a>
</React.Fragment>
))}
</nav>
);
}

View File

@ -0,0 +1,33 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
export interface PolicyPageProps {
/** Page title override. */
title?: string;
/** Rich content to render inside the page body (JSX). */
children?: React.ReactNode;
/** Optional extra class names. */
className?: string;
}
/**
* Generic policy/info page shell centered card with title + prose body.
* Used by ShippingPage, ReturnsPage, PrivacyPolicyPage, TermsPage.
*/
export function PolicyPage({ title, children, className }: PolicyPageProps) {
return (
<div className={cn("mx-auto max-w-3xl py-8", className)}>
<Card>
<CardHeader>
<CardTitle className="text-2xl">{title}</CardTitle>
</CardHeader>
<Separator />
<CardContent className="prose prose-sm dark:prose-invert max-w-none pt-6">
{children}
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,85 @@
import React from "react";
import { PolicyPage } from "./PolicyPage";
export interface PrivacyPolicyPageProps {
/** Business / site name used in the policy text. */
siteName?: string;
/** Contact email for privacy inquiries. */
contactEmail?: string;
/** Optional extra class names. */
className?: string;
}
/** Public page with privacy / cookie information. */
export function PrivacyPolicyPage({
siteName = "Our Store",
contactEmail = "privacy@example.com",
className,
}: PrivacyPolicyPageProps) {
return (
<PolicyPage title="Privacy Policy" className={className}>
<p>
At <strong>{siteName}</strong>, we are committed to protecting your personal information
and your right to privacy. This policy explains what information we collect, how we use
it, and what rights you have in relation to it.
</p>
<h3>Information We Collect</h3>
<ul>
<li>
<strong>Personal information:</strong> name, email, shipping address, and payment
details provided during checkout.
</li>
<li>
<strong>Usage data:</strong> pages visited, time spent, browser type, and device
information collected automatically.
</li>
<li>
<strong>Cookies:</strong> small data files stored on your device to improve your
browsing experience and remember your preferences.
</li>
</ul>
<h3>How We Use Your Information</h3>
<ul>
<li>To process and fulfill your orders.</li>
<li>To communicate with you about orders, updates, and promotions.</li>
<li>To improve our website and services.</li>
<li>To comply with legal obligations.</li>
</ul>
<h3>Data Sharing</h3>
<p>
We do not sell your personal data. We share information only with service providers
necessary to fulfill your order (e.g., payment processors, shipping carriers) and as
required by law.
</p>
<h3>Cookies</h3>
<p>
We use essential cookies for site functionality and optional analytics cookies to
understand usage patterns. You can manage cookie preferences through your browser
settings.
</p>
<h3>Your Rights</h3>
<p>
You may request access to, correction of, or deletion of your personal data at any
time by contacting us at{" "}
<a href={`mailto:${contactEmail}`} className="text-primary underline">
{contactEmail}
</a>
.
</p>
<h3>Contact Us</h3>
<p>
If you have questions about this privacy policy, please contact us at{" "}
<a href={`mailto:${contactEmail}`} className="text-primary underline">
{contactEmail}
</a>
.
</p>
</PolicyPage>
);
}

View File

@ -0,0 +1,62 @@
import React from "react";
import { RotateCcw, CheckCircle, XCircle, Clock } from "lucide-react";
import { PolicyPage } from "./PolicyPage";
export interface ReturnsPageProps {
/** Number of days for return window. */
returnWindowDays?: number;
/** Optional extra class names. */
className?: string;
}
/** Public page with refund / return policy. */
export function ReturnsPage({ returnWindowDays = 30, className }: ReturnsPageProps) {
return (
<PolicyPage title="Returns & Refund Policy" className={className}>
{/* Quick summary */}
<div className="not-prose mb-8 grid gap-4 sm:grid-cols-2">
{[
{ icon: Clock, label: `${returnWindowDays}-day return window` },
{ icon: RotateCcw, label: "Free returns on defective items" },
{ icon: CheckCircle, label: "Full refund to original payment" },
{ icon: XCircle, label: "No restocking fees" },
].map(({ icon: Icon, label }) => (
<div
key={label}
className="flex items-center gap-3 rounded-lg border border-border/50 bg-accent/5 p-4"
>
<Icon className="h-5 w-5 shrink-0 text-primary" />
<span className="text-sm font-medium">{label}</span>
</div>
))}
</div>
<h3>Eligibility</h3>
<p>
Items must be returned within <strong>{returnWindowDays} days</strong> of delivery in their
original, unused condition with all tags and packaging intact.
</p>
<h3>How to Initiate a Return</h3>
<ol>
<li>Contact our support team with your order number.</li>
<li>Receive a prepaid return label (for defective items) or return instructions.</li>
<li>Ship the item back using the provided label or your preferred carrier.</li>
</ol>
<h3>Refund Processing</h3>
<p>
Once we receive and inspect the returned item, your refund will be processed within
510 business days to your original payment method. You will receive an email
confirmation when the refund has been issued.
</p>
<h3>Exceptions</h3>
<p>
The following items are not eligible for return: gift cards, downloadable products,
and items marked as final sale. Perishable goods cannot be returned unless they arrive
damaged or defective.
</p>
</PolicyPage>
);
}

View File

@ -0,0 +1,88 @@
import React from "react";
import { Truck, Clock, Globe, DollarSign } from "lucide-react";
import { cn } from "@/lib/utils";
import { PolicyPage } from "./PolicyPage";
export interface ShippingRate {
region: string;
method: string;
estimate: string;
price: string;
}
export interface ShippingPageProps {
/** Custom shipping rates to display. Falls back to placeholder content. */
rates?: ShippingRate[];
/** Optional extra class names. */
className?: string;
}
const defaultRates: ShippingRate[] = [
{ region: "Domestic", method: "Standard", estimate: "57 business days", price: "$4.99" },
{ region: "Domestic", method: "Express", estimate: "23 business days", price: "$12.99" },
{ region: "International", method: "Standard", estimate: "1020 business days", price: "$14.99" },
{ region: "International", method: "Express", estimate: "58 business days", price: "$29.99" },
];
/** Public page with shipping info and rates. */
export function ShippingPage({ rates = defaultRates, className }: ShippingPageProps) {
return (
<PolicyPage title="Shipping Information" className={className}>
{/* Highlights */}
<div className="not-prose mb-8 grid gap-4 sm:grid-cols-2">
{[
{ icon: Truck, label: "Free shipping on orders over $75" },
{ icon: Clock, label: "Same-day dispatch on orders before 2 PM" },
{ icon: Globe, label: "We ship worldwide" },
{ icon: DollarSign, label: "No hidden fees at checkout" },
].map(({ icon: Icon, label }) => (
<div
key={label}
className="flex items-center gap-3 rounded-lg border border-border/50 bg-accent/5 p-4"
>
<Icon className="h-5 w-5 shrink-0 text-primary" />
<span className="text-sm font-medium">{label}</span>
</div>
))}
</div>
{/* Rates table */}
<h3>Shipping Rates</h3>
<div className="not-prose overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left text-muted-foreground">
<th className="pb-2 pr-4 font-medium">Region</th>
<th className="pb-2 pr-4 font-medium">Method</th>
<th className="pb-2 pr-4 font-medium">Estimate</th>
<th className="pb-2 font-medium text-right">Price</th>
</tr>
</thead>
<tbody>
{rates.map((r, i) => (
<tr key={i} className={cn("border-b border-border/30", i % 2 === 0 && "bg-accent/5")}>
<td className="py-2.5 pr-4">{r.region}</td>
<td className="py-2.5 pr-4">{r.method}</td>
<td className="py-2.5 pr-4 text-muted-foreground">{r.estimate}</td>
<td className="py-2.5 text-right font-medium">{r.price}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3>Processing Time</h3>
<p>
Orders placed before 2:00 PM (local time) on business days are typically processed and
shipped the same day. Orders placed after this cut-off or on weekends/holidays will be
processed the next business day.
</p>
<h3>Tracking</h3>
<p>
Once your order ships, you will receive a confirmation email with a tracking number.
You can use this number to track your package on the carrier's website.
</p>
</PolicyPage>
);
}

View File

@ -0,0 +1,77 @@
import React from "react";
import { PolicyPage } from "./PolicyPage";
export interface TermsPageProps {
/** Business / site name. */
siteName?: string;
/** Contact email for legal inquiries. */
contactEmail?: string;
/** Optional extra class names. */
className?: string;
}
/** Optional terms of service page. */
export function TermsPage({
siteName = "Our Store",
contactEmail = "legal@example.com",
className,
}: TermsPageProps) {
return (
<PolicyPage title="Terms of Service" className={className}>
<p>
By accessing and using <strong>{siteName}</strong>, you agree to be bound by these
Terms of Service.
</p>
<h3>Use of the Site</h3>
<p>
You agree to use this site only for lawful purposes and in a manner that does not
infringe on the rights of others or restrict their use and enjoyment of the site.
</p>
<h3>Products & Pricing</h3>
<p>
All product descriptions and prices are subject to change without notice. We reserve
the right to modify or discontinue any product at any time. Prices are displayed in
the store's base currency and may exclude taxes and shipping costs, which are
calculated at checkout.
</p>
<h3>Orders & Payment</h3>
<p>
By placing an order, you make an offer to purchase the selected products. We reserve
the right to refuse or cancel any order for any reason, including pricing errors or
suspected fraud.
</p>
<h3>Intellectual Property</h3>
<p>
All content on this site including text, images, logos, and software is the
property of {siteName} or its licensors and is protected by applicable intellectual
property laws.
</p>
<h3>Limitation of Liability</h3>
<p>
To the fullest extent permitted by law, {siteName} shall not be liable for any
indirect, incidental, or consequential damages arising from your use of the site or
purchase of products.
</p>
<h3>Changes to These Terms</h3>
<p>
We may update these Terms of Service from time to time. Continued use of the site
after changes constitutes acceptance of the revised terms.
</p>
<h3>Contact</h3>
<p>
For questions about these terms, contact us at{" "}
<a href={`mailto:${contactEmail}`} className="text-primary underline">
{contactEmail}
</a>
.
</p>
</PolicyPage>
);
}