'use client';
import { useState, useEffect, useCallback } from 'react';
import { motion } from 'framer-motion';
import { TrendingUp, TrendingDown, RefreshCw, Loader2, ArrowUpRight, ArrowDownRight, Activity, BarChart3 } from 'lucide-react';
import * as Tabs from '@radix-ui/react-tabs';
import { fetchMarkets, fetchHeatmap, fetchTopMovers } from '@/lib/api';
import type { FinanceMarket, HeatmapData, TopMovers, FinanceStock } from '@/lib/types';
const defaultMarkets: FinanceMarket[] = [
{ id: 'moex', name: 'MOEX', region: 'ru' },
{ id: 'crypto', name: 'Крипто', region: 'global' },
{ id: 'forex', name: 'Валюты', region: 'global' },
];
const timeRanges = [
{ id: '1d', label: '1Д' },
{ id: '1w', label: '1Н' },
{ id: '1m', label: '1М' },
{ id: '3m', label: '3М' },
{ id: '1y', label: '1Г' },
];
function formatPrice(price: number, market: string): string {
if (market === 'crypto' && price > 1000) {
return price.toLocaleString('ru-RU', { maximumFractionDigits: 0 });
}
return price.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function formatChange(change: number): string {
const prefix = change >= 0 ? '+' : '';
return `${prefix}${change.toFixed(2)}%`;
}
function StockRow({ stock, market, delay }: { stock: FinanceStock; market: string; delay: number }) {
const isPositive = stock.change >= 0;
return (
{stock.symbol.slice(0, 4)}
{stock.name}
{stock.sector && (
{stock.sector}
)}
{formatPrice(stock.price, market)}
{isPositive ? (
) : (
)}
{formatChange(stock.change)}
);
}
function MoversSection({ title, stocks, market, icon: Icon, color }: {
title: string;
stocks: FinanceStock[];
market: string;
icon: React.ElementType;
color: string;
}) {
if (!stocks || stocks.length === 0) return null;
return (
{stocks.slice(0, 5).map((stock, i) => (
))}
);
}
export default function FinancePage() {
const [markets, setMarkets] = useState(defaultMarkets);
const [currentMarket, setCurrentMarket] = useState('moex');
const [heatmapData, setHeatmapData] = useState(null);
const [moversData, setMoversData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [timeRange, setTimeRange] = useState('1d');
const loadMarkets = useCallback(async () => {
try {
const data = await fetchMarkets();
if (data && data.length > 0) {
setMarkets(data);
}
} catch (err) {
console.error('Failed to load markets:', err);
}
}, []);
const loadData = useCallback(async () => {
setIsLoading(true);
try {
const [heatmap, movers] = await Promise.all([
fetchHeatmap(currentMarket, timeRange).catch(() => null),
fetchTopMovers(currentMarket, 10).catch(() => null),
]);
setHeatmapData(heatmap);
setMoversData(movers);
} catch (err) {
console.error('Failed to load finance data:', err);
} finally {
setIsLoading(false);
}
}, [currentMarket, timeRange]);
useEffect(() => {
loadMarkets();
}, [loadMarkets]);
useEffect(() => {
loadData();
}, [loadData]);
return (
{/* Header */}
Финансы
Котировки и аналитика рынков
{/* Market Tabs */}
{markets.map((m) => (
{m.name}
))}
{/* Time Range */}
{timeRanges.map((range) => (
))}
{/* Content */}
{isLoading ? (
) : (
<>
{moversData && (
)}
{heatmapData && heatmapData.sectors && heatmapData.sectors.length > 0 && (
{heatmapData.sectors.map((sector) => (
{sector.name}
= 0 ? 'text-success' : 'text-error'}`}>
{formatChange(sector.change)}
{sector.tickers.slice(0, 3).map((stock, i) => (
))}
))}
)}
{!moversData && !heatmapData && (
Данные недоступны для выбранного рынка
)}
>
)}
);
}