From 9ecdd966c71f42132fdda722ab9d6dc27009bd3b Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Mon, 5 Oct 2026 10:12:29 +0700 Subject: [PATCH] Deck cost calculator: add a currency unit toggle (USD/EUR/GBP/sats) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A chip row at the top switches the whole sheet's unit: $ / € / £ / ₿ sats. Input labels follow the unit and results reformat (sats are shown rounded, no decimals). It relabels/reformats only — it does not convert values, since the app only has a BTC/USD rate (no fiat↔fiat or reliable fiat↔sats conversion); you enter amounts in the unit you think in. Choice persists with the rest. --- AGENTS.md | 6 ++- src/components/DeckCostCalculator.tsx | 61 ++++++++++++++++++++------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bb4d9c5..061653e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -819,7 +819,11 @@ session with **no other memory of this project**. So: calculator (2026-10-05):** the first in-app tool lives at `/diy/deck-cost` (`components/DeckCostCalculator.tsx`) — a self-contained (localStorage) model of labour, equipment amortisation, operations, materials, process time, packaging - and margin → selling price; the `deckCost` card links to it. Next: write our own + and margin → selling price; the `deckCost` card links to it. Includes a + **currency unit toggle** (USD/EUR/GBP/sats) that relabels/reformats the whole + sheet — it **does not convert** values (only a BTC/USD rate is available, so no + fiat↔fiat or reliable fiat↔sats conversion; enter amounts in the unit you + think in). 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 db87e34..245ddc4 100644 --- a/src/components/DeckCostCalculator.tsx +++ b/src/components/DeckCostCalculator.tsx @@ -3,6 +3,7 @@ import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { Plus, Trash2, RotateCcw } from 'lucide-react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { cn } from '@/lib/utils'; /** * Deck cost calculator — a self-contained tool for working out what a deck @@ -19,7 +20,12 @@ interface MaterialRow { id: string; name: string; orderCost: number; shipping: n interface ProcessRow { id: string; name: string; minutes: number; boards: number } interface PackageRow { id: string; name: string; minutesPerBoard: number; materialsPerBoard: number } +type CurrencyCode = 'USD' | 'EUR' | 'GBP' | 'sats'; + +const CURRENCY_SYMBOL: Record = { USD: '$', EUR: '€', GBP: '£', sats: '' }; + interface CalcState { + currency: CurrencyCode; hourlyRate: number; overheadFactor: number; boardsPerMonth: number; @@ -37,6 +43,7 @@ interface CalcState { const uid = () => Math.random().toString(36).slice(2, 9); const DEFAULT_CALC: CalcState = { + currency: 'USD', hourlyRate: 20, overheadFactor: 2.5, boardsPerMonth: 160, @@ -83,11 +90,16 @@ const DEFAULT_CALC: CalcState = { ], }; -const money = (n: number) => `$${(Number.isFinite(n) ? n : 0).toFixed(2)}`; - export function DeckCostCalculator() { const [s, setS] = useLocalStorage('skatespots:deck-cost', DEFAULT_CALC); + const cur = s.currency; + const unit = cur === 'sats' ? 'sats' : CURRENCY_SYMBOL[cur]; + const fmt = (n: number) => { + const v = Number.isFinite(n) ? n : 0; + return cur === 'sats' ? `${Math.round(v).toLocaleString()} sats` : `${CURRENCY_SYMBOL[cur]}${v.toFixed(2)}`; + }; + const r = useMemo(() => { const laborPerMin = (s.hourlyRate * s.overheadFactor) / 60; const equipment = s.equipment.reduce((a, e) => a + (e.boards > 0 ? e.cost / e.boards : 0), 0); @@ -130,12 +142,29 @@ export function DeckCostCalculator() { return (
+ {/* Currency — reformats the whole sheet (enter amounts in the unit you think in) */} +
+ Currency + {(['USD', 'EUR', 'GBP', 'sats'] as CurrencyCode[]).map((c) => ( + + ))} +
{/* Labor */}

Labor

@@ -156,7 +185,7 @@ export function DeckCostCalculator() {

Equipment

- + {s.equipment.map((e) => ( @@ -164,7 +193,7 @@ export function DeckCostCalculator() { - + ))} @@ -185,7 +214,7 @@ export function DeckCostCalculator() { -

Per board: {money(r.operations)}

+

Per board: {fmt(r.operations)}

{/* Materials */} @@ -193,7 +222,7 @@ export function DeckCostCalculator() {

Materials

EquipmentCost ($)Setup (h)# boardsPer board
EquipmentCost ({unit})Setup (h)# boardsPer board
{numInput(e.cost, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, cost: n } : x) }))} {numInput(e.setupHrs, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, setupHrs: n } : x) }))} {numInput(e.boards, (n) => patch({ equipment: s.equipment.map((x) => x.id === e.id ? { ...x, boards: n } : x) }))}{money(e.boards > 0 ? e.cost / e.boards : 0)}{fmt(e.boards > 0 ? e.cost / e.boards : 0)}
- + {s.materials.map((m) => { const per = m.boards > 0 ? (m.orderCost + m.shipping + (m.orderMin + m.unloadMin) * r.laborPerMin) / m.boards : 0; @@ -206,7 +235,7 @@ export function DeckCostCalculator() { - + ); @@ -234,7 +263,7 @@ export function DeckCostCalculator() { - + ); @@ -252,7 +281,7 @@ export function DeckCostCalculator() {

Packaging

MaterialOrder ($)Ship ($)# boardsOrder (m)Unload (m)Per board
MaterialOrder ({unit})Ship ({unit})# boardsOrder (m)Unload (m)Per board
{numInput(m.boards, (n) => up({ boards: n }))} {numInput(m.orderMin, (n) => up({ orderMin: n }))} {numInput(m.unloadMin, (n) => up({ unloadMin: n }))}{money(per)}{fmt(per)}
up({ name: e.target.value })} className="h-8 min-w-36" /> {numInput(p.minutes, (n) => up({ minutes: n }))} {numInput(p.boards, (n) => up({ boards: n }))}{money(per)}{fmt(per)}
- + {s.packaging.map((p) => { const per = p.materialsPerBoard + p.minutesPerBoard * r.laborPerMin; @@ -262,7 +291,7 @@ export function DeckCostCalculator() { - + ); @@ -281,18 +310,18 @@ export function DeckCostCalculator() {
-
Cost / board (no packaging)
{money(r.costNoPack)}
-
Cost / board (with packaging)
{money(r.costWithPack)}
+
Cost / board (no packaging)
{fmt(r.costNoPack)}
+
Cost / board (with packaging)
{fmt(r.costWithPack)}

Selling price · no packaging

-

{money(r.priceNoPack)}

+

{fmt(r.priceNoPack)}

Selling price · with packaging

-

{money(r.priceWithPack)}

+

{fmt(r.priceWithPack)}

Does not include shipping to the buyer.

PackagingTime/board (m)Materials/board ($)Per board
PackagingTime/board (m)Materials/board ({unit})Per board
up({ name: e.target.value })} className="h-8 min-w-36" /> {numInput(p.minutesPerBoard, (n) => up({ minutesPerBoard: n }))} {numInput(p.materialsPerBoard, (n) => up({ materialsPerBoard: n }))}{money(per)}{fmt(per)}