// nyashelf-data.jsx // Sample data for figures + utility helpers. // FigureArt: stylized SVG placeholder card art (no external images — keeps the // dreamy aesthetic uniform and works offline). // Prices are stored as base EUR amounts; nyaFormat() converts to selected currency. // Each figure also carries: // category — nin-nin-game.com top-level dept (Figures / Collectibles / TCG / …) // tags — free-form tags for filtering: franchise, character, theme, brand… // addedAt — when added to catalog (drives "Just added") // hot — true if currently in "Hot Pre-orders" / "Hot Products" const NYA_FIGURES = [ { id: 'f1', name: 'Hatsuki Yura', line: 'Aria Lullaby', maker: 'Kotobukiya', scale: '1/7', price: 189, status: 'preorder', release: '2026-08-15', payDays: 6, paid: 57, wishlist: false, hue: 285, tone: 'lilac', char: 'L', category: 'figures', addedAt: '2026-05-18', hot: true, tags: ['kotobukiya', 'bishoujo', 'idol', 'fantasy', '1/7', 'осень-26'] }, { id: 'f2', name: 'Tsukimiya Rei', line: 'Moonlit Dance', maker: 'Alter', scale: '1/8', price: 145, status: 'preorder', release: '2026-09-30', payDays: 12, paid: 0, wishlist: false, hue: 320, tone: 'pink', char: 'M', category: 'figures', addedAt: '2026-05-12', hot: false, tags: ['alter', 'school-life', 'magical-girl', 'kimono', '1/8'] }, { id: 'f3', name: 'Yumiko Sakura', line: 'Petal Dream', maker: 'GSC', scale: '1/7', price: 223, status: 'paid', release: '2026-06-20', payDays: 0, paid: 223, wishlist: false, hue: 340, tone: 'pink', char: 'S', category: 'figures', addedAt: '2026-03-02', hot: false, tags: ['good-smile', 'cherry-blossom', 'romance', 'spring', '1/7'] }, { id: 'f4', name: 'Kana Hoshino', line: 'Stellar Aria', maker: 'Max Factory', scale: '1/6', price: 268, status: 'shipped', release: '2026-05-25', payDays: 0, paid: 268, wishlist: false, hue: 250, tone: 'blue', char: 'K', category: 'figures', addedAt: '2026-02-18', hot: false, tags: ['max-factory', 'sci-fi', 'mech', '1/6', 'limited-edition'] }, { id: 'f5', name: 'Mei Asakura', line: 'Cherry Wind', maker: 'Kotobukiya', scale: '1/8', price: 128, status: 'soon', release: '2026-11-12', payDays: 30, paid: 0, wishlist: true, hue: 5, tone: 'rose', char: 'A', category: 'figures', addedAt: '2026-05-20', hot: true, tags: ['kotobukiya', 'school-life', 'kimono', 'romance', '1/8'] }, { id: 'f6', name: 'Hina Mochizuki',line: 'Night Garden', maker: 'Alter', scale: '1/7', price: 195, status: 'preorder', release: '2026-07-10', payDays: 2, paid: 98, wishlist: false, hue: 280, tone: 'lilac', char: 'H', category: 'figures', addedAt: '2026-04-05', hot: true, tags: ['alter', 'fantasy', 'magical-girl', 'limited-edition', '1/7'] }, { id: 'f7', name: 'Ren Hoshigami', line: 'Velvet Spell', maker: 'GSC', scale: '1/7', price: 210, status: 'preorder', release: '2026-10-05', payDays: 18, paid: 70, wishlist: false, hue: 300, tone: 'lilac', char: 'R', category: 'figures', addedAt: '2026-05-15', hot: false, tags: ['good-smile', 'magical-girl', 'witch', 'fantasy', '1/7'] }, { id: 'f8', name: 'Yui Mizuhara', line: 'Aurora Whisper',maker: 'Max Factory', scale: '1/8', price: 156, status: 'available',release: '2026-04-01', payDays: 0, paid: 156, wishlist: false, hue: 200, tone: 'blue', char: 'Y', category: 'collectibles', addedAt: '2026-01-10', hot: false, tags: ['max-factory', 'idol', 'pop-up-parade', '1/8'] }, { id: 'f9', name: 'Rika Tachibana',line: 'Glass Wings', maker: 'Alter', scale: '1/7', price: 234, status: 'soon', release: '2027-01-20', payDays: 60, paid: 0, wishlist: true, hue: 195, tone: 'blue', char: 'T', category: 'figures', addedAt: '2026-05-19', hot: true, tags: ['alter', 'fantasy', 'fairy', 'wings', 'limited-edition', '1/7'] }, { id: 'f10',name: 'Akari Tsukimi', line: 'Starlight Vow', maker: 'GSC', scale: '1/6', price: 289, status: 'soon', release: '2026-12-15', payDays: 45, paid: 0, wishlist: true, hue: 45, tone: 'gold', char: 'A', category: 'figures', addedAt: '2026-05-17', hot: true, tags: ['good-smile', 'fantasy', 'idol', '1/6', 'limited-edition'] }, { id: 'f11',name: 'Saki Yumemori', line: 'Lunar Lace', maker: 'Kotobukiya', scale: '1/8', price: 137, status: 'preorder', release: '2026-08-30', payDays: 9, paid: 41, wishlist: false, hue: 270, tone: 'lilac', char: 'S', category: 'figures', addedAt: '2026-04-22', hot: false, tags: ['kotobukiya', 'bishoujo', 'lace', 'fantasy', '1/8'] }, { id: 'f12',name: 'Aoi Kazehaya', line: 'Wind Maiden', maker: 'Alter', scale: '1/7', price: 201, status: 'preorder', release: '2026-09-15', payDays: 24, paid: 0, wishlist: true, hue: 215, tone: 'blue', char: 'K', category: 'figures', addedAt: '2026-05-16', hot: false, tags: ['alter', 'school-life', 'shrine-maiden', 'wind', '1/7'] }, ]; // Inline SVG category icons — kawaii minimal line art with rounded strokes // to match the bottom-nav icon vocabulary. const NYA_CAT_ICONS = { all: (s = 22) => ( ), figures: (s = 22) => ( ), collectibles: (s = 22) => ( ), tcg: (s = 22) => ( ), plush: (s = 22) => ( ), prepaid: (s = 22) => ( ), games: (s = 22) => ( ), food: (s = 22) => ( ), spring26: (s = 22) => ( ), }; // Categories — top-level departments mirrored from nin-nin-game.com const NYA_CATEGORIES = [ { id: 'figures', label: 'Figures' }, { id: 'collectibles', label: 'Collectibles' }, { id: 'tcg', label: 'TCG / Cards' }, { id: 'plush', label: 'Plush' }, { id: 'prepaid', label: 'Prepaid' }, { id: 'games', label: 'Video games' }, { id: 'food', label: 'Food & Snacks' }, { id: 'spring26', label: 'Spring Sales 26' }, ]; // Quick filter chips — match nin-nin-game.com homepage rails const NYA_QUICK_FILTERS = [ { id: 'just-added', label: 'just added', filter: (f) => new Date(f.addedAt) >= new Date('2026-05-10') }, { id: 'hot-pre', label: 'hot pre-orders', filter: (f) => f.hot && f.status === 'preorder' }, { id: 'hot', label: 'hot products', filter: (f) => f.hot }, ]; // Currencies — rate is multiplier from base EUR. Round step keeps prices "tidy". const NYA_CURRENCIES = { eur: { symbol: '€', label: '€ евро', rate: 1, locale: 'de-DE', round: 1 }, rub: { symbol: '₽', label: '₽ рубли', rate: 100, locale: 'ru-RU', round: 10 }, usd: { symbol: '$', label: '$ доллары', rate: 1.08, locale: 'en-US', round: 1 }, jpy: { symbol: '¥', label: '¥ иены', rate: 165, locale: 'ja-JP', round: 50 }, }; // React context — App lifts currency state, every screen reads via useContext. const CurrencyContext = React.createContext('eur'); function nyaFormat(amount, currency = 'eur') { if (amount == null || isNaN(amount) || amount === 0) { return '—'; } const c = NYA_CURRENCIES[currency] || NYA_CURRENCIES.eur; const v = Math.round(amount * c.rate / c.round) * c.round; const num = new Intl.NumberFormat(c.locale, { maximumFractionDigits: 0 }).format(v); // Symbol-front for €/$, symbol-back for ₽/¥ to match common conventions return currency === 'eur' || currency === 'usd' ? `${c.symbol}${num}` : `${num} ${c.symbol}`; } // Hook — pulls current currency from context and returns a curried formatter function useFormat() { const cur = React.useContext(CurrencyContext); return React.useCallback(n => nyaFormat(n, cur), [cur]); } const RU_MONTHS = ['янв','фев','мар','апр','май','июн','июл','авг','сен','окт','ноя','дек']; const RU_MONTHS_FULL = ['январь','февраль','март','апрель','май','июнь','июль','август','сентябрь','октябрь','ноябрь','декабрь']; function nyaFormatDate(iso) { if (!iso) return '—'; const d = new Date(iso); if (isNaN(d.getTime()) || d.getFullYear() <= 1970) return '—'; return `${d.getDate()} ${RU_MONTHS[d.getMonth()]} ${String(d.getFullYear()).slice(2)}`; } function nyaFormatMonth(iso) { if (!iso) return '—'; const d = new Date(iso); if (isNaN(d.getTime()) || d.getFullYear() <= 1970) return '—'; return `${RU_MONTHS_FULL[d.getMonth()]} ${d.getFullYear()}`; } // Russian plural — picks form based on last digit (with 11-14 exception). // Pass forms in [one, few, many] order: e.g. ['день','дня','дней']. function ruPlural(n, forms) { const abs = Math.abs(n); const mod100 = abs % 100; const mod10 = abs % 10; if (mod100 >= 11 && mod100 <= 19) return forms[2]; if (mod10 === 1) return forms[0]; if (mod10 >= 2 && mod10 <= 4) return forms[1]; return forms[2]; } // ─── FiguresContext — App provides a live array (from /api/figures) // when available, otherwise falls back to the mock NYA_FIGURES so the // design still renders without a backend. ──────────────────────────────────── const FiguresContext = React.createContext(NYA_FIGURES); function useFigures() { return React.useContext(FiguresContext); } // ─── FigureArt — real photo when the figure carries one, otherwise the // stylized SVG silhouette. The gallery is honored by view= ('back', 'box' // pick later items so the detail screen's view-switcher behaves naturally). function FigureArt({ figure, height = 180, view = 'front', style = {}, fit = 'contain' }) { const gallery = (figure && figure.image_urls) || (figure && figure.image_url ? [figure.image_url] : []); if (gallery && gallery.length) { // view can be a numeric gallery index OR a legacy mock view name. let idx; if (typeof view === 'number') { idx = Math.max(0, Math.min(view, gallery.length - 1)); } else if (view === 'back') idx = Math.min(1, gallery.length - 1); else if (view === 'box') idx = gallery.length - 1; else idx = 0; return (