diff --git a/AGENTS.md b/AGENTS.md index 758e5eb..e44b3d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -821,7 +821,8 @@ session with **no other memory of this project**. So: labour, equipment amortisation, operations, materials, process time, packaging and margin → selling price; the `deckCost` card links to it. **Currency (2026-10-05):** a **searchable currency selector** (Popover + search) covering - ~sats + every fiat Coinbase reports, and switching **converts every money field** + **sats + BTC** (both Bitcoin units) and every **fiat** Coinbase reports (crypto + is filtered out via `FIAT_CODES`), and switching **converts every money field** using live BTC rates (`useBtcRates` → `getBtcRates`, one Coinbase `exchange-rates?currency=BTC` call — all currencies, `sats` = 1e8; unitless fields are untouched). Falls back to relabel-only when rates are unavailable. @@ -836,7 +837,10 @@ session with **no other memory of this project**. So: (lazy-loaded `jspdf`) — a formatted summary of every section + the selling price; the reason it's a real PDF and not print-to-PDF is that `window.print()` doesn't work in the Capacitor WebView. A secondary **JSON** - export remains (for Import/backup); Import reads JSON. Next: write our own + export remains (for Import/backup); Import reads JSON. The default **Materials** + list is a full deck-build checklist (veneer/edge tape/logs, glue/epoxy/filler, + mold release/vacuum film, sandpaper/saw+drill bits/masking tape, stain/primer/ + paint/clearcoat/grip). Next: write our own step-by-step guides (NIP-23 `30023` with `t=diy`) to flip the coming-soon cards live. - **App Shop** (`/app-shop`, `src/pages/AppShopPage.tsx`): SkateSpots merch, to be diff --git a/src/components/DeckCostCalculator.tsx b/src/components/DeckCostCalculator.tsx index 279c6e6..4518a9e 100644 --- a/src/components/DeckCostCalculator.tsx +++ b/src/components/DeckCostCalculator.tsx @@ -52,15 +52,35 @@ const FIAT_CODES = new Set([ 'ZAR', 'ZMW', ]); -/** How a currency is shown: `$1.00`, `CHF 1.00`, `1,000 sats`. */ +/** How a currency is shown: `$1.00`, `CHF 1.00`, `1,000 sats`, `0.00012345 BTC`. */ function formatMoney(v: number, cur: string): string { if (cur === 'sats') return `${Math.round(v).toLocaleString()} sats`; + if (cur === 'BTC') { + const str = v.toFixed(8).replace(/0+$/, '').replace(/\.$/, ''); + return `${str || '0'} BTC`; + } const s = CURRENCY_SYMBOLS[cur]; const n = v.toFixed(2); if (!s) return `${cur} ${n}`; return /[A-Za-z]/.test(s) ? `${s} ${n}` : `${s}${n}`; } +/** Bitcoin is one asset shown two ways — `sats` and `BTC` are both offered. */ +const BITCOIN_UNITS: Record = { sats: 1e8, BTC: 1 }; + +/** Units of a currency per 1 BTC (sats/BTC = fixed; fiats come from rates). */ +function ratePerBtc(cur: string, rates?: Record): number | undefined { + return BITCOIN_UNITS[cur] ?? rates?.[cur]; +} + +/** Chip/trigger label, e.g. `₿ sats`, `₿ BTC`, `$ USD`. */ +function currencyLabel(cur: string): string { + if (cur === 'sats') return '₿ sats'; + if (cur === 'BTC') return '₿ BTC'; + const s = CURRENCY_SYMBOLS[cur]; + return s ? `${s} ${cur}` : cur; +} + interface CalcState { currency: CurrencyCode; hourlyRate: number; @@ -101,13 +121,28 @@ const DEFAULT_CALC: CalcState = { { id: uid(), name: 'Other Equipment', cost: 0, setupHrs: 0, boards: 100000 }, ], materials: [ - { id: uid(), name: 'Veneer', orderCost: 1500, shipping: 500, boards: 200, orderMin: 15, unloadMin: 120 }, - { id: uid(), name: 'Glue', orderCost: 35, shipping: 0, boards: 10, orderMin: 20, unloadMin: 0 }, + // Wood & core + { id: uid(), name: 'Veneer (maple plies)', orderCost: 1500, shipping: 500, boards: 200, orderMin: 15, unloadMin: 120 }, + { id: uid(), name: 'Veneer tape / edge tape', orderCost: 0, shipping: 0, boards: 100, orderMin: 5, unloadMin: 0 }, + { id: uid(), name: 'Logs (if milling your own veneer)', orderCost: 80, shipping: 0, boards: 500, orderMin: 0, unloadMin: 0 }, + // Adhesives & fillers + { id: uid(), name: 'Wood glue / epoxy resin', orderCost: 35, shipping: 0, boards: 10, orderMin: 20, unloadMin: 0 }, + { id: uid(), name: 'Wood filler / putty', orderCost: 0, shipping: 0, boards: 50, orderMin: 0, unloadMin: 0 }, + // Press / mold consumables + { id: uid(), name: 'Mold release / wax paper', orderCost: 0, shipping: 0, boards: 100, orderMin: 0, unloadMin: 0 }, + { id: uid(), name: 'Vacuum bag / breather film', orderCost: 0, shipping: 0, boards: 200, orderMin: 0, unloadMin: 0 }, + // Shaping consumables { id: uid(), name: 'Sandpaper', orderCost: 30, shipping: 0, boards: 100, orderMin: 5, unloadMin: 15 }, - { id: uid(), name: 'Bits', orderCost: 50, shipping: 0, boards: 500, orderMin: 5, unloadMin: 0 }, - { id: uid(), name: 'Saw Blades', orderCost: 20, shipping: 0, boards: 500, orderMin: 5, unloadMin: 0 }, - { id: uid(), name: 'Logs', orderCost: 80, shipping: 0, boards: 500, orderMin: 0, unloadMin: 0 }, - { id: uid(), name: 'Other (inks, coatings, etc.)', orderCost: 0, shipping: 0, boards: 50, orderMin: 5, unloadMin: 0 }, + { id: uid(), name: 'Saw blades', orderCost: 20, shipping: 0, boards: 500, orderMin: 5, unloadMin: 0 }, + { id: uid(), name: 'Drill bits / router bits', orderCost: 50, shipping: 0, boards: 500, orderMin: 5, unloadMin: 0 }, + { id: uid(), name: 'Masking tape', orderCost: 0, shipping: 0, boards: 100, orderMin: 0, unloadMin: 0 }, + // Finish & graphics + { id: uid(), name: 'Stain / dye', orderCost: 0, shipping: 0, boards: 50, orderMin: 0, unloadMin: 0 }, + { id: uid(), name: 'Primer / base coat', orderCost: 0, shipping: 0, boards: 50, orderMin: 0, unloadMin: 0 }, + { id: uid(), name: 'Paint / screen-print ink', orderCost: 0, shipping: 0, boards: 50, orderMin: 0, unloadMin: 0 }, + { id: uid(), name: 'Clearcoat / lacquer', orderCost: 0, shipping: 0, boards: 50, orderMin: 0, unloadMin: 0 }, + { id: uid(), name: 'Grip tape', orderCost: 0, shipping: 0, boards: 50, orderMin: 0, unloadMin: 0 }, + { id: uid(), name: 'Other (hardware, coatings, etc.)', orderCost: 0, shipping: 0, boards: 50, orderMin: 5, unloadMin: 0 }, ], processes: [ { id: uid(), name: 'Glue & Press', minutes: 20, boards: 3 }, @@ -154,7 +189,7 @@ export function DeckCostCalculator() { const fiat = rates ? Object.keys(rates).filter((c) => FIAT_CODES.has(c)).sort() : ['USD', 'EUR', 'GBP']; - return ['sats', ...fiat]; + return ['sats', 'BTC', ...fiat]; }, [rates]); const filteredCurrencies = query ? currencies.filter((c) => c.toLowerCase().includes(query.toLowerCase())) @@ -167,11 +202,14 @@ export function DeckCostCalculator() { setQuery(''); setS((prev) => { if (to === prev.currency) return prev; - const rf = prev.currency === 'sats' ? 1e8 : rates?.[prev.currency]; - const rt = to === 'sats' ? 1e8 : rates?.[to]; + const rf = ratePerBtc(prev.currency, rates); + const rt = ratePerBtc(to, rates); if (!rf || !rt) return { ...prev, currency: to }; // no rate → relabel only const k = rt / rf; - const r2 = (v: number) => (to === 'sats' ? Math.round(v * k) : Math.round(v * k * 100) / 100); + const r2 = (v: number) => + to === 'sats' ? Math.round(v * k) + : to === 'BTC' ? Math.round(v * k * 1e8) / 1e8 + : Math.round(v * k * 100) / 100; return { ...prev, currency: to, @@ -276,7 +314,7 @@ export function DeckCostCalculator() { setText(17, true, 15); doc.text('SkateSpots — Deck cost calculator', M, y + 8); y += 26; setText(9, false, 120); doc.text( - `${new Date().toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'long', day: 'numeric' })} · ${cur === 'sats' ? 'sats (Bitcoin)' : cur}`, + `${new Date().toLocaleDateString(dateLocaleCode(), { year: 'numeric', month: 'long', day: 'numeric' })} · ${cur === 'sats' ? 'sats (Bitcoin)' : cur === 'BTC' ? 'BTC (Bitcoin)' : cur}`, M, y, ); y += 10; @@ -390,7 +428,7 @@ export function DeckCostCalculator() { type="button" className="flex min-h-0 items-center gap-2 rounded-full border border-[#ccff00]/40 bg-[#ccff00]/10 px-3 py-1.5 text-xs font-bold text-[#ccff00] hover:bg-[#ccff00]/15" > - {cur === 'sats' ? '₿ sats' : `${CURRENCY_SYMBOLS[cur] ?? ''} ${cur}`.trim()} + {currencyLabel(cur)} @@ -416,7 +454,7 @@ export function DeckCostCalculator() { c === cur ? 'bg-[#ccff00]/15 text-[#ccff00]' : 'text-gray-300 hover:bg-white/5', )} > - {c === 'sats' ? '₿ sats' : `${CURRENCY_SYMBOLS[c] ?? ''} ${c}`.trim()} + {currencyLabel(c)} {c === cur && } ))}