feat(market): add 24h crypto sparkline chart with CoinGecko history and resilient fallback

This commit is contained in:
2026-03-10 15:12:48 +01:00
parent afb000f16b
commit 4537ce8b9c
6 changed files with 217 additions and 2 deletions

View File

@@ -13,6 +13,11 @@ export interface CryptoMarketSnapshot {
updatedAt: string;
}
export interface CryptoPricePoint {
timestamp: string;
priceUsd: number;
}
const COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3";
function buildCoinGeckoHeaders(): HeadersInit {
@@ -91,6 +96,43 @@ export async function fetchCryptoMarketSnapshot(
};
}
export async function fetchCryptoPriceHistory24h(
providerId: string,
): Promise<CryptoPricePoint[]> {
const url = `${COINGECKO_BASE_URL}/coins/${encodeURIComponent(
providerId,
)}/market_chart?vs_currency=usd&days=1`;
const response = await fetch(url, {
next: { revalidate: 60 },
headers: buildCoinGeckoHeaders(),
});
if (!response.ok) {
throw new Error(`Unable to load crypto price history (${response.status})`);
}
const payload = (await response.json()) as {
prices?: Array<[number, number]>;
};
const points = (payload.prices ?? [])
.filter(
(entry): entry is [number, number] =>
Array.isArray(entry) &&
entry.length >= 2 &&
Number.isFinite(entry[0]) &&
Number.isFinite(entry[1]) &&
entry[1] > 0,
)
.map(([timestamp, priceUsd]) => ({
timestamp: new Date(timestamp).toISOString(),
priceUsd,
}));
return points;
}
export async function fetchCryptoData(): Promise<CryptoRateResult> {
const ids = CRYPTO_ASSETS.map((asset) => asset.providerId).filter(
(id): id is string => Boolean(id)

View File

@@ -7,6 +7,10 @@ export interface CryptoMarketResponse {
change24hPct: number | null;
marketCapUsd: number | null;
volume24hUsd: number | null;
priceHistory24h: Array<{
timestamp: string;
priceUsd: number;
}>;
updatedAt: string;
source: string;
}
@@ -18,6 +22,15 @@ const marketResponseSchema = z.object({
change24hPct: z.number().nullable(),
marketCapUsd: z.number().nullable(),
volume24hUsd: z.number().nullable(),
priceHistory24h: z
.array(
z.object({
timestamp: z.string(),
priceUsd: z.number().positive(),
}),
)
.optional()
.default([]),
updatedAt: z.string(),
source: z.string(),
});

View File

@@ -1,4 +1,7 @@
import { fetchCryptoMarketSnapshot } from "@/lib/api/crypto";
import {
fetchCryptoMarketSnapshot,
fetchCryptoPriceHistory24h,
} from "@/lib/api/crypto";
export interface CachedCryptoMarketSnapshot {
code: string;
@@ -7,6 +10,10 @@ export interface CachedCryptoMarketSnapshot {
change24hPct: number | null;
marketCapUsd: number | null;
volume24hUsd: number | null;
priceHistory24h: Array<{
timestamp: string;
priceUsd: number;
}>;
updatedAt: string;
source: "CoinGecko";
}
@@ -44,6 +51,13 @@ export async function getCryptoMarketWithCache(params: {
const requestPromise = (async () => {
const snapshot = await fetchCryptoMarketSnapshot(params.providerId);
let history: Array<{ timestamp: string; priceUsd: number }> = [];
try {
history = await fetchCryptoPriceHistory24h(params.providerId);
} catch {
history = [];
}
const normalized: CachedCryptoMarketSnapshot = {
code: params.code,
@@ -52,6 +66,7 @@ export async function getCryptoMarketWithCache(params: {
change24hPct: snapshot.change24hPct,
marketCapUsd: snapshot.marketCapUsd,
volume24hUsd: snapshot.volume24hUsd,
priceHistory24h: history,
updatedAt: snapshot.updatedAt,
source: "CoinGecko",
};