ecommerce 2/3
This commit is contained in:
parent
d5c0a3704e
commit
9618998670
22
packages/ecommerce/dist-lib/EcommerceBundle.d.ts
vendored
Normal file
22
packages/ecommerce/dist-lib/EcommerceBundle.d.ts
vendored
Normal 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>;
|
||||
25
packages/ecommerce/dist-lib/checkout/CheckoutFlow.d.ts
vendored
Normal file
25
packages/ecommerce/dist-lib/checkout/CheckoutFlow.d.ts
vendored
Normal 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;
|
||||
@ -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;
|
||||
|
||||
9
packages/ecommerce/dist-lib/components/ui/badge.d.ts
vendored
Normal file
9
packages/ecommerce/dist-lib/components/ui/badge.d.ts
vendored
Normal 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 };
|
||||
6
packages/ecommerce/dist-lib/lib-export.d.ts
vendored
6
packages/ecommerce/dist-lib/lib-export.d.ts
vendored
@ -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
20
packages/ecommerce/dist-lib/purchases/PurchasesList.d.ts
vendored
Normal file
20
packages/ecommerce/dist-lib/purchases/PurchasesList.d.ts
vendored
Normal 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;
|
||||
82
packages/ecommerce/src/EcommerceBundle.tsx
Normal file
82
packages/ecommerce/src/EcommerceBundle.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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 */}
|
||||
|
||||
@ -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. */
|
||||
|
||||
104
packages/ecommerce/src/checkout/CheckoutFlow.tsx
Normal file
104
packages/ecommerce/src/checkout/CheckoutFlow.tsx
Normal 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} />;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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>} />
|
||||
|
||||
1
packages/ui/src/debug_env.ts
Normal file
1
packages/ui/src/debug_env.ts
Normal file
@ -0,0 +1 @@
|
||||
console.log('DEBUG: process.env.ENABLE_ECOMMERCE:', import.meta.env.VITE_ENABLE_ECOMMERCE);
|
||||
@ -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'));
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user