/ brutalist / br-catalog.jsx
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 });