From c048e377958be0200e75602e1e6c557c565a4c67 Mon Sep 17 00:00:00 2001 From: Foohoo Date: Sun, 17 Aug 2025 12:44:29 +0100 Subject: [PATCH] add cost for the variants using USD to GBP conversion --- src/components/CardItem.tsx | 29 +++++++++++++++-- src/lib/currency.ts | 18 +++++++++++ src/lib/useExchangeRate.ts | 64 +++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 src/lib/currency.ts create mode 100644 src/lib/useExchangeRate.ts diff --git a/src/components/CardItem.tsx b/src/components/CardItem.tsx index 8534f19..fdd25a8 100644 --- a/src/components/CardItem.tsx +++ b/src/components/CardItem.tsx @@ -4,10 +4,33 @@ import React from 'react'; import type { TcgCard } from '@/types/pokemon'; import SetBadge from './SetBadge'; import type { VariantKey, VariantState } from '@/lib/checklist'; +import { useUsdGbpRate } from '@/lib/useExchangeRate'; export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; checked: VariantState; onToggle: (id:string, key: VariantKey)=>void }) { + const rate = useUsdGbpRate(); const hasHolo = !!(card.variants?.holofoil || (card.tcgplayer as any)?.prices?.holofoil); const hasReverse = !!(card.variants?.reverseHolofoil || (card.tcgplayer as any)?.prices?.reverseHolofoil); + const prices = (card as any).tcgplayer?.prices || {}; + const getPrice = (key: VariantKey): number | undefined => { + const map: Record = { + base: 'normal', + holofoil: 'holofoil', + reverseHolofoil: 'reverseHolofoil', + }; + const entry = prices[map[key]]; + if (!entry) return undefined; + const value = entry.market ?? entry.mid ?? entry.low ?? entry.high; + return typeof value === 'number' ? value : undefined; + }; + const fmtGBP = (usd?: number) => { + if (typeof usd !== 'number') return undefined; + const gbp = usd * rate; + try { + return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', maximumFractionDigits: 2 }).format(gbp); + } catch { + return `£${gbp.toFixed(2)}`; + } + }; return (
@@ -34,7 +57,7 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch className="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500" aria-label={`Mark ${card.name} #${card.number} (base) as collected`} /> - Base + Base{fmtGBP(getPrice('base')) ? ` — ${fmtGBP(getPrice('base'))}` : ''} {hasHolo && ( )} {hasReverse && ( @@ -57,7 +80,7 @@ export default function CardItem({ card, checked, onToggle }:{ card: TcgCard; ch className="h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500" aria-label={`Mark ${card.name} #${card.number} (reverse) as collected`} /> - Reverse + Reverse{fmtGBP(getPrice('reverseHolofoil')) ? ` — ${fmtGBP(getPrice('reverseHolofoil'))}` : ''} )}
diff --git a/src/lib/currency.ts b/src/lib/currency.ts new file mode 100644 index 0000000..5973033 --- /dev/null +++ b/src/lib/currency.ts @@ -0,0 +1,18 @@ +export function getUsdGbpRate(): number { + // Allow override via env var; must be NEXT_PUBLIC_* to be available on client + const raw = process.env.NEXT_PUBLIC_USD_GBP_RATE; + const parsed = raw ? Number(raw) : NaN; + // Fallback default rate; update as needed + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0.78; +} + +export function formatUsdToGbp(usd?: number): string | undefined { + if (typeof usd !== 'number') return undefined; + const gbp = usd * getUsdGbpRate(); + try { + return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', maximumFractionDigits: 2 }).format(gbp); + } catch { + // Fallback simple formatting + return `£${gbp.toFixed(2)}`; + } +} diff --git a/src/lib/useExchangeRate.ts b/src/lib/useExchangeRate.ts new file mode 100644 index 0000000..6ec90da --- /dev/null +++ b/src/lib/useExchangeRate.ts @@ -0,0 +1,64 @@ +"use client"; +import { useEffect, useState } from 'react'; + +const CACHE_KEY = 'fx-usd-gbp-v1'; +const TTL_MS = 24 * 60 * 60 * 1000; // 24 hours +const DEFAULT_RATE = 0.78; + +function readCache(): { rate: number; ts: number } | null { + if (typeof window === 'undefined') return null; + try { + const raw = window.localStorage.getItem(CACHE_KEY); + if (!raw) return null; + const obj = JSON.parse(raw) as { rate: number; ts: number }; + if (typeof obj?.rate !== 'number' || typeof obj?.ts !== 'number') return null; + if (Date.now() - obj.ts > TTL_MS) return null; + return obj; + } catch { + return null; + } +} + +function writeCache(rate: number) { + if (typeof window === 'undefined') return; + try { + window.localStorage.setItem(CACHE_KEY, JSON.stringify({ rate, ts: Date.now() })); + } catch { + // ignore + } +} + +async function fetchUsdGbpRate(): Promise { + try { + const res = await fetch('https://api.frankfurter.app/latest?from=USD&to=GBP'); + if (!res.ok) return null; + const json = await res.json(); + const rate = json?.rates?.GBP; + return typeof rate === 'number' ? rate : null; + } catch { + return null; + } +} + +export function useUsdGbpRate(): number { + const [rate, setRate] = useState(DEFAULT_RATE); + + useEffect(() => { + const cached = readCache(); + if (cached) { + setRate(cached.rate); + return; + } + let cancelled = false; + (async () => { + const live = await fetchUsdGbpRate(); + if (!cancelled && typeof live === 'number') { + setRate(live); + writeCache(live); + } + })(); + return () => { cancelled = true; }; + }, []); + + return rate; +}