// Shared product data with 3-axis metadata for filter UX // 商品データ本体は products.js を編集してください(window.PRODUCTS として注入される)。 // products.js が未読込でも壊れないよう、空配列をフォールバックに用意します。 const PRODUCTS = (typeof window !== "undefined" && Array.isArray(window.PRODUCTS)) ? window.PRODUCTS : []; const SCENES = [ "展示会・カンファレンス", "周年記念・記念事業", "販促キャンペーン・新商品PR", "VIPギフト・上顧客向け", "社内イベント", "地域・自治体イベント", "災害備蓄・BCP", ]; // Map full label → short tag stored in product.scene const SCENE_KEY = { "展示会・カンファレンス": "展示会", "周年記念・記念事業": "周年記念", "販促キャンペーン・新商品PR": ["販促", "販促キャンペーン"], "VIPギフト・上顧客向け": "VIPギフト", "社内イベント": "社内イベント", "地域・自治体イベント": "地域・自治体", "災害備蓄・BCP": "災害備蓄", }; const BUDGETS = [ { id: "<500", label: "〜500円", sub: "大量配布向け" }, { id: "500-2000", label: "500〜2,000円", sub: "標準" }, { id: "2000+", label: "2,000円〜", sub: "プレミアム・VIP" }, ]; const LOTS = [ { id: "30-100", label: "30〜100個", sub: "少量・限定" }, { id: "100-500", label: "100〜500個", sub: "中規模" }, { id: "500+", label: "500個〜", sub: "大規模" }, ]; function matchesFilters(p, f) { // f: { scenes: Set, budgets: Set, lots: Set, keyword?: string } if (f.scenes && f.scenes.size) { const ok = [...f.scenes].some((label) => { const key = SCENE_KEY[label]; const keys = Array.isArray(key) ? key : [key]; return keys.some((k) => p.scene.includes(k)); }); if (!ok) return false; } if (f.budgets && f.budgets.size && !f.budgets.has(p.budget)) return false; if (f.lots && f.lots.size && !f.lots.has(p.lotRange)) return false; if (f.keyword && f.keyword.trim()) { const k = f.keyword.trim().toLowerCase(); const haystack = [p.name, p.en, p.cat, p.desc, p.spec].filter(Boolean).join(" ").toLowerCase(); if (!haystack.includes(k)) return false; } return true; } // ---- Hero search → Catalog filter bridge ---- // Hero short labels → Catalog full scene labels (data.jsx の SCENES と同じ表記) const HERO_SCENE_TO_LABEL = { "展示会": "展示会・カンファレンス", "周年記念": "周年記念・記念事業", "VIPギフト": "VIPギフト・上顧客向け", "災害備蓄": "災害備蓄・BCP", "自治体": "地域・自治体イベント", "社内イベント": "社内イベント", }; const BUDGET_LABEL_TO_ID = Object.fromEntries(BUDGETS.map((b) => [b.label, b.id])); const LOT_LABEL_TO_ID = Object.fromEntries(LOTS.map((l) => [l.label, l.id])); // Last-applied search snapshot so catalogs mounted after submit can read it window.__noveltySearch = window.__noveltySearch || { keyword: "", scene: "", budget: "", lot: "" }; function buildFiltersFromSearch(s) { const scenes = new Set(); const budgets = new Set(); const lots = new Set(); if (s.scene && HERO_SCENE_TO_LABEL[s.scene]) scenes.add(HERO_SCENE_TO_LABEL[s.scene]); if (s.budget && BUDGET_LABEL_TO_ID[s.budget]) budgets.add(BUDGET_LABEL_TO_ID[s.budget]); if (s.lot && LOT_LABEL_TO_ID[s.lot]) lots.add(LOT_LABEL_TO_ID[s.lot]); return { scenes, budgets, lots, keyword: (s.keyword || "").trim() }; } function applyHeroSearch(s) { const next = { keyword: (s.keyword || "").trim(), scene: s.scene || "", budget: s.budget || "", lot: s.lot || "", }; window.__noveltySearch = next; window.dispatchEvent(new CustomEvent("novelty:search", { detail: next })); const el = document.getElementById("catalog"); if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); } // ---- レスポンシブ判定用フック(全コンポーネントで共有) ---- function useViewport() { const get = () => (typeof window !== "undefined" ? window.innerWidth : 1280); const [w, setW] = React.useState(get); React.useEffect(() => { const onResize = () => setW(window.innerWidth); window.addEventListener("resize", onResize); return () => window.removeEventListener("resize", onResize); }, []); return { width: w, isMobile: w < 768, isTablet: w >= 768 && w < 1024, isDesktop: w >= 1024, }; } Object.assign(window, { PRODUCTS, SCENES, SCENE_KEY, BUDGETS, LOTS, matchesFilters, HERO_SCENE_TO_LABEL, BUDGET_LABEL_TO_ID, LOT_LABEL_TO_ID, applyHeroSearch, buildFiltersFromSearch, useViewport, });