feat(market): add CoinGecko market data panel with cached /api/market endpoint

This commit is contained in:
2026-03-09 17:55:24 +01:00
parent 3aaa6707c5
commit 1f86a5ead4
10 changed files with 521 additions and 12 deletions

View File

@@ -5,6 +5,14 @@ export interface CryptoRateResult {
updatedAt: string;
}
export interface CryptoMarketSnapshot {
priceUsd: number;
change24hPct: number | null;
marketCapUsd: number | null;
volume24hUsd: number | null;
updatedAt: string;
}
const COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3";
function buildCoinGeckoHeaders(): HeadersInit {
@@ -39,6 +47,50 @@ function buildCoinGeckoHeaders(): HeadersInit {
return headers;
}
export async function fetchCryptoMarketSnapshot(
providerId: string,
): Promise<CryptoMarketSnapshot> {
const url = `${COINGECKO_BASE_URL}/coins/markets?vs_currency=usd&ids=${encodeURIComponent(
providerId,
)}&price_change_percentage=24h`;
const response = await fetch(url, {
next: { revalidate: 60 },
headers: buildCoinGeckoHeaders(),
});
if (!response.ok) {
throw new Error(`Unable to load crypto market data (${response.status})`);
}
const payload = (await response.json()) as Array<{
current_price?: number;
market_cap?: number;
total_volume?: number;
price_change_percentage_24h?: number;
last_updated?: string;
}>;
const market = payload[0];
if (!market?.current_price || market.current_price <= 0) {
throw new Error("Crypto market data is unavailable");
}
return {
priceUsd: market.current_price,
change24hPct:
typeof market.price_change_percentage_24h === "number"
? market.price_change_percentage_24h
: null,
marketCapUsd:
typeof market.market_cap === "number" ? market.market_cap : null,
volume24hUsd:
typeof market.total_volume === "number" ? market.total_volume : null,
updatedAt: market.last_updated ?? new Date().toISOString(),
};
}
export async function fetchCryptoData(): Promise<CryptoRateResult> {
const ids = CRYPTO_ASSETS.map((asset) => asset.providerId).filter(
(id): id is string => Boolean(id)

9
lib/api/url.ts Normal file
View File

@@ -0,0 +1,9 @@
export function buildApiUrl(path: string): string {
const base = process.env.NEXT_PUBLIC_API_BASE_URL?.trim();
if (!base) {
return path;
}
return `${base.replace(/\/$/, "")}${path}`;
}