borsa / nextjs-app /src /components /StockDetail.tsx
bot
diag: add /api/diag/telegram to inspect bot credentials + reachability
fb9081e
'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'
// Lazy load the heavy chart component
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>
)
}