ecommerce 2/3

This commit is contained in:
lovebird 2026-02-18 17:04:33 +01:00
parent d5c0a3704e
commit 9618998670
18 changed files with 1998 additions and 1367 deletions

View File

@ -0,0 +1,22 @@
import { default as React } from 'react';
export interface EcommerceBundleDependencies {
user: {
id?: string;
email?: string;
user_metadata?: {
display_name?: string;
};
} | null;
toast: {
success: (message: string) => void;
error: (message: string) => void;
};
onFetchAddresses: (userId: string) => Promise<any[]>;
onSaveAddress: (userId: string, addresses: any[]) => Promise<void>;
onPlaceOrder: (data: any) => Promise<void>;
onFetchTransactions: () => Promise<any[]>;
onNavigate: (path: string) => void;
siteName?: string;
contactEmail?: string;
}
export declare const EcommerceBundle: React.FC<EcommerceBundleDependencies>;

View File

@ -0,0 +1,25 @@
import { SavedAddress } from './CheckoutPage';
export interface CheckoutFlowProps {
/** The authenticated user's ID. */
userId?: string;
/** The authenticated user's display name. */
userDisplayName?: string;
/** The authenticated user's email. */
userEmail?: string;
/** Async function to fetch saved addresses for the user. */
onFetchAddresses: (userId: string) => Promise<SavedAddress[]>;
/** Async function to save a new address (or update list). */
onSaveAddress: (userId: string, addresses: SavedAddress[]) => Promise<void>;
/** Async function to place the order (create transaction). */
onPlaceOrder: (data: any) => Promise<void>;
/** Navigation callback to go back to cart. */
onBackToCart: () => void;
/** Callback after successful order placement (e.g. navigate to purchases). */
onOrderSuccess: () => void;
/** Toast notification handler. */
toast?: {
success: (msg: string) => void;
error: (msg: string) => void;
};
}
export declare function CheckoutFlow({ userId, userDisplayName, userEmail, onFetchAddresses, onSaveAddress, onPlaceOrder, onBackToCart, onOrderSuccess, toast, }: CheckoutFlowProps): import("react/jsx-runtime").JSX.Element;

View File

@ -22,6 +22,8 @@ export interface CheckoutPageProps {
shipping: ShippingAddress;
paymentMethod: PaymentMethod;
}) => void;
/** Called when user wants to save the entered address to their profile. */
onSaveAddress?: (address: ShippingAddress) => void;
/** Called when user clicks "Back to Cart". */
onBackToCart?: () => void;
/** Pre-fill shipping form fields (e.g. from user profile). */
@ -39,4 +41,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, initialShipping, savedAddresses, tax, shipping, className, }: CheckoutPageProps): import("react/jsx-runtime").JSX.Element;
export declare function CheckoutPage({ onPlaceOrder, onSaveAddress, onBackToCart, initialShipping, savedAddresses, tax, shipping, className, }: CheckoutPageProps): import("react/jsx-runtime").JSX.Element;

View File

@ -0,0 +1,9 @@
import { VariantProps } from 'class-variance-authority';
import * as React from "react";
declare const badgeVariants: (props?: {
variant?: "default" | "destructive" | "outline" | "secondary";
} & import('class-variance-authority/types').ClassProp) => string;
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {
}
declare function Badge({ className, variant, ...props }: BadgeProps): import("react/jsx-runtime").JSX.Element;
export { Badge, badgeVariants };

View File

