Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

blank: simplify to single $128 tier with product image

- Replace wireframe laptop with Lenovo product photo (preloaded)
- Single price ($128), no currency toggle or tier selection
- Shipping: hand delivery ($0) or USPS Priority Mail ($16)
- USD only, US-only shipping
- Add "refurbished / used" to all copy
- Add product image to Stripe checkout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+59 -412
+24 -119
system/netlify/functions/blank.mjs
··· 31 31 : process.env.STRIPE_API_PRIV_KEY; 32 32 33 33 // Pricing (cents) 34 - const pricing = { 35 - usd: { min: 9600, suggested: 12800, max: 51200 }, 36 - dkk: { min: 67200, suggested: 89600, max: 358400 }, 37 - }; 34 + const AMOUNT = 12800; // $128 38 35 39 36 // Shipping options 40 37 const shippingOptions = [ ··· 52 49 { 53 50 shipping_rate_data: { 54 51 type: "fixed_amount", 55 - fixed_amount: { amount: 1200, currency: "usd" }, 52 + fixed_amount: { amount: 1600, currency: "usd" }, 56 53 display_name: "USPS Priority Mail", 57 54 delivery_estimate: { 58 55 minimum: { unit: "business_day", value: 2 }, 59 56 maximum: { unit: "business_day", value: 5 }, 60 - }, 61 - }, 62 - }, 63 - { 64 - shipping_rate_data: { 65 - type: "fixed_amount", 66 - fixed_amount: { amount: 2500, currency: "usd" }, 67 - display_name: "International (DK / EU)", 68 - delivery_estimate: { 69 - minimum: { unit: "business_day", value: 7 }, 70 - maximum: { unit: "business_day", value: 21 }, 71 57 }, 72 58 }, 73 59 }, 74 60 ]; 75 61 76 - // DKK shipping options (same structure, different currency) 77 - const shippingOptionsDKK = [ 78 - { 79 - shipping_rate_data: { 80 - type: "fixed_amount", 81 - fixed_amount: { amount: 0, currency: "dkk" }, 82 - display_name: "Hand delivery", 83 - delivery_estimate: { 84 - minimum: { unit: "business_day", value: 1 }, 85 - maximum: { unit: "business_day", value: 14 }, 86 - }, 87 - }, 88 - }, 89 - { 90 - shipping_rate_data: { 91 - type: "fixed_amount", 92 - fixed_amount: { amount: 8400, currency: "dkk" }, 93 - display_name: "PostNord / International", 94 - delivery_estimate: { 95 - minimum: { unit: "business_day", value: 5 }, 96 - maximum: { unit: "business_day", value: 14 }, 97 - }, 98 - }, 99 - }, 100 - ]; 101 - 102 - function tierFromAmount(amount) { 103 - if (amount >= 51200) return "tutorial"; 104 - if (amount > 9600) return "support"; 105 - return "blank"; 106 - } 107 62 108 63 export async function handler(event) { 109 64 // CORS preflight ··· 115 70 if (event.httpMethod === "GET") { 116 71 return respond(200, { 117 72 product: "AC Blank", 118 - model: "Lenovo ThinkPad Yoga 11e (Gen 4/5)", 73 + model: "Lenovo ThinkPad 11e Yoga Gen 6", 119 74 description: 120 - "A surplus laptop running AC Native OS — a pared-down creative computing instrument with only stable, permanent commands. Like a blank tape waiting to be filled.", 121 - pricing, 122 - tiers: { 123 - blank: { label: "AC Blank", description: "Laptop + AC Native OS" }, 124 - support: { 125 - label: "AC Blank + Support", 126 - description: "Supports AC development", 127 - }, 128 - tutorial: { 129 - label: "AC Blank + Tutorial", 130 - description: "In-person meeting & tutorial in LA", 131 - }, 132 - }, 75 + "Buy a @jeffrey approved Thinkpad 11e Yoga Gen 6 (refurbished / used) pre-flashed with AC Native today! Comes decorated with recovery USB. (No USB C Charger Included)", 76 + amount: AMOUNT, 133 77 }); 134 78 } 135 79 ··· 146 90 // 1. Create checkout session 147 91 if (event.queryStringParameters?.new === "true") { 148 92 try { 149 - const body = JSON.parse(event.body || "{}"); 150 - const currency = (body.currency || "usd").toLowerCase(); 151 - const currencyConfig = pricing[currency]; 152 - 153 - if (!currencyConfig) { 154 - return respond(400, { error: `Unsupported currency: ${currency}` }); 155 - } 156 - 157 - const amount = parseInt(body.amount) || currencyConfig.suggested; 158 - 159 - if (amount < currencyConfig.min || amount > currencyConfig.max) { 160 - return respond(400, { 161 - error: `Amount must be between ${currencyConfig.min} and ${currencyConfig.max} for ${currency.toUpperCase()}`, 162 - }); 163 - } 164 - 165 - const tier = tierFromAmount(amount); 166 93 const domain = dev 167 94 ? `https://${event.headers.host}` 168 95 : "https://aesthetic.computer"; 169 96 170 - const amountDisplay = 171 - currency === "dkk" 172 - ? `${(amount / 100).toFixed(0)} kr` 173 - : `$${(amount / 100).toFixed(2)}`; 174 - 175 - let productName = "AC Blank"; 176 - if (tier === "tutorial") productName += " + Tutorial"; 177 - 178 97 const sessionConfig = { 179 98 line_items: [ 180 99 { 181 100 price_data: { 182 - currency, 101 + currency: "usd", 183 102 product_data: { 184 - name: productName, 103 + name: "AC Blank", 185 104 description: 186 - tier === "tutorial" 187 - ? "AC Native Laptop + in-person tutorial in Los Angeles" 188 - : "Surplus laptop running AC Native OS", 105 + "Thinkpad 11e Yoga Gen 6 (refurbished / used) pre-flashed with AC Native. Comes decorated with recovery USB.", 106 + images: [ 107 + "https://p3-ofp.static.pub/fes/cms/2022/03/28/4wfdaky6aue1z6x5xmxkl9ms8gdpmz225363.png", 108 + ], 189 109 }, 190 - unit_amount: amount, 110 + unit_amount: AMOUNT, 191 111 }, 192 112 quantity: 1, 193 113 }, 194 114 ], 195 115 metadata: { 196 116 type: "blank", 197 - tier, 198 - model: "yoga-11e", 199 - amount: amountDisplay, 200 - currency, 117 + model: "yoga-11e-gen6", 118 + amount: `$${(AMOUNT / 100).toFixed(0)}`, 119 + currency: "usd", 201 120 }, 202 121 mode: "payment", 203 - shipping_address_collection: { allowed_countries: ["US", "DK"] }, 204 - shipping_options: 205 - currency === "dkk" ? shippingOptionsDKK : shippingOptions, 122 + shipping_address_collection: { allowed_countries: ["US"] }, 123 + shipping_options: shippingOptions, 206 124 success_url: `${domain}/blank~thanks`, 207 125 cancel_url: `${domain}/blank`, 208 126 automatic_tax: { enabled: true }, 209 127 custom_text: { 210 128 submit: { 211 129 message: 212 - tier === "tutorial" 213 - ? "We'll reach out to schedule your in-person session in LA." 214 - : "We'll flash your Blank with AC Native OS and ship it your way.", 130 + "We'll flash your Blank with AC Native OS and ship it your way.", 215 131 }, 216 132 }, 217 133 custom_fields: [ ··· 270 186 } 271 187 272 188 const metadata = hookEvent.data.object.metadata; 273 - const tier = metadata.tier || "blank"; 274 189 const customerEmail = session.customer_details?.email; 275 190 const customerName = session.shipping_details?.name || "Friend"; 276 191 const note = 277 192 session.custom_fields?.find((f) => f.key === "note")?.text?.value || null; 278 193 279 194 console.log( 280 - `✅ Blank order: ${tier} tier, ${metadata.amount}, ${customerEmail}`, 195 + `✅ Blank order: ${metadata.amount}, ${customerEmail}`, 281 196 ); 282 197 283 198 // Store order in MongoDB ··· 295 210 }, 296 211 product: { 297 212 model: metadata.model, 298 - tier, 299 - amount: parseInt(metadata.amount) || 0, 300 - currency: metadata.currency, 213 + amount: AMOUNT, 214 + currency: "usd", 301 215 }, 302 216 note, 303 217 status: "paid", ··· 311 225 } 312 226 313 227 // Send confirmation email to buyer 314 - const tierLabel = 315 - tier === "tutorial" 316 - ? "AC Blank + Tutorial" 317 - : tier === "support" 318 - ? "AC Blank (thank you for your support!)" 319 - : "AC Blank"; 320 - 321 228 await email({ 322 229 to: customerEmail, 323 230 subject: "your blank is coming!", 324 231 html: ` 325 - <h2>${tierLabel}</h2> 232 + <h2>AC Blank</h2> 326 233 <p>hi ${customerName},</p> 327 234 <p>thank you for your order! we'll flash your blank with AC Native OS and get it to you soon.</p> 328 - ${tier === "tutorial" ? "<p><b>we'll reach out separately to schedule your in-person tutorial session in los angeles.</b></p>" : ""} 329 235 ${note ? `<p>your note: <em>${note}</em></p>` : ""} 330 236 <p> 331 237 <b><a href="https://aesthetic.computer">aesthetic.computer</a></b><br> ··· 337 243 // Notify us internally 338 244 await email({ 339 245 to: "mail@aesthetic.computer", 340 - subject: `new blank order! (${tier}) — ${metadata.amount}`, 246 + subject: `new blank order! — ${metadata.amount}`, 341 247 html: ` 342 248 <h2>New Blank Order</h2> 343 - <p><b>Tier:</b> ${tier}</p> 344 249 <p><b>Amount:</b> ${metadata.amount}</p> 345 250 <p><b>Customer:</b> ${customerName} (${customerEmail})</p> 346 251 <p><b>Shipping:</b><br> ··· 354 259 `, 355 260 }); 356 261 357 - return respond(200, { received: true, tier }); 262 + return respond(200, { received: true }); 358 263 } 359 264 360 265 return respond(400, { message: `Unhandled event: ${hookEvent.type}` });
+35 -293
system/public/aesthetic.computer/disks/blank.mjs
··· 1 1 // blank, 26.03.20 2 2 // AC Blank — AC Native Laptop product page & checkout 3 3 4 - const { floor, sin, cos, abs, min, max, PI, sqrt } = Math; 4 + const { floor, sin, min, max } = Math; 5 5 6 - // Pricing (cents) 7 - const pricing = { 8 - usd: { min: 9600, suggested: 12800, max: 51200, symbol: "$" }, 9 - dkk: { min: 67200, suggested: 89600, max: 358400, symbol: "kr" }, 10 - }; 11 - 12 - const tiers = [ 13 - { amount: 9600, dkk: 67200, label: "AC Blank", desc: "laptop + AC OS" }, 14 - { amount: 12800, dkk: 89600, label: "AC Blank", desc: "+ support AC" }, 15 - { amount: 51200, dkk: 358400, label: "AC Blank", desc: "+ tutorial in LA" }, 16 - ]; 6 + const PRODUCT_IMG = 7 + "https://p3-ofp.static.pub/fes/cms/2022/03/28/4wfdaky6aue1z6x5xmxkl9ms8gdpmz225363.png"; 17 8 18 9 // Module state 19 10 let amount = 12800; 20 - let currency = "usd"; 11 + let productImg = null; 21 12 let checkoutUrl = null; 22 13 let checkoutReady = false; 23 14 let checkoutError = null; 24 15 let buyPending = false; 25 16 let thanks = false; 26 17 27 - // UI elements (TextButtons) 18 + // UI elements 28 19 let buyBtn = null; 29 - let tierBtns = []; 30 - let currencyBtn = null; 31 20 32 21 // Animation 33 - let frame = 0; 34 22 let scrollY = 0; 35 23 36 24 const charH = 16; 37 25 38 26 // Scrolling text lines 39 27 const scrollLines = [ 40 - "AC Native Laptop", 41 - "", 42 - "A surplus laptop running AC Native OS.", 43 - "Stable commands. Nothing extra.", 44 - "Like a blank tape waiting to be filled.", 28 + "Buy a @jeffrey approved", 29 + "Thinkpad 11e Yoga Gen 6 (refurbished / used)", 30 + "(what he uses to develop AC OS)", 31 + "pre-flashed with AC Native today!", 45 32 "", 46 - "Lenovo ThinkPad Yoga 11e", 47 - "11.6\" touchscreen · flip design", 33 + "Comes decorated with recovery USB.", 34 + "(No USB C Charger Included)", 48 35 "", 49 36 ]; 50 37 51 - function displayAmount(amt, cur) { 52 - if (cur === "dkk") return `${(amt / 100).toFixed(0)} kr`; 38 + function displayAmount(amt) { 53 39 return `$${(amt / 100).toFixed(0)}`; 54 40 } 55 41 56 42 function getBuyText() { 57 43 if (buyPending) return "CHECKING OUT..."; 58 - return `BUY ${displayAmount(amount, currency)}`; 44 + return `BUY ${displayAmount(amount)}`; 59 45 } 60 46 61 - async function boot({ params, ui, screen, cursor, hud, api, dark }) { 47 + async function boot({ params, ui, screen, cursor, hud, api, net, dark }) { 62 48 cursor("native"); 63 49 hud.labelBack(); 64 50 ··· 69 55 70 56 setupButtons(ui, screen); 71 57 fetchCheckout(api); 72 - } 73 58 74 - function tierText(i) { 75 - const t = tiers[i]; 76 - const tierAmt = currency === "dkk" ? t.dkk : t.amount; 77 - return `${displayAmount(tierAmt, currency)} ${t.desc}`; 59 + // Load product image 60 + net.preload(PRODUCT_IMG).then((result) => { 61 + if (result?.img) productImg = result.img; 62 + }); 78 63 } 79 64 80 65 function setupButtons(ui, screen) { 81 66 buyBtn = new ui.TextButton(getBuyText(), { center: "x", bottom: 20, screen }); 82 - 83 - const gap = buyBtn.height + 6; 84 - const tierStartY = buyBtn.btn.box.y - gap * tiers.length - 8; 85 - tierBtns = tiers.map((t, i) => { 86 - return new ui.TextButton(tierText(i), { 87 - center: "x", 88 - y: tierStartY + i * gap, 89 - screen, 90 - }); 91 - }); 92 - 93 - currencyBtn = new ui.TextButton(currency.toUpperCase(), { 94 - top: 8, 95 - right: 8, 96 - screen, 97 - }); 98 67 } 99 68 100 69 async function fetchCheckout(api) { ··· 110 79 const res = await fetch("/api/blank?new=true", { 111 80 method: "POST", 112 81 headers, 113 - body: JSON.stringify({ amount, currency }), 82 + body: JSON.stringify({ amount, currency: "usd" }), 114 83 }); 115 84 116 85 if (!res.ok) { ··· 131 100 } 132 101 133 102 function paint($) { 134 - const { wipe, ink, write, write3D, box, line, screen, dark: isDark } = $; 135 - frame += 1; 103 + const { wipe, ink, screen, dark: isDark } = $; 136 104 const w = screen.width; 137 105 const h = screen.height; 138 106 ··· 141 109 const fg = isDark ? 255 : 20; 142 110 const fgDim = isDark ? 120 : 100; 143 111 const fgFaint = isDark ? 60 : 160; 144 - const btnFill = isDark ? [22, 22, 25] : [230, 230, 232]; 145 - const btnOutline = isDark ? [60, 60, 65] : [180, 180, 185]; 146 - const btnText = isDark ? [140, 140, 145] : [60, 60, 65]; 147 - const selFill = isDark ? [40, 50, 40] : [220, 235, 220]; 148 - const selOutline = isDark ? [120, 200, 120] : [60, 150, 60]; 149 - const selText = isDark ? [220, 255, 220] : [30, 80, 30]; 150 - const hoverFill = isDark ? [35, 35, 40] : [240, 240, 245]; 151 - const hoverOutline = isDark ? [150, 150, 180] : [120, 120, 150]; 152 - const hoverText = isDark ? [200, 200, 220] : [40, 40, 60]; 153 - const wireAlpha = isDark ? 140 : 120; 154 - 155 112 wipe(...bg); 156 - 157 - // Debug: test write3D with a flat plane (identity projection, scale 3) 158 - ink(255, 0, 0).write3D("TEST", { 159 - origin: [20, 20, 0], 160 - right: [1, 0, 0], 161 - down: [0, 1, 0], 162 - project: ([x, y, z]) => [x, y], 163 - }, 3); 164 113 165 114 // Thanks page 166 115 if (thanks) { ··· 172 121 173 122 // Compute button zone height 174 123 const btnH = buyBtn ? buyBtn.height + 6 : 26; 175 - const uiZoneH = btnH * (tiers.length + 1) + 20; 124 + const uiZoneH = btnH * 2 + 20; 176 125 const contentBottom = h - uiZoneH - 20; 177 126 178 - // 💻 Wireframe laptop (turntable swivel) 179 - { 180 - const cx = floor(w / 2); 181 - const laptopTop = 20; 182 - const cy = floor((laptopTop + contentBottom) / 2); 183 - const size = min(w * 0.45, (contentBottom - laptopTop) * 0.35); 184 - const fov = 260; 185 - 186 - // Turntable rotation (slow steady swivel) 187 - const ay = frame * 0.008; 188 - const ax = 0.3; // fixed downward tilt 189 - 190 - // Fixed open angle (~120 degrees) 191 - const hingeAngle = PI * 0.67; 192 - 193 - // Half-box dimensions 194 - const hw = 1.4, hh = 0.08, hd = 0.9; 195 - 196 - // Base (keyboard half) 197 - const base = [ 198 - [-hw, -hh, -hd], [hw, -hh, -hd], [hw, hh, -hd], [-hw, hh, -hd], 199 - [-hw, -hh, hd], [hw, -hh, hd], [hw, hh, hd], [-hw, hh, hd], 200 - ]; 201 - 202 - // Lid — hinged at back edge 203 - const lidLocal = [ 204 - [-hw, -hh, 0], [hw, -hh, 0], [hw, hh, 0], [-hw, hh, 0], 205 - [-hw, -hh, 2 * hd], [hw, -hh, 2 * hd], [hw, hh, 2 * hd], [-hw, hh, 2 * hd], 206 - ]; 207 - const cosH = cos(hingeAngle), sinH = sin(hingeAngle); 208 - const lid = lidLocal.map(([lx, ly, lz]) => { 209 - const ry = ly * cosH - lz * sinH; 210 - const rz = ly * sinH + lz * cosH; 211 - return [lx, ry + hh, rz - hd]; 212 - }); 213 - 214 - const halfEdges = [ 215 - [0, 1], [1, 2], [2, 3], [3, 0], 216 - [4, 5], [5, 6], [6, 7], [7, 4], 217 - [0, 4], [1, 5], [2, 6], [3, 7], 218 - ]; 219 - 220 - const project = ([x, y, z]) => { 221 - let rx = x * cos(ay) - z * sin(ay); 222 - let rz = x * sin(ay) + z * cos(ay); 223 - let ry = y * cos(ax) - rz * sin(ax); 224 - rz = y * sin(ax) + rz * cos(ax); 225 - const scale = fov / (fov + rz * size); 226 - return [cx + rx * size * scale, cy + ry * size * scale, rz]; 227 - }; 228 - 229 - const projBase = base.map(project); 230 - const projLid = lid.map(project); 231 - 232 - const waveOffset = frame * 0.05; 233 - const drawEdges = (proj, edgeOffset) => { 234 - halfEdges.forEach(([a, b], i) => { 235 - const depth = (proj[a][2] + proj[b][2]) / 2; 236 - const brightness = 0.55 + depth * 0.15; 237 - const hue = (((i + edgeOffset) * 0.37 + sin(frame * 0.02 + i + edgeOffset) * 0.3) + waveOffset) % 1; 238 - const sector = abs(hue) * 6; 239 - const f = sector - floor(sector); 240 - let r, g, bl; 241 - const s = floor(sector) % 6; 242 - if (s === 0) { r = 1; g = f; bl = 0; } 243 - else if (s === 1) { r = 1 - f; g = 1; bl = 0; } 244 - else if (s === 2) { r = 0; g = 1; bl = f; } 245 - else if (s === 3) { r = 0; g = 1 - f; bl = 1; } 246 - else if (s === 4) { r = f; g = 0; bl = 1; } 247 - else { r = 1; g = 0; bl = 1 - f; } 248 - ink( 249 - floor(r * 255 * brightness), 250 - floor(g * 255 * brightness), 251 - floor(bl * 255 * brightness), 252 - wireAlpha, 253 - ).line(proj[a][0], proj[a][1], proj[b][0], proj[b][1]); 254 - }); 255 - }; 256 - 257 - drawEdges(projBase, 0); 258 - drawEdges(projLid, 12); 259 - 260 - // Vertex particles 261 - const particleColors = [ 262 - [255, 80, 200], [80, 255, 220], [255, 255, 80], 263 - [80, 200, 255], [255, 120, 80], [180, 80, 255], 264 - ]; 265 - [...projBase, ...projLid].forEach(([px, py], i) => { 266 - const pColor = particleColors[(i + floor(frame * 0.04)) % particleColors.length]; 267 - const flicker = 0.6 + sin(frame * 0.1 + i * 1.3) * 0.4; 268 - ink(...pColor, floor(flicker * 150)).box(px - 1, py - 1, 2, 2); 269 - }); 270 - 271 - // 🖥️ "AC Blank" projected in 3D on the lid screen 272 - // Screen face: inner face of lid (y=-hh before hinge) 273 - const inset = 0.15; 274 - const screenTL = [-hw + inset, -hh - 0.002, inset]; 275 - const screenTR = [hw - inset, -hh - 0.002, inset]; 276 - const screenBL = [-hw + inset, -hh - 0.002, 2 * hd - inset]; 277 - 278 - // Transform screen corners through hinge rotation 279 - const hingeXform = ([lx, ly, lz]) => { 280 - const ry = ly * cosH - lz * sinH; 281 - const rz = ly * sinH + lz * cosH; 282 - return [lx, ry + hh, rz - hd]; 283 - }; 284 - const sTL = hingeXform(screenTL); 285 - const sTR = hingeXform(screenTR); 286 - const sBL = hingeXform(screenBL); 287 - 288 - // Compute plane vectors (right = TL→TR, down = TL→BL) 289 - const planeRight = [sTR[0] - sTL[0], sTR[1] - sTL[1], sTR[2] - sTL[2]]; 290 - const planeDown = [sBL[0] - sTL[0], sBL[1] - sTL[1], sBL[2] - sTL[2]]; 291 - 292 - // Screen-space normal check (is it facing the camera?) 293 - const projTL = project(sTL); 294 - const projTR = project(sTR); 295 - const projBL = project(sBL); 296 - const ex1 = projTR[0] - projTL[0], ey1 = projTR[1] - projTL[1]; 297 - const ex2 = projBL[0] - projTL[0], ey2 = projBL[1] - projTL[1]; 298 - const cross = ex1 * ey2 - ey1 * ex2; 299 - 300 - if (cross > 0) { 301 - const maxCross = size * size * 0.5; 302 - const facing = min(1, abs(cross) / maxCross); 303 - const textAlpha = floor(facing * 255); 304 - 305 - // Plane width in world units 306 - const planeW = sqrt(planeRight[0] ** 2 + planeRight[1] ** 2 + planeRight[2] ** 2); 307 - const planeH = sqrt(planeDown[0] ** 2 + planeDown[1] ** 2 + planeDown[2] ** 2); 308 - 309 - // Normalize plane vectors per glyph-pixel unit 310 - // A glyph pixel at scale 1 = 1/planeW of the right vector (scaled to fit ~20 chars) 311 - const glyphScale = planeW / (6 * 14); // fit ~14 chars across screen width 312 - const rn = [planeRight[0] / planeW * glyphScale, 313 - planeRight[1] / planeW * glyphScale, 314 - planeRight[2] / planeW * glyphScale]; 315 - const dn = [planeDown[0] / planeH * glyphScale, 316 - planeDown[1] / planeH * glyphScale, 317 - planeDown[2] / planeH * glyphScale]; 318 - 319 - // Center "AC Blank" (8 chars × 6px = 48px wide, 10px tall) 320 - const textW = 8 * 6; // glyph pixels 321 - const textH = 10; 322 - const offsetR = (planeW / glyphScale - textW) / 2; 323 - const offsetD = (planeH / glyphScale - textH) / 2; 324 - 325 - // Origin = screen TL + centering offset 326 - const textOrigin = [ 327 - sTL[0] + offsetR * rn[0] + offsetD * dn[0], 328 - sTL[1] + offsetR * rn[1] + offsetD * dn[1], 329 - sTL[2] + offsetR * rn[2] + offsetD * dn[2], 330 - ]; 331 - 332 - const titleColor = isDark ? [255, 255, 255] : [20, 20, 20]; 333 - ink(titleColor[0], titleColor[1], titleColor[2], textAlpha) 334 - .write3D("AC Blank", { 335 - origin: textOrigin, 336 - right: rn, 337 - down: dn, 338 - project, 339 - }); 340 - } 127 + // 💻 Product image (centered above buttons) 128 + if (productImg) { 129 + const areaH = contentBottom - 20; 130 + const maxW = floor(w * 0.85); 131 + const maxH = floor(areaH * 0.85); 132 + const scale = min(maxW / productImg.width, maxH / productImg.height); 133 + const drawW = floor(productImg.width * scale); 134 + const drawH = floor(productImg.height * scale); 135 + const dx = floor((w - drawW) / 2); 136 + const dy = floor(20 + (areaH - drawH) / 2); 137 + ink(255).paste(productImg, dx, dy, { width: drawW, height: drawH }); 138 + } else { 139 + ink(fgDim).write("loading...", { center: "x", y: floor(contentBottom / 2), screen }); 341 140 } 342 141 343 142 // Scrolling text (description, between laptop and buttons) ··· 365 164 } 366 165 } 367 166 368 - // Tier buttons 369 167 const $btn = { ink }; 370 - tierBtns.forEach((btn, i) => { 371 - const t = tiers[i]; 372 - const tierAmt = currency === "dkk" ? t.dkk : t.amount; 373 - const isSelected = amount === tierAmt; 374 - btn.paint( 375 - $btn, 376 - isSelected ? [selFill, selOutline, selText] : [btnFill, btnOutline, btnText], 377 - [hoverFill, hoverOutline, hoverText], 378 - ); 379 - }); 380 168 381 169 // Buy button 382 170 if (buyBtn) { ··· 409 197 buyBtn.paint($btn, scheme, hover); 410 198 } 411 199 412 - // Currency toggle 413 - if (currencyBtn) { 414 - const curDefault = isDark 415 - ? [[12, 12, 14], [50, 50, 55], [80, 80, 85]] 416 - : [[245, 243, 240], [180, 180, 185], [100, 100, 105]]; 417 - const curHover = isDark 418 - ? [[12, 12, 14], [120, 120, 130], [200, 200, 210]] 419 - : [[245, 243, 240], [100, 100, 110], [40, 40, 50]]; 420 - currencyBtn.paint($btn, curDefault, curHover); 421 - } 422 200 } 423 201 424 202 function act({ event: e, screen, jump, sound, ui, api }) { ··· 428 206 setupButtons(ui, screen); 429 207 } 430 208 431 - tierBtns.forEach((btn, i) => { 432 - btn.act(e, { 433 - down: () => { 434 - sound?.synth({ type: "sine", tone: 500 + i * 100, duration: 0.03, volume: 0.2 }); 435 - }, 436 - push: () => { 437 - const t = tiers[i]; 438 - const newAmount = currency === "dkk" ? t.dkk : t.amount; 439 - if (newAmount !== amount) { 440 - amount = newAmount; 441 - sound?.synth({ type: "sine", tone: 600 + i * 150, duration: 0.06, volume: 0.3 }); 442 - tierBtns.forEach((tb, j) => tb.replaceLabel(tierText(j))); 443 - fetchCheckout(api); 444 - } 445 - }, 446 - }); 447 - }); 448 - 449 - currencyBtn?.act(e, { 450 - push: () => { 451 - currency = currency === "usd" ? "dkk" : "usd"; 452 - const tierIdx = tiers.findIndex( 453 - (t) => (currency === "usd" ? t.dkk : t.amount) === amount, 454 - ); 455 - if (tierIdx >= 0) { 456 - amount = currency === "dkk" ? tiers[tierIdx].dkk : tiers[tierIdx].amount; 457 - } else { 458 - amount = pricing[currency].suggested; 459 - } 460 - sound?.synth({ type: "sine", tone: 700, duration: 0.05, volume: 0.2 }); 461 - currencyBtn.reposition({ top: 8, right: 8, screen }, currency.toUpperCase()); 462 - tierBtns.forEach((tb, j) => tb.replaceLabel(tierText(j))); 463 - fetchCheckout(api); 464 - }, 465 - }); 466 - 467 209 buyBtn?.act(e, { 468 210 down: () => { 469 211 sound?.synth({ type: "sine", tone: 440, duration: 0.05, volume: 0.3 }); ··· 509 251 function meta() { 510 252 return { 511 253 title: "AC Blank", 512 - desc: "A surplus laptop running AC Native OS. Stable commands. Nothing extra. Like a blank tape waiting to be filled.", 254 + desc: "Buy a @jeffrey approved Thinkpad 11e Yoga Gen 6 (refurbished / used) pre-flashed with AC Native today! Comes decorated with recovery USB. (No USB C Charger Included)", 513 255 }; 514 256 } 515 257