'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 (

{title}

{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 && (

Данные недоступны для выбранного рынка

)} )}
); }