| 'use client' |
|
|
| import { useEffect, useState } from 'react' |
| import dynamic from 'next/dynamic' |
| import { TechnicalIndicators } from './TechnicalIndicators' |
| import { MLPredictions } from './MLPredictions' |
| import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils' |
| import { TrendingUp, TrendingDown, ArrowLeft, Star, BarChart3, Activity, Brain, ChevronDown, ChevronUp } from 'lucide-react' |
| import Link from 'next/link' |
| import { useAuth } from '@/contexts/AuthContext' |
| import { loadFavoritesAsync, toggleFavoriteAsync } from '@/lib/favorites' |
| import type { TechnicalIndicator } from '@/types' |
|
|
| |
| const AdvancedStockChart = dynamic(() => import('./AdvancedStockChart'), { |
| ssr: false, |
| loading: () => ( |
| <div className="bg-gray-900 rounded-2xl p-8 animate-pulse"> |
| <div className="h-8 bg-gray-800 rounded w-48 mb-6" /> |
| <div className="h-96 bg-gray-800 rounded-xl" /> |
| </div> |
| ), |
| }) |
|
|
| interface StockData { |
| stock: { symbol: string; name?: string; sector?: string } |
| latestPrice: { last_price?: number; change_pct?: number; day_high?: number; day_low?: number; volume?: number } | null |
| indicators: TechnicalIndicator | null |
| predictions: Record<string, unknown>[] |
| } |
|
|
| export function StockDetail({ symbol }: { symbol: string }) { |
| const { user } = useAuth() |
| const [data, setData] = useState<StockData | null>(null) |
| const [loading, setLoading] = useState(true) |
| const [isFav, setIsFav] = useState(false) |
| const [showOldIndicators, setShowOldIndicators] = useState(false) |
|
|
| const userKey = user?.id |
|
|
| useEffect(() => { |
| loadFavoritesAsync(userKey).then(favs => { |
| setIsFav(favs.some((f) => f.symbol === String(symbol).toUpperCase())) |
| }) |
| }, [symbol, userKey]) |
|
|
| useEffect(() => { |
| async function fetchStockData() { |
| try { |
| const response = await fetch(`/api/stocks/${symbol}`) |
| const stockData = await response.json() |
| setData(stockData) |
| } catch (error) { |
| console.error('Failed to fetch stock data:', error) |
| } finally { |
| setLoading(false) |
| } |
| } |
|
|
| fetchStockData() |
| }, [symbol]) |
|
|
| if (loading) { |
| return ( |
| <div className="min-h-screen bg-gray-950 p-6 space-y-6"> |
| <div className="h-32 bg-gray-900 rounded-xl animate-pulse" /> |
| <div className="h-96 bg-gray-900 rounded-xl animate-pulse" /> |
| <div className="h-48 bg-gray-900 rounded-xl animate-pulse" /> |
| </div> |
| ) |
| } |
|
|
| if (!data) { |
| return ( |
| <div className="min-h-screen bg-gray-950 p-6"> |
| <div className="bg-gray-900 rounded-xl border border-gray-800 p-12 text-center"> |
| <p className="text-gray-400 text-lg">Hisse bilgisi bulunamadı</p> |
| <Link href="/" className="text-blue-400 hover:text-blue-300 mt-4 inline-block"> |
| Ana sayfaya dön |
| </Link> |
| </div> |
| </div> |
| ) |
| } |
|
|
| const { stock, latestPrice } = data |
| const changePct = latestPrice?.change_pct || 0 |
| const isPositive = changePct > 0 |
|
|
| return ( |
| <div className="min-h-screen bg-gray-950 px-4 py-6 sm:px-6 lg:px-8"> |
| <div className="max-w-7xl mx-auto space-y-5"> |
| |
| {/* ─── Back Button ───────────────────── */} |
| <Link |
| href="/" |
| className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors text-sm" |
| > |
| <ArrowLeft className="h-4 w-4" /> |
| <span>Geri Dön</span> |
| </Link> |
| |
| {/* ─── Header Card ───────────────────── */} |
| <div className="bg-gray-900 rounded-xl border border-gray-800 p-5"> |
| <div className="flex items-start justify-between flex-wrap gap-4"> |
| <div className="flex items-center gap-4"> |
| <div className="w-14 h-14 rounded-xl bg-gradient-to-br from-blue-600 to-purple-700 flex items-center justify-center"> |
| <span className="text-white font-bold text-lg"> |
| {stock.symbol?.slice(0, 2) || '?'} |
| </span> |
| </div> |
| <div> |
| <div className="flex items-center gap-3"> |
| <h1 className="text-2xl font-bold text-white">{stock.symbol}</h1> |
| <button |
| type="button" |
| onClick={async () => { |
| const res = await toggleFavoriteAsync(stock.symbol, userKey) |
| setIsFav(res.nowFavorite) |
| }} |
| className="p-1.5 rounded-lg hover:bg-gray-800 transition-colors" |
| aria-label={isFav ? 'Favorilerden çıkar' : 'Favorilere ekle'} |
| title={isFav ? 'Favorilerden çıkar' : 'Favorilere ekle'} |
| > |
| <Star className={isFav ? 'h-5 w-5 text-amber-400 fill-amber-400' : 'h-5 w-5 text-gray-600'} /> |
| </button> |
| </div> |
| <p className="text-gray-400 text-sm">{stock.name}</p> |
| {stock.sector && ( |
| <span className="inline-block mt-1 px-2 py-0.5 bg-gray-800 text-gray-400 text-xs rounded"> |
| {stock.sector} |
| </span> |
| )} |
| </div> |
| </div> |
| |
| <div className="text-right"> |
| <div className="text-3xl font-bold text-white font-mono"> |
| {latestPrice?.last_price ? `₺${Number(latestPrice.last_price).toFixed(2)}` : '-'} |
| </div> |
| {latestPrice && ( |
| <div className={`flex items-center justify-end gap-1.5 mt-1 ${ |
| isPositive ? 'text-emerald-400' : 'text-red-400' |
| }`}> |
| {isPositive ? <TrendingUp className="h-4 w-4" /> : <TrendingDown className="h-4 w-4" />} |
| <span className="text-lg font-semibold font-mono"> |
| {isPositive ? '+' : ''}{changePct.toFixed(2)}% |
| </span> |
| </div> |
| )} |
| </div> |
| </div> |
| |
| {/* Key stats */} |
| <div className="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-5 pt-5 border-t border-gray-800"> |
| <DarkStatItem label="Açılış" value={latestPrice?.last_price} format="currency" /> |
| <DarkStatItem label="Yüksek" value={latestPrice?.day_high} format="currency" /> |
| <DarkStatItem label="Düşük" value={latestPrice?.day_low} format="currency" /> |
| <DarkStatItem label="Hacim" value={latestPrice?.volume} format="number" /> |
| </div> |
| </div> |
| |
| {/* ─── Advanced Chart & Analysis ──────── */} |
| <div> |
| <div className="flex items-center gap-2 mb-3"> |
| <BarChart3 className="h-5 w-5 text-blue-400" /> |
| <h2 className="text-lg font-semibold text-white">Teknik Analiz & Grafik</h2> |
| </div> |
| <AdvancedStockChart symbol={symbol} /> |
| </div> |
| |
| {/* ─── Classic Indicators (collapsible) ── */} |
| {data.indicators && ( |
| <div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden"> |
| <button |
| onClick={() => setShowOldIndicators((v) => !v)} |
| className="w-full flex items-center justify-between p-4 hover:bg-gray-800/50 transition-colors" |
| > |
| <div className="flex items-center gap-2"> |
| <Activity className="h-5 w-5 text-cyan-400" /> |
| <span className="text-white font-medium">Ham İndikatör Değerleri</span> |
| </div> |
| {showOldIndicators ? ( |
| <ChevronUp className="h-4 w-4 text-gray-400" /> |
| ) : ( |
| <ChevronDown className="h-4 w-4 text-gray-400" /> |
| )} |
| </button> |
| {showOldIndicators && ( |
| <div className="border-t border-gray-800 p-4"> |
| <TechnicalIndicators indicators={data.indicators} /> |
| </div> |
| )} |
| </div> |
| )} |
| |
| {/* ─── ML Predictions ────────────────── */} |
| {data.predictions && data.predictions.length > 0 && ( |
| <div> |
| <div className="flex items-center gap-2 mb-3"> |
| <Brain className="h-5 w-5 text-purple-400" /> |
| <h2 className="text-lg font-semibold text-white">ML Tahminleri</h2> |
| </div> |
| <MLPredictions predictions={data.predictions} /> |
| </div> |
| )} |
| </div> |
| </div> |
| ) |
| } |
|
|
| function DarkStatItem({ |
| label, |
| value, |
| format, |
| }: { |
| label: string |
| value: number | string | null | undefined |
| format: 'currency' | 'number' | 'percent' |
| }) { |
| const formatValue = () => { |
| if (value === null || value === undefined) return '-' |
| const num = typeof value === 'string' ? parseFloat(value) : value |
| if (isNaN(num)) return String(value) |
| switch (format) { |
| case 'currency': return formatCurrency(num) |
| case 'number': return formatNumber(num) |
| case 'percent': return formatPercent(num) |
| default: return num |
| } |
| } |
|
|
| return ( |
| <div className="bg-gray-800/50 rounded-lg p-3"> |
| <div className="text-xs text-gray-500">{label}</div> |
| <div className="text-sm font-semibold text-gray-200 mt-0.5 font-mono">{formatValue()}</div> |
| </div> |
| ) |
| } |
|
|