// app.jsx — アプリ本体(ヘッダー / ルーティング / フッター / Tweaks) const { useState: useStateA, useEffect: useEffectA } = React; const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "theme": "wamodern", "accent": "#6d7a3c", "fontSize": 16, "radius": "default" }/*EDITMODE-END*/; const THEME_LABELS = { wamodern: "和モダン", natural: "ナチュラル", database: "データベース" }; function Header({ view, onNav, query, setQuery, onSearch }) { const [menu, setMenu] = useStateA(false); return (
onNav("top")}>
干し芋ナビ
HOSHIIMO NAVI
{ e.preventDefault(); onSearch(); }}> setQuery(e.target.value)} placeholder="商品名・メーカー・品種で検索" />
{menu && (
{ e.preventDefault(); onSearch(); setMenu(false); }}> setQuery(e.target.value)} placeholder="検索" />
{ onNav("top"); setMenu(false); }}>ホーム { onNav("list"); setMenu(false); }}>商品一覧
)}
); } function Footer({ onNav, onJump }) { const J = (id) => (e) => { e.preventDefault(); onJump(id); }; const toList = (e) => { e.preventDefault(); onNav("list"); }; return ( ); } function App() { const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); const [view, setView] = useStateA("top"); const [listFilters, setListFilters] = useStateA(null); const [modal, setModal] = useStateA(null); const [query, setQuery] = useStateA(""); const [activeQuery, setActiveQuery] = useStateA(""); const [, forceTick] = useStateA(0); // 管理画面でデータが変わったら公開サイトも更新(別タブ・同タブ両対応) useEffectA(() => { const refresh = () => { window.PRODUCTS = loadProducts(); forceTick((n) => n + 1); }; window.addEventListener("storage", refresh); window.addEventListener("products-changed", refresh); window.addEventListener("masters-changed", refresh); return () => { window.removeEventListener("storage", refresh); window.removeEventListener("products-changed", refresh); window.removeEventListener("masters-changed", refresh); }; }, []); // テーマ適用 useEffectA(() => { document.documentElement.setAttribute("data-theme", t.theme); document.documentElement.style.setProperty("--base-fz", t.fontSize + "px"); if (t.accent) { document.documentElement.style.setProperty("--olive", t.accent); // 濃色は accent をやや暗く document.documentElement.style.setProperty("--olive-deep", `color-mix(in srgb, ${t.accent} 78%, #1c2010)`); } if (t.radius === "sharp") { document.documentElement.style.setProperty("--radius", "4px"); document.documentElement.style.setProperty("--radius-sm", "3px"); } else if (t.radius === "round") { document.documentElement.style.setProperty("--radius", "24px"); document.documentElement.style.setProperty("--radius-sm", "16px"); } else { document.documentElement.style.removeProperty("--radius"); document.documentElement.style.removeProperty("--radius-sm"); } }, [t.theme, t.accent, t.fontSize, t.radius]); const goList = (presetFilters) => { setListFilters(presetFilters ? { variety: [], origin: [], shape: [], hardness: [], maker: [], tag: [], price: null, minRating: null, ...presetFilters } : null); setActiveQuery(""); setView("list"); window.scrollTo({ top: 0 }); }; const nav = (v) => { setView(v); if (v === "top") setActiveQuery(""); window.scrollTo({ top: 0 }); }; const doSearch = () => { setActiveQuery(query); setListFilters(null); setView("list"); window.scrollTo({ top: 0 }); }; const jumpTo = (id) => { setView("top"); setActiveQuery(""); setTimeout(() => document.getElementById(id)?.scrollIntoView?.({ behavior: "smooth" }), 70); }; return (
{view === "top" ? : }
); } // マウント関数。CMSモード(__CMS_MODE)のときは、データ取得後に bootstrap から呼ぶ。 window.mountApp = function () { ReactDOM.createRoot(document.getElementById("root")).render(); }; if (!window.__CMS_MODE) { window.mountApp(); }