Spaces:
Runtime error
Runtime error
| import React, { useEffect, useMemo, useState } from "react"; | |
| import { | |
| LayoutDashboard, | |
| ShoppingCart, | |
| BarChart3, | |
| Wallet, | |
| Table2, | |
| Settings, | |
| LogOut, | |
| RefreshCw, | |
| Search, | |
| ChevronDown, | |
| Check, | |
| X, | |
| Pencil, | |
| Trash2, | |
| Percent, | |
| BadgeCheck, | |
| } from "lucide-react"; | |
| /** | |
| * iStore POS (Apple-only) — single-file UI mock inspired by the provided POS layout. | |
| * - Product grid with category tabs + search | |
| * - Cart/order summary with totals, discount, and simple order meta | |
| * - Clean, responsive layout | |
| */ | |
| // ---------- Helpers | |
| const money = (n) => | |
| new Intl.NumberFormat(undefined, { | |
| style: "currency", | |
| currency: "USD", | |
| }).format(n); | |
| const nowLabel = () => { | |
| const d = new Date(); | |
| const weekday = d.toLocaleDateString(undefined, { weekday: "long" }); | |
| const date = d.toLocaleDateString(undefined, { day: "2-digit", month: "short", year: "numeric" }); | |
| const time = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }); | |
| return `${weekday}, ${date} • ${time}`; | |
| }; | |
| const pill = (active) => | |
| `inline-flex items-center gap-2 rounded-full px-3 py-1 text-sm transition ${ | |
| active | |
| ? "bg-slate-900 text-white shadow-sm" | |
| : "bg-white text-slate-700 hover:bg-slate-50 border border-slate-200" | |
| }`; | |
| function dataUriCard({ title, subtitle, glyph = "" }) { | |
| // Simple “photo-like” SVG card so images work offline. | |
| const svg = ` | |
| <svg xmlns='http://www.w3.org/2000/svg' width='900' height='600'> | |
| <defs> | |
| <linearGradient id='g' x1='0' y1='0' x2='1' y2='1'> | |
| <stop offset='0' stop-color='#0f172a'/> | |
| <stop offset='0.45' stop-color='#111827'/> | |
| <stop offset='1' stop-color='#1f2937'/> | |
| </linearGradient> | |
| <radialGradient id='r' cx='70%' cy='20%' r='70%'> | |
| <stop offset='0' stop-color='#93c5fd' stop-opacity='0.75'/> | |
| <stop offset='0.55' stop-color='#60a5fa' stop-opacity='0.25'/> | |
| <stop offset='1' stop-color='#60a5fa' stop-opacity='0'/> | |
| </radialGradient> | |
| <filter id='noise'> | |
| <feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/> | |
| <feColorMatrix type='matrix' values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.06 0'/> | |
| </filter> | |
| </defs> | |
| <rect width='100%' height='100%' fill='url(#g)'/> | |
| <rect width='100%' height='100%' fill='url(#r)'/> | |
| <rect width='100%' height='100%' filter='url(#noise)'/> | |
| <circle cx='120' cy='120' r='70' fill='rgba(255,255,255,0.09)'/> | |
| <text x='120' y='140' text-anchor='middle' font-size='64' fill='rgba(255,255,255,0.95)' font-family='ui-sans-serif, -apple-system, Segoe UI, Roboto'>${glyph}</text> | |
| <text x='40' y='520' font-size='44' fill='white' font-weight='700' | |
| font-family='ui-sans-serif, -apple-system, Segoe UI, Roboto'>${escapeXml(title)}</text> | |
| <text x='40' y='565' font-size='24' fill='rgba(255,255,255,0.78)' | |
| font-family='ui-sans-serif, -apple-system, Segoe UI, Roboto'>${escapeXml(subtitle)}</text> | |
| <path d='M820 70c-60 35-120 55-180 60' fill='none' stroke='rgba(255,255,255,0.16)' stroke-width='10' stroke-linecap='round'/> | |
| <path d='M830 150c-70 40-140 62-210 66' fill='none' stroke='rgba(255,255,255,0.10)' stroke-width='10' stroke-linecap='round'/> | |
| </svg>`; | |
| return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg.trim())}`; | |
| } | |
| function escapeXml(s) { | |
| return String(s) | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| } | |
| // ---------- Data | |
| const PRODUCTS = [ | |
| { | |
| id: "iphone-15-pro", | |
| name: "iPhone 15 Pro", | |
| price: 999, | |
| category: "iPhone", | |
| variant: "128GB", | |
| available: true, | |
| glyph: "📱", | |
| }, | |
| { | |
| id: "iphone-15", | |
| name: "iPhone 15", | |
| price: 799, | |
| category: "iPhone", | |
| variant: "128GB", | |
| available: true, | |
| glyph: "📱", | |
| }, | |
| { | |
| id: "macbook-air-m3", | |
| name: "MacBook Air", | |
| price: 1099, | |
| category: "Mac", | |
| variant: "M3 • 13-inch", | |
| available: true, | |
| glyph: "💻", | |
| }, | |
| { | |
| id: "macbook-pro-m3", | |
| name: "MacBook Pro", | |
| price: 1599, | |
| category: "Mac", | |
| variant: "M3 • 14-inch", | |
| available: true, | |
| glyph: "💻", | |
| }, | |
| { | |
| id: "imac-m3", | |
| name: "iMac", | |
| price: 1299, | |
| category: "Mac", | |
| variant: "M3 • 24-inch", | |
| available: true, | |
| glyph: "🖥️", | |
| }, | |
| { | |
| id: "mac-mini", | |
| name: "Mac mini", | |
| price: 599, | |
| category: "Mac", | |
| variant: "M2", | |
| available: true, | |
| glyph: "🧊", | |
| }, | |
| { | |
| id: "ipad-pro", | |
| name: "iPad Pro", | |
| price: 999, | |
| category: "iPad", | |
| variant: "11-inch", | |
| available: true, | |
| glyph: "📟", | |
| }, | |
| { | |
| id: "ipad-air", | |
| name: "iPad Air", | |
| price: 599, | |
| category: "iPad", | |
| variant: "10.9-inch", | |
| available: true, | |
| glyph: "📟", | |
| }, | |
| { | |
| id: "apple-watch-ultra", | |
| name: "Apple Watch Ultra", | |
| price: 799, | |
| category: "Watch", | |
| variant: "49mm", | |
| available: true, | |
| glyph: "⌚", | |
| }, | |
| { | |
| id: "apple-watch-series", | |
| name: "Apple Watch Series", | |
| price: 399, | |
| category: "Watch", | |
| variant: "41mm", | |
| available: true, | |
| glyph: "⌚", | |
| }, | |
| { | |
| id: "airpods-pro", | |
| name: "AirPods Pro", | |
| price: 249, | |
| category: "Audio", | |
| variant: "2nd gen", | |
| available: true, | |
| glyph: "🎧", | |
| }, | |
| { | |
| id: "airpods-max", | |
| name: "AirPods Max", | |
| price: 549, | |
| category: "Audio", | |
| variant: "Over-ear", | |
| available: true, | |
| glyph: "🎧", | |
| }, | |
| { | |
| id: "homepod", | |
| name: "HomePod", | |
| price: 299, | |
| category: "Audio", | |
| variant: "2nd gen", | |
| available: true, | |
| glyph: "🔊", | |
| }, | |
| { | |
| id: "airtag-4pack", | |
| name: "AirTag (4‑pack)", | |
| price: 99, | |
| category: "Accessories", | |
| variant: "4 pack", | |
| available: true, | |
| glyph: "🏷️", | |
| }, | |
| { | |
| id: "magic-keyboard", | |
| name: "Magic Keyboard", | |
| price: 99, | |
| category: "Accessories", | |
| variant: "Wireless", | |
| available: false, | |
| glyph: "⌨️", | |
| }, | |
| ]; | |
| const CATEGORIES = ["All", "iPhone", "Mac", "iPad", "Watch", "Audio", "Accessories"]; | |
| const SEED_ORDER_ID = () => `#I${Math.floor(100000 + Math.random() * 900000)}`; | |
| // ---------- UI bits | |
| function Chip({ children, active, onClick }) { | |
| return ( | |
| <button className={pill(active)} onClick={onClick}> | |
| {children} | |
| </button> | |
| ); | |
| } | |
| function IconDot({ ok }) { | |
| return ( | |
| <span | |
| className={`inline-flex items-center gap-2 rounded-full px-2 py-0.5 text-xs font-semibold ${ | |
| ok ? "bg-emerald-50 text-emerald-700" : "bg-rose-50 text-rose-700" | |
| }`} | |
| > | |
| <span | |
| className={`h-1.5 w-1.5 rounded-full ${ok ? "bg-emerald-500" : "bg-rose-500"}`} | |
| /> | |
| {ok ? "Available" : "Out of stock"} | |
| </span> | |
| ); | |
| } | |
| function Divider() { | |
| return <div className="h-px w-full bg-slate-200" />; | |
| } | |
| function SmallField({ label, children }) { | |
| return ( | |
| <div className="space-y-1"> | |
| <div className="text-xs font-semibold text-slate-500">{label}</div> | |
| {children} | |
| </div> | |
| ); | |
| } | |
| // ---------- Main | |
| export default function IStorePOS() { | |
| const [activeNav, setActiveNav] = useState("Menu Order"); | |
| const [category, setCategory] = useState("All"); | |
| const [query, setQuery] = useState(""); | |
| const [cart, setCart] = useState(() => { | |
| // Prefill like the screenshot (optional): | |
| const pre = [ | |
| { id: "airpods-pro", qty: 1, note: "—" }, | |
| { id: "macbook-air-m3", qty: 1, note: "Silver" }, | |
| { id: "iphone-15-pro", qty: 1, note: "Natural Titanium" }, | |
| ]; | |
| return pre; | |
| }); | |
| const [applyDiscount, setApplyDiscount] = useState(true); | |
| const [orderId, setOrderId] = useState(SEED_ORDER_ID); | |
| const [clock, setClock] = useState(nowLabel()); | |
| const [orderType, setOrderType] = useState("In-store"); | |
| const [counter, setCounter] = useState("Counter A"); | |
| useEffect(() => { | |
| const t = setInterval(() => setClock(nowLabel()), 15_000); | |
| return () => clearInterval(t); | |
| }, []); | |
| const productsWithImages = useMemo(() => { | |
| return PRODUCTS.map((p) => ({ | |
| ...p, | |
| image: | |
| p.image || | |
| dataUriCard({ | |
| title: p.name, | |
| subtitle: `${p.variant} • ${money(p.price)}`, | |
| glyph: p.glyph, | |
| }), | |
| })); | |
| }, []); | |
| const counts = useMemo(() => { | |
| const c = { All: productsWithImages.length }; | |
| for (const cat of CATEGORIES.slice(1)) { | |
| c[cat] = productsWithImages.filter((p) => p.category === cat).length; | |
| } | |
| return c; | |
| }, [productsWithImages]); | |
| const filtered = useMemo(() => { | |
| const q = query.trim().toLowerCase(); | |
| return productsWithImages | |
| .filter((p) => (category === "All" ? true : p.category === category)) | |
| .filter((p) => | |
| q | |
| ? `${p.name} ${p.variant} ${p.category}`.toLowerCase().includes(q) | |
| : true | |
| ); | |
| }, [productsWithImages, category, query]); | |
| const cartLines = useMemo(() => { | |
| return cart | |
| .map((line) => { | |
| const p = productsWithImages.find((x) => x.id === line.id); | |
| if (!p) return null; | |
| return { | |
| ...line, | |
| product: p, | |
| lineTotal: p.price * line.qty, | |
| }; | |
| }) | |
| .filter(Boolean); | |
| }, [cart, productsWithImages]); | |
| const subtotal = useMemo(() => cartLines.reduce((s, l) => s + l.lineTotal, 0), [cartLines]); | |
| const taxRate = 0.1; | |
| const taxes = useMemo(() => subtotal * taxRate, [subtotal]); | |
| const discountEligible = subtotal >= 500; | |
| const discount = useMemo(() => (applyDiscount && discountEligible ? subtotal * 0.1 : 0), [applyDiscount, discountEligible, subtotal]); | |
| const total = useMemo(() => Math.max(0, subtotal + taxes - discount), [subtotal, taxes, discount]); | |
| const addToCart = (id) => { | |
| setCart((prev) => { | |
| const idx = prev.findIndex((x) => x.id === id); | |
| if (idx >= 0) { | |
| const next = [...prev]; | |
| next[idx] = { ...next[idx], qty: next[idx].qty + 1 }; | |
| return next; | |
| } | |
| return [...prev, { id, qty: 1, note: "—" }]; | |
| }); | |
| }; | |
| const setQty = (id, qty) => { | |
| setCart((prev) => | |
| prev | |
| .map((x) => (x.id === id ? { ...x, qty } : x)) | |
| .filter((x) => x.qty > 0) | |
| ); | |
| }; | |
| const removeLine = (id) => setCart((prev) => prev.filter((x) => x.id !== id)); | |
| const randomizeOrder = () => { | |
| setOrderId(SEED_ORDER_ID()); | |
| setQuery(""); | |
| setCategory("All"); | |
| }; | |
| const navItems = [ | |
| { label: "Dashboard", icon: LayoutDashboard }, | |
| { label: "Menu Order", icon: ShoppingCart }, | |
| { label: "Analytics", icon: BarChart3 }, | |
| { label: "Withdrawal", icon: Wallet }, | |
| { label: "Manage Table", icon: Table2 }, | |
| ]; | |
| return ( | |
| <div className="min-h-screen w-full bg-gradient-to-b from-slate-100 to-slate-50 p-4"> | |
| <div className="mx-auto max-w-[1400px] overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm"> | |
| <div className="flex min-h-[820px]"> | |
| {/* Sidebar */} | |
| <aside className="w-[260px] border-r border-slate-200 bg-white p-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="grid h-10 w-10 place-items-center rounded-xl bg-slate-900 text-white shadow-sm"> | |
| <span className="text-lg"></span> | |
| </div> | |
| <div> | |
| <div className="text-sm font-semibold text-slate-900">iStore</div> | |
| <div className="text-xs text-slate-500">POS • Apple Products</div> | |
| </div> | |
| </div> | |
| <div className="mt-6 space-y-1"> | |
| {navItems.map((it) => { | |
| const Icon = it.icon; | |
| const active = activeNav === it.label; | |
| return ( | |
| <button | |
| key={it.label} | |
| onClick={() => setActiveNav(it.label)} | |
| className={`flex w-full items-center gap-3 rounded-xl px-3 py-2 text-sm font-semibold transition ${ | |
| active | |
| ? "bg-slate-900 text-white shadow-sm" | |
| : "text-slate-600 hover:bg-slate-50" | |
| }`} | |
| > | |
| <Icon className="h-4 w-4" /> | |
| {it.label} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| <div className="mt-6 rounded-2xl border border-slate-200 bg-slate-50 p-3"> | |
| <div className="flex items-start gap-3"> | |
| <div className="grid h-9 w-9 place-items-center rounded-xl bg-white shadow-sm"> | |
| <BadgeCheck className="h-5 w-5 text-slate-800" /> | |
| </div> | |
| <div> | |
| <div className="text-sm font-semibold text-slate-900">Store Status</div> | |
| <div className="mt-1 inline-flex items-center gap-2 rounded-full bg-emerald-50 px-2.5 py-1 text-xs font-semibold text-emerald-700"> | |
| <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" /> | |
| Open | |
| </div> | |
| <div className="mt-2 text-xs text-slate-500">Apple-only catalog • Demo UI</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="mt-auto pt-6 space-y-2"> | |
| <button className="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-sm font-semibold text-slate-600 hover:bg-slate-50"> | |
| <Settings className="h-4 w-4" /> | |
| Settings | |
| </button> | |
| <button className="flex w-full items-center gap-3 rounded-xl px-3 py-2 text-sm font-semibold text-slate-600 hover:bg-slate-50"> | |
| <LogOut className="h-4 w-4" /> | |
| Logout | |
| </button> | |
| </div> | |
| </aside> | |
| {/* Main */} | |
| <main className="flex-1 bg-slate-50"> | |
| {/* Top bar */} | |
| <div className="flex items-center justify-between gap-4 border-b border-slate-200 bg-white px-5 py-4"> | |
| <div className="flex items-center gap-3"> | |
| <div className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2"> | |
| <div className="text-xs font-semibold text-slate-500">Shop</div> | |
| <div className="flex items-center gap-2 text-sm font-semibold text-slate-900"> | |
| iStore Downtown | |
| <ChevronDown className="h-4 w-4 text-slate-500" /> | |
| </div> | |
| </div> | |
| <div className="hidden sm:flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-700"> | |
| <span className="text-slate-500">{clock}</span> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| onClick={randomizeOrder} | |
| className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50" | |
| title="Refresh" | |
| > | |
| <RefreshCw className="h-4 w-4" /> | |
| Refresh | |
| </button> | |
| <div className="relative w-[240px]"> | |
| <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" /> | |
| <input | |
| value={query} | |
| onChange={(e) => setQuery(e.target.value)} | |
| placeholder="Search products" | |
| className="w-full rounded-xl border border-slate-200 bg-white py-2 pl-9 pr-3 text-sm font-semibold text-slate-800 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-900/10" | |
| /> | |
| </div> | |
| <button className="inline-flex items-center gap-2 rounded-xl bg-slate-900 px-3 py-2 text-sm font-semibold text-white shadow-sm"> | |
| <span className="grid h-7 w-7 place-items-center rounded-lg bg-white/10">MO</span> | |
| <span className="hidden md:block">Michael Olise</span> | |
| <ChevronDown className="h-4 w-4 opacity-80" /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Content */} | |
| <div className="grid grid-cols-1 gap-4 p-5 lg:grid-cols-[1fr_360px]"> | |
| {/* Left: menu */} | |
| <section className="space-y-4"> | |
| <div className="flex flex-wrap items-center justify-between gap-3"> | |
| <div className="flex flex-wrap items-center gap-2"> | |
| {CATEGORIES.map((c) => ( | |
| <Chip key={c} active={category === c} onClick={() => setCategory(c)}> | |
| <span>{c}</span> | |
| <span | |
| className={`rounded-full px-2 py-0.5 text-xs font-bold ${ | |
| category === c ? "bg-white/15 text-white" : "bg-slate-100 text-slate-600" | |
| }`} | |
| > | |
| {counts[c] ?? 0} | |
| </span> | |
| </Chip> | |
| ))} | |
| </div> | |
| <div className="text-sm font-semibold text-slate-600"> | |
| Showing <span className="text-slate-900">{filtered.length}</span> items | |
| </div> | |
| </div> | |
| {/* Grid */} | |
| <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4"> | |
| {filtered.map((p) => { | |
| const inStock = p.available; | |
| return ( | |
| <div | |
| key={p.id} | |
| className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm" | |
| > | |
| <div className="relative"> | |
| <img | |
| src={p.image} | |
| alt={p.name} | |
| className="h-36 w-full object-cover" | |
| /> | |
| <div className="absolute right-3 top-3"> | |
| <IconDot ok={inStock} /> | |
| </div> | |
| </div> | |
| <div className="space-y-2 p-3"> | |
| <div className="flex items-start justify-between gap-2"> | |
| <div> | |
| <div className="text-sm font-bold text-slate-900">{p.name}</div> | |
| <div className="text-xs font-semibold text-slate-500">{p.variant}</div> | |
| </div> | |
| <div className="text-sm font-extrabold text-slate-900">{money(p.price)}</div> | |
| </div> | |
| <button | |
| disabled={!inStock} | |
| onClick={() => addToCart(p.id)} | |
| className={`w-full rounded-xl px-3 py-2 text-sm font-semibold transition ${ | |
| inStock | |
| ? "bg-slate-900 text-white hover:bg-slate-800" | |
| : "bg-slate-100 text-slate-400 cursor-not-allowed" | |
| }`} | |
| > | |
| {inStock ? "+ Add to Cart" : "× Not Available"} | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </section> | |
| {/* Right: order summary */} | |
| <aside className="rounded-2xl border border-slate-200 bg-white shadow-sm"> | |
| <div className="flex items-center justify-between gap-3 border-b border-slate-200 p-4"> | |
| <div> | |
| <div className="text-sm font-extrabold text-slate-900">Order Summary</div> | |
| <div className="text-xs font-semibold text-slate-500">Apple products only</div> | |
| </div> | |
| <div className="text-sm font-extrabold text-slate-900">{orderId}</div> | |
| </div> | |
| <div className="space-y-3 p-4"> | |
| {/* Lines */} | |
| <div className="space-y-3"> | |
| {cartLines.length === 0 ? ( | |
| <div className="rounded-xl border border-dashed border-slate-200 bg-slate-50 p-4 text-center"> | |
| <div className="text-sm font-semibold text-slate-700">Cart is empty</div> | |
| <div className="mt-1 text-xs text-slate-500">Add an Apple product from the grid</div> | |
| </div> | |
| ) : ( | |
| cartLines.map((l) => ( | |
| <div key={l.id} className="flex gap-3"> | |
| <img | |
| src={l.product.image} | |
| alt={l.product.name} | |
| className="h-14 w-14 rounded-xl object-cover" | |
| /> | |
| <div className="min-w-0 flex-1"> | |
| <div className="flex items-start justify-between gap-2"> | |
| <div className="min-w-0"> | |
| <div className="truncate text-sm font-bold text-slate-900"> | |
| {l.product.name} <span className="text-slate-500">({l.qty})</span> | |
| </div> | |
| <div className="text-xs font-semibold text-slate-500"> | |
| Note: {l.note} • {l.product.variant} | |
| </div> | |
| </div> | |
| <div className="text-sm font-extrabold text-slate-900"> | |
| {money(l.product.price * l.qty)} | |
| </div> | |
| </div> | |
| <div className="mt-2 flex items-center justify-between"> | |
| <div className="inline-flex items-center rounded-xl border border-slate-200 bg-white"> | |
| <button | |
| className="px-2.5 py-1.5 text-sm font-bold text-slate-600 hover:bg-slate-50 rounded-l-xl" | |
| onClick={() => setQty(l.id, Math.max(0, l.qty - 1))} | |
| aria-label="Decrease" | |
| > | |
| − | |
| </button> | |
| <div className="px-2.5 py-1.5 text-sm font-bold text-slate-800"> | |
| {l.qty} | |
| </div> | |
| <button | |
| className="px-2.5 py-1.5 text-sm font-bold text-slate-600 hover:bg-slate-50 rounded-r-xl" | |
| onClick={() => setQty(l.id, l.qty + 1)} | |
| aria-label="Increase" | |
| > | |
| + | |
| </button> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <button | |
| className="inline-flex items-center gap-1.5 rounded-xl border border-slate-200 px-2.5 py-1.5 text-xs font-bold text-slate-700 hover:bg-slate-50" | |
| onClick={() => { | |
| const note = prompt( | |
| `Edit note for ${l.product.name}:`, | |
| l.note ?? "—" | |
| ); | |
| if (note === null) return; | |
| setCart((prev) => | |
| prev.map((x) => (x.id === l.id ? { ...x, note } : x)) | |
| ); | |
| }} | |
| title="Edit note" | |
| > | |
| <Pencil className="h-3.5 w-3.5" /> | |
| Edit | |
| </button> | |
| <button | |
| className="inline-flex items-center gap-1.5 rounded-xl border border-slate-200 px-2.5 py-1.5 text-xs font-bold text-slate-700 hover:bg-rose-50 hover:text-rose-700 hover:border-rose-200" | |
| onClick={() => removeLine(l.id)} | |
| title="Remove" | |
| > | |
| <Trash2 className="h-3.5 w-3.5" /> | |
| Remove | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| <Divider /> | |
| {/* Totals */} | |
| <div className="space-y-2"> | |
| <div className="flex items-center justify-between text-sm font-semibold text-slate-700"> | |
| <span>Subtotal</span> | |
| <span className="text-slate-900">{money(subtotal)}</span> | |
| </div> | |
| <div className="flex items-center justify-between text-sm font-semibold text-slate-700"> | |
| <span>Taxes (10%)</span> | |
| <span className="text-slate-900">{money(taxes)}</span> | |
| </div> | |
| <div className="flex items-center justify-between text-sm font-semibold text-slate-700"> | |
| <span className="inline-flex items-center gap-2"> | |
| <Percent className="h-4 w-4" /> Discount (10%) | |
| </span> | |
| <span className={`${discount > 0 ? "text-emerald-700" : "text-slate-900"}`}> | |
| {discount > 0 ? `−${money(discount)}` : money(0)} | |
| </span> | |
| </div> | |
| <div className="mt-2 flex items-center justify-between rounded-xl bg-slate-50 px-3 py-2"> | |
| <span className="text-sm font-extrabold text-slate-900">Total Payment</span> | |
| <span className="text-sm font-extrabold text-slate-900">{money(total)}</span> | |
| </div> | |
| </div> | |
| {/* Meta */} | |
| <Divider /> | |
| <div className="grid grid-cols-2 gap-3"> | |
| <SmallField label="Order Type"> | |
| <select | |
| value={orderType} | |
| onChange={(e) => setOrderType(e.target.value)} | |
| className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 focus:outline-none focus:ring-2 focus:ring-slate-900/10" | |
| > | |
| <option>In-store</option> | |
| <option>Pickup</option> | |
| <option>Delivery</option> | |
| </select> | |
| </SmallField> | |
| <SmallField label="Select Counter"> | |
| <select | |
| value={counter} | |
| onChange={(e) => setCounter(e.target.value)} | |
| className="w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-800 focus:outline-none focus:ring-2 focus:ring-slate-900/10" | |
| > | |
| <option>Counter A</option> | |
| <option>Counter B</option> | |
| <option>Counter C</option> | |
| </select> | |
| </SmallField> | |
| </div> | |
| <div className="rounded-2xl border border-slate-200 bg-slate-50 p-3"> | |
| <div className="flex items-center justify-between gap-3"> | |
| <div className="min-w-0"> | |
| <div className="text-sm font-extrabold text-slate-900">10% iStore Discount</div> | |
| <div className="text-xs font-semibold text-slate-500"> | |
| {discountEligible | |
| ? "Eligible (subtotal ≥ $500)" | |
| : "Add more items to reach $500"} | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => setApplyDiscount((v) => !v)} | |
| disabled={!discountEligible} | |
| className={`inline-flex h-9 w-14 items-center rounded-full p-1 transition ${ | |
| applyDiscount && discountEligible | |
| ? "bg-slate-900" | |
| : "bg-slate-200" | |
| } ${!discountEligible ? "opacity-50 cursor-not-allowed" : ""}`} | |
| aria-label="Toggle discount" | |
| > | |
| <span | |
| className={`grid h-7 w-7 place-items-center rounded-full bg-white shadow-sm transition ${ | |
| applyDiscount && discountEligible ? "translate-x-5" : "translate-x-0" | |
| }`} | |
| > | |
| {applyDiscount && discountEligible ? ( | |
| <Check className="h-4 w-4 text-slate-900" /> | |
| ) : ( | |
| <X className="h-4 w-4 text-slate-500" /> | |
| )} | |
| </span> | |
| </button> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => { | |
| if (cartLines.length === 0) { | |
| alert("Add at least one product to the cart."); | |
| return; | |
| } | |
| alert( | |
| `Payment confirmed!\n\nOrder: ${orderId}\nTotal: ${money(total)}\nType: ${orderType}\nCounter: ${counter}` | |
| ); | |
| setCart([]); | |
| setOrderId(SEED_ORDER_ID()); | |
| }} | |
| className="w-full rounded-xl bg-slate-900 px-4 py-3 text-sm font-extrabold text-white shadow-sm hover:bg-slate-800" | |
| > | |
| Confirm Payment | |
| </button> | |
| <button | |
| onClick={() => setCart([])} | |
| className="w-full rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm font-extrabold text-slate-800 hover:bg-slate-50" | |
| > | |
| Clear Cart | |
| </button> | |
| <div className="pt-2 text-center text-xs font-semibold text-slate-500"> | |
| Tip: Click <span className="font-bold text-slate-700">Edit</span> to add notes like color or storage. | |
| </div> | |
| </div> | |
| </aside> | |
| </div> | |
| </main> | |
| </div> | |
| </div> | |
| <div className="mx-auto mt-4 max-w-[1400px] text-center text-xs font-semibold text-slate-500"> | |
| iStore POS UI mock • Frontend only (no backend) | |
| </div> | |
| </div> | |
| ); | |
| } | |