Shopping Cart
A shopping cart with add, remove, and checkout functionality using individual stage components and StageRenderer.
Overview
This example demonstrates a comprehensive e-commerce shopping cart with the following features:
- Cart Management: Add, remove, and update item quantities
- Individual Stage Components: Each stage is a separate React component
- StageRenderer: Automatic stage rendering based on current stage
- State Persistence: Cart items persist across stage transitions
- Checkout Flow: Complete checkout process with shipping information
- Order Processing: Simulated order processing with loading states
- Order Completion: Success state with order confirmation
- Product Catalog: Browse and add products to cart
Key Features to Observe
- Individual Stage Components: Each stage (browsing, checkout, confirmation) is a separate component
- StageRenderer Usage: Automatic stage rendering without conditional rendering
- Cart Management: Add, remove, and update item quantities
- State Persistence: Cart items persist across stage transitions
- Checkout Flow: Complete checkout process with shipping information
- Order Processing: Simulated order processing with loading states
- Order Completion: Success state with order confirmation
- Product Catalog: Browse and add products to cart
Live Example
Live Editor
// import { StageFlowEngine } from '@stage-flow/core'; // import { StageFlowProvider, useStageFlow } from '@stage-flow/react'; function ShoppingCart() { // Common UI Components function FormContainer({ children, title = "Shopping Cart" }) { return ( <div style={{ padding: "20px", border: "1px solid #ddd", borderRadius: "8px", maxWidth: "800px", backgroundColor: "white", }} > <h2 style={{ margin: "0 0 20px 0", color: "#2c3e50" }}>{title}</h2> {children} </div> ); } function FormButton({ onClick, children, variant = "primary", disabled = false, style = {} }) { const buttonStyles = { primary: { backgroundColor: "#007bff", color: "white", }, secondary: { backgroundColor: "#6c757d", color: "white", }, success: { backgroundColor: "#28a745", color: "white", }, danger: { backgroundColor: "#dc3545", color: "white", }, }; const buttonStyle = { padding: "10px 20px", border: "none", borderRadius: "4px", cursor: disabled ? "not-allowed" : "pointer", fontSize: "14px", fontWeight: "500", opacity: disabled ? 0.6 : 1, ...buttonStyles[variant], ...style, }; return ( <button onClick={onClick} style={buttonStyle} disabled={disabled}> {children} </button> ); } function FormInput({ type = "text", placeholder, value, onChange, style = {} }) { const inputStyle = { padding: "10px", border: "1px solid #ddd", borderRadius: "4px", width: "100%", marginBottom: "10px", color: "#2c3e50", fontSize: "14px", backgroundColor: "white", ...style, }; return ( <input type={type} placeholder={placeholder} value={value || ""} onChange={onChange} style={inputStyle} /> ); } function ProductCard({ product, onAddToCart }) { return ( <div style={{ border: "1px solid #ddd", padding: "15px", borderRadius: "4px", textAlign: "center", }} > <h4 style={{ margin: "0 0 10px 0", color: "#2c3e50" }}>{product.name}</h4> <p style={{ color: "#007bff", fontWeight: "bold", margin: "0 0 10px 0", }} > ${product.price} </p> <FormButton onClick={() => onAddToCart(product)} variant="success"> Add to Cart </FormButton> </div> ); } function CartItem({ item, onUpdateQuantity, onRemoveItem }) { return ( <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", padding: "10px", border: "1px solid #eee", marginBottom: "10px", borderRadius: "4px", backgroundColor: "white", }} > <span style={{ color: "#2c3e50", fontWeight: "500" }}>{item.name}</span> <div style={{ display: "flex", alignItems: "center", gap: "10px" }}> <FormButton onClick={() => onUpdateQuantity(item.id, item.quantity - 1)} variant="danger" style={{ padding: "4px 8px" }} > - </FormButton> <span style={{ color: "#2c3e50", fontWeight: "500", minWidth: "20px", textAlign: "center" }}> {item.quantity} </span> <FormButton onClick={() => onUpdateQuantity(item.id, item.quantity + 1)} variant="success" style={{ padding: "4px 8px" }} > + </FormButton> <span style={{ color: "#2c3e50", fontWeight: "500", minWidth: "60px", textAlign: "right" }}> ${item.price * item.quantity} </span> </div> </div> ); } function DebugInfo() { const { currentStage, data } = useStageFlow(); return ( <div style={{ marginBottom: "20px", padding: "15px", backgroundColor: "#f8f9fa", borderRadius: "4px", border: "1px solid #e9ecef", maxWidth: "800px", width: "100%" }}> <div style={{ marginBottom: "10px" }}> <h4 style={{ margin: "0", color: "#495057" }}>Debug Info:</h4> </div> <div> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Current Stage:</strong> {currentStage} </p> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Stage Components:</strong> browsing, checkout, confirmation </p> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Current Data:</strong> {JSON.stringify(data)} </p> </div> </div> ); } // Browsing stage component function BrowsingStage({ data, send }) { const { engine } = useStageFlow(); const products = [ { id: 1, name: "Laptop", price: 999 }, { id: 2, name: "Mouse", price: 25 }, { id: 3, name: "Keyboard", price: 75 }, ]; const addToCart = React.useCallback((product) => { const existingItem = data?.items?.find(item => item.id === product.id); let updatedItems; if (existingItem) { updatedItems = data.items.map(item => (item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item)); } else { updatedItems = [...(data?.items || []), { ...product, quantity: 1 }]; } const total = updatedItems.reduce((sum, item) => sum + item.price * item.quantity, 0); engine.setStageData({ ...(data || {}), items: updatedItems, total, }); }, [data, engine]); const removeFromCart = React.useCallback((productId) => { const updatedItems = data?.items?.filter(item => item.id !== productId); const total = updatedItems?.reduce((sum, item) => sum + item.price * item.quantity, 0) || 0; engine.setStageData({ ...(data || {}), items: updatedItems, total, }); }, [data, engine]); const updateQuantity = React.useCallback((productId, quantity) => { if (quantity <= 0) { removeFromCart(productId); return; } const updatedItems = data?.items?.map(item => (item.id === productId ? { ...item, quantity } : item)); const total = updatedItems?.reduce((sum, item) => sum + item.price * item.quantity, 0) || 0; engine.setStageData({ ...(data || {}), items: updatedItems, total, }); }, [data, engine, removeFromCart]); return ( <FormContainer> <div style={{ marginBottom: "20px" }}> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Products</h3> <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: "15px", }} > {products.map(product => ( <ProductCard key={product.id} product={product} onAddToCart={addToCart} /> ))} </div> </div> {data?.items && data.items.length > 0 ? ( <div> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Cart Items</h3> {data.items.map(item => ( <CartItem key={item.id} item={item} onUpdateQuantity={updateQuantity} onRemoveItem={removeFromCart} /> ))} <div style={{ marginTop: "20px" }}> <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "10px" }}> <strong style={{ color: "#2c3e50" }}>Total:</strong> <strong style={{ color: "#2c3e50" }}>${data.total}</strong> </div> <FormButton onClick={() => send("proceedToCheckout", { ...(data || {}) })} style={{ width: "100%", fontSize: "16px" }} > Proceed to Checkout </FormButton> </div> </div> ) : ( <div style={{ textAlign: "center", padding: "40px 0" }}> <h3 style={{ margin: "0 0 15px 0", color: "#6c757d" }}>Your cart is empty</h3> <p style={{ color: "#6c757d" }}>Add some items to get started!</p> </div> )} </FormContainer> ); } // Checkout stage component function CheckoutStage({ data, send }) { const { engine } = useStageFlow(); const updateShippingInfo = React.useCallback((field, value) => { engine.setStageData({ ...data, [field]: value }); }, [data, engine]); const calculateShipping = React.useCallback(() => { if (data?.total > 100) return 0; // Free shipping for orders over $100 switch (data?.shippingMethod) { case "express": return 15; case "overnight": return 25; default: return 5; // standard } }, [data?.total, data?.shippingMethod]); const getTotalWithShipping = React.useCallback(() => { return (data?.total || 0) + calculateShipping(); }, [data?.total, calculateShipping]); return ( <FormContainer> <h3 style={{ margin: "0 0 20px 0", color: "#2c3e50" }}>Checkout</h3> <div style={{ marginBottom: "20px" }}> <h4 style={{ margin: "0 0 10px 0", color: "#2c3e50" }}>Order Summary</h4> {data?.items && data.items.length > 0 ? ( <> {data.items.map(item => ( <div key={item.id} style={{ display: "flex", justifyContent: "space-between", padding: "5px 0", color: "#2c3e50", }} > <span> {item.name} x {item.quantity} </span> <span>${item.price * item.quantity}</span> </div> ))} <div style={{ borderTop: "1px solid #eee", paddingTop: "10px", marginTop: "10px", fontWeight: "bold", }} > <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "5px", color: "#2c3e50" }}> <span>Subtotal:</span> <span>${data?.total || 0}</span> </div> <div style={{ display: "flex", justifyContent: "space-between", marginBottom: "5px", color: "#2c3e50" }}> <span>Shipping:</span> <span>${calculateShipping()}</span> </div> <div style={{ display: "flex", justifyContent: "space-between", color: "#2c3e50", fontSize: "16px" }}> <span> <strong>Total:</strong> </span> <span> <strong>${getTotalWithShipping()}</strong> </span> </div> </div> </> ) : ( <div style={{ color: "#6c757d", textAlign: "center", padding: "20px" }}>No items in cart</div> )} </div> <div style={{ marginBottom: "20px" }}> <h4 style={{ margin: "0 0 10px 0", color: "#2c3e50" }}>Shipping Information</h4> <FormInput placeholder="Shipping Address" value={data?.shippingAddress || ""} onChange={e => updateShippingInfo("shippingAddress", e.target.value)} /> <select value={data?.shippingMethod || "standard"} onChange={e => updateShippingInfo("shippingMethod", e.target.value)} style={{ padding: "10px", border: "1px solid #ddd", borderRadius: "4px", width: "100%", backgroundColor: "white", color: "#2c3e50", fontSize: "14px", }} > <option value="standard">Standard Shipping (3-5 days) - $5</option> <option value="express">Express Shipping (1-2 days) - $15</option> <option value="overnight">Overnight Shipping - $25</option> </select> </div> <div style={{ display: "flex", gap: "10px" }}> <FormButton onClick={() => send("backToBrowsing")} variant="secondary"> Back to Cart </FormButton> <FormButton onClick={() => send("placeOrder", { ...(data || {}) })} variant="success" style={{ flex: 1 }}> Place Order </FormButton> </div> </FormContainer> ); } // Confirmation stage component function ConfirmationStage({ data, send }) { const calculateShipping = React.useCallback(() => { if (data?.total > 100) return 0; switch (data?.shippingMethod) { case "express": return 15; case "overnight": return 25; default: return 5; } }, [data?.total, data?.shippingMethod]); const getTotalWithShipping = React.useCallback(() => { return (data?.total || 0) + calculateShipping(); }, [data?.total, calculateShipping]); return ( <FormContainer> <div style={{ textAlign: "center", padding: "40px 0" }}> <h3 style={{ margin: "0 0 15px 0", color: "#28a745" }}>Order Complete!</h3> <p style={{ color: "#666", marginBottom: "20px" }}>Thank you for your order. Your items will be shipped to:</p> <p style={{ color: "#333", marginBottom: "20px", fontWeight: "bold" }}>{data?.shippingAddress || "No address provided"}</p> <p style={{ color: "#666", marginBottom: "20px" }}>Shipping method: {data?.shippingMethod || "Standard"}</p> <p style={{ color: "#666", marginBottom: "20px" }}>Total paid: ${getTotalWithShipping()}</p> <FormButton onClick={() => send("continueShopping", { items: [], shippingAddress: "", shippingMethod: "standard", total: 0, }) } > Start New Order </FormButton> </div> </FormContainer> ); } // Create engine directly without useRef const engine = new StageFlowEngine({ initial: "browsing", data: { items: [], shippingAddress: "", shippingMethod: "standard", total: 0, }, stages: [ { name: "browsing", transitions: [ { target: "checkout", event: "proceedToCheckout" }, { target: "browsing", event: "addItem" }, { target: "browsing", event: "removeItem" }, { target: "browsing", event: "updateQuantity" }, ], }, { name: "checkout", transitions: [ { target: "confirmation", event: "placeOrder" }, { target: "browsing", event: "backToBrowsing" }, { target: "checkout", event: "updateShipping" }, ], }, { name: "confirmation", transitions: [{ target: "browsing", event: "continueShopping" }], }, ], }); engine.start(); return ( <StageFlowProvider engine={engine}> <DebugInfo /> <StageRenderer stageComponents={{ browsing: BrowsingStage, checkout: CheckoutStage, confirmation: ConfirmationStage, }} /> </StageFlowProvider> ); }
Result
Loading...
Debug Interface
The debug interface provides real-time state monitoring:
- Current Stage: Shows the active stage name
- Stage Components: Lists all available stage components
- Current Data: Displays the current form data in JSON format
This helps developers monitor form state and debug state transitions during development.