// 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')}
onNav('push', 'wishlist')}>
setQuery(e.target.value)} placeholder={lang === 'jp' ? '商品・ブランド・カテゴリを検索' : 'Search products, brands, categories'} />
{/* Promo banner carousel */}
onNav('push', 'scopedMarket', { offer: DATA.offers[offerIdx] })}>
{DATA.offers[offerIdx].tag}
{DATA.offers[offerIdx].name}
{DATA.offers[offerIdx].scope} · {lang === 'jp' ? 'タップして商品を見る' : 'Tap to shop this offer'}
{DATA.offers.map((_, i) => )}
{/* Category strip */}
{DATA.categories.map(c => setCat(c)}>{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 */}
onNav('push', 'cart')}>
{cartCount > 0 && {cartCount} }
);
};
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 &&
{ e.stopPropagation(); onFav(); }}> }
{p.brand}
{lang === 'jp' ? p.nameJp : p.name}
{fmt(p.price)} r
{!p.oos && (
setQty(Math.max(1, qty - 1))}>
{qty}
setQty(qty + 1)}>
{ onAdd(qty); toast(lang === 'jp' ? 'カートに追加しました' : 'Added to cart'); }}>{lang === 'jp' ? '追加' : 'Add'}
)}
);
};
/* ============ 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} }
setImgIdx((imgIdx - 1 + imgs.length) % imgs.length)}>
setImgIdx((imgIdx + 1) % imgs.length)}>
{imgs.map((_, i) => )}
{product.brand}
{lang === 'jp' ? product.nameJp : product.name}
{/* Balance line */}
{lang === 'jp' ? '残高' : 'Your balance'}:
{fmt(DATA.balance)} r
{!afford && · {lang === 'jp' ? '残高不足' : 'Insufficient balance'} }
{/* Quantity */}
{lang === 'jp' ? '数量' : 'Quantity'}
setQty(Math.max(1, qty - 1))}>
{qty}
setQty(qty + 1)}>
{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}
setQty(c.id, c.qty - 1)}>
{c.qty}
setQty(c.id, c.qty + 1)}>
{fmt(c.price * c.qty)} r
remove(c.id)} style={{ color: 'var(--ink-300)', alignSelf: 'flex-start' }}>
))}
{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 && { setSelect(!selectMode); setSelected([]); }}>{selectMode ? (lang === 'jp' ? 'キャンセル' : 'Cancel') : (lang === 'jp' ? '選択' : 'Select')} } />
{items.length === 0
?
{lang === 'jp' ? 'まだ何もありません' : 'Nothing here yet'}
{lang === 'jp' ? '気になる商品にハートをタップして保存しましょう' : 'Tap the heart on a product to save it'}
: <>
{items.map(p => (
{selectMode &&
setSelected(s => s.includes(p.id) ? s.filter(x => x !== p.id) : [...s, p.id])}>
}
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} />
{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 => (
setAddresses(addresses.map(x => ({ ...x, selected: x.id === a.id })))}>
{a.name}
{a.zip} {a.line} · {a.extra}
{a.phone}
e.stopPropagation()}>
))}
{addresses.length < 3 &&
{ toast(lang === 'jp' ? '住所フォームを開きます' : 'Add address form'); }}>
{lang === 'jp' ? '新しい住所を追加' : 'Add a new address'}
}
{lang === 'jp' ? '配送メモ(任意)' : 'Delivery note (optional)'}
)}
{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 });