function Hub({ filters, setFilters, onOpen }) {
const { SYSTEMS, COCOMS, LEVELS } = window.GUIDON_TAX;
const INDEX = window.GUIDON_INDEX;
const set = (patch) => setFilters({ ...filters, ...patch });
const clear = () => setFilters({ q: "", system: null, cocom: null, level: null, acuity: null });
const counts = useMemo(() => {
const bySys = {}; let live = 0;
INDEX.forEach(s => {
bySys[s.system] = bySys[s.system] || { total: 0, live: 0 };
bySys[s.system].total++;
if (window.isLive(s.id)) { bySys[s.system].live++; live++; }
});
return { bySys, live };
}, [INDEX]);
const results = useMemo(() => {
const q = filters.q.trim().toLowerCase();
return INDEX.filter(s => {
if (filters.system && s.system !== filters.system) return false;
if (filters.acuity && s.acuity !== filters.acuity) return false;
if (filters.cocom && !s.cocom.includes(filters.cocom)) return false;
if (filters.level != null && s.level !== filters.level) return false;
if (q && !(s.title.toLowerCase().includes(q) || s.code.toLowerCase().includes(q) || s.oneLiner.toLowerCase().includes(q) || s.system.toLowerCase().includes(q))) return false;
return true;
});
}, [filters, INDEX]);
const activeFilterCount = ["system", "cocom", "acuity"].filter(k => filters[k]).length + (filters.level != null ? 1 : 0) + (filters.q ? 1 : 0);
return (
<div className="hub">
{/* masthead */}
<div className="masthead ticks">
<span className="tick-tr" /><span className="tick-br" />
<div className="wrap masthead-inner">
<div className="masthead-l">
<div className="kicker kicker-olive">Clinical Reasoning · Austere &amp; Operational Medicine</div>
<h1 className="masthead-title">Where there is no doctor,<br />there is a medic.</h1>
<p className="masthead-lede">
A scenario library for independent SOF medics, SF medical sergeants, SARCs, and
civil-affairs medics operating in denied, remote, and resource-limited environments.
Reason through real presentations across body systems, resource levels, and theaters —
from medic bag to regional hospital.
</p>
<div className="masthead-doctrine mono">
JTS CPGs · TCCC / CoTCCC · PCC Guidelines · WTND philosophy
</div>
</div>
<div className="masthead-stats">
<Stat n={INDEX.length} l="Scenarios" />
<Stat n={SYSTEMS.length} l="Body systems" />
<Stat n={counts.live} l="Built end-to-end" accent />
<Stat n={COCOMS.length} l="Theaters / settings" />
</div>
</div>
</div>
<div className="wrap hub-body">
{/* sidebar */}
<aside className="hub-rail">
<div className="rail-head kicker">Body Systems</div>
<button className={"rail-item" + (!filters.system ? " on" : "")} onClick={() => set({ system: null })}>
<span className="rail-code mono">ALL</span>
<span className="rail-name">All systems</span>
<span className="rail-count mono">{INDEX.length}</span>
</button>
{SYSTEMS.map(sys => {
const c = counts.bySys[sys.key] || { total: 0, live: 0 };
const on = filters.system === sys.key;
return (
<button key={sys.key} className={"rail-item" + (on ? " on" : "")} onClick={() => set({ system: on ? null : sys.key })}>
<span className="rail-code mono">{sys.code}</span>
<span className="rail-name">{sys.key}</span>
<span className="rail-count mono">
{c.live > 0 && <i className="rail-live" title={c.live + " built"} />}
{c.total}
</span>
</button>
);
})}
</aside>
{/* main */}
<main className="hub-main">
{/* toolbar */}
<div className="toolbar">
<div className="search">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3" /></svg>
<input
value={filters.q}
onChange={e => set({ q: e.target.value })}
placeholder="Search scenarios, codes, presentations…"
spellCheck="false"
/>
{filters.q && <button className="search-x" onClick={() => set({ q: "" })}>×</button>}
</div>
<div className="filter-group">
<span className="filter-label kicker">Acuity</span>
{["critical", "urgent", "routine"].map(a => (
<button key={a} className={"f-acuity" + (filters.acuity === a ? " on" : "")} data-a={a}
onClick={() => set({ acuity: filters.acuity === a ? null : a })}>
<span className={"acuity-dot dot-" + a} />{window.ACUITY_LABEL[a]}
</button>
))}
</div>
<label className="sel">
<span className="filter-label kicker">Theater</span>
<select value={filters.cocom || ""} onChange={e => set({ cocom: e.target.value || null })}>
<option value="">Any</option>
{COCOMS.map(c => <option key={c} value={c}>{c}</option>)}
</select>
</label>
<label className="sel">
<span className="filter-label kicker">Resource</span>
<select value={filters.level == null ? "" : filters.level} onChange={e => set({ level: e.target.value === "" ? null : Number(e.target.value) })}>
<option value="">Any</option>
{LEVELS.map(l => <option key={l.n} value={l.n}>{l.short}</option>)}
</select>
</label>
{activeFilterCount > 0 && <button className="btn btn-ghost btn-sm clear-btn" onClick={clear}>Clear ({activeFilterCount})</button>}
</div>
{/* result count */}
<div className="result-bar">
<span className="mono result-n">{String(results.length).padStart(3, "0")}</span>
<span className="result-label kicker">{results.length === 1 ? "scenario" : "scenarios"} {activeFilterCount ? "matched" : "in library"}</span>
<span className="result-spacer" />
<span className="legend">
<span className="legend-i"><i className="rail-live" /> built end-to-end</span>
<span className="legend-i"><span className="acuity-dot dot-critical" /> immediate</span>
<span className="legend-i"><span className="acuity-dot dot-urgent" /> priority</span>
<span className="legend-i"><span className="acuity-dot dot-routine" /> routine</span>
</span>
</div>
{/* grid */}
{results.length === 0 ? (
<div className="empty mono">— NO SCENARIOS MATCH THESE FILTERS —<br /><button className="btn btn-sm" style={{ marginTop: 14 }} onClick={clear}>Reset filters</button></div>
) : (
<div className="grid">
{results.map(s => <CaseCard key={s.id} s={s} onOpen={onOpen} />)}
</div>
)}
</main>
</div>
<footer className="hub-foot">
<div className="wrap mono foot-inner">
<span>GUIDON · AUSTERE MEDICINE TRAINER</span>
<span className="foot-mid">TRAINING USE ONLY — NOT A SUBSTITUTE FOR CLINICAL JUDGMENT OR CURRENT CPGs</span>
<span>v0.9 · {counts.live} CASES LIVE</span>
</div>
</footer>
</div>
);
}
function Stat({ n, l, accent }) {
return (
<div className={"stat" + (accent ? " stat-accent" : "")}>
<div className="stat-n mono">{n}</div>
<div className="stat-l kicker">{l}</div>
</div>
);
}
function CaseCard({ s, onOpen }) {
const live = window.isLive(s.id);
return (
<button className={"card" + (live ? " card-live" : " card-draft")} onClick={() => onOpen(s.id)}>
<div className="card-top">
<span className="card-code mono">{s.code}</span>
<span className="card-top-r">
{live ? <span className="card-live-tag mono"><i className="rail-live" />LIVE</span>
: <span className="card-dev-tag mono">INDEX</span>}
<AcuityDot acuity={s.acuity} />
</span>
</div>
<h3 className="card-title">{s.title}</h3>
<p className="card-line">{s.oneLiner}</p>
<div className="card-foot">
<div className="card-tags">
{s.cocom.slice(0, 2).map(c => <span key={c} className="tag-cocom mono">{c}</span>)}
</div>
<LevelBars level={s.level} />
</div>
<span className="card-go mono">{live ? "OPEN CASE →" : "VIEW BRIEF →"}</span>
</button>
);
}
window.Hub = Hub;