// QReward Customer App — Core: icons, data, nav, shared components
const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React;
/* ============ ICONS (Lucide-style, 24px stroke) ============ */
const I = {
home: 'M3 9.5 12 3l9 6.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z',
points: 'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6',
redeem: 'M20 12v9H4v-9M2 7h20v5H2zM12 22V7M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z',
card: 'M2 5h20v14H2zM2 10h20',
profile: 'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
bell: 'M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10.3 21a1.94 1.94 0 0 0 3.4 0',
chevR: 'M9 18l6-6-6-6',
chevL: 'M15 18l-6-6 6-6',
chevD: 'M6 9l6 6 6-6',
chevU: 'M18 15l-6-6-6 6',
x: 'M18 6 6 18M6 6l12 12',
check: 'M20 6 9 17l-5-5',
plus: 'M12 5v14M5 12h14',
minus: 'M5 12h14',
search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3',
heart: 'M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1L12 21l7.7-7.6 1.1-1a5.5 5.5 0 0 0 0-7.8z',
cart: 'M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6M9 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM20 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2z',
eye: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
eyeOff: 'M17.9 17.9A10.1 10.1 0 0 1 12 20C5 20 1 12 1 12a18.5 18.5 0 0 1 5.1-5.9m3.8-1.8A9.1 9.1 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.2 3.2m-6.7-1a3 3 0 1 1-4.2-4.2M1 1l22 22',
lock: 'M5 11h14v10H5zM7 11V7a5 5 0 0 1 10 0v4',
unlock: 'M5 11h14v10H5zM7 11V7a5 5 0 0 1 9.9-1',
faceId: 'M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2M9 9v1M15 9v1M12 8v4l1 1M9 15a4 4 0 0 0 6 0',
gift: 'M20 12v9H4v-9M2 7h20v5H2zM12 22V7M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z',
link: 'M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7',
arrowR: 'M5 12h14M12 5l7 7-7 7',
arrowUp: 'M12 19V5M5 12l7-7 7 7',
arrowDown: 'M12 5v14M19 12l-7 7-7-7',
clock: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2',
info: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 16v-4M12 8h.01',
alert: 'M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0zM12 9v4M12 17h.01',
truck: 'M1 3h15v13H1zM16 8h4l3 3v5h-7V8zM5.5 21a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM18.5 21a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z',
pkg: 'M16.5 9.4 7.5 4.2M21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.3 7l8.7 5 8.7-5M12 22V12',
trash: 'M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2',
edit: 'M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7M18.5 2.5a2.1 2.1 0 0 1 3 3L12 15l-4 1 1-4z',
settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z',
globe: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM2 12h20M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10 15 15 0 0 1 4-10z',
key: 'M21 2l-2 2m-7.6 7.6a5.5 5.5 0 1 1-7.8 7.8 5.5 5.5 0 0 1 7.8-7.8zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3',
shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
doc: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8',
user: 'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
phone: 'M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3-8.6A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1 1 .4 1.9.7 2.8a2 2 0 0 1-.5 2.1L8.1 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.6 2.8.7a2 2 0 0 1 1.7 2z',
mail: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM22 6l-10 7L2 6',
mapPin: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0zM12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
tag: 'M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L2 12V2h10l8.6 8.6a2 2 0 0 1 0 2.8zM7 7h.01',
star: 'M12 2l3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z',
spark: 'M12 3v3M5.6 5.6l2.1 2.1M3 12h3M5.6 18.4l2.1-2.1M12 21v-3M18.4 18.4l-2.1-2.1M21 12h-3M18.4 5.6l-2.1 2.1M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z',
refresh: 'M23 4v6h-6M1 20v-6h6M3.5 9a9 9 0 0 1 14.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0 0 20.5 15',
filter: 'M22 3H2l8 9.5V19l4 2v-8.5z',
copy: 'M9 9h11v11H9zM5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1',
bookmark: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z',
qrLogo: '',
wallet: 'M20 12V8H6a2 2 0 0 1 0-4h12v4M4 6v12a2 2 0 0 0 2 2h14v-4M18 12a2 2 0 0 0 0 4h4v-4z',
trending: 'M23 6l-9.5 9.5-5-5L1 18M17 6h6v6',
device: 'M5 2h14a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zM12 18h.01',
sun: 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4',
moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z',
};
const Icon = ({ n, s = 22, sw = 1.9, c, style, fill = 'none' }) => (
);
/* ============ FORMAT ============ */
const fmt = (n) => new Intl.NumberFormat('en-US').format(Math.round(n));
// QReward points glyph — the "r" suffix is part of brand identity
const Pts = ({ v, big, cls = '' }) => (
{fmt(v)} r
);
/* ============ L10N (EN/JP representative subset) ============ */
const STR = {
home: { en: 'Home', jp: 'ホーム' },
myPoints: { en: 'My Points', jp: 'ポイント' },
redeem: { en: 'Redeem', jp: '交換' },
myCard: { en: 'My Card', jp: 'カード' },
profile: { en: 'Profile', jp: 'プロフィール' },
signIn: { en: 'Sign in', jp: 'サインイン' },
marketplace: { en: 'Marketplace', jp: 'マーケット' },
totalBalance: { en: 'Total balance', jp: '合計残高' },
featuredOffers: { en: 'Featured offers', jp: 'おすすめ' },
recentActivity: { en: 'Recent activity', jp: '最近の履歴' },
viewAll: { en: 'View all', jp: 'すべて見る' },
quickActions: { en: 'Quick actions', jp: 'クイック操作' },
applyForCard: { en: 'Apply for card', jp: 'カード申請' },
linking: { en: 'Linking', jp: '連携' },
continue: { en: 'Continue', jp: '次へ' },
addToCart: { en: 'Add to cart', jp: 'カートに追加' },
redeemNow: { en: 'Redeem now', jp: '今すぐ交換' },
};
const L = (lang, key) => (STR[key] ? STR[key][lang] : key);
/* ============ MOCK DATA ============ */
const DATA = {
user: { name: 'Giri', nameJp: 'ギリ', initials: 'G', email: 'giri.tanaka@gmail.com', emailMasked: 'g***@gmail.com', joined: '2025/11/12' },
balance: 12540,
lifecard: 7180,
qualia: 5360,
card: {
// status: notapplied | submitted | activated | declined
status: 'submitted',
submittedDate: '2026/05/01 14:22',
activatedDate: '2026/05/03 10:31',
masked: '•••• •••• •••• 4821',
expiry: '08/30',
holder: 'GIRI TANAKA',
},
sources: [
{ id: 'lifecard', name: 'LifeCard', linked: true, lastSync: '2026/06/07 10:31', lifetime: 18200, color: '#0ea5e9', letter: 'L' },
{ id: 'qualia', name: 'QUALIA', linked: true, lastSync: '2026/06/07 09:14', lifetime: 14600, color: '#7c3aed', letter: 'Q' },
],
offers: [
{ id: 'o1', name: 'Golden Week 15% off', scope: 'Platform-wide', tag: '15% OFF', theme: '' },
{ id: 'o2', name: 'Coffee & Tea Cashback', scope: 'Food & Drink', tag: 'CASHBACK', theme: 'warm' },
{ id: 'o3', name: 'First Buy Bonus', scope: '+3 categories', tag: 'BONUS', theme: 'green' },
{ id: 'o4', name: 'Sakura Gift Promo', scope: 'Gift Cards', tag: 'NEW', theme: 'pink' },
],
activity: [
{ id: 'a1', src: 'lifecard', type: 'earn', desc: 'LifeCard CSV import — June 2026', amt: 840, bal: 12540, date: '2026/06/07 10:31' },
{ id: 'a2', src: 'redeem', type: 'redeem', desc: 'Starbucks e-Gift ¥500 ×1', amt: -500, bal: 11700, date: '2026/06/06 14:22' },
{ id: 'a3', src: 'qualia', type: 'earn', desc: 'QUALIA daily batch', amt: 320, bal: 12200, date: '2026/06/05 09:14' },
{ id: 'a4', src: 'admin', type: 'earn', desc: 'Refund credit — order QR-90122', amt: 200, bal: 11880, date: '2026/06/03 09:42' },
{ id: 'a5', src: 'redeem', type: 'redeem', desc: 'JR East Travel Voucher ×1', amt: -15000, bal: 11680, date: '2026/05/28 16:08' },
{ id: 'a6', src: 'lifecard', type: 'earn', desc: 'LifeCard CSV import — May 2026', amt: 1240, bal: 26680, date: '2026/05/15 10:24' },
],
categories: ['All', 'Food & Drink', 'Electronics', 'Gift Cards', 'Lifestyle', 'Travel', 'Experiences'],
products: [
{ id: 'p1', nameJp: 'スターバックス e-ギフト ¥500', name: 'Starbucks e-Gift ¥500', brand: 'Starbucks', price: 500, cat: 'Food & Drink', img: '#1a7a5e', offer: '15% OFF', fav: true },
{ id: 'p2', nameJp: 'アマゾンギフト ¥1,000', name: 'Amazon Gift ¥1,000', brand: 'Amazon', price: 1000, cat: 'Gift Cards', img: '#d98324', fav: false },
{ id: 'p3', nameJp: 'ソニー WH-1000XM5', name: 'Sony WH-1000XM5', brand: 'Sony', price: 38000, cat: 'Electronics', img: '#1f2937', fav: false },
{ id: 'p4', nameJp: '無印良品 ホームセット', name: 'MUJI Home Set', brand: 'MUJI', price: 4500, cat: 'Lifestyle', img: '#b1442f', fav: true },
{ id: 'p5', nameJp: 'ネスプレッソ コーヒーセット', name: 'Nespresso Coffee Set', brand: 'Nespresso', price: 6800, cat: 'Food & Drink', img: '#6b3410', fav: false },
{ id: 'p6', nameJp: 'JR東日本 旅行券', name: 'JR East Travel Voucher', brand: 'JR East', price: 15000, cat: 'Travel', img: '#0e7490', fav: false },
{ id: 'p7', nameJp: '銀座資生堂スパ', name: 'Ginza Shiseido Spa', brand: 'Shiseido', price: 22000, cat: 'Experiences', img: '#be185d', fav: false, oos: true },
{ id: 'p8', nameJp: '伊勢丹ディナー券', name: 'Isetan Dining Voucher', brand: 'Isetan', price: 12000, cat: 'Food & Drink', img: '#6d28d9', fav: false },
],
addresses: [
{ id: 'ad1', name: 'Giri Tanaka', zip: '〒107-0061', line: 'Tokyo, Minato-ku, Kita-Aoyama 3-6-7', extra: 'Floor 12, Apt 1208', phone: '090-1234-5678', selected: true },
{ id: 'ad2', name: 'Giri Tanaka (Office)', zip: '〒100-0005', line: 'Tokyo, Chiyoda, Marunouchi 1-9-1', extra: 'Tokyo Bldg 22F', phone: '090-1234-5678', selected: false },
],
notifications: [
{ id: 'n1', type: 'promo', icon: 'tag', title: 'Golden Week 15% off is live', body: 'Check it out before May 6', time: '2m', unread: true, cat: 'Promotions' },
{ id: 'n2', type: 'points', icon: 'arrowUp', title: '+840 r added to your QReward balance', body: 'LifeCard CSV import — June 2026', time: '1h', unread: true, cat: 'Account' },
{ id: 'n3', type: 'wishlist', icon: 'heart', title: 'MUJI Home Set is now on offer', body: 'A wishlisted item just got cheaper', time: '3h', unread: true, cat: 'Promotions' },
{ id: 'n4', type: 'order', icon: 'truck', title: 'Your order is Out for Delivery', body: 'Order QR-90138 · arriving today', time: '5h', unread: false, cat: 'Orders' },
{ id: 'n5', type: 'cart', icon: 'cart', title: 'You left 2 items in your cart', body: 'Finish redeeming before they sell out', time: '1d', unread: false, cat: 'Account' },
{ id: 'n6', type: 'legal', icon: 'doc', title: 'Please review our updated Privacy Policy', body: 'A new version is available', time: '2d', unread: false, cat: 'Account' },
],
orderSteps: ['Processing', 'Dispatched', 'Out for Delivery', 'Delivered'],
};
/* ============ NAV CONTEXT ============ */
// A simple screen stack + tab system inside the phone.
const NavCtx = createContext(null);
const useNav = () => useContext(NavCtx);
/* ============ SHARED UI ============ */
const StatusPill = ({ kind, label, icon }) => {
const map = {
active: 'pill-green', linked: 'pill-green', activated: 'pill-green', delivered: 'pill-green',
locked: 'pill-purple', submitted: 'pill-blue', processing: 'pill-blue', dispatched: 'pill-blue',
declined: 'pill-red', notdelivered: 'pill-red', unlinked: 'pill-neutral', ended: 'pill-neutral',
scheduled: 'pill-teal', paused: 'pill-amber', ofd: 'pill-amber',
};
return (
{icon ?