/ brutalist / br-product.jsx
br-product.jsx
  1  // Brutalist PRODUCT / DOSSIER — upgraded version of Concept 2.
  2  // Reuses the naval-yard spec-sheet composition with a proper add-to-manifest
  3  // flow, config options, and cross-sell.
  4  
  5  function BRProduct({ seed = "KRV-IC-2849-AX", onNav, onAddToCart, cartCount }) {
  6    const ship = makeShip(seed);
  7    const [config, setConfig] = React.useState("STANDARD");
  8    const configs = [
  9      ["STANDARD", 1.00, "Factory loadout"],
 10      ["TACTICAL", 1.18, "Hardpoint-heavy"],
 11      ["CARGO",    1.09, "Expanded hold"],
 12      ["CIVIL",    0.94, "Export-grade"],
 13    ];
 14    const configMul = configs.find(c=>c[0]===config)[1];
 15    const configPrice = Math.round(ship.price * configMul);
 16  
 17    return (
 18      <BRPage minHeight={2000}>
 19        <BRNav active="ship" cartCount={cartCount} onNav={onNav}/>
 20  
 21        {/* BREADCRUMB */}
 22        <div style={{padding:"12px 40px", borderBottom:`1px solid ${BR.ink}`, fontFamily:"IBM Plex Mono", fontSize:10, letterSpacing:2, color:BR.mute, textTransform:"uppercase"}}>
 23          INDEX ▸ <a onClick={()=>onNav("catalog")} style={{color:BR.ink, cursor:"pointer"}}>CATALOG</a> ▸ <a onClick={()=>onNav("catalog")} style={{color:BR.ink, cursor:"pointer"}}>{ship.cls.name.toUpperCase()}</a> ▸ <span style={{color:BR.ink, fontWeight:700}}>{ship.serial}</span>
 24        </div>
 25  
 26        {/* HEADER */}
 27        <div style={{padding:"20px 40px 12px", display:"grid", gridTemplateColumns:"auto 1fr auto", gap:28, alignItems:"end", borderBottom:`1px solid ${BR.ink}`}}>
 28          <div style={{fontFamily:"Oswald", fontWeight:900, fontSize:160, lineHeight:0.82, color:BR.ink, letterSpacing:-4}}>{ship.cls.code}</div>
 29          <div>
 30            <div style={{fontFamily:"IBM Plex Mono", fontSize:11, letterSpacing:3, color:BR.mute, textTransform:"uppercase"}}>{ship.cls.name} · {ship.cls.role}</div>
 31            <div style={{fontFamily:"Oswald", fontWeight:700, fontSize:64, lineHeight:0.95, color:BR.ink, letterSpacing:-1, textTransform:"uppercase", marginTop:4}}>{ship.model}</div>
 32            <div style={{fontFamily:"IBM Plex Sans", fontSize:13, color:BR.ink2, marginTop:6, maxWidth:520, lineHeight:1.5}}>
 33              Manufactured by {ship.mfg.name} at {ship.mfg.loc}. {ship.cond.toLowerCase().replace("_"," ")} hull; {ship.drive.toLowerCase()} propulsion. Intended service: {ship.cls.role.toLowerCase()}.
 34            </div>
 35          </div>
 36          <div style={{textAlign:"right"}}>
 37            <div style={{fontFamily:"IBM Plex Mono", fontSize:10, letterSpacing:2, color:BR.mute, textTransform:"uppercase"}}>UNIT PRICE ({config})</div>
 38            <div style={{fontFamily:"Oswald", fontWeight:900, fontSize:48, color:BR.ink, lineHeight:1, letterSpacing:-1}}>₵{formatCred(configPrice)}</div>
 39            <BRChip rust>{ship.avail}</BRChip>
 40          </div>
 41        </div>
 42  
 43        {/* BODY */}
 44        <div style={{display:"grid", gridTemplateColumns:"1.1fr 1.4fr 1fr"}}>
 45  
 46          {/* LEFT */}
 47          <div style={{borderRight:`1px solid ${BR.ink}`, padding:"20px 28px"}}>
 48            <BRH n="01">Dimensional</BRH>
 49            <BRSpec k="Length" v={ship.length_m} unit="m"/>
 50            <BRSpec k="Mass" v={ship.mass.toLocaleString()} unit="t"/>
 51            <BRSpec k="Cargo" v={ship.cargo_t.toLocaleString()} unit="t"/>
 52            <BRSpec k="Crew" v={ship.crew} unit="pax"/>
 53            <BRSpec k="Year" v={ship.year} unit="cmn"/>
 54  
 55            <div style={{height:18}}/>
 56            <BRH n="02">Propulsion</BRH>
 57            <BRSpec k="Drive" v={ship.drive}/>
 58            <BRSpec k="Top spd" v={ship.topSpeed.toLocaleString()} unit="m/s"/>
 59            <BRSpec k="Thrust" v={ship.thrust} unit="g"/>
 60            <BRSpec k="Jump" v={ship.jumpRange} unit="ly"/>
 61  
 62            <div style={{height:18}}/>
 63            <BRH n="03">Combat</BRH>
 64            <BRSpec k="Armor" v={ship.armor.toLocaleString()} unit="mm"/>
 65            <BRSpec k="Shield" v={ship.shields.toLocaleString()} unit="GJ"/>
 66            <BRSpec k="Hardpt" v={ship.hardpoints} unit="mt"/>
 67            <BRSpec k="Sig" v={ship.signature} unit="dB"/>
 68  
 69            <div style={{height:18}}/>
 70            <BRH n="04">Provenance</BRH>
 71            <div style={{fontFamily:"IBM Plex Mono", fontSize:11, color:BR.ink2, lineHeight:1.7}}>
 72              {[
 73                ["2396.114","INTAKE · Strata⁄Fleet"],
 74                ["2396.102","OVERHAUL · drive core"],
 75                ["2394.064","TRANSFER · 3rd owner"],
 76                ["2391.001","COMMISSION · "+ship.mfg.name],
 77              ].map(([t,e],i)=>(
 78                <div key={i} style={{display:"grid", gridTemplateColumns:"80px 1fr", gap:10, padding:"4px 0", borderBottom:i<3?`1px dotted ${BR.ink}33`:"none"}}>
 79                  <span style={{color:BR.mute, letterSpacing:1}}>{t}</span>
 80                  <span>{e}</span>
 81                </div>
 82              ))}
 83            </div>
 84          </div>
 85  
 86          {/* CENTER */}
 87          <div style={{borderRight:`1px solid ${BR.ink}`, padding:"20px 28px"}}>
 88            <div style={{display:"flex", justifyContent:"space-between", fontFamily:"IBM Plex Mono", fontSize:10, letterSpacing:2, color:BR.mute, textTransform:"uppercase", marginBottom:10}}>
 89              <span>Plate IV · Orthographic</span><span>1:{Math.round(ship.length_m/8)}</span>
 90            </div>
 91            <div style={{position:"relative", height:280, border:`1px solid ${BR.ink}`, background:`repeating-linear-gradient(0deg, transparent 0 19px, ${BR.ink}11 19px 20px), repeating-linear-gradient(90deg, transparent 0 19px, ${BR.ink}11 19px 20px)`}}>
 92              <div style={{position:"absolute", inset:"15%"}}>
 93                <ShipSilhouette ship={ship} stroke={BR.ink} glow={false} strokeWidth={1.4}/>
 94              </div>
 95            </div>
 96  
 97            <div style={{marginTop:16, display:"grid", gridTemplateColumns:"1fr 1fr 1fr", gap:12}}>
 98              {["BOW","BEAM","STERN"].map((label,i)=>(
 99                <div key={label} style={{border:`1px solid ${BR.ink}`, padding:8}}>
100                  <div style={{fontFamily:"IBM Plex Mono", fontSize:9, letterSpacing:2, color:BR.mute, textTransform:"uppercase"}}>View {String.fromCharCode(65+i)} · {label}</div>
101                  <div style={{height:68, position:"relative", marginTop:6}}>
102                    <div style={{position:"absolute", inset:0, transform: i===1 ? "rotate(90deg)" : i===2 ? "scaleX(-1)" : "none"}}>
103                      <ShipSilhouette ship={ship} stroke={BR.ink} glow={false} strokeWidth={1.2} detail="min"/>
104                    </div>
105                  </div>
106                </div>
107              ))}
108            </div>
109  
110            <div style={{marginTop:18}}>
111              <BRH n="05">Loadout Configuration</BRH>
112              <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:10}}>
113                {configs.map(([name, mul, desc]) => (
114                  <div key={name} onClick={()=>setConfig(name)} style={{
115                    border:`2px solid ${BR.ink}`, padding:"12px 14px", cursor:"pointer",
116                    background: config===name ? BR.ink : BR.paper,
117                    color: config===name ? BR.paper : BR.ink,
118                  }}>
119                    <div style={{display:"flex", justifyContent:"space-between", alignItems:"baseline"}}>
120                      <div style={{fontFamily:"Oswald", fontWeight:700, fontSize:16, letterSpacing:2}}>{name}</div>
121                      <div style={{fontFamily:"IBM Plex Mono", fontSize:11}}>{mul >= 1 ? "+" : ""}{Math.round((mul-1)*100)}%</div>
122                    </div>
123                    <div style={{fontFamily:"IBM Plex Sans", fontSize:11, opacity:0.7, marginTop:2}}>{desc}</div>
124                  </div>
125                ))}
126              </div>
127            </div>
128  
129            <div style={{marginTop:18}}>
130              <BRH n="06">Nota Bene</BRH>
131              <div style={{fontFamily:"IBM Plex Sans", fontSize:12, color:BR.ink2, lineHeight:1.55, columnCount:2, columnGap:20, textAlign:"justify"}}>
132                Unit is {ship.cond.toLowerCase().replace("_"," ")}, origin {ship.mfg.loc}. Buyer shall verify transponder re-coding prior to jump. Warranty: {ship.warranty.toLowerCase()}. Buyer assumes all liability for flagged faction variants ({ship.faction}). Delivery in {ship.delivery} cycles via bonded relay. Export license required; all sales final at receipt of possession.
133              </div>
134            </div>
135          </div>
136  
137          {/* RIGHT */}
138          <div style={{padding:"20px 28px", display:"flex", flexDirection:"column"}}>
139            <BRH n="07">Procurement</BRH>
140  
141            <FormRow label="Buyer" value="Cmdr. Orlan Veyr" hand/>
142            <FormRow label="Dept" value="7th Expeditionary · LOG"/>
143            <FormRow label="PO Ref" value={`PO-${(hashStr(seed)%100000).toString().padStart(5,"0")}`}/>
144            <FormRow label="Qty" value="1 unit"/>
145            <FormRow label="Escrow" value="Stellar Bank · Tier 3"/>
146  
147            <div style={{height:12}}/>
148            <BRH n="08">Terms</BRH>
149            <BRSpec k="Delivery" v={`${ship.delivery} cycles`} unit="cyc"/>
150            <BRSpec k="Lease/mo" v={`₵${formatCred(Math.round(configPrice/120))}`} unit="cred"/>
151            <BRSpec k="Insure" v="1.8% annual"/>
152            <BRSpec k="Warranty" v={ship.warranty}/>
153  
154            <div style={{marginTop:"auto", paddingTop:20}}>
155              <div style={{border:`2px dashed ${BR.rust}`, padding:12, fontFamily:"IBM Plex Mono", fontSize:11, color:BR.rust, textTransform:"uppercase", letterSpacing:2, marginBottom:14}}>
156                ⚑ Requires Commodore-grade authorization
157              </div>
158              <button onClick={() => onAddToCart(ship, configPrice, config)} style={{
159                width:"100%", padding:"18px 0", background: BR.ink, color: BR.paper, border:`2px solid ${BR.ink}`,
160                fontFamily:"Oswald", fontWeight:700, fontSize:14, letterSpacing:3, cursor:"pointer", marginBottom:8,
161              }}>ADD TO MANIFEST ▸</button>
162              <div style={{display:"grid", gridTemplateColumns:"1fr 1fr", gap:8}}>
163                <button style={{padding:"14px 0", background:"transparent", color:BR.ink, border:`2px solid ${BR.ink}`, fontFamily:"Oswald", fontWeight:700, fontSize:12, letterSpacing:2, cursor:"pointer"}}>HOLD</button>
164                <button style={{padding:"14px 0", background:"transparent", color:BR.ink, border:`2px solid ${BR.ink}`, fontFamily:"Oswald", fontWeight:700, fontSize:12, letterSpacing:2, cursor:"pointer"}}>REQUEST DEMO</button>
165              </div>
166            </div>
167          </div>
168        </div>
169  
170        {/* CROSS-SELL */}
171        <div style={{padding:"28px 40px", borderTop:`2px solid ${BR.ink}`}}>
172          <BRH n="09" size={16}>Comparable Lots</BRH>
173          <div style={{display:"grid", gridTemplateColumns:"repeat(4, 1fr)", gap:0, border:`2px solid ${BR.ink}`}}>
174            {Array.from({length:4}).map((_,i) => {
175              const s = makeShip("CROSS-"+seed+"-"+i);
176              return <LotCard key={i} ship={s} borderR={i<3} onOpen={()=>onNav("ship", s.seed)}/>;
177            })}
178          </div>
179        </div>
180  
181        <BRFooter/>
182      </BRPage>
183    );
184  }
185  
186  function FormRow({ label, value, hand }) {
187    return (
188      <div style={{display:"grid", gridTemplateColumns:"72px 1fr", gap:10, alignItems:"baseline", padding:"8px 0", borderBottom:`1px solid ${BR.ink}`}}>
189        <div style={{fontFamily:"IBM Plex Mono", fontSize:9, letterSpacing:2, color:BR.mute, textTransform:"uppercase"}}>{label}</div>
190        <div style={{fontFamily: hand ? "'Caveat','Space Grotesk',cursive" : "IBM Plex Mono", fontSize: hand ? 18 : 12, fontWeight: 500, color:BR.ink}}>{value}</div>
191      </div>
192    );
193  }
194  
195  Object.assign(window, { BRProduct });