br-catalog.jsx
1 // Brutalist CATALOG / SEARCH — dense newspaper-listings page. 2 // Left rail: filters (facets) as checkbox lists. Main: sortable table + 3 // toggle to grid. Top: search bar + active filter chips + result count. 4 5 function BRCatalog({ onNav, onShip }) { 6 const [view, setView] = React.useState("table"); 7 const [query, setQuery] = React.useState(""); 8 const [sortKey, setSortKey] = React.useState("price"); 9 const [activeClass, setActiveClass] = React.useState(null); 10 11 // Generate a deterministic catalog of 24 ships 12 const all = React.useMemo(() => Array.from({length: 24}).map((_,i) => makeShip("CAT-"+i+"-"+i*7)), []); 13 const filtered = all.filter(s => { 14 if (activeClass && s.cls.code !== activeClass) return false; 15 if (query && !(s.model+s.serial+s.mfg.name).toLowerCase().includes(query.toLowerCase())) return false; 16 return true; 17 }); 18 const sorted = [...filtered].sort((a,b) => { 19 if (sortKey === "price") return a.price - b.price; 20 if (sortKey === "mass") return a.mass - b.mass; 21 if (sortKey === "jump") return b.jumpRange - a.jumpRange; 22 if (sortKey === "year") return b.year - a.year; 23 return 0; 24 }); 25 26 return ( 27 <BRPage minHeight={2000}> 28 <BRNav active="catalog" cartCount={2} onNav={onNav}/> 29 30 {/* BREADCRUMB + SEARCH */} 31 <div style={{padding:"16px 40px", borderBottom:`1px solid ${BR.ink}`, display:"flex", alignItems:"center", gap:20}}> 32 <div style={{fontFamily:"IBM Plex Mono", fontSize:10, letterSpacing:2, color:BR.mute, textTransform:"uppercase"}}> 33 INDEX ▸ <span style={{color:BR.ink, fontWeight:700}}>CATALOG</span> 34 </div> 35 <div style={{flex:1, display:"flex", border:`2px solid ${BR.ink}`, background:BR.paper}}> 36 <div style={{padding:"12px 16px", borderRight:`2px solid ${BR.ink}`, fontFamily:"IBM Plex Mono", fontSize:12, color:BR.mute, letterSpacing:2}}>SN ▸</div> 37 <input 38 value={query} onChange={e=>setQuery(e.target.value)} 39 placeholder="Serial, model, manufacturer, drive, sector…" 40 style={{flex:1, border:"none", background:"transparent", fontFamily:"IBM Plex Mono", fontSize:13, outline:"none", padding:"0 12px"}} 41 /> 42 <div style={{padding:"12px 22px", background:BR.ink, color:BR.paper, fontFamily:"Oswald", fontSize:12, letterSpacing:3, cursor:"pointer"}}>SEARCH</div> 43 </div> 44 </div> 45 46 {/* ACTIVE FILTER STRIP */} 47 <div style={{padding:"10px 40px", borderBottom:`1px solid ${BR.ink}`, display:"flex", justifyContent:"space-between", alignItems:"center", background:BR.paper2}}> 48 <div style={{display:"flex", gap:8, alignItems:"center", flexWrap:"wrap"}}> 49 <span style={{fontFamily:"IBM Plex Mono", fontSize:10, letterSpacing:2, color:BR.mute, textTransform:"uppercase"}}>ACTIVE FILTERS:</span> 50 {activeClass && <BRChip rust>CLASS · {activeClass} ×</BRChip>} 51 <BRChip>BONDED TIER-3 ×</BRChip> 52 <BRChip>SECTOR · G/H/I ×</BRChip> 53 <BRChip>PRICE ≤ ₵10B ×</BRChip> 54 <span onClick={() => { setActiveClass(null); setQuery(""); }} style={{marginLeft:8, fontFamily:"IBM Plex Mono", fontSize:10, letterSpacing:2, color:BR.rust, textDecoration:"underline", cursor:"pointer"}}>CLEAR ALL</span> 55 </div> 56 <div style={{fontFamily:"IBM Plex Mono", fontSize:11, letterSpacing:1.5, color:BR.ink2}}> 57 <span style={{fontWeight:700}}>{sorted.length}</span> lots · of 14,208 total 58 </div> 59 </div> 60 61 {/* MAIN */} 62 <div style={{display:"grid", gridTemplateColumns:"260px 1fr"}}> 63 64 {/* LEFT — FACETS */} 65 <div style={{borderRight:`1px solid ${BR.ink}`, padding:"20px 24px", background:BR.paper}}> 66 <BRH n="A" size={12}>Class</BRH> 67 {[ 68 ["IC","Interceptor",2104], 69 ["FR","Frigate",812], 70 ["CH","Hauler",3772], 71 ["YT","Yacht",941], 72 ["MN","Mining Rig",520], 73 ["CR","Cruiser",612], 74 ["EX","Explorer",1104], 75 ["DR","Dreadnought",88], 76 ["SC","Science",446], 77 ["GB","Gunboat",1401], 78 ].map(([c,n,ct]) => ( 79 <Facet key={c} active={activeClass===c} label={`${c} · ${n}`} count={ct} onClick={() => setActiveClass(activeClass===c ? null : c)}/> 80 ))} 81 82 <div style={{height:18}}/> 83 <BRH n="B" size={12}>Forge</BRH> 84 {[["KRV","Krovas",2104],["AUR","Aurelia",1821],["OBS","Obsidian",996],["HEL","Helion",1412],["VNT","Vantari",2660]].map(([c,n,ct])=>( 85 <Facet key={c} label={`${c} · ${n}`} count={ct}/> 86 ))} 87 88 <div style={{height:18}}/> 89 <BRH n="C" size={12}>Drive</BRH> 90 {["Fusion Torch","Ion Rift","Quantum Fold","Antimatter Core","Plasma Pulse","Grav-Slip"].map(d => ( 91 <Facet key={d} label={d} count={Math.floor(Math.random()*2000+200)}/> 92 ))} 93 94 <div style={{height:18}}/> 95 <BRH n="D" size={12}>Price</BRH> 96 <div style={{padding:"6px 0"}}> 97 <div style={{display:"flex", gap:8}}> 98 <input placeholder="MIN ₵" style={{flex:1, border:`2px solid ${BR.ink}`, padding:"6px 8px", fontFamily:"IBM Plex Mono", fontSize:11, background:BR.paper}}/> 99 <input placeholder="MAX ₵" style={{flex:1, border:`2px solid ${BR.ink}`, padding:"6px 8px", fontFamily:"IBM Plex Mono", fontSize:11, background:BR.paper}}/> 100 </div> 101 <div style={{marginTop:10, height:40, position:"relative"}}> 102 {/* histogram */} 103 <div style={{display:"flex", alignItems:"flex-end", gap:1, height:"100%"}}> 104 {[8,14,22,30,36,40,38,32,24,18,12,8,5,3,2,1].map((h,i) => ( 105 <div key={i} style={{flex:1, height:`${h*2}%`, background: i>=2 && i<=11 ? BR.ink : BR.mute2}}/> 106 ))} 107 </div> 108 </div> 109 <div style={{display:"flex", justifyContent:"space-between", fontFamily:"IBM Plex Mono", fontSize:9, color:BR.mute, letterSpacing:1, marginTop:4}}> 110 <span>0</span><span>₵10B</span> 111 </div> 112 </div> 113 114 <div style={{height:18}}/> 115 <BRH n="E" size={12}>Availability</BRH> 116 {["IN STOCK","PRE-ORDER","BUILD TO ORDER","LAST UNIT","BROKERED"].map(a => <Facet key={a} label={a} count={Math.floor(Math.random()*3000+500)}/>)} 117 118 <div style={{marginTop:20, border:`2px solid ${BR.rust}`, padding:12, fontFamily:"IBM Plex Mono", fontSize:10, color:BR.rust, letterSpacing:1.5, textTransform:"uppercase"}}> 119 ⚑ Saved search: “EX-class ≤ ₵800M” 120 </div> 121 </div> 122 123 {/* RIGHT — results */} 124 <div> 125 {/* Sort + view toggle */} 126 <div style={{padding:"12px 28px", borderBottom:`1px solid ${BR.ink}`, display:"flex", justifyContent:"space-between", alignItems:"center", background:BR.paper}}> 127 <div style={{display:"flex", gap:12, alignItems:"center"}}> 128 <span style={{fontFamily:"IBM Plex Mono", fontSize:10, letterSpacing:2, color:BR.mute, textTransform:"uppercase"}}>SORT BY</span> 129 {[["price","Price"],["mass","Mass"],["jump","Jump"],["year","Year"]].map(([k,l]) => ( 130 <div key={k} onClick={() => setSortKey(k)} style={{ 131 padding:"5px 14px", fontFamily:"Oswald", fontWeight:700, fontSize:11, letterSpacing:2, 132 border:`2px solid ${BR.ink}`, cursor:"pointer", 133 background: sortKey===k ? BR.ink : "transparent", color: sortKey===k ? BR.paper : BR.ink, 134 }}>{l.toUpperCase()}</div> 135 ))} 136 </div> 137 <div style={{display:"flex", gap:0, border:`2px solid ${BR.ink}`}}> 138 <div onClick={() => setView("table")} style={{padding:"5px 14px", fontFamily:"Oswald", fontSize:11, letterSpacing:2, cursor:"pointer", background: view==="table" ? BR.ink : "transparent", color: view==="table" ? BR.paper : BR.ink, borderRight:`2px solid ${BR.ink}`}}>TABLE</div> 139 <div onClick={() => setView("grid")} style={{padding:"5px 14px", fontFamily:"Oswald", fontSize:11, letterSpacing:2, cursor:"pointer", background: view==="grid" ? BR.ink : "transparent", color: view==="grid" ? BR.paper : BR.ink}}>GRID</div> 140 </div> 141 </div> 142 143 {view === "table" ? ( 144 <table style={{width:"100%", borderCollapse:"collapse", fontFamily:"IBM Plex Mono", fontSize:12}}> 145 <thead> 146 <tr style={{background:BR.paper2, borderBottom:`2px solid ${BR.ink}`}}> 147 <th style={thStyle}>Serial</th> 148 <th style={thStyle}>Plate</th> 149 <th style={thStyle}>Model / Class</th> 150 <th style={thStyle}>Forge</th> 151 <th style={{...thStyle, textAlign:"right"}}>Mass</th> 152 <th style={{...thStyle, textAlign:"right"}}>Jump</th> 153 <th style={thStyle}>Status</th> 154 <th style={{...thStyle, textAlign:"right"}}>Price</th> 155 <th style={thStyle}></th> 156 </tr> 157 </thead> 158 <tbody> 159 {sorted.map((s, i) => ( 160 <tr key={s.seed} style={{borderBottom:`1px dotted ${BR.ink}44`, cursor:"pointer", background: i%2 ? BR.paper : BR.paper2}} onClick={() => onShip && onShip(s.seed)}> 161 <td style={tdStyle}><span style={{fontWeight:700}}>{s.serial}</span></td> 162 <td style={{...tdStyle, padding:"4px 8px"}}> 163 <div style={{width:110, height:44, border:`1px solid ${BR.ink}33`, position:"relative"}}> 164 <div style={{position:"absolute", inset:"8% 6%"}}> 165 <ShipSilhouette ship={s} stroke={BR.ink} glow={false} strokeWidth={1} detail="min"/> 166 </div> 167 </div> 168 </td> 169 <td style={tdStyle}> 170 <div style={{fontFamily:"Oswald", fontWeight:700, fontSize:15, letterSpacing:0.5, textTransform:"uppercase"}}>{s.model}</div> 171 <div style={{color:BR.mute, fontSize:10, letterSpacing:1, marginTop:2}}>{s.cls.code} · {s.cls.name}</div> 172 </td> 173 <td style={tdStyle}>{s.mfg.code}<div style={{color:BR.mute, fontSize:10}}>{s.mfg.loc}</div></td> 174 <td style={{...tdStyle, textAlign:"right"}}>{s.mass.toLocaleString()} t</td> 175 <td style={{...tdStyle, textAlign:"right"}}>{s.jumpRange} ly</td> 176 <td style={tdStyle}><BRChip rust={s.avail==="LAST UNIT"} ok={s.avail==="IN STOCK"}>{s.avail}</BRChip></td> 177 <td style={{...tdStyle, textAlign:"right", fontFamily:"Oswald", fontWeight:700, fontSize:16}}>₵{formatCred(s.price)}</td> 178 <td style={{...tdStyle, textAlign:"right"}}> 179 <span style={{fontFamily:"Oswald", fontWeight:700, fontSize:11, letterSpacing:2, borderBottom:`1.5px solid ${BR.ink}`}}>OPEN ▸</span> 180 </td> 181 </tr> 182 ))} 183 </tbody> 184 </table> 185 ) : ( 186 <div style={{padding:"18px 28px", display:"grid", gridTemplateColumns:"repeat(3, 1fr)", gap:0, border:`2px solid ${BR.ink}`}}> 187 {sorted.slice(0,9).map((s,i) => ( 188 <LotCard key={s.seed} ship={s} borderR={i%3!==2} borderB={i<6} onOpen={() => onShip && onShip(s.seed)}/> 189 ))} 190 </div> 191 )} 192 193 {/* PAGINATION */} 194 <div style={{padding:"20px 28px", borderTop:`1px solid ${BR.ink}`, display:"flex", justifyContent:"space-between", alignItems:"center", background:BR.paper2}}> 195 <div style={{fontFamily:"IBM Plex Mono", fontSize:11, color:BR.mute, letterSpacing:1.5}}>PAGE 01 OF 592 · SHOWING 1–{sorted.length} OF 14,208</div> 196 <div style={{display:"flex", gap:0, border:`2px solid ${BR.ink}`}}> 197 {["◀ PREV","01","02","03","04","05","…","592","NEXT ▶"].map((p,i) => ( 198 <div key={i} style={{ 199 padding:"8px 14px", fontFamily:"Oswald", fontSize:12, letterSpacing:2, cursor:"pointer", 200 borderRight: i<8 ? `1px solid ${BR.ink}` : "none", 201 background: p==="01" ? BR.ink : "transparent", color: p==="01" ? BR.paper : BR.ink, 202 }}>{p}</div> 203 ))} 204 </div> 205 </div> 206 </div> 207 </div> 208 209 <BRFooter/> 210 </BRPage> 211 ); 212 } 213 214 const thStyle = {padding:"10px 12px", textAlign:"left", fontFamily:"IBM Plex Mono", fontSize:10, letterSpacing:2, color:"#7a756a", textTransform:"uppercase", fontWeight:500}; 215 const tdStyle = {padding:"10px 12px", verticalAlign:"middle"}; 216 217 function Facet({ label, count, active, onClick }) { 218 return ( 219 <div onClick={onClick} style={{display:"flex", justifyContent:"space-between", alignItems:"center", padding:"4px 0", cursor:"pointer", borderBottom:`1px dotted ${BR.ink}22`}}> 220 <div style={{display:"flex", alignItems:"center", gap:8, fontFamily:"IBM Plex Mono", fontSize:11, color:BR.ink}}> 221 <div style={{width:12, height:12, border:`1.5px solid ${BR.ink}`, background: active ? BR.ink : "transparent"}}/> 222 <span style={{fontWeight: active ? 700 : 400}}>{label}</span> 223 </div> 224 <span style={{fontFamily:"IBM Plex Mono", fontSize:10, color:BR.mute}}>{count.toLocaleString()}</span> 225 </div> 226 ); 227 } 228 229 Object.assign(window, { BRCatalog });