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 });