@ -20,3 +20,9 @@ export { TermsPage } from './policies/TermsPage';
export type { TermsPageProps } from './policies/TermsPage';
export { PolicyLinks } from './policies/PolicyLinks';
export type { PolicyLinksProps, PolicyLink } from './policies/PolicyLinks';
export { CheckoutFlow } from './checkout/CheckoutFlow';
export type { CheckoutFlowProps } from './checkout/CheckoutFlow';
export { PurchasesList } from './purchases/PurchasesList';
export type { PurchasesListProps, Transaction } from './purchases/PurchasesList';
export { EcommerceBundle } from './EcommerceBundle';
export type { EcommerceBundleDependencies } from './EcommerceBundle';

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,20 @@
export interface Transaction {
id: string;
created_at: string;
status: string;
total_amount: number;
currency: string;
product_info: any[];
shipping_info: any;
}
export interface PurchasesListProps {
/** Async function to fetch user transactions. */
onFetchTransactions: () => Promise<Transaction[]>;
/** Navigation callback. */
onNavigate: (path: string) => void;
/** Toast notification handler. */
toast?: {
error: (msg: string) => void;
};
}
export declare function PurchasesList({ onFetchTransactions, onNavigate, toast }: PurchasesListProps): import("react/jsx-runtime").JSX.Element;

View File

@ -0,0 +1,82 @@
import React from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import { CartPage } from "./cart/CartPage";
import { CheckoutFlow, CheckoutFlowProps } from "./checkout/CheckoutFlow";
import { PurchasesList, PurchasesListProps } from "./purchases/PurchasesList";
import { ShippingPage } from "./policies/ShippingPage";
import { ReturnsPage } from "./policies/ReturnsPage";
import { PrivacyPolicyPage } from "./policies/PrivacyPolicyPage";
import { TermsPage } from "./policies/TermsPage";
export interface EcommerceBundleDependencies {
user: {
id?: string;
email?: string;
user_metadata?: {
display_name?: string;
};
} | null;
toast: {
success: (message: string) => void;
error: (message: string) => void;
};
onFetchAddresses: (userId: string) => Promise<any[]>;
onSaveAddress: (userId: string, addresses: any[]) => Promise<void>;
onPlaceOrder: (data: any) => Promise<void>;
onFetchTransactions: () => Promise<any[]>;
onNavigate: (path: string) => void;
// Optional config
siteName?: string;
contactEmail?: string;
}
export const EcommerceBundle: React.FC<EcommerceBundleDependencies> = (props) => {
const location = typeof window !== 'undefined' ? window.location : { pathname: '' };
// Debug logging
React.useEffect(() => {
console.log("EcommerceBundle mounted at:", location.pathname);
}, [location.pathname]);
return (
<div className="ecommerce-bundle-root border-2 border-red-500 p-4">
{/* Temporary debug border */}
<div className="bg-yellow-100 p-2 text-xs mb-4">
DEBUG: EcommerceBundle Active. Current Path: {location.pathname}
</div>
<Routes>
<Route path="/cart" element={<CartPage onCheckout={() => props.onNavigate('/checkout')} />} />
<Route path="/checkout" element={
<CheckoutFlow
userId={props.user?.id}
userDisplayName={props.user?.user_metadata?.display_name}
userEmail={props.user?.email}
onFetchAddresses={props.onFetchAddresses}
onSaveAddress={props.onSaveAddress}
onPlaceOrder={props.onPlaceOrder}
onBackToCart={() => props.onNavigate('/cart')}
onOrderSuccess={() => props.onNavigate('/purchases')}
toast={props.toast}
/>
} />
<Route path="/purchases" element={
<PurchasesList
onFetchTransactions={props.onFetchTransactions}
onNavigate={props.onNavigate}
toast={props.toast}
/>
} />
<Route path="/shipping" element={<ShippingPage />} />
<Route path="/returns" element={<ReturnsPage />} />
<Route path="/privacy" element={<PrivacyPolicyPage siteName={props.siteName || "PolyMech"} contactEmail={props.contactEmail || "privacy@polymech.org"} />} />
<Route path="/terms" element={<TermsPage siteName={props.siteName || "PolyMech"} contactEmail={props.contactEmail || "legal@polymech.org"} />} />
<Route path="*" element={<div className="p-8 text-red-500 font-bold">Ecommerce Bundle 404: No route matched for {location.pathname}</div>} />
</Routes>
</div>
);
};

View File

