// QReward Customer — Marketplace, Product, Cart, Wishlist, Checkout, Order status const { useState: uS4, useEffect: uE4, useMemo: uM4 } = React; /* ============ MARKETPLACE ============ */ const MarketplaceScreen = ({ lang, onNav, cart, fav, onFav, onAdd, toast }) => { const [cat, setCat] = uS4('All'); const [query, setQuery] = uS4(''); const [offerIdx, setOfferIdx] = uS4(0); uE4(() => { const t = setInterval(() => setOfferIdx(i => (i + 1) % DATA.offers.length), 5000); return () => clearInterval(t); }, []); const products = DATA.products.filter(p => (cat === 'All' || p.cat === cat) && (!query || p.name.toLowerCase().includes(query.toLowerCase()) || p.brand.toLowerCase().includes(query.toLowerCase())) ); const cartCount = cart.reduce((a, c) => a + c.qty, 0); return (
{L(lang, 'marketplace')}
setQuery(e.target.value)} placeholder={lang === 'jp' ? '商品・ブランド・カテゴリを検索' : 'Search products, brands, categories'} />
{/* Promo banner carousel */}
{/* Category strip */}
{DATA.categories.map(c => )}
{/* Product grid */} {products.length === 0 ?
{lang === 'jp' ? '商品が見つかりません' : 'Nothing matches'}
{lang === 'jp' ? '検索やフィルターを変更してください' : 'Try a different search or category'}
:
{products.map(p => onFav(p.id)} onAdd={(qty) => onAdd(p, qty)} onOpen={() => onNav('push', 'product', { product: p })} toast={toast} />)}
}
{/* Floating cart */}
); }; const ProductTile = ({ p, lang, fav, onFav, onAdd, onOpen, toast }) => { const [qty, setQty] = uS4(1); return (
{p.offer && {p.offer}} {p.oos &&
{lang === 'jp' ? '在庫切れ' : 'Unavailable'}
} {!p.oos && }
{p.brand}
{lang === 'jp' ? p.nameJp : p.name}
{fmt(p.price)} r
{!p.oos && (
{qty}
)}
); }; /* ============ SCOPED MARKETPLACE (from banner) ============ */ const ScopedMarketScreen = ({ lang, onBack, offer, fav, onFav, onAdd, onNav, toast }) => { const products = DATA.products.filter(p => offer.scope === 'Platform-wide' || p.cat === offer.scope || ['p1', 'p5', 'p8'].includes(p.id)).slice(0, 6); return (
{offer.tag}

{offer.name}

{offer.scope}

{products.map(p => onFav(p.id)} onAdd={(qty) => onAdd(p, qty)} onOpen={() => onNav('push', 'product', { product: { ...p, offer: offer.tag } })} toast={toast} />)}
); }; /* ============ PRODUCT PAGE ============ */ const ProductScreen = ({ lang, product, onBack, fav, onFav, onAdd, onNav, toast }) => { const [qty, setQty] = uS4(1); const [imgIdx, setImgIdx] = uS4(0); const total = product.price * qty; const afford = DATA.balance >= total; const imgs = [product.img, '#1f2937', '#475569']; return (
} />
{/* Image gallery */}
{product.offer && {product.offer}}
{imgs.map((_, i) => )}
{product.brand}
{lang === 'jp' ? product.nameJp : product.name}
{fmt(product.price)}r
{/* Balance line */}
{lang === 'jp' ? '残高' : 'Your balance'}: {fmt(DATA.balance)} r {!afford && · {lang === 'jp' ? '残高不足' : 'Insufficient balance'}}
{/* Quantity */}
{lang === 'jp' ? '数量' : 'Quantity'}
{qty}
{lang === 'jp' ? '合計' : 'Total cost'} {fmt(total)} r
{/* Description */}
{lang === 'jp' ? '商品説明' : 'Description'}
{lang === 'jp' ? '上質なギフトをポイントで。提携ブランドの公式商品をお届けします。デジタルコードは即時発行、配送商品は3〜5営業日でお届けします。' : 'A premium reward redeemable with your points. Official partner product with instant digital codes or 3–5 day shipping for physical items.'}
{lang === 'jp' ? '主な仕様' : 'Key specifications'}
  • {lang === 'jp' ? '公式正規品' : 'Official authentic product'}
  • {lang === 'jp' ? 'ポイント交換専用' : 'Points redemption only'}
  • {lang === 'jp' ? '有効期限:発行から12ヶ月' : 'Valid 12 months from issue'}
{/* Pinned CTA */}
{!afford &&
{lang === 'jp' ? `あと ${fmt(total - DATA.balance)} r 必要です` : `You'll need ${fmt(total - DATA.balance)} r more`}
} onNav('push', 'checkout', { items: [{ ...product, qty }], direct: true })}> {product.oos ? (lang === 'jp' ? '在庫切れ' : 'Unavailable') : L(lang, 'redeemNow')}
); }; /* ============ CART ============ */ const CartScreen = ({ lang, onBack, cart, setCart, onNav, toast }) => { const total = cart.reduce((a, c) => a + c.price * c.qty, 0); const afford = DATA.balance >= total; const setQty = (id, q) => setCart(cart.map(c => c.id === id ? { ...c, qty: Math.max(1, q) } : c)); const remove = (id) => { setCart(cart.filter(c => c.id !== id)); toast(lang === 'jp' ? '削除しました' : 'Removed from cart'); }; return (
{cart.length === 0 ?
{lang === 'jp' ? 'カートは空です' : 'Your cart is empty'}
{lang === 'jp' ? 'マーケットプレイスで商品を探しましょう' : 'Browse the marketplace to find something'}
{lang === 'jp' ? 'マーケットを見る' : 'Browse marketplace'}
: <>
{cart.map(c => (
{lang === 'jp' ? c.nameJp : c.name}
{c.brand}
{c.qty}
{fmt(c.price * c.qty)} r
))}
{lang === 'jp' ? '合計ポイント' : 'Total points'}{fmt(total)} r
{lang === 'jp' ? '現在の残高' : 'Balance now'}{fmt(DATA.balance)} r
{lang === 'jp' ? '交換後の残高' : 'Balance after'}{fmt(DATA.balance - total)} r
{!afford &&
{lang === 'jp' ? '残高が不足しています。商品を減らしてください。' : 'Total exceeds balance — remove some items.'}
} onNav('push', 'checkout', { items: cart })}>{L(lang, 'continue')}
}
); }; /* ============ WISHLIST ============ */ const WishlistScreen = ({ lang, onBack, fav, onFav, onAdd, onNav, toast }) => { const items = DATA.products.filter(p => fav.includes(p.id)); const [selectMode, setSelect] = uS4(false); const [selected, setSelected] = uS4([]); return (
0 && } /> {items.length === 0 ?
{lang === 'jp' ? 'まだ何もありません' : 'Nothing here yet'}
{lang === 'jp' ? '気になる商品にハートをタップして保存しましょう' : 'Tap the heart on a product to save it'}
: <>
{items.map(p => (
{selectMode && } onFav(p.id)} onAdd={(qty) => onAdd(p, qty)} onOpen={() => !selectMode && onNav('push', 'product', { product: p })} toast={toast} />
))}
{selectMode &&
{ selected.forEach(id => onFav(id)); toast(lang === 'jp' ? '削除しました' : 'Removed'); setSelect(false); setSelected([]); }}>{lang === 'jp' ? '削除' : 'Remove'} { const its = items.filter(p => selected.includes(p.id)).map(p => ({ ...p, qty: 1 })); onNav('push', 'checkout', { items: its }); }}>{lang === 'jp' ? `${selected.length}件を交換` : `Redeem ${selected.length}`}
} }
); }; /* ============ CHECKOUT (3-step) ============ */ const CheckoutScreen = ({ lang, onBack, items, onComplete, toast }) => { const [step, setStep] = uS4(1); const [addresses, setAddresses] = uS4(DATA.addresses); const [note, setNote] = uS4(''); const [adding, setAdding] = uS4(false); const total = items.reduce((a, c) => a + c.price * c.qty, 0); const selectedAddr = addresses.find(a => a.selected); return (
step === 1 ? onBack() : setStep(step - 1)} lang={lang} />
{[1, 2, 3].map(s =>
)}
{step === 1 && (
{items.map(c => (
{lang === 'jp' ? c.nameJp : c.name}
{lang === 'jp' ? '数量' : 'Qty'} {c.qty}
{fmt(c.price * c.qty)} r
))}
{lang === 'jp' ? '現在の残高' : 'Balance now'}{fmt(DATA.balance)} r
{lang === 'jp' ? '交換ポイント' : 'Redemption'}−{fmt(total)} r
{lang === 'jp' ? '交換後の残高' : 'Balance after'}{fmt(DATA.balance - total)} r
)} {step === 2 && (
{addresses.map(a => (
))} {addresses.length < 3 && }
)} {step === 3 && (
{lang === 'jp' ? '商品' : 'Items'}
{items.map(c => (
{lang === 'jp' ? c.nameJp : c.name} ×{c.qty} {fmt(c.price * c.qty)} r
))}
{lang === 'jp' ? 'お届け先' : 'Delivery to'}
{selectedAddr?.name} · {selectedAddr?.zip} {selectedAddr?.line}
{note &&
{lang === 'jp' ? 'メモ' : 'Note'}: {note}
}
{lang === 'jp' ? '現在の残高' : 'Balance now'}{fmt(DATA.balance)} r
{lang === 'jp' ? '交換ポイント' : 'Redemption'}−{fmt(total)} r
{lang === 'jp' ? '交換後の残高' : 'Balance after'}{fmt(DATA.balance - total)} r
{lang === 'jp' ? '確定時に生体認証またはPINで確認します。' : 'Your biometric or PIN confirms this redemption.'}
)}
step < 3 ? setStep(step + 1) : onComplete({ items, total, addr: selectedAddr })}> {step < 3 ? L(lang, 'continue') : (lang === 'jp' ? '交換を確定' : 'Confirm redemption')}
); }; /* ============ ORDER CONFIRMED ============ */ const OrderConfirmedScreen = ({ lang, order, onViewStatus, onBackToMarket }) => (
{lang === 'jp' ? '交換が完了しました' : 'Order confirmed'}
{lang === 'jp' ? 'ご注文ありがとうございます。' : 'Thank you for your redemption.'}
{lang === 'jp' ? '注文ID' : 'Order ID'}QR-90142
{lang === 'jp' ? '使用ポイント' : 'Points spent'}{fmt(order.total)} r
{lang === 'jp' ? '新しい残高' : 'New balance'}{fmt(DATA.balance - order.total)} r
{lang === 'jp' ? 'お届け予定' : 'Est. delivery'}2026/06/12
{lang === 'jp' ? '注文状況を見る' : 'View order status'} {lang === 'jp' ? 'マーケットに戻る' : 'Back to marketplace'}
); /* ============ ORDER STATUS ============ */ const OrderStatusScreen = ({ lang, onBack, order, stage = 2 }) => { const steps = lang === 'jp' ? ['処理中', '発送済み', '配達中', '配達完了'] : DATA.orderSteps; const times = ['2026/06/08 14:22', '2026/06/09 09:10', '2026/06/12 08:30', '']; return (
QR-90142
{lang === 'jp' ? '注文日' : 'Ordered'} 2026/06/08 · {fmt(order?.total || 500)} r
{steps.map((s, i) => (
{i < stage ? : i === stage ? : null}
{s}
{times[i] && i <= stage &&
{times[i]}
}
))}
{lang === 'jp' ? '商品' : 'Item'}
Starbucks e-Gift ¥500
{lang === 'jp' ? 'お届け先' : 'To'}: Kita-Aoyama 3-6-7
{lang === 'jp' ? 'サポートに連絡' : 'Contact support'}
); }; Object.assign(window, { MarketplaceScreen, ScopedMarketScreen, ProductScreen, CartScreen, WishlistScreen, CheckoutScreen, OrderConfirmedScreen, OrderStatusScreen });