// src/App.jsx — Audical Services main app
const { useState, useEffect, useContext, createContext, useCallback, useRef, useMemo } = React;
// ── Context ──────────────────────────────────────────────────
const AppContext = createContext(null);
const useApp = () => useContext(AppContext);
// ── i18n ─────────────────────────────────────────────────────
const TRANSLATIONS = {
en: {
home: 'Home', products: 'Products', cart: 'Cart', login: 'Login',
logout: 'Logout', admin: 'Admin', search: 'Search products…',
addToCart: 'Add to Cart', checkout: 'Checkout', total: 'Total',
empty_cart: 'Your cart is empty', proceed: 'Proceed to Checkout',
otp_sent: 'OTP sent to your email', verify: 'Verify', resend: 'Resend',
name: 'Name', email: 'Email', price: 'Price', stock: 'In Stock',
dashboard: 'Dashboard', orders: 'Orders', customers: 'Customers',
settings: 'Settings', products_admin: 'Products', upload: 'Upload',
save: 'Save', cancel: 'Cancel', delete: 'Delete', edit: 'Edit',
add_product: 'Add Product', order_id: 'Order #', status: 'Status',
revenue: 'Revenue', welcome: 'Welcome back,', theme_toggle: 'Toggle theme',
language: 'Language', filter: 'Filter', all: 'All', category: 'Category',
description: 'Description', quantity: 'Qty', remove: 'Remove',
continue_shopping: 'Continue Shopping', place_order: 'Place Order',
payment_method: 'Payment Method', shipping: 'Shipping Info',
address: 'Address', phone: 'Phone', city: 'City', zip: 'ZIP',
export_pdf: 'Export PDF', new: 'New', active: 'Active', inactive: 'Inactive',
pending: 'Pending', completed: 'Completed', cancelled: 'Cancelled',
upload_image: 'Upload Image', drag_drop: 'Drag & drop or click to upload',
notifications: 'Notifications', mark_read: 'Mark all read',
no_notifications: 'No new notifications', role: 'Role',
password: 'Password', confirm_password: 'Confirm Password',
search_placeholder: 'Search by name, category…',
},
es: {
home: 'Inicio', products: 'Productos', cart: 'Carrito', login: 'Iniciar',
logout: 'Salir', admin: 'Admin', search: 'Buscar productos…',
addToCart: 'Agregar', checkout: 'Pagar', total: 'Total',
empty_cart: 'Tu carrito está vacío', proceed: 'Proceder al pago',
otp_sent: 'OTP enviado a tu correo', verify: 'Verificar', resend: 'Reenviar',
name: 'Nombre', email: 'Correo', price: 'Precio', stock: 'Disponible',
dashboard: 'Panel', orders: 'Pedidos', customers: 'Clientes',
settings: 'Ajustes', products_admin: 'Productos', upload: 'Subir',
save: 'Guardar', cancel: 'Cancelar', delete: 'Eliminar', edit: 'Editar',
add_product: 'Agregar Producto', order_id: 'Pedido #', status: 'Estado',
revenue: 'Ingresos', welcome: 'Bienvenido,', theme_toggle: 'Cambiar tema',
language: 'Idioma', filter: 'Filtro', all: 'Todo', category: 'Categoría',
description: 'Descripción', quantity: 'Cant', remove: 'Quitar',
continue_shopping: 'Seguir comprando', place_order: 'Hacer pedido',
payment_method: 'Método de pago', shipping: 'Envío',
address: 'Dirección', phone: 'Teléfono', city: 'Ciudad', zip: 'CP',
export_pdf: 'Exportar PDF', new: 'Nuevo', active: 'Activo', inactive: 'Inactivo',
pending: 'Pendiente', completed: 'Completado', cancelled: 'Cancelado',
upload_image: 'Subir imagen', drag_drop: 'Arrastra o haz clic',
notifications: 'Notificaciones', mark_read: 'Marcar todo leído',
no_notifications: 'Sin notificaciones', role: 'Rol',
password: 'Contraseña', confirm_password: 'Confirmar contraseña',
search_placeholder: 'Buscar por nombre, categoría…',
},
vi: {
home: 'Trang chủ', products: 'Sản phẩm', cart: 'Giỏ hàng', login: 'Đăng nhập',
logout: 'Đăng xuất', admin: 'Quản trị', search: 'Tìm sản phẩm…',
addToCart: 'Thêm vào giỏ', checkout: 'Thanh toán', total: 'Tổng',
empty_cart: 'Giỏ hàng trống', proceed: 'Tiến hành thanh toán',
otp_sent: 'OTP đã gửi', verify: 'Xác nhận', resend: 'Gửi lại',
name: 'Tên', email: 'Email', price: 'Giá', stock: 'Còn hàng',
dashboard: 'Bảng điều khiển', orders: 'Đơn hàng', customers: 'Khách hàng',
settings: 'Cài đặt', products_admin: 'Sản phẩm', upload: 'Tải lên',
save: 'Lưu', cancel: 'Hủy', delete: 'Xóa', edit: 'Sửa',
add_product: 'Thêm sản phẩm', order_id: 'Đơn #', status: 'Trạng thái',
revenue: 'Doanh thu', welcome: 'Chào mừng,', theme_toggle: 'Đổi giao diện',
language: 'Ngôn ngữ', filter: 'Lọc', all: 'Tất cả', category: 'Danh mục',
description: 'Mô tả', quantity: 'SL', remove: 'Xóa',
continue_shopping: 'Tiếp tục mua', place_order: 'Đặt hàng',
payment_method: 'Phương thức', shipping: 'Vận chuyển',
address: 'Địa chỉ', phone: 'SĐT', city: 'Thành phố', zip: 'Bưu chính',
export_pdf: 'Xuất PDF', new: 'Mới', active: 'Hoạt động', inactive: 'Không hoạt động',
pending: 'Chờ xử lý', completed: 'Hoàn thành', cancelled: 'Đã hủy',
upload_image: 'Tải ảnh', drag_drop: 'Kéo thả hoặc nhấn để tải',
notifications: 'Thông báo', mark_read: 'Đánh dấu đã đọc',
no_notifications: 'Không có thông báo', role: 'Vai trò',
password: 'Mật khẩu', confirm_password: 'Xác nhận mật khẩu',
search_placeholder: 'Tìm theo tên, danh mục…',
}
};
// ── Mock Data ─────────────────────────────────────────────────
const MOCK_PRODUCTS = [
{ id:1, name:'Digital Audiometer DA-500', category:'Audiometry', price:2499, image:'https://picsum.photos/seed/aud1/480/360', description:'Professional-grade digital audiometer for comprehensive hearing assessments.', stock:14, active:true },
{ id:2, name:'Hearing Aid HX Pro Max', category:'Hearing Aids', price:899, image:'https://picsum.photos/seed/aud2/480/360', description:'Advanced behind-the-ear hearing aid with Bluetooth connectivity.', stock:31, active:true },
{ id:3, name:'Otoscope OtoVision 3', category:'Diagnostic', price:349, image:'https://picsum.photos/seed/aud3/480/360', description:'LED fiber-optic otoscope with HD video capture.', stock:8, active:true },
{ id:4, name:'Tympanometer TM-200', category:'Audiometry', price:3199, image:'https://picsum.photos/seed/aud4/480/360', description:'Multi-frequency tympanometer with automated test sequences.', stock:5, active:true },
{ id:5, name:'Ear Cleaning Kit Pro', category:'Accessories', price:49, image:'https://picsum.photos/seed/aud5/480/360', description:'Professional irrigation kit with 3 tip sizes and pressure control.', stock:200, active:true },
{ id:6, name:'Sound Booth SB-Mini', category:'Audiometry', price:7499, image:'https://picsum.photos/seed/aud6/480/360', description:'Portable sound-attenuation booth for in-clinic audiometric testing.', stock:2, active:true },
{ id:7, name:'OAE Screener Swift', category:'Diagnostic', price:1850, image:'https://picsum.photos/seed/aud7/480/360', description:'Automated OAE hearing screening for newborn and pediatric use.', stock:12, active:true },
{ id:8, name:'Earmold Impression Kit', category:'Accessories', price:89, image:'https://picsum.photos/seed/aud8/480/360', description:'Silicone impression material set with mixing tips, 20 pairs.', stock:75, active:true },
{ id:9, name:'ABR System NeuroAud', category:'Diagnostic', price:12500, image:'https://picsum.photos/seed/aud9/480/360', description:'Auditory Brainstem Response system for neurodiagnostic testing.', stock:3, active:true },
];
const MOCK_ORDERS = [
{ id:'ORD-0041', customer:'Maria Lopez', email:'m.lopez@email.com', total:2499, status:'completed', date:'2025-05-18', items:1 },
{ id:'ORD-0042', customer:'James Kim', email:'j.kim@email.com', total:349, status:'pending', date:'2025-05-19', items:2 },
{ id:'ORD-0043', customer:'Sarah Chen', email:'s.chen@email.com', total:7499, status:'completed', date:'2025-05-20', items:1 },
{ id:'ORD-0044', customer:'Ahmed Osei', email:'a.osei@email.com', total:938, status:'cancelled', date:'2025-05-21', items:3 },
{ id:'ORD-0045', customer:'Lin Nguyen', email:'l.nguyen@email.com', total:1850, status:'pending', date:'2025-05-22', items:1 },
{ id:'ORD-0046', customer:'Tom Blake', email:'t.blake@email.com', total:138, status:'completed', date:'2025-05-23', items:4 },
];
const MOCK_CUSTOMERS = [
{ id:1, name:'Maria Lopez', email:'m.lopez@email.com', role:'customer', orders:4, joined:'2024-11-02', active:true },
{ id:2, name:'James Kim', email:'j.kim@email.com', role:'customer', orders:2, joined:'2025-01-15', active:true },
{ id:3, name:'Sarah Chen', email:'s.chen@email.com', role:'customer', orders:7, joined:'2024-08-30', active:true },
{ id:4, name:'Ahmed Osei', email:'a.osei@email.com', role:'customer', orders:1, joined:'2025-03-10', active:false },
{ id:5, name:'Lin Nguyen', email:'l.nguyen@email.com', role:'admin', orders:0, joined:'2024-06-01', active:true },
];
const CATEGORIES = ['All', 'Audiometry', 'Hearing Aids', 'Diagnostic', 'Accessories'];
// ── Icons ─────────────────────────────────────────────────────
const Icon = ({ name, size = 18, cls = '' }) => {
const icons = {
home: ,
products: ,
cart: ,
user: ,
sun: ,
moon: ,
search: ,
plus: ,
minus: ,
trash: ,
edit: ,
x: ,
check: ,
chevron_right: ,
chevron_left: ,
bell: ,
upload: ,
download: ,
bar_chart: ,
users: ,
settings: ,
credit_card: ,
package: ,
globe: ,
arrow_up: ,
arrow_down: ,
filter: ,
menu: ,
mail: ,
lock: ,
star: ,
activity: ,
trending_up: ,
};
return icons[name] || {name} ;
};
// ── Toast System ──────────────────────────────────────────────
let toastCounter = 0;
const toastListeners = [];
const addToast = (msg, type = 'info', duration = 3500) => {
const id = ++toastCounter;
toastListeners.forEach(fn => fn({ id, msg, type, duration }));
};
const PORTAL54_URL = '/dashboard';
// ── Auth token + cart session (Sanctum Bearer / X-Cart-Session) ──
const getToken = () => localStorage.getItem('auth_token');
const setToken = (token) => token ? localStorage.setItem('auth_token', token) : localStorage.removeItem('auth_token');
const getCartSession = () => {
let id = localStorage.getItem('cart_session');
if (!id) {
id = (crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`);
localStorage.setItem('cart_session', id);
}
return id;
};
const apiRequest = async (path, options = {}) => {
const headers = { Accept: 'application/json', 'X-Cart-Session': getCartSession(), ...(options.headers || {}) };
const token = getToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(path, { ...options, headers });
const json = await res.json().catch(() => ({}));
if (!res.ok || json?.success === false) {
if (res.status === 401) setToken(null);
const firstError = json?.errors ? Object.values(json.errors).flat()[0] : null;
throw new Error(firstError || json?.error || json?.message || 'Request failed');
}
return json;
};
const ToastContainer = () => {
const [toasts, setToasts] = useState([]);
useEffect(() => {
const fn = t => setToasts(prev => [...prev, t]);
toastListeners.push(fn);
return () => toastListeners.splice(toastListeners.indexOf(fn), 1);
}, []);
const remove = id => setToasts(prev => prev.filter(t => t.id !== id));
const colors = { success: '#22c55e', error: '#ef4444', info: '#01c7fc', warning: '#f59e0b' };
return (
{toasts.map(t => (
{t.type === 'success' ? : t.type === 'error' ? : }
{t.msg}
remove(t.id)} className="btn-ghost btn-sm" style={{ padding:'2px', minHeight:'unset' }}>
{setTimeout(() => remove(t.id), t.duration) && null}
))}
);
};
// ── Navbar ────────────────────────────────────────────────────
const Navbar = () => {
const { page, setPage, cart, dark, setDark, lang, setLang, user, logout, t, notifications } = useApp();
const [mobileOpen, setMobileOpen] = useState(false);
const [notifOpen, setNotifOpen] = useState(false);
const cartCount = cart.reduce((s, i) => s + i.qty, 0);
const unread = notifications.filter(n => !n.read).length;
return (
{/* Logo */}
{ setPage('home'); setMobileOpen(false); }} className="nav-logo" style={{ background:'none', border:'none', cursor:'pointer' }}>
AudicalServices
{/* Desktop links */}
{[['home','home'],['products','products'],].map(([k,p]) => (
{ setPage(p); setMobileOpen(false); }}
className={`nav-link${page===p?' active':''}`}>{t[k]}
))}
{user && (
{ setPage('orders'); setMobileOpen(false); }}
className={`nav-link${page==='orders'?' active':''}`}>{t.orders}
)}
{user?.role === 'admin' && (
{ setPage('admin'); setMobileOpen(false); }}
className={`nav-link${page==='admin'?' active':''}`}>{t.admin}
)}
{/* Language */}
setLang(e.target.value)}
style={{ background:'var(--color-surface)', border:'1px solid var(--color-border)', borderRadius:'var(--radius-md)', padding:'6px 10px', fontSize:13, color:'var(--color-text)', cursor:'pointer' }}>
🇺🇸 EN
🇪🇸 ES
🇻🇳 VI
{/* Theme */}
setDark(!dark)} className="btn btn-ghost btn-sm" title={t.theme_toggle}>
{dark ? : }
{/* Notifications */}
setNotifOpen(!notifOpen)} className="btn btn-ghost btn-sm" style={{ position:'relative' }}>
{unread > 0 && (
)}
{notifOpen && (
{t.notifications}
{unread}
{notifications.slice(0,5).map(n => (
))}
{notifications.length === 0 && (
{t.no_notifications}
)}
setNotifOpen(false)} style={{ display:'block', width:'100%', padding:'10px', fontSize:13, color:'var(--color-primary)', fontWeight:600, background:'none', border:'none', cursor:'pointer' }}>{t.mark_read}
)}
{/* Cart */}
setPage('cart')} className="btn btn-outline btn-sm" style={{ position:'relative', gap:6 }}>
{cartCount > 0 && (
{cartCount}
)}
{/* Auth */}
{user ? (
{t.logout}
) : (
setPage('login')} className="btn btn-primary btn-sm">
{t.login}
)}
{/* Hamburger */}
setMobileOpen(!mobileOpen)}>
);
};
// ── Calibration Trace Canvas ────────────────────────────────────
const CalibrationTrace = () => {
const canvasRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
let raf, t = 0;
function resize() {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * devicePixelRatio;
canvas.height = rect.height * devicePixelRatio;
ctx.setTransform(1,0,0,1,0,0);
ctx.scale(devicePixelRatio, devicePixelRatio);
}
function draw() {
const w = canvas.clientWidth, h = canvas.clientHeight;
ctx.clearRect(0, 0, w, h);
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1;
for (let i = 1; i < 5; i++) { ctx.beginPath(); ctx.moveTo(0, h*i/5); ctx.lineTo(w, h*i/5); ctx.stroke(); }
ctx.beginPath();
ctx.strokeStyle = '#01C7FC';
ctx.lineWidth = 1.6;
for (let x = 0; x <= w; x += 2) {
const y = h/2 + Math.sin(x*0.05 + t)*18*Math.sin(x*0.008 + t*0.4) + Math.sin(x*0.13 + t*1.7)*6;
x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
if (!reduce) { t += 0.03; raf = requestAnimationFrame(draw); }
}
resize();
draw();
window.addEventListener('resize', resize);
return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize); };
}, []);
return ;
};
// ── Home Page ─────────────────────────────────────────────────
const HomePage = () => {
const { setPage, addToCart, products, t } = useApp();
const shopProducts = products;
const pillars = [
{
tag: 'PROTOCOL_01',
title: 'Repair & Calibration',
desc: 'Audiometers, tympanometers, and OAE screeners serviced and calibrated to manufacturer and ANSI spec.',
items: ['Bench & on-site diagnostics', 'OEM-approved parts', 'Documented calibration report'],
icon: ,
},
{
tag: 'PROTOCOL_02',
title: 'Sound Booth Lifecycle',
desc: 'Install, dismantle, relocate, and rebuild sound-attenuating test suites without downtime surprises.',
items: ['Site survey & acoustic spec', 'Full teardown / rebuild crews', 'New & certified pre-owned units'],
icon: ,
},
{
tag: 'PROTOCOL_03',
title: 'State Certification',
desc: 'Testing environments and equipment certified to meet State sound-quality and performance standards.',
items: ['Ambient noise verification', 'Compliance documentation', 'Renewal scheduling & reminders'],
icon: ,
},
];
const capabilities = [
['Diagnostic Audiometers', 'Repair · Calibration', '24–48 hrs'],
['Tympanometers', 'Repair · Calibration', '24–48 hrs'],
['OAE / Screening Systems', 'Repair · Calibration', '48–72 hrs'],
['Sound Booths', 'Install · Relocate · Rebuild', 'Scheduled'],
['Test Environments', 'State Certification', 'Scheduled'],
];
return (
AUDICAL // FIELD SERVICE NETWORK
00 10 20 30 40 50
EST. COVERAGE 14 STATES
{/* Hero */}
Calibrated Field Service
Audiometric equipment, serviced to spec , every time.
We repair, calibrate, and certify the instruments hearing clinics and hospitals depend on — audiometers, tympanometers, and OAE systems — plus full sound booth install, relocation, and rebuild.
setPage('login')} className="hp-btn solid">
Request Service
setPage('products')} className="hp-btn">View Capabilities
LIVE CALIBRATION TRACE
250Hz – 8kHz
THRESHOLD DEVIATION: 0.3dB STATUS: PASS
{/* Stats */}
4,200+
Instruments serviced annually
14
States with certified technicians
48hr
Average turnaround, standard repair
100%
State-compliant calibration records
{/* Capabilities */}
Core Capabilities
Three disciplines. One accountable vendor.
From diagnostic equipment on the bench to a fully built sound-attenuated suite — one team, one service record.
{pillars.map((p, i) => (
{p.tag}
{p.icon}
{p.title}
{p.desc}
{p.items.map((it, j) => {it} )}
))}
{/* Equipment for sale */}
Equipment Catalog
New and certified pre-owned, ready to ship.
The same instruments we repair and calibrate every day — sold, installed, and backed by the team that services them.
setPage('products')} className="hp-btn">View Full Catalog
{shopProducts.slice(0, 4).map(p => (
{p.category}
{p.name}
${p.price.toLocaleString()}
SERIAL UNIT
addToCart(p)} className="hp-product-add">
{t.addToCart}
))}
{/* Capability matrix */}
Capability Matrix
What we service, by equipment class
Equipment Service Turnaround
{capabilities.map(([eq, svc, time], i) => (
{eq} {svc} {time}
))}
Note
Every service ticket closes with a written calibration or compliance record — the same document your next State inspection or accreditation review will ask for.
We do not service hearing aids. Our scope is the instrumentation and environments clinics use to test and evaluate hearing.
setPage('login')} className="hp-btn">Request a Site Survey
{/* Certification */}
STATE CERTIFIED
AUDICAL SERVICES
Compliance
Certified to the standard your inspector actually checks.
We test and document ambient noise levels, equipment calibration, and booth performance against your State's audiology equipment requirements — filed and ready before renewal season.
{['CA','TX','FL','NY','PA','OH','IL','+7 more'].map(s => {s} )}
{/* CTA */}
Get your equipment back in spec.
);
};
// ── Product Card ──────────────────────────────────────────────
const ProductCard = ({ product }) => {
const { addToCart, viewProduct, t } = useApp();
return (
viewProduct(product.id)} role="button" tabIndex={0}
onKeyDown={e => { if (e.key === 'Enter') viewProduct(product.id); }}>
{product.category}
{product.name}
{(product.description || '').slice(0, 80)}…
${product.price.toLocaleString()}
{ e.stopPropagation(); addToCart(product); }} className="btn btn-primary btn-sm">
{t.addToCart}
{product.manufacturer ? `${product.manufacturer} · ` : ''}Individually serialized unit
);
};
// ── Products Page ─────────────────────────────────────────────
const ProductsPage = () => {
const { t, products, categories } = useApp();
const [query, setQuery] = useState('');
const [category, setCategory] = useState('All');
const [page, setPage] = useState(1);
const PER_PAGE = 6;
const filtered = useMemo(() => products.filter(p =>
(category === 'All' || p.category === category) &&
p.name.toLowerCase().includes(query.toLowerCase())
), [products, query, category]);
const pages = Math.ceil(filtered.length / PER_PAGE);
const paginated = filtered.slice((page-1)*PER_PAGE, page*PER_PAGE);
return (
{t.products}
{/* Search & Filter */}
{/* Results */}
{filtered.length} product{filtered.length !== 1 ? 's' : ''} found
{paginated.length > 0 ? (
) : (
No products match your search.
)}
{/* Pagination */}
{pages > 1 && (
setPage(p=>Math.max(1,p-1))} disabled={page===1}>
{Array.from({length:pages},(_,i)=>i+1).map(n => (
setPage(n)}>{n}
))}
setPage(p=>Math.min(pages,p+1))} disabled={page===pages}>
)}
);
};
// ── Product Detail Page ─────────────────────────────────────────
const ProductDetailPage = () => {
const { selectedProductId, setPage, addToCart, t } = useApp();
const [product, setProduct] = useState(null);
const [activeImage, setActiveImage] = useState(0);
const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
useEffect(() => {
setLoading(true);
setNotFound(false);
setProduct(null);
setActiveImage(0);
if (!selectedProductId) return;
apiRequest(`/api/products/${selectedProductId}`)
.then(json => setProduct(json.data.product))
.catch(() => setNotFound(true))
.finally(() => setLoading(false));
}, [selectedProductId]);
if (loading) {
return (
Loading…
);
}
if (notFound || !product) {
return (
Product not found.
setPage('products')} className="btn btn-outline btn-sm">
{t.products}
);
}
const images = product.images && product.images.length > 0
? product.images
: [{ file_path: product.image, alt_text: product.name, is_primary: true }];
return (
setPage('products')} className="btn btn-ghost btn-sm" style={{ marginBottom:20 }}>
{t.products}
{images.length > 1 && (
{images.map((img, i) => (
setActiveImage(i)} />
))}
)}
{product.category}
{product.name}
{product.model_number && (
Model #{product.model_number}
)}
${product.price.toLocaleString()}
{product.description || 'No description available.'}
{product.manufacturer ? `${product.manufacturer} · ` : ''}Individually serialized unit
addToCart(product)} className="btn btn-primary" style={{ width:'100%' }}>
{t.addToCart}
);
};
// ── Cart Page ─────────────────────────────────────────────────
const CartPage = () => {
const { cart, removeFromCart, t, setPage } = useApp();
const total = cart.reduce((s, i) => s + i.price * i.qty, 0);
if (cart.length === 0) return (
{t.empty_cart}
Add some products to get started
setPage('products')} className="btn btn-primary">
{t.continue_shopping}
);
return (
{t.cart}
{/* Items */}
{cart.map(item => (
{item.name}
${item.price.toLocaleString()}
QTY 1 · SERIAL UNIT
${(item.price * item.qty).toLocaleString()}
removeFromCart(item.id)} className="btn btn-ghost btn-sm" style={{ color:'var(--color-danger)' }}>
))}
{/* Summary */}
Order Summary
{cart.map(i => (
{i.name} × {i.qty}
${(i.price * i.qty).toLocaleString()}
))}
{t.total}
${total.toLocaleString()}
setPage('checkout')} className="btn btn-primary btn-full btn-lg">
{t.proceed}
setPage('products')} className="btn btn-ghost btn-full" style={{ marginTop:8, fontSize:13 }}>
{t.continue_shopping}
);
};
// ── Checkout Page ─────────────────────────────────────────────
const CheckoutPage = () => {
const { cart, refreshCart, t, user, setPage, addNotification } = useApp();
const [step, setStep] = useState(1);
const [loading, setLoading] = useState(false);
const [order, setOrder] = useState(null);
const [payError, setPayError] = useState(null);
const [form, setForm] = useState({ name: user?.name||'', email: user?.email||'', address:'', city:'', zip:'', phone:'' });
const stripeRef = useRef(null);
const elementsRef = useRef(null);
const paymentElRef = useRef(null);
const total = cart.reduce((s, i) => s + i.price * i.qty, 0);
const field = (k, label, type='text') => (
{label}
setForm({...form,[k]:e.target.value})} />
);
const beginPayment = async () => {
setLoading(true);
setPayError(null);
try {
const json = await apiRequest('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ shipping: form }),
});
const { client_secret, publishable_key, order: createdOrder } = json.data;
if (!publishable_key) throw new Error('Payments are not configured yet.');
setOrder(createdOrder);
setStep(2);
stripeRef.current = Stripe(publishable_key);
elementsRef.current = stripeRef.current.elements({ clientSecret: client_secret });
setTimeout(() => {
const paymentElement = elementsRef.current.create('payment');
paymentElement.mount(paymentElRef.current);
}, 0);
} catch (err) {
addToast(err.message || 'Unable to start checkout', 'error');
if (/no longer available|empty/i.test(err.message || '')) refreshCart();
} finally {
setLoading(false);
}
};
const pollOrderStatus = async (orderNumber) => {
for (let attempt = 0; attempt < 15; attempt++) {
try {
const json = await apiRequest(`/api/orders/${orderNumber}`);
const status = json.data.order.status;
if (status === 'paid') return 'paid';
if (status === 'payment_failed' || status === 'requires_refund') return status;
} catch (err) { /* transient; keep polling */ }
await new Promise(r => setTimeout(r, 2000));
}
return 'processing';
};
const placeOrder = async () => {
if (!stripeRef.current || !elementsRef.current) return;
setLoading(true);
setPayError(null);
const { error } = await stripeRef.current.confirmPayment({
elements: elementsRef.current,
redirect: 'if_required',
confirmParams: { return_url: window.location.origin + '/' },
});
if (error) {
setPayError(error.message);
setLoading(false);
return;
}
const status = await pollOrderStatus(order.order_number);
setLoading(false);
if (status === 'paid' || status === 'processing') {
refreshCart();
addNotification(`Order ${order.order_number} placed for $${total.toLocaleString()}`);
addToast(status === 'paid' ? 'Payment confirmed!' : 'Payment received — order is processing.', 'success');
setStep(3);
} else {
setPayError('Payment could not be completed. You have not been charged, or a refund will be issued.');
}
};
if (!user) return (
Login Required
Sign in with your email to complete checkout.
setPage('login')} className="btn btn-primary">{t.login}
);
if (cart.length === 0 && step !== 3) return (
No items in cart
setPage('products')} className="btn btn-primary" style={{ marginTop:20 }}>Shop Now
);
return (
{/* Steps */}
{[1,2,3].map((s,i) => (
= s ? 'var(--color-primary)' : 'var(--color-surface)', border: `2px solid ${step >= s ? 'var(--color-primary)' : 'var(--color-border)'}`, display:'flex', alignItems:'center', justifyContent:'center', color: step >= s ? '#fff' : 'var(--color-muted)', fontFamily:'Poppins', fontWeight:700, fontSize:14 }}>
{step > s ? : s}
= s ? 'var(--color-primary)' : 'var(--color-muted)', textTransform:'uppercase', letterSpacing:'0.06em' }}>
{['Shipping','Payment','Confirm'][i]}
{i < 2 && s+0.5 ? 'var(--color-primary)' : 'var(--color-border)', marginTop:17, alignSelf:'flex-start' }} />}
))}
{step === 1 && (
{t.shipping}
{field('name', t.name)}
{field('email', t.email, 'email')}
{field('phone', t.phone, 'tel')}
{field('address', t.address)}
{field('city', t.city)}
{field('zip', t.zip)}
{loading ? <> Preparing…> : <>Continue to Payment >}
)}
{step === 2 && (
{t.payment_method}
{order && (
Order {order.order_number}
)}
{payError && (
{payError}
)}
{t.total}
${total.toLocaleString()}
{loading ? <> Processing…> : <> {t.place_order}>}
)}
{step === 3 && (
Order Confirmed!
Thank you for your order. A confirmation email has been sent.
setPage('products')} className="btn btn-primary">
Continue Shopping
setPage('home')} className="btn btn-outline">
Home
)}
);
};
// ── OTP Login ─────────────────────────────────────────────────
const LoginPage = () => {
const { setUser, setPage, refreshCart, t } = useApp();
const [stage, setStage] = useState('email');
const [email, setEmail] = useState('');
const [otp, setOtp] = useState(['','','','','','']);
const [loading, setLoading] = useState(false);
const [countdown, setCountdown] = useState(0);
const inputRefs = useRef([]);
const sendOtp = async (e) => {
e.preventDefault();
if (!email) return;
setLoading(true);
try {
await apiRequest('/api/auth/otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
setStage('otp');
setCountdown(60);
addToast(t.otp_sent, 'success');
} catch (err) {
addToast(err.message || 'Unable to send OTP', 'error');
} finally {
setLoading(false);
}
};
const resendOtp = async () => {
if (!email || loading || countdown > 0) return;
setLoading(true);
try {
await apiRequest('/api/auth/otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
setOtp(['','','','','','']);
setCountdown(60);
addToast('New OTP sent!', 'info');
} catch (err) {
addToast(err.message || 'Unable to resend OTP', 'error');
} finally {
setLoading(false);
}
};
useEffect(() => {
if (countdown > 0) {
const t = setTimeout(() => setCountdown(c => c-1), 1000);
return () => clearTimeout(t);
}
}, [countdown]);
const handleOtpKey = (i, val) => {
if (!/^\d?$/.test(val)) return;
const next = [...otp];
next[i] = val;
setOtp(next);
if (val && i < 5) inputRefs.current[i+1]?.focus();
};
const handleOtpPaste = (e) => {
const text = e.clipboardData.getData('text').replace(/\D/g, '').slice(0,6);
if (text.length === 6) {
setOtp(text.split(''));
inputRefs.current[5]?.focus();
}
};
const verifyOtp = async (e) => {
e.preventDefault();
const code = otp.join('');
if (code.length < 6) return;
setLoading(true);
try {
const json = await apiRequest('/api/auth/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, otp: code }),
});
const verifiedUser = json.data.user;
setToken(json.data.token);
setUser(verifiedUser);
addToast(`Welcome, ${verifiedUser.name}!`, 'success');
if (verifiedUser.role === 'admin') {
window.location.assign(json.data.sso_url || PORTAL54_URL);
return;
}
refreshCart();
setPage('home');
} catch (err) {
addToast(err.message || 'Unable to verify OTP', 'error');
} finally {
setLoading(false);
}
};
return (
{stage === 'email' ? t.login : 'Verify OTP'}
{stage === 'email' ? 'Enter your email to receive a one-time code' : `Code sent to ${email}`}
{stage === 'email' ? (
) : (
)}
);
};
// ── Customer Orders ───────────────────────────────────────────
const OrdersPage = () => {
const { t, setPage, user } = useApp();
const [orders, setOrders] = useState(null);
useEffect(() => {
if (!user) return;
apiRequest('/api/orders')
.then(json => setOrders(json.data.orders || []))
.catch(() => setOrders([]));
}, [user]);
if (!user) return (
Login Required
setPage('login')} className="btn btn-primary" style={{ marginTop:12 }}>{t.login}
);
const badge = (status) => status === 'paid' ? 'success'
: (status === 'payment_failed' || status === 'requires_refund' || status === 'expired') ? 'danger'
: 'primary';
return (
{t.orders}
{orders === null ? (
Loading…
) : orders.length === 0 ? (
No orders yet.
setPage('products')} className="btn btn-primary" style={{ marginTop:20 }}>{t.continue_shopping}
) : (
{t.order_id} {t.status} Date {t.total}
{orders.map(o => (
{o.order_number}
{o.status}
{(o.date || '').slice(0, 10)}
${o.grand_total.toLocaleString()}
))}
)}
);
};
// ── Admin Layout ──────────────────────────────────────────────
const AdminLayout = ({ children, activeTab, setActiveTab }) => {
const { t, user } = useApp();
const nav = [
{ key:'dashboard', icon:'bar_chart', label: t.dashboard },
{ key:'products', icon:'package', label: t.products_admin },
{ key:'orders', icon:'trending_up', label: t.orders },
{ key:'customers', icon:'users', label: t.customers },
{ key:'upload', icon:'upload', label: t.upload },
{ key:'settings', icon:'settings', label: t.settings },
];
return (
{t.welcome} {user?.name}
Admin
{nav.map(item => (
setActiveTab(item.key)}
style={{ display:'flex', alignItems:'center', gap:10, width:'100%', padding:'10px 12px', borderRadius:'var(--radius-md)', border:'none', cursor:'pointer', background: activeTab===item.key ? 'rgba(1,199,252,0.1)' : 'transparent', color: activeTab===item.key ? 'var(--color-primary)' : 'var(--color-muted)', fontFamily:'Poppins', fontWeight: activeTab===item.key ? 600 : 500, fontSize:14, transition:'all 0.2s', textAlign:'left', marginBottom:2 }}>
{item.label}
))}
{children}
);
};
// ── Admin Dashboard ───────────────────────────────────────────
const AdminDashboard = () => {
const stats = [
{ label:'Revenue', value:'$48,230', change:'+12.4%', up:true, icon:'trending_up' },
{ label:'Orders', value:'184', change:'+8.1%', up:true, icon:'package' },
{ label:'Products', value:MOCK_PRODUCTS.length, change:'+2', up:true, icon:'products' },
{ label:'Customers', value:MOCK_CUSTOMERS.length, change:'+5.3%', up:true, icon:'users' },
];
return (
Dashboard
{/* Stats */}
{stats.map((s,i) => (
{s.value}
{s.up ? : } {s.change}
))}
{/* Recent Orders */}
Recent Orders
Order Customer Total Status Date
{MOCK_ORDERS.slice(0,5).map(o => (
{o.id}
{o.customer}
${o.total.toLocaleString()}
{o.status}
{o.date}
))}
);
};
// ── Admin Products ────────────────────────────────────────────
const AdminProducts = () => {
const { t } = useApp();
const [products, setProducts] = useState(MOCK_PRODUCTS);
const [modal, setModal] = useState(null);
const [form, setForm] = useState({});
const openEdit = (p) => { setForm(p ? {...p} : { name:'', category:'Audiometry', price:'', description:'', stock:'', active:true }); setModal(p ? 'edit' : 'add'); };
const save = () => {
if (modal === 'edit') setProducts(ps => ps.map(p => p.id === form.id ? form : p));
else setProducts(ps => [...ps, { ...form, id: Date.now(), image: `https://picsum.photos/seed/${Date.now()}/480/360`, price: Number(form.price), stock: Number(form.stock) }]);
setModal(null);
addToast('Product saved', 'success');
};
const del = (id) => { setProducts(ps => ps.filter(p => p.id !== id)); addToast('Product deleted', 'error'); };
return (
{t.products_admin}
openEdit(null)} className="btn btn-primary btn-sm">
{t.add_product}
Image Name Category Price Stock Status Actions
{products.map(p => (
{p.name}
{p.category}
${p.price.toLocaleString()}
{p.stock}
{p.active?'Active':'Inactive'}
openEdit(p)} className="btn btn-outline btn-sm">
del(p.id)} className="btn btn-danger btn-sm">
))}
{modal && (
e.target === e.currentTarget && setModal(null)}>
{modal === 'edit' ? t.edit : t.add_product}
setModal(null)} className="btn btn-ghost btn-sm">
setModal(null)} className="btn btn-outline" style={{ flex:1 }}>{t.cancel}
{t.save}
)}
);
};
// ── Admin Orders ──────────────────────────────────────────────
const AdminOrders = () => {
const { t } = useApp();
const [orders, setOrders] = useState(MOCK_ORDERS);
const [filter, setFilter] = useState('all');
const filtered = filter === 'all' ? orders : orders.filter(o => o.status === filter);
const changeStatus = (id, status) => {
setOrders(os => os.map(o => o.id === id ? {...o, status} : o));
addToast(`Status updated to ${status}`, 'success');
};
return (
{t.orders}
{['all','pending','completed','cancelled'].map(f => (
setFilter(f)} className={`tag${filter===f?' active':''}`}>{f}
))}
Order Customer Items Total Status Date Action
{filtered.map(o => (
{o.id}
{o.customer}
{o.email}
{o.items}
${o.total.toLocaleString()}
{o.status}
{o.date}
changeStatus(o.id,e.target.value)} style={{ background:'var(--color-surface)', border:'1px solid var(--color-border)', borderRadius:'var(--radius-sm)', padding:'4px 8px', fontSize:12, color:'var(--color-text)', cursor:'pointer' }}>
Pending
Completed
Cancelled
))}
);
};
// ── Admin Customers ───────────────────────────────────────────
const AdminCustomers = () => {
const { t } = useApp();
return (
{t.customers}
Name Email Role Orders Joined Status
{MOCK_CUSTOMERS.map(c => (
{c.name}
{c.email}
{c.role}
{c.orders}
{c.joined}
{c.active?'Active':'Inactive'}
))}
);
};
// ── Admin Upload ──────────────────────────────────────────────
const AdminUpload = () => {
const { t } = useApp();
const [dragging, setDragging] = useState(false);
const [files, setFiles] = useState([]);
const inputRef = useRef();
const handleDrop = (e) => {
e.preventDefault();
setDragging(false);
const dropped = [...e.dataTransfer.files].filter(f => f.type.startsWith('image/'));
setFiles(prev => [...prev, ...dropped.map(f => ({ name:f.name, size:f.size, url:URL.createObjectURL(f) }))]);
addToast(`${dropped.length} file(s) added`, 'success');
};
const handleInput = (e) => {
const picked = [...e.target.files].filter(f => f.type.startsWith('image/'));
setFiles(prev => [...prev, ...picked.map(f => ({ name:f.name, size:f.size, url:URL.createObjectURL(f) }))]);
addToast(`${picked.length} file(s) added`, 'success');
};
return (
{t.upload}
{e.preventDefault();setDragging(true);}}
onDragLeave={()=>setDragging(false)}
onDrop={handleDrop}
onClick={()=>inputRef.current?.click()}>
{t.drag_drop}
PNG, JPG, WEBP up to 10MB
Browse Files
{files.length > 0 && (
Uploaded ({files.length})
{files.map((f,i) => (
{f.name}
{(f.size/1024).toFixed(1)}KB
setFiles(fs => fs.filter((_,j)=>j!==i))} className="btn btn-ghost btn-sm" style={{ marginTop:6, color:'var(--color-danger)', padding:'2px', minHeight:'unset' }}>
))}
)}
);
};
// ── Admin Settings ────────────────────────────────────────────
const AdminSettings = () => {
const { dark, setDark, lang, setLang, t } = useApp();
const [saved, setSaved] = useState(false);
const save = () => { setSaved(true); addToast('Settings saved', 'success'); setTimeout(() => setSaved(false), 2000); };
return (
{t.settings}
Appearance
Dark Mode
Toggle dark/light theme
setDark(!dark)} style={{ width:52, height:28, borderRadius:99, background: dark ? 'var(--color-primary)' : 'var(--color-border)', border:'none', cursor:'pointer', transition:'all 0.3s', position:'relative' }}>
Language
Interface language
setLang(e.target.value)} style={{ background:'var(--color-surface)', border:'1px solid var(--color-border)', borderRadius:'var(--radius-md)', padding:'6px 12px', fontSize:13, color:'var(--color-text)' }}>
English
Español
Tiếng Việt
{saved ? <> Saved!> : <>{t.save} Settings>}
);
};
// ── Admin Page ────────────────────────────────────────────────
const AdminPage = () => {
const { user, setPage } = useApp();
const [tab, setTab] = useState('dashboard');
if (!user || user.role !== 'admin') return (
Admin Access Required
Please log in with admin credentials.
setPage('login')} className="btn btn-primary">Login
);
const tabs = { dashboard:
, products:
, orders:
, customers:
, upload:
, settings:
};
return
{tabs[tab] || } ;
};
// ── App Root ──────────────────────────────────────────────────
const App = () => {
const [page, setPage] = useState('home');
const [selectedProductId, setSelectedProductId] = useState(null);
const [cart, setCart] = useState([]);
const [products, setProducts] = useState([]);
const [categories, setCategories] = useState(['All']);
const [dark, setDark] = useState(false);
const [lang, setLang] = useState('en');
const [user, setUser] = useState(null);
const [notifications, setNotifications] = useState([]);
const t = TRANSLATIONS[lang] || TRANSLATIONS.en;
const refreshCart = useCallback(async () => {
try {
const json = await apiRequest('/api/cart');
setCart(json.data.items || []);
} catch (err) {
// cart is non-critical on load; keep whatever we have
}
}, []);
// Initial load: catalog, cart, and (if a token survives) the session
useEffect(() => {
apiRequest('/api/products?limit=100')
.then(json => {
setProducts(json.data.products || []);
setCategories(['All', ...(json.data.filters?.categories || [])]);
})
.catch(() => addToast('Unable to load products', 'error'));
refreshCart();
if (getToken()) {
apiRequest('/api/me')
.then(json => setUser(json.data.user))
.catch(() => setToken(null));
}
}, [refreshCart]);
const addToCart = useCallback(async (product) => {
try {
const json = await apiRequest('/api/cart/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ equipment_id: product.id }),
});
setCart(json.data.items || []);
addToast(`${product.name} added to cart`, 'success');
} catch (err) {
addToast(err.message || 'Unable to add to cart', 'error');
}
}, []);
const removeFromCart = useCallback(async (equipmentId) => {
try {
const json = await apiRequest(`/api/cart/items/${equipmentId}`, { method: 'DELETE' });
setCart(json.data.items || []);
} catch (err) {
addToast(err.message || 'Unable to update cart', 'error');
}
}, []);
const logout = useCallback(async () => {
try { await apiRequest('/api/auth/otp-logout', { method: 'POST' }); } catch (err) { /* token may already be dead */ }
setToken(null);
setUser(null);
addToast('Logged out', 'info');
}, []);
const addNotification = useCallback((msg) => {
setNotifications(prev => [{ id: Date.now(), msg, time:'just now', read:false }, ...prev]);
}, []);
const viewProduct = useCallback((productId) => {
setSelectedProductId(productId);
setPage('product');
window.scrollTo({ top: 0, behavior: 'smooth' });
}, []);
// Dark mode class on HTML
useEffect(() => {
document.documentElement.classList.toggle('dark', dark);
}, [dark]);
const PAGES = { home:
, products:
, product:
, cart:
, login:
, checkout:
, orders:
, admin:
};
return (
{PAGES[page] ||
}
{/* Footer */}
);
};
// Mount
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);