/* ============ Works — grid, filter, detail ============ */ function Placeholder({ label = "SCREEN", k = 0 }) { return ( ); } function DeviceFrame({ type = "phone", children, label }) { if (type === "hardware") { return (
{Array.from({length:18}).map((_,i)=>)}
{children || }
PWR
); } if (type === "desktop") { return (
{label || "app"}
{children || }
); } if (type === "doc") { return (
{children || }
); } if (type === "tablet") { return
{children || }
; } return (
{children || }
); } function orderScreens(screens, initialId) { if (!screens.length) return []; const first = initialId || screens[0].id; const initial = screens.find(s => s.id === first) || screens[0]; return [initial, ...screens.filter(s => s !== initial)]; } function WorkPrototypeThumb({ w, idx }) { const cfg = w.demoConfig || {}; const flowScreens = Array.isArray(cfg.screens) ? orderScreens(cfg.screens, cfg.initial).map((s, i) => ({ id: s.id || `screen-${i}`, name: s.name || s.id || `Screen ${i + 1}`, image: s.image || "", kind: "image" })) : []; const legacyMap = window.DEMO_SCREENS || {}; const legacyKeys = Object.keys(legacyMap); const legacyScreens = w.demo && !flowScreens.length && legacyKeys.length ? orderScreens(legacyKeys.map(k => ({ id: k, ...legacyMap[k] })), cfg.initial || "home").map(s => ({ ...s, kind: "component" })) : []; const screens = flowScreens.length ? flowScreens : legacyScreens; const [pos, setPos] = useState(0); useEffect(() => { setPos(0); }, [w.id]); useEffect(() => { if (!w.demo || screens.length < 2) return; const delay = 2800 + (idx % 3) * 320; const timer = setInterval(() => setPos(p => (p + 1) % screens.length), delay); return () => clearInterval(timer); }, [w.demo, screens.length, idx]); if (!w.demo || !screens.length) { return ; } const cur = screens[pos % screens.length]; const device = cfg.device || w.device || "phone"; const Comp = cur.Comp; return (
{device === "desktop" &&
}
{cur.kind === "component" && Comp ? : cur.image ? {cur.name : }
{screens.length > 1 &&
{screens.map((s, i) => )}
}
); } function WorkCard({ w, idx, onOpen }) { const [ref, seen] = useInView(); const cats = Array.isArray(w.cat) ? w.cat : []; const href = `/works?id=${encodeURIComponent(w.id || idx + 1)}`; const open = (e) => { if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1) return; e.preventDefault(); onOpen(w); }; return (
ID:{String(w.id || "work").slice(0,6).toUpperCase()} {String(w.device || "phone").toUpperCase()} OPEN →
{w.demo && LIVE DEMO}
{w.year}

{w.title || "Untitled"}{w.en || w.id}

{w.desc}

{cats.map(c => {c})}
); } function WorksSection({ onOpen, sel, setSel }) { const data = (window.HASHBIT_DATA && window.HASHBIT_DATA.works) || []; const cats = ["All", ...Array.from(new Set(data.flatMap(w => Array.isArray(w.cat) ? w.cat : [])))]; const list = sel === "All" ? data : data.filter(w => w.cat.includes(sel)); return (
03 Works
{String(data.length).padStart(2,"0")} PROJECTS / 2023—2025
{cats.map(c => ( ))}
{list.map((w, i) => )}
); } /* SEO: swap /description/canonical/JSON-LD for the open work, restore them on close */ function useWorkSEO(w) { useEffect(() => { if (!w) return; const defaultTitle = document.title; const metaDesc = document.querySelector('meta[name="description"]'); const defaultDesc = metaDesc ? metaDesc.getAttribute("content") : ""; const seoTitle = w.seoTitle || `${w.title}${w.en ? `「${w.en}」` : ""} | HASHBIT株式会社`; const seoDesc = w.seoDescription || w.catch || w.desc || w.overview || defaultDesc; const url = `${window.location.origin}/works?id=${encodeURIComponent(w.id)}`; document.title = seoTitle; if (metaDesc) metaDesc.setAttribute("content", seoDesc); let canonical = document.querySelector('link[rel="canonical"]'); const hadCanonical = !!canonical; if (!canonical) { canonical = document.createElement("link"); canonical.setAttribute("rel", "canonical"); document.head.appendChild(canonical); } canonical.setAttribute("href", url); const ld = document.createElement("script"); ld.type = "application/ld+json"; ld.id = "work-jsonld"; ld.textContent = JSON.stringify({ "@context": "https://schema.org", "@type": "Product", name: w.title, alternateName: w.en, description: seoDesc, category: (w.cat || []).join(", "), brand: { "@type": "Organization", name: "HASHBIT株式会社", url: window.location.origin }, url }); document.head.appendChild(ld); return () => { document.title = defaultTitle; if (metaDesc) metaDesc.setAttribute("content", defaultDesc); if (!hadCanonical && canonical) canonical.remove(); document.getElementById("work-jsonld")?.remove(); }; }, [w]); } function InfoBlock({ label, children, mono }) { return ( <div className="wd-block"> <div className="wd-block-label mono">{label}</div> <div className={mono ? "mono wd-block-body" : "wd-block-body"}>{children}</div> </div> ); } function WorkDetail({ w, onClose, go }) { useWorkSEO(w); useEffect(() => { document.body.style.overflow = "hidden"; const onKey = (e) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); return () => { document.body.style.overflow = ""; window.removeEventListener("keydown", onKey); }; }, []); return ( <div className="wd-overlay"> <div className="wd-scroll"> <div className="wd-bar"> <button className="wd-back" onClick={onClose}>← Works一覧</button> <span className="mono">{w.en.toUpperCase()} / {w.year}</span> </div> <div className="wd-hero wrap"> <div className="wd-hero-l"> <div className="kicker-row">{(w.cat || []).map(c => <span key={c} className="tag dot">{c}</span>)}</div> <h2 className="wd-title">{w.title}</h2> <div className="wd-en mono">{w.en}</div> <p className="wd-catch">{w.catch}</p> <div className="wd-quickfacts"> <div><span className="mono">CLIENT</span>{w.client}</div> <div><span className="mono">PERIOD</span>{w.period}</div> </div> </div> </div> <div className="wd-demo-area wrap"> {w.demo ? <InteractiveDemo initial={(w.demoConfig && w.demoConfig.initial) || "home"} config={w.demoConfig} title={w.title || w.en} /> : <div className="wd-static-frame"><DeviceFrame type={w.device} label={String(w.en || w.id || "work").toLowerCase()+".app"} /> <div className="wd-gallery">{[0,1,2].map(i=><div key={i} className="wd-thumb"><Placeholder label={"SHOT 0"+(i+1)} k={i} /></div>)}</div> </div>} </div> <div className="wd-body wrap"> <div className="wd-main"> <InfoBlock label="OVERVIEW / 概要"><p>{w.overview}</p></InfoBlock> <div className="wd-2col"> <InfoBlock label="PROBLEM / 課題"><p>{w.problem}</p></InfoBlock> <InfoBlock label="SOLUTION / 解決方法"><p>{w.solution}</p></InfoBlock> </div> <InfoBlock label="FEATURES / 開発した機能"> <ul className="wd-features">{(w.features || []).map(f => <li key={f}><span className="mono">→</span>{f}</li>)}</ul> </InfoBlock> </div> <aside className="wd-side"> <InfoBlock label="TECH STACK" mono> <div className="kicker-row">{(w.tech || []).map(t => <span key={t} className="tag">{t}</span>)}</div> </InfoBlock> <InfoBlock label="SCOPE / 担当範囲"><p>{w.scope}</p></InfoBlock> <InfoBlock label="CATEGORY"> <div className="kicker-row">{(w.cat || []).map(c => <span key={c} className="tag">{c}</span>)}</div> </InfoBlock> </aside> </div> <div className="wd-cta wrap"> <div className="wd-cta-box"> <div> <div className="eyebrow"><span className="idx">+</span> Contact</div> <h3>同じような仕組みを、つくりませんか。</h3> </div> <button className="btn accent" onClick={() => { onClose(); setTimeout(()=>go("contact"), 60); }}>相談する <span className="arr">→</span></button> </div> </div> </div> </div> ); } Object.assign(window, { Placeholder, DeviceFrame, WorksSection, WorkDetail });