@ -47,6 +47,14 @@ export function CartItemRow({ item, className }: CartItemProps) {
<span className="text-sm text-muted-foreground">
${item.price.toFixed(2)} each
</span>
{item.vendorSlug && (
<a
href={`/user/${item.vendorSlug}`}
className="text-xs text-primary hover:underline"
>
Sold by {item.vendorSlug}
</a>
)}
</div>
{/* Quantity stepper */}

View File

@ -12,6 +12,10 @@ export interface CartItem {
quantity: number;
/** Optional variant label (e.g. "Red / XL"). */
variant?: string;
/** Vendor/seller username slug (for linking to vendor profile). */
vendorSlug?: string;
/** Page slug (for linking back to the product page). */
pageSlug?: string;
}
/** Actions exposed by the cart store. */

View File

@ -0,0 +1,104 @@
import React, { useState, useEffect } from "react";
import { CheckoutPage, CheckoutPageProps, SavedAddress } from "./CheckoutPage";
export interface CheckoutFlowProps {
/** The authenticated user's ID. */
userId?: string;
/** The authenticated user's display name. */
userDisplayName?: string;
/** The authenticated user's email. */
userEmail?: string;
/** Async function to fetch saved addresses for the user. */
onFetchAddresses: (userId: string) => Promise<SavedAddress[]>;
/** Async function to save a new address (or update list). */
onSaveAddress: (userId: string, addresses: SavedAddress[]) => Promise<void>;
/** Async function to place the order (create transaction). */
onPlaceOrder: (data: any) => Promise<void>;
/** Navigation callback to go back to cart. */
onBackToCart: () => void;
/** Callback after successful order placement (e.g. navigate to purchases). */
onOrderSuccess: () => void;
/** Toast notification handler. */
toast?: {
success: (msg: string) => void;
error: (msg: string) => void;
};
}
export function CheckoutFlow({
userId,
userDisplayName,
userEmail,
onFetchAddresses,
onSaveAddress,
onPlaceOrder,
onBackToCart,
onOrderSuccess,
toast,
}: CheckoutFlowProps) {
const [savedAddresses, setSavedAddresses] = useState<SavedAddress[]>([]);
// 1. Fetch addresses on mount if user exists
useEffect(() => {
if (!userId) return;
let mounted = true;
onFetchAddresses(userId)
.then((addrs) => {
if (mounted) setSavedAddresses(addrs);
})
.catch((err) => console.error("Failed to fetch addresses:", err));
return () => { mounted = false; };
}, [userId, onFetchAddresses]);
// 2. Handle saving a new address
const handleSaveAddress = async (address: any) => {
if (!userId) return;
try {
// Re-fetch latest to be safe (or use local state if we trust it completely)
const existing = await onFetchAddresses(userId);
const newAddr: SavedAddress = {
...address,
id: crypto.randomUUID(),
label: address.address?.split(',')[0] || 'Checkout address',
phone: '',
note: '',
// If it's the first address, make it default
isDefault: existing.length === 0,
};
const updated = [...existing, newAddr];
await onSaveAddress(userId, updated);
setSavedAddresses(updated);
toast?.success("Address saved to your profile");
} catch (err) {
console.error("Failed to save address:", err);
toast?.error("Failed to save address");
}
};
// 3. Handle order placement
const handlePlaceOrder = async (data: any) => {
try {
await onPlaceOrder(data);
toast?.success("Order placed successfully!");
onOrderSuccess();
} catch (err) {
console.error("Failed to place order:", err);
toast?.error("Failed to place order. Please try again.");
}
};
const checkoutProps: CheckoutPageProps = {
onPlaceOrder: handlePlaceOrder,
onSaveAddress: handleSaveAddress,
onBackToCart,
savedAddresses,
initialShipping: {
fullName: userDisplayName ?? "",
email: userEmail ?? "",
},
};
return <CheckoutPage {...checkoutProps} />;
}

View File

@ -35,9 +35,16 @@ export function OrderSummary({ tax, shipping, className }: OrderSummaryProps) {
<ul className="space-y-2 text-sm">
{items.map((item) => (
<li key={item.id} className="flex justify-between gap-2">
<span className="truncate text-muted-foreground">
{item.title} × {item.quantity}
</span>
<div className="flex flex-col min-w-0">
<span className="truncate text-muted-foreground">
{item.title} × {item.quantity}
</span>
{item.vendorSlug && (
<a href={`/user/${item.vendorSlug}`} className="text-xs text-primary hover:underline">
via {item.vendorSlug}
</a>
)}
</div>
<span className="shrink-0 tabular-nums">
${(item.price * item.quantity).toFixed(2)}
</span>

View File

@ -39,3 +39,14 @@ export type { TermsPageProps } from "./policies/TermsPage";
export { PolicyLinks } from "./policies/PolicyLinks";
export type { PolicyLinksProps, PolicyLink } from "./policies/PolicyLinks";
// === Flows ===
export { CheckoutFlow } from "./checkout/CheckoutFlow";
export type { CheckoutFlowProps } from "./checkout/CheckoutFlow";
export { PurchasesList } from "./purchases/PurchasesList";
export type { PurchasesListProps, Transaction } from "./purchases/PurchasesList";
// === Bundle ===
export { EcommerceBundle } from "./EcommerceBundle";
export type { EcommerceBundleDependencies } from "./EcommerceBundle";

View File

@ -1,12 +1,35 @@
import React, { useState, useEffect } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useAuth } from "@/hooks/useAuth";
import { Package, Clock, CheckCircle, XCircle, ArrowLeft, ExternalLink, RefreshCw } from "lucide-react";
import { Package, Clock, CheckCircle, XCircle, ArrowLeft, ExternalLink, RefreshCw, Store } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { listTransactions, type Transaction } from "@/modules/ecommerce/client-ecommerce";
import { toast } from "sonner";
// We need to define or import Transaction type and statusConfig locally or from a shared type file
// Since client-ecommerce.ts is outside, let's redefine minimal types here or ask for them to be passed in.
// Ideally, `client-ecommerce.ts` types should be imported from @polymech/ecommerce if possible,
// OR @polymech/ecommerce should define the types and client-ecommerce uses them.
// For now, let's define the props generically.
export interface Transaction {
id: string;
created_at: string;
status: string;
total_amount: number;
currency: string;
product_info: any[];
shipping_info: any;
}
export interface PurchasesListProps {
/** Async function to fetch user transactions. */
onFetchTransactions: () => Promise<Transaction[]>;
/** Navigation callback. */
onNavigate: (path: string) => void;
/** Toast notification handler. */
toast?: {
error: (msg: string) => void;
};
}
const statusConfig: Record<string, { label: string; variant: "default" | "secondary" | "destructive" | "outline"; icon: React.ReactNode }> = {
pending: { label: "Pending", variant: "secondary", icon: <Clock className="h-3.5 w-3.5" /> },
@ -17,35 +40,28 @@ const statusConfig: Record<string, { label: string; variant: "default" | "second
cancelled: { label: "Cancelled", variant: "destructive", icon: <XCircle className="h-3.5 w-3.5" /> },
};
const PurchasesPage = () => {
const { user, loading: authLoading } = useAuth();
const navigate = useNavigate();
export function PurchasesList({ onFetchTransactions, onNavigate, toast }: PurchasesListProps) {
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (authLoading) return;
if (!user) {
navigate("/auth");
return;
}
loadTransactions();
}, [user, authLoading]);
let mounted = true;
setLoading(true);
onFetchTransactions()
.then((data) => {
if (mounted) setTransactions(data);
})
.catch((err) => {
console.error("Failed to load transactions:", err);
toast?.error("Failed to load purchases");
})
.finally(() => {
if (mounted) setLoading(false);
});
return () => { mounted = false; };
}, [onFetchTransactions]);
const loadTransactions = async () => {
try {
setLoading(true);
const data = await listTransactions();
setTransactions(data);
} catch (err) {
console.error("Failed to load transactions:", err);
toast.error("Failed to load purchases");
} finally {
setLoading(false);
}
};
if (authLoading || loading) {
if (loading) {
return (
<div className="flex items-center justify-center py-24">
<RefreshCw className="h-6 w-6 animate-spin text-muted-foreground" />
@ -53,8 +69,6 @@ const PurchasesPage = () => {
);
}
if (!user) return null;
return (
<div className="mx-auto max-w-4xl px-4 py-8 space-y-6">
<div className="flex items-center justify-between">
@ -62,7 +76,7 @@ const PurchasesPage = () => {
<Package className="h-6 w-6" />
<h1 className="text-2xl font-bold">My Purchases</h1>
</div>
<Button variant="outline" size="sm" onClick={() => navigate("/")}>
<Button variant="outline" size="sm" onClick={() => onNavigate("/")}>
<ArrowLeft className="h-4 w-4 mr-1" />
Back
</Button>
@ -73,7 +87,7 @@ const PurchasesPage = () => {
<CardContent className="flex flex-col items-center justify-center py-16 gap-4">
<Package className="h-12 w-12 text-muted-foreground" />
<p className="text-muted-foreground">No purchases yet.</p>
<Button variant="outline" onClick={() => navigate("/")}>
<Button variant="outline" onClick={() => onNavigate("/")}>
Browse Products
</Button>
</CardContent>
@ -137,15 +151,26 @@ const PurchasesPage = () => {
{" · "}
{tx.currency} {Number(item.price || 0).toFixed(2)}
</p>
{item.vendorSlug && (
<a
href={`/user/${item.vendorSlug}`}
onClick={(e) => { e.preventDefault(); onNavigate(`/user/${item.vendorSlug}`); }}
className="text-xs text-primary hover:underline flex items-center gap-1 mt-0.5 cursor-pointer"
>
<Store className="h-3 w-3" />
{item.vendorSlug}
</a>
)}
</div>
{item.pageUrl && (
<Link
to={item.pageUrl}
className="text-primary hover:underline text-xs flex items-center gap-1 shrink-0"
{item.vendorSlug && item.pageSlug && (
<a
href={`/user/${item.vendorSlug}/pages/${item.pageSlug}`}
onClick={(e) => { e.preventDefault(); onNavigate(`/user/${item.vendorSlug}/pages/${item.pageSlug}`); }}
className="text-primary hover:underline text-xs flex items-center gap-1 shrink-0 cursor-pointer"
>
View
<ExternalLink className="h-3 w-3" />
</Link>
</a>
)}
</div>
))}
@ -165,6 +190,4 @@ const PurchasesPage = () => {
)}
</div>
);
};
export default PurchasesPage;
}

View File

@ -20,8 +20,11 @@ import GlobalDragDrop from "@/components/GlobalDragDrop";
// Register all widgets on app boot
registerAllWidgets();
import "./debug_env";
import Index from "./pages/Index";
import Auth from "./pages/Auth";
import Profile from "./pages/Profile";
import Post from "./pages/Post";
import UserProfile from "./pages/UserProfile";
@ -31,11 +34,11 @@ import NewCollection from "./pages/NewCollection";
const UserPage = React.lazy(() => import("./modules/pages/UserPage"));
import NewPage from "./modules/pages/NewPage";
import NewPost from "./pages/NewPost";
import TagPage from "./pages/TagPage";
import SearchResults from "./pages/SearchResults";
import Wizard from "./pages/Wizard";
import NewPost from "./pages/NewPost";
import Organizations from "./pages/Organizations";
import LogsPage from "./components/logging/LogsPage";
@ -56,82 +59,80 @@ const TypesPlayground = React.lazy(() => import("@/modules/types/TypesPlayground
const VariablePlayground = React.lazy(() => import("./components/variables/VariablesEditor").then(module => ({ default: module.VariablesEditor })));
const Tetris = React.lazy(() => import("./apps/tetris/Tetris"));
const I18nPlayground = React.lazy(() => import("./components/playground/I18nPlayground"));
const PurchasesPage = React.lazy(() => import("./pages/PurchasesPage"));
const VersionMap = React.lazy(() => import("./pages/VersionMap"));
// Ecommerce pages (from @polymech/ecommerce library)
const EcommerceCartPage = React.lazy(() => import("@polymech/ecommerce").then(m => ({ default: m.CartPage })));
const EcommerceCheckoutPage = React.lazy(() => import("@polymech/ecommerce").then(m => ({ default: m.CheckoutPage })));
const EcommerceShippingPage = React.lazy(() => import("@polymech/ecommerce").then(m => ({ default: m.ShippingPage })));
const EcommerceReturnsPage = React.lazy(() => import("@polymech/ecommerce").then(m => ({ default: m.ReturnsPage })));
const EcommercePrivacyPage = React.lazy(() => import("@polymech/ecommerce").then(m => ({ default: m.PrivacyPolicyPage })));
const EcommerceTermsPage = React.lazy(() => import("@polymech/ecommerce").then(m => ({ default: m.TermsPage })));
// <GlobalDebug />
// Ecommerce route wrappers (need useNavigate which requires component context)
const CartRouteWrapper = () => {
const navigate = useNavigate();
return <EcommerceCartPage onCheckout={() => navigate('/checkout')} />;
};
const CheckoutRouteWrapper = () => {
const navigate = useNavigate();
const EcommerceBundle = React.lazy(() => import("@polymech/ecommerce").then(m => ({ default: m.EcommerceBundle })));
const EcommerceBundleWrapper = () => {
const { user } = useAuth();
const [savedAddresses, setSavedAddresses] = React.useState<any[]>([]);
const navigate = useNavigate();
React.useEffect(() => {
if (!user?.id) return;
import('@/modules/user/client-user').then(({ getShippingAddresses }) =>
getShippingAddresses(user.id).then((addrs) => setSavedAddresses(addrs as any)),
);
}, [user?.id]);
// Memoize dependencies to prevent re-renders
const dependencies = React.useMemo(() => {
return {
user: user ? {
id: user.id,
email: user.email,
user_metadata: {
display_name: user.user_metadata?.display_name
}
} : null,
toast: {
success: (msg: string) => import('sonner').then(s => s.toast.success(msg)),
error: (msg: string) => import('sonner').then(s => s.toast.error(msg))
},
onFetchAddresses: async (userId: string) => {
const { getShippingAddresses } = await import('@/modules/user/client-user');
return getShippingAddresses(userId) as Promise<any[]>;
},
onSaveAddress: async (userId: string, addresses: any[]) => {
const { saveShippingAddresses } = await import('@/modules/user/client-user');
await saveShippingAddresses(userId, addresses);
},
onPlaceOrder: async (data: any) => {
const { useCartStore } = await import('@polymech/ecommerce');
const { createTransaction } = await import('@/modules/ecommerce/client-ecommerce');
const items = useCartStore.getState().items;
const subtotal = useCartStore.getState().subtotal;
const handlePlaceOrder = async (data: any) => {
try {
const { useCartStore } = await import('@polymech/ecommerce');
const { createTransaction } = await import('@/modules/ecommerce/client-ecommerce');
const { toast } = await import('sonner');
const items = useCartStore.getState().items;
const subtotal = useCartStore.getState().subtotal;
await createTransaction({
shipping_info: data.shipping,
vendor_info: {},
product_info: items.map((i: any) => ({
id: i.id,
title: i.title,
image: i.image,
price: i.price,
quantity: i.quantity,
variant: i.variant,
vendorSlug: i.vendorSlug,
pageSlug: i.pageSlug,
})),
total_amount: subtotal,
payment_provider: data.paymentMethod,
});
useCartStore.getState().clearCart();
},
onFetchTransactions: async () => {
const { listTransactions } = await import('@/modules/ecommerce/client-ecommerce');
return listTransactions();
},
onNavigate: (path: string) => navigate(path),
siteName: "PolyMech",
contactEmail: "legal@polymech.org"
};
}, [user, navigate]);
await createTransaction({
shipping_info: data.shipping,
vendor_info: {},
product_info: items.map((i: any) => ({
id: i.id,
title: i.title,
image: i.image,
price: i.price,
quantity: i.quantity,
variant: i.variant,
})),
total_amount: subtotal,
payment_provider: data.paymentMethod,
});
useCartStore.getState().clearCart();
toast.success('Order placed successfully!');
navigate('/purchases');
} catch (err) {
console.error('Failed to create transaction:', err);
const { toast } = await import('sonner');
toast.error('Failed to place order. Please try again.');
}
};
// Note: savedAddresses prop requires ecommerce package rebuild to type-check
const checkoutProps: any = {
onPlaceOrder: handlePlaceOrder,
onBackToCart: () => navigate('/cart'),
savedAddresses,
initialShipping: {
fullName: user?.user_metadata?.display_name ?? '',
email: user?.email ?? '',
},
};
return <EcommerceCheckoutPage {...checkoutProps} />;
return (
<React.Suspense fallback={<div className="flex items-center justify-center p-12">Loading...</div>}>
<EcommerceBundle {...dependencies} />
</React.Suspense>
);
};
@ -144,6 +145,8 @@ const AppWrapper = () => {
? "flex flex-col min-h-svh transition-colors duration-200 h-full"
: "mx-auto 2xl:max-w-7xl flex flex-col min-h-svh transition-colors duration-200 h-full";
const ecommerce = import.meta.env.VITE_ENABLE_ECOMMERCE === 'true';
console.log('DEBUG: ecommerce:', ecommerce);
return (
<div className={containerClassName}>
{!isFullScreenPage && <TopNavigation />}
@ -200,13 +203,17 @@ const AppWrapper = () => {
<Route path="/app/tetris" element={<React.Suspense fallback={<div>Loading...</div>}><Tetris /></React.Suspense>} />
{/* Ecommerce Routes */}
<Route path="/cart" element={<React.Suspense fallback={<div>Loading...</div>}><CartRouteWrapper /></React.Suspense>} />
<Route path="/checkout" element={<React.Suspense fallback={<div>Loading...</div>}><CheckoutRouteWrapper /></React.Suspense>} />
<Route path="/shipping" element={<React.Suspense fallback={<div>Loading...</div>}><EcommerceShippingPage /></React.Suspense>} />
<Route path="/returns" element={<React.Suspense fallback={<div>Loading...</div>}><EcommerceReturnsPage /></React.Suspense>} />
<Route path="/privacy" element={<React.Suspense fallback={<div>Loading...</div>}><EcommercePrivacyPage siteName="PolyMech" contactEmail="privacy@polymech.org" /></React.Suspense>} />
<Route path="/terms" element={<React.Suspense fallback={<div>Loading...</div>}><EcommerceTermsPage siteName="PolyMech" contactEmail="legal@polymech.org" /></React.Suspense>} />
<Route path="/purchases" element={<React.Suspense fallback={<div>Loading...</div>}><PurchasesPage /></React.Suspense>} />
{(ecommerce) && (
<>
<Route path="/cart/*" element={<EcommerceBundleWrapper />} />
<Route path="/checkout/*" element={<EcommerceBundleWrapper />} />
<Route path="/shipping/*" element={<EcommerceBundleWrapper />} />
<Route path="/returns/*" element={<EcommerceBundleWrapper />} />
<Route path="/privacy/*" element={<EcommerceBundleWrapper />} />
<Route path="/terms/*" element={<EcommerceBundleWrapper />} />
<Route path="/purchases/*" element={<EcommerceBundleWrapper />} />
</>
)}
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<React.Suspense fallback={<div>Loading...</div>}><NotFound /></React.Suspense>} />

View File

@ -0,0 +1 @@
console.log('DEBUG: process.env.ENABLE_ECOMMERCE:', import.meta.env.VITE_ENABLE_ECOMMERCE);

View File

@ -68,11 +68,17 @@ export const PageActions = ({
const handleAddToCart = () => {
if (productPrice === null) return;
// Extract vendor username from URL: /user/{username}/pages/{slug}
const pathParts = window.location.pathname.split('/');
const userIdx = pathParts.indexOf('user');
const vendorSlug = userIdx >= 0 ? pathParts[userIdx + 1] : undefined;
addItem({
id: `page-${page.id}`,
title: page.title,
price: productPrice,
image: page.meta?.thumbnail || undefined,
vendorSlug,
pageSlug: page.slug,
});
toast.success(translate('Added to cart'));
};