onerozbey commited on
Commit ·
991ee9f
1
Parent(s): 6a126bf
Add Next.js components
Browse files- nextjs-app/src/components/AuthForm.tsx +138 -0
- nextjs-app/src/components/MLPredictionCard.tsx +212 -0
- nextjs-app/src/components/MLPredictions.tsx +114 -0
- nextjs-app/src/components/MarketOverview.tsx +119 -0
- nextjs-app/src/components/Navigation.tsx +200 -0
- nextjs-app/src/components/NewsCard.tsx +89 -0
- nextjs-app/src/components/PriceChart.tsx +132 -0
- nextjs-app/src/components/ProtectedRoute.tsx +31 -0
- nextjs-app/src/components/SentimentAnalyzer.tsx +145 -0
- nextjs-app/src/components/StockDetail.tsx +163 -0
- nextjs-app/src/components/StockList.tsx +176 -0
- nextjs-app/src/components/TechnicalIndicators.tsx +104 -0
- nextjs-app/src/components/TopMLPredictions.tsx +153 -0
- nextjs-app/src/components/TopMovers.tsx +109 -0
nextjs-app/src/components/AuthForm.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import { useAuth } from '@/contexts/AuthContext';
|
| 5 |
+
import { useRouter } from 'next/navigation';
|
| 6 |
+
import { Mail, Lock, User, AlertCircle, Loader2 } from 'lucide-react';
|
| 7 |
+
|
| 8 |
+
export default function AuthForm() {
|
| 9 |
+
const [isLogin, setIsLogin] = useState(true);
|
| 10 |
+
const [email, setEmail] = useState('');
|
| 11 |
+
const [password, setPassword] = useState('');
|
| 12 |
+
const [loading, setLoading] = useState(false);
|
| 13 |
+
const [error, setError] = useState('');
|
| 14 |
+
const { signIn, signUp } = useAuth();
|
| 15 |
+
const router = useRouter();
|
| 16 |
+
|
| 17 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 18 |
+
e.preventDefault();
|
| 19 |
+
setError('');
|
| 20 |
+
setLoading(true);
|
| 21 |
+
|
| 22 |
+
try {
|
| 23 |
+
const { error } = isLogin
|
| 24 |
+
? await signIn(email, password)
|
| 25 |
+
: await signUp(email, password);
|
| 26 |
+
|
| 27 |
+
if (error) {
|
| 28 |
+
setError(error.message);
|
| 29 |
+
} else {
|
| 30 |
+
if (!isLogin) {
|
| 31 |
+
setError('Kayıt başarılı! Lütfen email adresinizi doğrulayın.');
|
| 32 |
+
} else {
|
| 33 |
+
router.push('/portfolio');
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
} catch (err) {
|
| 37 |
+
setError('Bir hata oluştu. Lütfen tekrar deneyin.');
|
| 38 |
+
} finally {
|
| 39 |
+
setLoading(false);
|
| 40 |
+
}
|
| 41 |
+
};
|
| 42 |
+
|
| 43 |
+
return (
|
| 44 |
+
<div className="max-w-md mx-auto">
|
| 45 |
+
<div className="bg-white rounded-lg shadow-lg p-8">
|
| 46 |
+
<div className="text-center mb-8">
|
| 47 |
+
<User className="w-12 h-12 text-primary-600 mx-auto mb-4" />
|
| 48 |
+
<h2 className="text-2xl font-bold text-gray-900">
|
| 49 |
+
{isLogin ? 'Giriş Yap' : 'Kayıt Ol'}
|
| 50 |
+
</h2>
|
| 51 |
+
<p className="text-gray-600 mt-2">
|
| 52 |
+
{isLogin
|
| 53 |
+
? 'Portföyünüze erişmek için giriş yapın'
|
| 54 |
+
: 'Yeni bir hesap oluşturun'}
|
| 55 |
+
</p>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<form onSubmit={handleSubmit} className="space-y-6">
|
| 59 |
+
<div>
|
| 60 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 61 |
+
Email
|
| 62 |
+
</label>
|
| 63 |
+
<div className="relative">
|
| 64 |
+
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
| 65 |
+
<input
|
| 66 |
+
type="email"
|
| 67 |
+
value={email}
|
| 68 |
+
onChange={(e) => setEmail(e.target.value)}
|
| 69 |
+
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
| 70 |
+
placeholder="ornek@email.com"
|
| 71 |
+
required
|
| 72 |
+
/>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div>
|
| 77 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 78 |
+
Şifre
|
| 79 |
+
</label>
|
| 80 |
+
<div className="relative">
|
| 81 |
+
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
|
| 82 |
+
<input
|
| 83 |
+
type="password"
|
| 84 |
+
value={password}
|
| 85 |
+
onChange={(e) => setPassword(e.target.value)}
|
| 86 |
+
className="w-full pl-10 pr-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
| 87 |
+
placeholder="••••••••"
|
| 88 |
+
required
|
| 89 |
+
minLength={6}
|
| 90 |
+
/>
|
| 91 |
+
</div>
|
| 92 |
+
{!isLogin && (
|
| 93 |
+
<p className="text-xs text-gray-500 mt-1">En az 6 karakter</p>
|
| 94 |
+
)}
|
| 95 |
+
</div>
|
| 96 |
+
|
| 97 |
+
{error && (
|
| 98 |
+
<div className="flex items-start gap-2 p-4 bg-red-50 border border-red-200 rounded-lg">
|
| 99 |
+
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
|
| 100 |
+
<p className="text-sm text-red-600">{error}</p>
|
| 101 |
+
</div>
|
| 102 |
+
)}
|
| 103 |
+
|
| 104 |
+
<button
|
| 105 |
+
type="submit"
|
| 106 |
+
disabled={loading}
|
| 107 |
+
className="w-full bg-primary-600 text-white py-3 rounded-lg hover:bg-primary-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center justify-center gap-2 font-medium"
|
| 108 |
+
>
|
| 109 |
+
{loading ? (
|
| 110 |
+
<>
|
| 111 |
+
<Loader2 className="w-5 h-5 animate-spin" />
|
| 112 |
+
İşlem yapılıyor...
|
| 113 |
+
</>
|
| 114 |
+
) : isLogin ? (
|
| 115 |
+
'Giriş Yap'
|
| 116 |
+
) : (
|
| 117 |
+
'Kayıt Ol'
|
| 118 |
+
)}
|
| 119 |
+
</button>
|
| 120 |
+
</form>
|
| 121 |
+
|
| 122 |
+
<div className="mt-6 text-center">
|
| 123 |
+
<button
|
| 124 |
+
onClick={() => {
|
| 125 |
+
setIsLogin(!isLogin);
|
| 126 |
+
setError('');
|
| 127 |
+
}}
|
| 128 |
+
className="text-primary-600 hover:text-primary-800 font-medium"
|
| 129 |
+
>
|
| 130 |
+
{isLogin
|
| 131 |
+
? 'Hesabınız yok mu? Kayıt olun'
|
| 132 |
+
: 'Zaten hesabınız var mı? Giriş yapın'}
|
| 133 |
+
</button>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
</div>
|
| 137 |
+
);
|
| 138 |
+
}
|
nextjs-app/src/components/MLPredictionCard.tsx
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react';
|
| 4 |
+
import { TrendingUp, TrendingDown, Minus, Brain, Target, Activity } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
interface PredictionData {
|
| 7 |
+
symbol: string;
|
| 8 |
+
current_price: number;
|
| 9 |
+
predicted_price: number;
|
| 10 |
+
prediction_change: number;
|
| 11 |
+
confidence: number;
|
| 12 |
+
signal: 'BUY' | 'SELL' | 'HOLD';
|
| 13 |
+
factors: {
|
| 14 |
+
trend: string;
|
| 15 |
+
momentum: string;
|
| 16 |
+
volatility: string;
|
| 17 |
+
volume: string;
|
| 18 |
+
};
|
| 19 |
+
timestamp: string;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
interface MLPredictionCardProps {
|
| 23 |
+
symbol: string;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export default function MLPredictionCard({ symbol }: MLPredictionCardProps) {
|
| 27 |
+
const [prediction, setPrediction] = useState<PredictionData | null>(null);
|
| 28 |
+
const [loading, setLoading] = useState(true);
|
| 29 |
+
const [error, setError] = useState<string | null>(null);
|
| 30 |
+
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
fetchPrediction();
|
| 33 |
+
}, [symbol]);
|
| 34 |
+
|
| 35 |
+
const fetchPrediction = async () => {
|
| 36 |
+
try {
|
| 37 |
+
setLoading(true);
|
| 38 |
+
setError(null);
|
| 39 |
+
|
| 40 |
+
const response = await fetch(`/api/ml/predict?symbol=${symbol}`);
|
| 41 |
+
|
| 42 |
+
if (!response.ok) {
|
| 43 |
+
throw new Error('Failed to fetch prediction');
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
const data = await response.json();
|
| 47 |
+
setPrediction(data);
|
| 48 |
+
} catch (err) {
|
| 49 |
+
setError(err instanceof Error ? err.message : 'An error occurred');
|
| 50 |
+
} finally {
|
| 51 |
+
setLoading(false);
|
| 52 |
+
}
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
if (loading) {
|
| 56 |
+
return (
|
| 57 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 58 |
+
<div className="flex items-center gap-2 mb-4">
|
| 59 |
+
<Brain className="w-5 h-5 text-purple-600" />
|
| 60 |
+
<h2 className="text-lg font-semibold">ML Tahmin Analizi</h2>
|
| 61 |
+
</div>
|
| 62 |
+
<div className="animate-pulse space-y-4">
|
| 63 |
+
<div className="h-20 bg-gray-200 rounded"></div>
|
| 64 |
+
<div className="h-32 bg-gray-200 rounded"></div>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if (error || !prediction) {
|
| 71 |
+
return (
|
| 72 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 73 |
+
<div className="flex items-center gap-2 mb-4">
|
| 74 |
+
<Brain className="w-5 h-5 text-purple-600" />
|
| 75 |
+
<h2 className="text-lg font-semibold">ML Tahmin Analizi</h2>
|
| 76 |
+
</div>
|
| 77 |
+
<p className="text-red-600">Tahmin yüklenemedi: {error}</p>
|
| 78 |
+
</div>
|
| 79 |
+
);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
const getSignalColor = (signal: string) => {
|
| 83 |
+
switch (signal) {
|
| 84 |
+
case 'BUY':
|
| 85 |
+
return 'bg-green-100 text-green-800 border-green-300';
|
| 86 |
+
case 'SELL':
|
| 87 |
+
return 'bg-red-100 text-red-800 border-red-300';
|
| 88 |
+
default:
|
| 89 |
+
return 'bg-gray-100 text-gray-800 border-gray-300';
|
| 90 |
+
}
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
const getSignalIcon = (signal: string) => {
|
| 94 |
+
switch (signal) {
|
| 95 |
+
case 'BUY':
|
| 96 |
+
return <TrendingUp className="w-5 h-5" />;
|
| 97 |
+
case 'SELL':
|
| 98 |
+
return <TrendingDown className="w-5 h-5" />;
|
| 99 |
+
default:
|
| 100 |
+
return <Minus className="w-5 h-5" />;
|
| 101 |
+
}
|
| 102 |
+
};
|
| 103 |
+
|
| 104 |
+
const getConfidenceColor = (confidence: number) => {
|
| 105 |
+
if (confidence >= 80) return 'text-green-600';
|
| 106 |
+
if (confidence >= 60) return 'text-yellow-600';
|
| 107 |
+
return 'text-red-600';
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
return (
|
| 111 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 112 |
+
<div className="flex items-center justify-between mb-6">
|
| 113 |
+
<div className="flex items-center gap-2">
|
| 114 |
+
<Brain className="w-5 h-5 text-purple-600" />
|
| 115 |
+
<h2 className="text-lg font-semibold">ML Tahmin Analizi</h2>
|
| 116 |
+
</div>
|
| 117 |
+
<button
|
| 118 |
+
onClick={fetchPrediction}
|
| 119 |
+
className="text-sm text-purple-600 hover:text-purple-800"
|
| 120 |
+
>
|
| 121 |
+
Yenile
|
| 122 |
+
</button>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
{/* Main Prediction Card */}
|
| 126 |
+
<div className={`border-2 rounded-lg p-4 mb-6 ${getSignalColor(prediction.signal)}`}>
|
| 127 |
+
<div className="flex items-center justify-between mb-4">
|
| 128 |
+
<div className="flex items-center gap-2">
|
| 129 |
+
{getSignalIcon(prediction.signal)}
|
| 130 |
+
<span className="text-2xl font-bold">{prediction.signal}</span>
|
| 131 |
+
</div>
|
| 132 |
+
<div className="text-right">
|
| 133 |
+
<div className="text-sm text-gray-600">Güven Skoru</div>
|
| 134 |
+
<div className={`text-2xl font-bold ${getConfidenceColor(prediction.confidence)}`}>
|
| 135 |
+
{prediction.confidence}%
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-current border-opacity-20">
|
| 141 |
+
<div>
|
| 142 |
+
<div className="text-sm opacity-75">Mevcut Fiyat</div>
|
| 143 |
+
<div className="text-xl font-semibold">
|
| 144 |
+
₺{prediction.current_price.toFixed(2)}
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
<div>
|
| 148 |
+
<div className="text-sm opacity-75">Tahmini Fiyat (5 Gün)</div>
|
| 149 |
+
<div className="text-xl font-semibold">
|
| 150 |
+
₺{prediction.predicted_price.toFixed(2)}
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<div className="mt-4 pt-4 border-t border-current border-opacity-20">
|
| 156 |
+
<div className="flex items-center justify-between">
|
| 157 |
+
<span className="text-sm opacity-75">Beklenen Değişim</span>
|
| 158 |
+
<span className={`text-lg font-bold ${prediction.prediction_change >= 0 ? 'text-green-700' : 'text-red-700'}`}>
|
| 159 |
+
{prediction.prediction_change >= 0 ? '+' : ''}
|
| 160 |
+
{prediction.prediction_change.toFixed(2)}%
|
| 161 |
+
</span>
|
| 162 |
+
</div>
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
{/* Factors Analysis */}
|
| 167 |
+
<div className="space-y-3">
|
| 168 |
+
<h3 className="font-semibold text-gray-700 flex items-center gap-2">
|
| 169 |
+
<Target className="w-4 h-4" />
|
| 170 |
+
Analiz Faktörleri
|
| 171 |
+
</h3>
|
| 172 |
+
|
| 173 |
+
<div className="grid grid-cols-2 gap-3">
|
| 174 |
+
<div className="bg-gray-50 rounded-lg p-3">
|
| 175 |
+
<div className="text-xs text-gray-600 mb-1">Trend</div>
|
| 176 |
+
<div className="font-semibold text-sm">{prediction.factors.trend}</div>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<div className="bg-gray-50 rounded-lg p-3">
|
| 180 |
+
<div className="text-xs text-gray-600 mb-1">Momentum</div>
|
| 181 |
+
<div className="font-semibold text-sm">{prediction.factors.momentum}</div>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
<div className="bg-gray-50 rounded-lg p-3">
|
| 185 |
+
<div className="text-xs text-gray-600 mb-1">Volatilite</div>
|
| 186 |
+
<div className="font-semibold text-sm">{prediction.factors.volatility}</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div className="bg-gray-50 rounded-lg p-3">
|
| 190 |
+
<div className="text-xs text-gray-600 mb-1">Hacim Trendi</div>
|
| 191 |
+
<div className="font-semibold text-sm">{prediction.factors.volume}</div>
|
| 192 |
+
</div>
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
{/* Methodology Note */}
|
| 197 |
+
<div className="mt-6 pt-4 border-t border-gray-200">
|
| 198 |
+
<div className="flex items-start gap-2 text-xs text-gray-500">
|
| 199 |
+
<Activity className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
| 200 |
+
<p>
|
| 201 |
+
Bu tahmin, SMA, EMA, RSI, MACD, Bollinger Bands ve ATR gibi teknik göstergelerin
|
| 202 |
+
makine öğrenmesi algoritmaları ile analiz edilmesiyle oluşturulmuştur.
|
| 203 |
+
Yatırım tavsiyesi değildir.
|
| 204 |
+
</p>
|
| 205 |
+
</div>
|
| 206 |
+
<div className="text-xs text-gray-400 mt-2">
|
| 207 |
+
Son Güncelleme: {new Date(prediction.timestamp).toLocaleString('tr-TR')}
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
</div>
|
| 211 |
+
);
|
| 212 |
+
}
|
nextjs-app/src/components/MLPredictions.tsx
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { MLPrediction } from '@/types'
|
| 4 |
+
import { formatCurrency, formatPercent } from '@/lib/utils'
|
| 5 |
+
import { Brain, TrendingUp, TrendingDown } from 'lucide-react'
|
| 6 |
+
|
| 7 |
+
export function MLPredictions({ predictions }: { predictions: MLPrediction[] }) {
|
| 8 |
+
return (
|
| 9 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 10 |
+
<div className="flex items-center space-x-2 mb-6">
|
| 11 |
+
<Brain className="h-6 w-6 text-primary-600" />
|
| 12 |
+
<h2 className="text-xl font-bold text-gray-900">ML Tahminleri</h2>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
<div className="space-y-4">
|
| 16 |
+
{predictions.map((prediction) => (
|
| 17 |
+
<PredictionCard key={prediction.id} prediction={prediction} />
|
| 18 |
+
))}
|
| 19 |
+
</div>
|
| 20 |
+
</div>
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
function PredictionCard({ prediction }: { prediction: MLPrediction }) {
|
| 25 |
+
const isPositive = prediction.predicted_change_pct > 0
|
| 26 |
+
const confidencePercent = (prediction.confidence_score * 100).toFixed(1)
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
<div className="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
|
| 30 |
+
<div className="flex items-start justify-between mb-3">
|
| 31 |
+
<div>
|
| 32 |
+
<div className="flex items-center space-x-2">
|
| 33 |
+
<span className="text-sm font-semibold text-gray-900">
|
| 34 |
+
{prediction.model_type}
|
| 35 |
+
</span>
|
| 36 |
+
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-medium ${
|
| 37 |
+
prediction.confidence_score > 0.8
|
| 38 |
+
? 'bg-green-100 text-green-800'
|
| 39 |
+
: prediction.confidence_score > 0.6
|
| 40 |
+
? 'bg-yellow-100 text-yellow-800'
|
| 41 |
+
: 'bg-gray-100 text-gray-800'
|
| 42 |
+
}`}>
|
| 43 |
+
Güven: {confidencePercent}%
|
| 44 |
+
</span>
|
| 45 |
+
</div>
|
| 46 |
+
<div className="text-xs text-gray-500 mt-1">
|
| 47 |
+
Tahmin: {new Date(prediction.prediction_date).toLocaleDateString('tr-TR')} →
|
| 48 |
+
Hedef: {new Date(prediction.target_date).toLocaleDateString('tr-TR')}
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<div className="text-right">
|
| 53 |
+
<div className="text-2xl font-bold text-gray-900">
|
| 54 |
+
{formatCurrency(prediction.predicted_price)}
|
| 55 |
+
</div>
|
| 56 |
+
<div className={`flex items-center justify-end space-x-1 mt-1 ${
|
| 57 |
+
isPositive ? 'text-success' : 'text-danger'
|
| 58 |
+
}`}>
|
| 59 |
+
{isPositive ? (
|
| 60 |
+
<TrendingUp className="h-4 w-4" />
|
| 61 |
+
) : (
|
| 62 |
+
<TrendingDown className="h-4 w-4" />
|
| 63 |
+
)}
|
| 64 |
+
<span className="text-sm font-semibold">
|
| 65 |
+
{formatPercent(prediction.predicted_change_pct)}
|
| 66 |
+
</span>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
{prediction.lower_bound && prediction.upper_bound && (
|
| 72 |
+
<div className="bg-gray-50 rounded p-3 mt-3">
|
| 73 |
+
<div className="text-xs text-gray-600 mb-2">Tahmin Aralığı</div>
|
| 74 |
+
<div className="flex items-center justify-between text-sm">
|
| 75 |
+
<div>
|
| 76 |
+
<span className="text-gray-500">Alt: </span>
|
| 77 |
+
<span className="font-semibold text-gray-900">
|
| 78 |
+
{formatCurrency(prediction.lower_bound)}
|
| 79 |
+
</span>
|
| 80 |
+
</div>
|
| 81 |
+
<div>
|
| 82 |
+
<span className="text-gray-500">Üst: </span>
|
| 83 |
+
<span className="font-semibold text-gray-900">
|
| 84 |
+
{formatCurrency(prediction.upper_bound)}
|
| 85 |
+
</span>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</div>
|
| 89 |
+
)}
|
| 90 |
+
|
| 91 |
+
{prediction.actual_price && (
|
| 92 |
+
<div className="mt-3 pt-3 border-t border-gray-200">
|
| 93 |
+
<div className="flex items-center justify-between text-sm">
|
| 94 |
+
<span className="text-gray-600">Gerçekleşen:</span>
|
| 95 |
+
<div className="flex items-center space-x-4">
|
| 96 |
+
<span className="font-semibold text-gray-900">
|
| 97 |
+
{formatCurrency(prediction.actual_price)}
|
| 98 |
+
</span>
|
| 99 |
+
{prediction.prediction_error !== null && (
|
| 100 |
+
<span className={`text-xs ${
|
| 101 |
+
Math.abs(prediction.prediction_error) < 5
|
| 102 |
+
? 'text-green-600'
|
| 103 |
+
: 'text-red-600'
|
| 104 |
+
}`}>
|
| 105 |
+
Hata: {prediction.prediction_error.toFixed(2)}%
|
| 106 |
+
</span>
|
| 107 |
+
)}
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
)}
|
| 112 |
+
</div>
|
| 113 |
+
)
|
| 114 |
+
}
|
nextjs-app/src/components/MarketOverview.tsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import { TrendingUp, TrendingDown, Activity } from 'lucide-react'
|
| 5 |
+
|
| 6 |
+
interface MarketStats {
|
| 7 |
+
totalStocks: number
|
| 8 |
+
gainers: number
|
| 9 |
+
losers: number
|
| 10 |
+
unchanged: number
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function MarketOverview() {
|
| 14 |
+
const [stats, setStats] = useState<MarketStats | null>(null)
|
| 15 |
+
const [loading, setLoading] = useState(true)
|
| 16 |
+
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
async function fetchStats() {
|
| 19 |
+
try {
|
| 20 |
+
const response = await fetch('/api/stocks')
|
| 21 |
+
const data = await response.json()
|
| 22 |
+
const stocks = Array.isArray(data) ? data : []
|
| 23 |
+
|
| 24 |
+
const statsData = {
|
| 25 |
+
totalStocks: stocks.length,
|
| 26 |
+
gainers: stocks.filter((s: any) => s.change_pct > 0).length,
|
| 27 |
+
losers: stocks.filter((s: any) => s.change_pct < 0).length,
|
| 28 |
+
unchanged: stocks.filter((s: any) => s.change_pct === 0).length,
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
setStats(statsData)
|
| 32 |
+
} catch (error) {
|
| 33 |
+
console.error('Failed to fetch market stats:', error)
|
| 34 |
+
setStats({ totalStocks: 0, gainers: 0, losers: 0, unchanged: 0 })
|
| 35 |
+
} finally {
|
| 36 |
+
setLoading(false)
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
fetchStats()
|
| 41 |
+
}, [])
|
| 42 |
+
|
| 43 |
+
if (loading) {
|
| 44 |
+
return (
|
| 45 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
| 46 |
+
{[...Array(4)].map((_, i) => (
|
| 47 |
+
<div key={i} className="bg-white rounded-lg shadow p-6 animate-pulse">
|
| 48 |
+
<div className="h-4 bg-gray-200 rounded w-1/2 mb-4"></div>
|
| 49 |
+
<div className="h-8 bg-gray-200 rounded w-3/4"></div>
|
| 50 |
+
</div>
|
| 51 |
+
))}
|
| 52 |
+
</div>
|
| 53 |
+
)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if (!stats) return null
|
| 57 |
+
|
| 58 |
+
return (
|
| 59 |
+
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
| 60 |
+
<StatCard
|
| 61 |
+
title="Toplam Hisse"
|
| 62 |
+
value={stats.totalStocks}
|
| 63 |
+
icon={<Activity className="h-6 w-6" />}
|
| 64 |
+
color="blue"
|
| 65 |
+
/>
|
| 66 |
+
<StatCard
|
| 67 |
+
title="Yükselenler"
|
| 68 |
+
value={stats.gainers}
|
| 69 |
+
icon={<TrendingUp className="h-6 w-6" />}
|
| 70 |
+
color="green"
|
| 71 |
+
/>
|
| 72 |
+
<StatCard
|
| 73 |
+
title="Düşenler"
|
| 74 |
+
value={stats.losers}
|
| 75 |
+
icon={<TrendingDown className="h-6 w-6" />}
|
| 76 |
+
color="red"
|
| 77 |
+
/>
|
| 78 |
+
<StatCard
|
| 79 |
+
title="Değişmeyenler"
|
| 80 |
+
value={stats.unchanged}
|
| 81 |
+
icon={<Activity className="h-6 w-6" />}
|
| 82 |
+
color="gray"
|
| 83 |
+
/>
|
| 84 |
+
</div>
|
| 85 |
+
)
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
function StatCard({
|
| 89 |
+
title,
|
| 90 |
+
value,
|
| 91 |
+
icon,
|
| 92 |
+
color
|
| 93 |
+
}: {
|
| 94 |
+
title: string
|
| 95 |
+
value: number
|
| 96 |
+
icon: React.ReactNode
|
| 97 |
+
color: 'blue' | 'green' | 'red' | 'gray'
|
| 98 |
+
}) {
|
| 99 |
+
const colorClasses = {
|
| 100 |
+
blue: 'bg-blue-50 text-blue-600',
|
| 101 |
+
green: 'bg-green-50 text-green-600',
|
| 102 |
+
red: 'bg-red-50 text-red-600',
|
| 103 |
+
gray: 'bg-gray-50 text-gray-600',
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
return (
|
| 107 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 108 |
+
<div className="flex items-center justify-between">
|
| 109 |
+
<div>
|
| 110 |
+
<p className="text-sm font-medium text-gray-600">{title}</p>
|
| 111 |
+
<p className="text-3xl font-bold text-gray-900 mt-2">{value}</p>
|
| 112 |
+
</div>
|
| 113 |
+
<div className={`p-3 rounded-lg ${colorClasses[color]}`}>
|
| 114 |
+
{icon}
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
)
|
| 119 |
+
}
|
nextjs-app/src/components/Navigation.tsx
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import Link from 'next/link'
|
| 4 |
+
import { useState } from 'react'
|
| 5 |
+
import {
|
| 6 |
+
TrendingUp, BarChart3, Brain, Sparkles, Briefcase, LogIn, LogOut,
|
| 7 |
+
Activity, Search, Zap, Building2, History, Newspaper, Target, Menu, X
|
| 8 |
+
} from 'lucide-react'
|
| 9 |
+
import { useAuth } from '@/contexts/AuthContext'
|
| 10 |
+
|
| 11 |
+
export function Navigation() {
|
| 12 |
+
const { user, signOut, loading } = useAuth();
|
| 13 |
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
| 14 |
+
|
| 15 |
+
return (
|
| 16 |
+
<nav className="bg-white shadow-sm border-b sticky top-0 z-50">
|
| 17 |
+
<div className="container mx-auto px-4">
|
| 18 |
+
<div className="flex items-center justify-between h-16">
|
| 19 |
+
<Link href="/" className="flex items-center space-x-2">
|
| 20 |
+
<TrendingUp className="h-8 w-8 text-blue-600" />
|
| 21 |
+
<span className="text-xl font-bold text-gray-900">BIST Analiz</span>
|
| 22 |
+
</Link>
|
| 23 |
+
|
| 24 |
+
{/* Desktop Menu */}
|
| 25 |
+
<div className="hidden lg:flex items-center space-x-1">
|
| 26 |
+
<NavLink href="/" icon={<BarChart3 className="h-4 w-4" />}>
|
| 27 |
+
Piyasa
|
| 28 |
+
</NavLink>
|
| 29 |
+
<NavLink href="/bist100" icon={<Activity className="h-4 w-4" />}>
|
| 30 |
+
BIST100
|
| 31 |
+
</NavLink>
|
| 32 |
+
<NavLink href="/scanner" icon={<Search className="h-4 w-4" />}>
|
| 33 |
+
Tarayıcı
|
| 34 |
+
</NavLink>
|
| 35 |
+
<NavLink href="/ml-scan" icon={<Zap className="h-4 w-4" />}>
|
| 36 |
+
ML Tarama
|
| 37 |
+
</NavLink>
|
| 38 |
+
<NavLink href="/ai-analysis" icon={<Brain className="h-4 w-4" />}>
|
| 39 |
+
AI Analiz
|
| 40 |
+
</NavLink>
|
| 41 |
+
<NavLink href="/backtest" icon={<Target className="h-4 w-4" />}>
|
| 42 |
+
Backtest
|
| 43 |
+
</NavLink>
|
| 44 |
+
<NavLink href="/sentiment" icon={<Sparkles className="h-4 w-4" />}>
|
| 45 |
+
Duygu Analizi
|
| 46 |
+
</NavLink>
|
| 47 |
+
<NavLink href="/news" icon={<Newspaper className="h-4 w-4" />}>
|
| 48 |
+
Haberler
|
| 49 |
+
</NavLink>
|
| 50 |
+
<NavLink href="/profiles" icon={<Building2 className="h-4 w-4" />}>
|
| 51 |
+
Profiller
|
| 52 |
+
</NavLink>
|
| 53 |
+
|
| 54 |
+
{!loading && (
|
| 55 |
+
<>
|
| 56 |
+
{user ? (
|
| 57 |
+
<>
|
| 58 |
+
<NavLink href="/history" icon={<History className="h-4 w-4" />}>
|
| 59 |
+
Geçmiş
|
| 60 |
+
</NavLink>
|
| 61 |
+
<NavLink href="/portfolio" icon={<Briefcase className="h-4 w-4" />}>
|
| 62 |
+
Portföy
|
| 63 |
+
</NavLink>
|
| 64 |
+
<button
|
| 65 |
+
onClick={() => signOut()}
|
| 66 |
+
className="flex items-center space-x-2 px-3 py-2 rounded-md text-gray-700 hover:bg-gray-100 transition-colors text-sm"
|
| 67 |
+
>
|
| 68 |
+
<LogOut className="h-4 w-4" />
|
| 69 |
+
<span className="font-medium">Çıkış</span>
|
| 70 |
+
</button>
|
| 71 |
+
</>
|
| 72 |
+
) : (
|
| 73 |
+
<NavLink href="/login" icon={<LogIn className="h-4 w-4" />}>
|
| 74 |
+
Giriş
|
| 75 |
+
</NavLink>
|
| 76 |
+
)}
|
| 77 |
+
</>
|
| 78 |
+
)}
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
{/* Mobile Menu Button */}
|
| 82 |
+
<button
|
| 83 |
+
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
| 84 |
+
className="lg:hidden p-2 rounded-md text-gray-700 hover:bg-gray-100"
|
| 85 |
+
>
|
| 86 |
+
{mobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
|
| 87 |
+
</button>
|
| 88 |
+
</div>
|
| 89 |
+
|
| 90 |
+
{/* Mobile Menu */}
|
| 91 |
+
{mobileMenuOpen && (
|
| 92 |
+
<div className="lg:hidden py-4 space-y-2 border-t">
|
| 93 |
+
<MobileNavLink href="/" icon={<BarChart3 className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 94 |
+
Piyasa
|
| 95 |
+
</MobileNavLink>
|
| 96 |
+
<MobileNavLink href="/bist100" icon={<Activity className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 97 |
+
BIST100
|
| 98 |
+
</MobileNavLink>
|
| 99 |
+
<MobileNavLink href="/scanner" icon={<Search className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 100 |
+
Teknik Tarayıcı
|
| 101 |
+
</MobileNavLink>
|
| 102 |
+
<MobileNavLink href="/ml-scan" icon={<Zap className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 103 |
+
ML Toplu Tarama
|
| 104 |
+
</MobileNavLink>
|
| 105 |
+
<MobileNavLink href="/ai-analysis" icon={<Brain className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 106 |
+
AI Analiz
|
| 107 |
+
</MobileNavLink>
|
| 108 |
+
<MobileNavLink href="/backtest" icon={<Target className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 109 |
+
ML Backtest
|
| 110 |
+
</MobileNavLink>
|
| 111 |
+
<MobileNavLink href="/sentiment" icon={<Sparkles className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 112 |
+
Duygu Analizi
|
| 113 |
+
</MobileNavLink>
|
| 114 |
+
<MobileNavLink href="/news" icon={<Newspaper className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 115 |
+
Haberler
|
| 116 |
+
</MobileNavLink>
|
| 117 |
+
<MobileNavLink href="/profiles" icon={<Building2 className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 118 |
+
Hisse Profilleri
|
| 119 |
+
</MobileNavLink>
|
| 120 |
+
|
| 121 |
+
{!loading && user && (
|
| 122 |
+
<>
|
| 123 |
+
<MobileNavLink href="/history" icon={<History className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 124 |
+
Analiz Geçmişi
|
| 125 |
+
</MobileNavLink>
|
| 126 |
+
<MobileNavLink href="/portfolio" icon={<Briefcase className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 127 |
+
Portföyüm
|
| 128 |
+
</MobileNavLink>
|
| 129 |
+
</>
|
| 130 |
+
)}
|
| 131 |
+
|
| 132 |
+
{!loading && (
|
| 133 |
+
<div className="pt-2 border-t">
|
| 134 |
+
{user ? (
|
| 135 |
+
<button
|
| 136 |
+
onClick={() => {
|
| 137 |
+
signOut();
|
| 138 |
+
setMobileMenuOpen(false);
|
| 139 |
+
}}
|
| 140 |
+
className="w-full flex items-center space-x-3 px-4 py-3 rounded-md text-gray-700 hover:bg-gray-100 transition-colors"
|
| 141 |
+
>
|
| 142 |
+
<LogOut className="h-5 w-5" />
|
| 143 |
+
<span className="font-medium">Çıkış Yap</span>
|
| 144 |
+
</button>
|
| 145 |
+
) : (
|
| 146 |
+
<MobileNavLink href="/login" icon={<LogIn className="h-5 w-5" />} onClick={() => setMobileMenuOpen(false)}>
|
| 147 |
+
Giriş Yap
|
| 148 |
+
</MobileNavLink>
|
| 149 |
+
)}
|
| 150 |
+
</div>
|
| 151 |
+
)}
|
| 152 |
+
</div>
|
| 153 |
+
)}
|
| 154 |
+
</div>
|
| 155 |
+
</nav>
|
| 156 |
+
)
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function NavLink({
|
| 160 |
+
href,
|
| 161 |
+
icon,
|
| 162 |
+
children
|
| 163 |
+
}: {
|
| 164 |
+
href: string
|
| 165 |
+
icon: React.ReactNode
|
| 166 |
+
children: React.ReactNode
|
| 167 |
+
}) {
|
| 168 |
+
return (
|
| 169 |
+
<Link
|
| 170 |
+
href={href}
|
| 171 |
+
className="flex items-center space-x-1.5 px-3 py-2 rounded-md text-gray-700 hover:bg-gray-100 hover:text-blue-600 transition-colors text-sm"
|
| 172 |
+
>
|
| 173 |
+
{icon}
|
| 174 |
+
<span className="font-medium">{children}</span>
|
| 175 |
+
</Link>
|
| 176 |
+
)
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
function MobileNavLink({
|
| 180 |
+
href,
|
| 181 |
+
icon,
|
| 182 |
+
children,
|
| 183 |
+
onClick
|
| 184 |
+
}: {
|
| 185 |
+
href: string
|
| 186 |
+
icon: React.ReactNode
|
| 187 |
+
children: React.ReactNode
|
| 188 |
+
onClick?: () => void
|
| 189 |
+
}) {
|
| 190 |
+
return (
|
| 191 |
+
<Link
|
| 192 |
+
href={href}
|
| 193 |
+
onClick={onClick}
|
| 194 |
+
className="flex items-center space-x-3 px-4 py-3 rounded-md text-gray-700 hover:bg-gray-100 hover:text-blue-600 transition-colors"
|
| 195 |
+
>
|
| 196 |
+
{icon}
|
| 197 |
+
<span className="font-medium">{children}</span>
|
| 198 |
+
</Link>
|
| 199 |
+
)
|
| 200 |
+
}
|
nextjs-app/src/components/NewsCard.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { ExternalLink, TrendingUp, TrendingDown, Minus } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
interface NewsItem {
|
| 6 |
+
id: number;
|
| 7 |
+
symbol: string;
|
| 8 |
+
title: string;
|
| 9 |
+
content: string;
|
| 10 |
+
source: string;
|
| 11 |
+
published_at: string;
|
| 12 |
+
url: string;
|
| 13 |
+
sentiment: 'positive' | 'negative' | 'neutral';
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export default function NewsCard({ news }: { news: NewsItem }) {
|
| 17 |
+
const getSentimentIcon = (sentiment: string) => {
|
| 18 |
+
switch (sentiment) {
|
| 19 |
+
case 'positive':
|
| 20 |
+
return <TrendingUp className="w-4 h-4 text-green-600" />;
|
| 21 |
+
case 'negative':
|
| 22 |
+
return <TrendingDown className="w-4 h-4 text-red-600" />;
|
| 23 |
+
default:
|
| 24 |
+
return <Minus className="w-4 h-4 text-gray-600" />;
|
| 25 |
+
}
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
const getSentimentColor = (sentiment: string) => {
|
| 29 |
+
switch (sentiment) {
|
| 30 |
+
case 'positive':
|
| 31 |
+
return 'bg-green-50 border-green-200';
|
| 32 |
+
case 'negative':
|
| 33 |
+
return 'bg-red-50 border-red-200';
|
| 34 |
+
default:
|
| 35 |
+
return 'bg-gray-50 border-gray-200';
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
const formatDate = (dateString: string) => {
|
| 40 |
+
const date = new Date(dateString);
|
| 41 |
+
const now = new Date();
|
| 42 |
+
const diffMs = now.getTime() - date.getTime();
|
| 43 |
+
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
| 44 |
+
const diffDays = Math.floor(diffHours / 24);
|
| 45 |
+
|
| 46 |
+
if (diffHours < 1) {
|
| 47 |
+
const diffMins = Math.floor(diffMs / (1000 * 60));
|
| 48 |
+
return `${diffMins} dakika önce`;
|
| 49 |
+
} else if (diffHours < 24) {
|
| 50 |
+
return `${diffHours} saat önce`;
|
| 51 |
+
} else if (diffDays < 7) {
|
| 52 |
+
return `${diffDays} gün önce`;
|
| 53 |
+
} else {
|
| 54 |
+
return date.toLocaleDateString('tr-TR');
|
| 55 |
+
}
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
return (
|
| 59 |
+
<div
|
| 60 |
+
className={`p-4 rounded-lg border ${getSentimentColor(
|
| 61 |
+
news.sentiment
|
| 62 |
+
)} hover:shadow-md transition-shadow`}
|
| 63 |
+
>
|
| 64 |
+
<div className="flex items-start justify-between mb-2">
|
| 65 |
+
<div className="flex items-center gap-2">
|
| 66 |
+
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs font-semibold rounded">
|
| 67 |
+
{news.symbol}
|
| 68 |
+
</span>
|
| 69 |
+
<span className="text-xs text-gray-500">{news.source}</span>
|
| 70 |
+
{getSentimentIcon(news.sentiment)}
|
| 71 |
+
</div>
|
| 72 |
+
<span className="text-xs text-gray-500">{formatDate(news.published_at)}</span>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<h3 className="font-semibold text-gray-900 mb-2">{news.title}</h3>
|
| 76 |
+
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{news.content}</p>
|
| 77 |
+
|
| 78 |
+
<a
|
| 79 |
+
href={news.url}
|
| 80 |
+
target="_blank"
|
| 81 |
+
rel="noopener noreferrer"
|
| 82 |
+
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800"
|
| 83 |
+
>
|
| 84 |
+
Detayları Gör
|
| 85 |
+
<ExternalLink className="w-3 h-3" />
|
| 86 |
+
</a>
|
| 87 |
+
</div>
|
| 88 |
+
);
|
| 89 |
+
}
|
nextjs-app/src/components/PriceChart.tsx
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import {
|
| 5 |
+
LineChart,
|
| 6 |
+
Line,
|
| 7 |
+
XAxis,
|
| 8 |
+
YAxis,
|
| 9 |
+
CartesianGrid,
|
| 10 |
+
Tooltip,
|
| 11 |
+
Legend,
|
| 12 |
+
ResponsiveContainer,
|
| 13 |
+
} from 'recharts'
|
| 14 |
+
import { formatCurrency } from '@/lib/utils'
|
| 15 |
+
|
| 16 |
+
interface PriceData {
|
| 17 |
+
date: string
|
| 18 |
+
close: number
|
| 19 |
+
high: number
|
| 20 |
+
low: number
|
| 21 |
+
volume: number
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export function PriceChart({ symbol }: { symbol: string }) {
|
| 25 |
+
const [data, setData] = useState<PriceData[]>([])
|
| 26 |
+
const [loading, setLoading] = useState(true)
|
| 27 |
+
const [period, setPeriod] = useState(30)
|
| 28 |
+
|
| 29 |
+
useEffect(() => {
|
| 30 |
+
async function fetchPrices() {
|
| 31 |
+
try {
|
| 32 |
+
const response = await fetch(`/api/stocks/${symbol}/prices?days=${period}`)
|
| 33 |
+
const prices = await response.json()
|
| 34 |
+
setData(prices)
|
| 35 |
+
} catch (error) {
|
| 36 |
+
console.error('Failed to fetch prices:', error)
|
| 37 |
+
} finally {
|
| 38 |
+
setLoading(false)
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
fetchPrices()
|
| 43 |
+
}, [symbol, period])
|
| 44 |
+
|
| 45 |
+
if (loading) {
|
| 46 |
+
return (
|
| 47 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 48 |
+
<div className="h-96 bg-gray-100 rounded animate-pulse"></div>
|
| 49 |
+
</div>
|
| 50 |
+
)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
return (
|
| 54 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 55 |
+
<div className="flex items-center justify-between mb-6">
|
| 56 |
+
<h2 className="text-xl font-bold text-gray-900">Fiyat Grafiği</h2>
|
| 57 |
+
<div className="flex space-x-2">
|
| 58 |
+
{[7, 30, 90, 180].map((days) => (
|
| 59 |
+
<button
|
| 60 |
+
key={days}
|
| 61 |
+
onClick={() => setPeriod(days)}
|
| 62 |
+
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
| 63 |
+
period === days
|
| 64 |
+
? 'bg-primary-600 text-white'
|
| 65 |
+
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
| 66 |
+
}`}
|
| 67 |
+
>
|
| 68 |
+
{days}G
|
| 69 |
+
</button>
|
| 70 |
+
))}
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
{data.length > 0 ? (
|
| 75 |
+
<ResponsiveContainer width="100%" height={400}>
|
| 76 |
+
<LineChart data={data}>
|
| 77 |
+
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
| 78 |
+
<XAxis
|
| 79 |
+
dataKey="date"
|
| 80 |
+
stroke="#6b7280"
|
| 81 |
+
tick={{ fontSize: 12 }}
|
| 82 |
+
/>
|
| 83 |
+
<YAxis
|
| 84 |
+
stroke="#6b7280"
|
| 85 |
+
tick={{ fontSize: 12 }}
|
| 86 |
+
tickFormatter={(value) => `₺${value}`}
|
| 87 |
+
/>
|
| 88 |
+
<Tooltip
|
| 89 |
+
contentStyle={{
|
| 90 |
+
backgroundColor: '#fff',
|
| 91 |
+
border: '1px solid #e5e7eb',
|
| 92 |
+
borderRadius: '8px',
|
| 93 |
+
}}
|
| 94 |
+
formatter={(value: any) => formatCurrency(value)}
|
| 95 |
+
/>
|
| 96 |
+
<Legend />
|
| 97 |
+
<Line
|
| 98 |
+
type="monotone"
|
| 99 |
+
dataKey="close"
|
| 100 |
+
stroke="#0ea5e9"
|
| 101 |
+
strokeWidth={2}
|
| 102 |
+
dot={false}
|
| 103 |
+
name="Kapanış"
|
| 104 |
+
/>
|
| 105 |
+
<Line
|
| 106 |
+
type="monotone"
|
| 107 |
+
dataKey="high"
|
| 108 |
+
stroke="#10b981"
|
| 109 |
+
strokeWidth={1}
|
| 110 |
+
dot={false}
|
| 111 |
+
strokeDasharray="5 5"
|
| 112 |
+
name="Yüksek"
|
| 113 |
+
/>
|
| 114 |
+
<Line
|
| 115 |
+
type="monotone"
|
| 116 |
+
dataKey="low"
|
| 117 |
+
stroke="#ef4444"
|
| 118 |
+
strokeWidth={1}
|
| 119 |
+
dot={false}
|
| 120 |
+
strokeDasharray="5 5"
|
| 121 |
+
name="Düşük"
|
| 122 |
+
/>
|
| 123 |
+
</LineChart>
|
| 124 |
+
</ResponsiveContainer>
|
| 125 |
+
) : (
|
| 126 |
+
<div className="h-96 flex items-center justify-center text-gray-500">
|
| 127 |
+
Fiyat verisi bulunamadı
|
| 128 |
+
</div>
|
| 129 |
+
)}
|
| 130 |
+
</div>
|
| 131 |
+
)
|
| 132 |
+
}
|
nextjs-app/src/components/ProtectedRoute.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect } from 'react';
|
| 4 |
+
import { useAuth } from '@/contexts/AuthContext';
|
| 5 |
+
import { useRouter } from 'next/navigation';
|
| 6 |
+
import { Loader2 } from 'lucide-react';
|
| 7 |
+
|
| 8 |
+
export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
| 9 |
+
const { user, loading } = useAuth();
|
| 10 |
+
const router = useRouter();
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
if (!loading && !user) {
|
| 14 |
+
router.push('/login');
|
| 15 |
+
}
|
| 16 |
+
}, [user, loading, router]);
|
| 17 |
+
|
| 18 |
+
if (loading) {
|
| 19 |
+
return (
|
| 20 |
+
<div className="flex items-center justify-center min-h-screen">
|
| 21 |
+
<Loader2 className="w-8 h-8 animate-spin text-primary-600" />
|
| 22 |
+
</div>
|
| 23 |
+
);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
if (!user) {
|
| 27 |
+
return null;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
return <>{children}</>;
|
| 31 |
+
}
|
nextjs-app/src/components/SentimentAnalyzer.tsx
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react';
|
| 4 |
+
import { MessageSquare, TrendingUp, TrendingDown, Minus, Sparkles } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
interface SentimentResult {
|
| 7 |
+
label: 'POSITIVE' | 'NEGATIVE' | 'NEUTRAL';
|
| 8 |
+
score: number;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
interface SentimentData {
|
| 12 |
+
text: string;
|
| 13 |
+
sentiment: SentimentResult;
|
| 14 |
+
timestamp: string;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export default function SentimentAnalyzer() {
|
| 18 |
+
const [text, setText] = useState('');
|
| 19 |
+
const [result, setResult] = useState<SentimentData | null>(null);
|
| 20 |
+
const [loading, setLoading] = useState(false);
|
| 21 |
+
|
| 22 |
+
const analyzeSentiment = async () => {
|
| 23 |
+
if (!text.trim()) return;
|
| 24 |
+
|
| 25 |
+
setLoading(true);
|
| 26 |
+
try {
|
| 27 |
+
const response = await fetch('/api/sentiment', {
|
| 28 |
+
method: 'POST',
|
| 29 |
+
headers: { 'Content-Type': 'application/json' },
|
| 30 |
+
body: JSON.stringify({ text })
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
const data = await response.json();
|
| 34 |
+
setResult(data);
|
| 35 |
+
} catch (error) {
|
| 36 |
+
console.error('Sentiment analysis failed:', error);
|
| 37 |
+
} finally {
|
| 38 |
+
setLoading(false);
|
| 39 |
+
}
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const getSentimentColor = (label: string) => {
|
| 43 |
+
switch (label) {
|
| 44 |
+
case 'POSITIVE':
|
| 45 |
+
return 'text-green-600 bg-green-50 border-green-200';
|
| 46 |
+
case 'NEGATIVE':
|
| 47 |
+
return 'text-red-600 bg-red-50 border-red-200';
|
| 48 |
+
default:
|
| 49 |
+
return 'text-gray-600 bg-gray-50 border-gray-200';
|
| 50 |
+
}
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
const getSentimentIcon = (label: string) => {
|
| 54 |
+
switch (label) {
|
| 55 |
+
case 'POSITIVE':
|
| 56 |
+
return <TrendingUp className="w-5 h-5" />;
|
| 57 |
+
case 'NEGATIVE':
|
| 58 |
+
return <TrendingDown className="w-5 h-5" />;
|
| 59 |
+
default:
|
| 60 |
+
return <Minus className="w-5 h-5" />;
|
| 61 |
+
}
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
const getSentimentLabel = (label: string) => {
|
| 65 |
+
switch (label) {
|
| 66 |
+
case 'POSITIVE':
|
| 67 |
+
return 'Pozitif';
|
| 68 |
+
case 'NEGATIVE':
|
| 69 |
+
return 'Negatif';
|
| 70 |
+
default:
|
| 71 |
+
return 'Nötr';
|
| 72 |
+
}
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
return (
|
| 76 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 77 |
+
<div className="flex items-center gap-2 mb-6">
|
| 78 |
+
<Sparkles className="w-5 h-5 text-indigo-600" />
|
| 79 |
+
<h2 className="text-lg font-semibold">Haber Duygu Analizi</h2>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<div className="space-y-4">
|
| 83 |
+
<div>
|
| 84 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 85 |
+
Haber Metni (Türkçe)
|
| 86 |
+
</label>
|
| 87 |
+
<textarea
|
| 88 |
+
value={text}
|
| 89 |
+
onChange={(e) => setText(e.target.value)}
|
| 90 |
+
placeholder="Örnek: THYAO hisseleri bugün %5 yükseliş gösterdi ve yeni rekor kırdı..."
|
| 91 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none"
|
| 92 |
+
rows={4}
|
| 93 |
+
/>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<button
|
| 97 |
+
onClick={analyzeSentiment}
|
| 98 |
+
disabled={loading || !text.trim()}
|
| 99 |
+
className="w-full bg-indigo-600 text-white py-3 rounded-lg hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
| 100 |
+
>
|
| 101 |
+
<MessageSquare className="w-5 h-5" />
|
| 102 |
+
{loading ? 'Analiz Ediliyor...' : 'Duygu Analizi Yap'}
|
| 103 |
+
</button>
|
| 104 |
+
|
| 105 |
+
{result && (
|
| 106 |
+
<div className={`mt-6 p-4 border-2 rounded-lg ${getSentimentColor(result.sentiment.label)}`}>
|
| 107 |
+
<div className="flex items-center justify-between mb-4">
|
| 108 |
+
<div className="flex items-center gap-2">
|
| 109 |
+
{getSentimentIcon(result.sentiment.label)}
|
| 110 |
+
<span className="text-xl font-bold">
|
| 111 |
+
{getSentimentLabel(result.sentiment.label)}
|
| 112 |
+
</span>
|
| 113 |
+
</div>
|
| 114 |
+
<div className="text-right">
|
| 115 |
+
<div className="text-sm opacity-75">Güven</div>
|
| 116 |
+
<div className="text-2xl font-bold">
|
| 117 |
+
{Math.round(result.sentiment.score * 100)}%
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
|
| 122 |
+
<div className="mt-4 pt-4 border-t border-current border-opacity-20">
|
| 123 |
+
<div className="text-sm opacity-75">Analiz Edilen Metin:</div>
|
| 124 |
+
<div className="mt-2 text-sm italic">"{result.text}"</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
)}
|
| 128 |
+
|
| 129 |
+
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
| 130 |
+
<div className="flex items-start gap-2 text-sm text-blue-800">
|
| 131 |
+
<MessageSquare className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
| 132 |
+
<div>
|
| 133 |
+
<p className="font-semibold mb-1">Türkçe NLP ile Duygu Analizi</p>
|
| 134 |
+
<p className="text-xs text-blue-700">
|
| 135 |
+
Haber metinleri, sosyal medya paylaşımları ve finansal raporların
|
| 136 |
+
duygusal tonunu analiz eder. HuggingFace BERT modelini kullanır,
|
| 137 |
+
fallback olarak kural tabanlı analiz yapar.
|
| 138 |
+
</p>
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
</div>
|
| 144 |
+
);
|
| 145 |
+
}
|
nextjs-app/src/components/StockDetail.tsx
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import { PriceChart } from './PriceChart'
|
| 5 |
+
import { TechnicalIndicators } from './TechnicalIndicators'
|
| 6 |
+
import { MLPredictions } from './MLPredictions'
|
| 7 |
+
import MLPredictionCard from './MLPredictionCard'
|
| 8 |
+
import { formatCurrency, formatPercent } from '@/lib/utils'
|
| 9 |
+
import { TrendingUp, TrendingDown, ArrowLeft } from 'lucide-react'
|
| 10 |
+
import Link from 'next/link'
|
| 11 |
+
|
| 12 |
+
interface StockData {
|
| 13 |
+
stock: any
|
| 14 |
+
latestPrice: any
|
| 15 |
+
indicators: any
|
| 16 |
+
predictions: any[]
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export function StockDetail({ symbol }: { symbol: string }) {
|
| 20 |
+
const [data, setData] = useState<StockData | null>(null)
|
| 21 |
+
const [loading, setLoading] = useState(true)
|
| 22 |
+
|
| 23 |
+
useEffect(() => {
|
| 24 |
+
async function fetchStockData() {
|
| 25 |
+
try {
|
| 26 |
+
const response = await fetch(`/api/stocks/${symbol}`)
|
| 27 |
+
const stockData = await response.json()
|
| 28 |
+
setData(stockData)
|
| 29 |
+
} catch (error) {
|
| 30 |
+
console.error('Failed to fetch stock data:', error)
|
| 31 |
+
} finally {
|
| 32 |
+
setLoading(false)
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
fetchStockData()
|
| 37 |
+
}, [symbol])
|
| 38 |
+
|
| 39 |
+
if (loading) {
|
| 40 |
+
return (
|
| 41 |
+
<div className="space-y-6">
|
| 42 |
+
<div className="h-32 bg-white rounded-lg shadow animate-pulse"></div>
|
| 43 |
+
<div className="h-96 bg-white rounded-lg shadow animate-pulse"></div>
|
| 44 |
+
</div>
|
| 45 |
+
)
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
if (!data) {
|
| 49 |
+
return (
|
| 50 |
+
<div className="bg-white rounded-lg shadow p-12 text-center">
|
| 51 |
+
<p className="text-gray-500">Hisse bilgisi bulunamadı</p>
|
| 52 |
+
<Link href="/" className="text-primary-600 hover:text-primary-800 mt-4 inline-block">
|
| 53 |
+
Ana sayfaya dön
|
| 54 |
+
</Link>
|
| 55 |
+
</div>
|
| 56 |
+
)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
const { stock, latestPrice } = data
|
| 60 |
+
const changePct = latestPrice?.change_pct || 0
|
| 61 |
+
const isPositive = changePct > 0
|
| 62 |
+
|
| 63 |
+
return (
|
| 64 |
+
<div className="space-y-6">
|
| 65 |
+
{/* Back button */}
|
| 66 |
+
<Link
|
| 67 |
+
href="/"
|
| 68 |
+
className="inline-flex items-center space-x-2 text-gray-600 hover:text-gray-900"
|
| 69 |
+
>
|
| 70 |
+
<ArrowLeft className="h-5 w-5" />
|
| 71 |
+
<span>Geri</span>
|
| 72 |
+
</Link>
|
| 73 |
+
|
| 74 |
+
{/* Header */}
|
| 75 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 76 |
+
<div className="flex items-start justify-between">
|
| 77 |
+
<div>
|
| 78 |
+
<h1 className="text-3xl font-bold text-gray-900">{stock.symbol}</h1>
|
| 79 |
+
<p className="text-lg text-gray-600 mt-1">{stock.name}</p>
|
| 80 |
+
<p className="text-sm text-gray-500 mt-1">{stock.sector}</p>
|
| 81 |
+
</div>
|
| 82 |
+
<div className="text-right">
|
| 83 |
+
<div className="text-3xl font-bold text-gray-900">
|
| 84 |
+
{latestPrice?.last_price ? formatCurrency(latestPrice.last_price) : '-'}
|
| 85 |
+
</div>
|
| 86 |
+
{latestPrice && (
|
| 87 |
+
<div className={`flex items-center justify-end space-x-2 mt-2 ${
|
| 88 |
+
isPositive ? 'text-success' : 'text-danger'
|
| 89 |
+
}`}>
|
| 90 |
+
{isPositive ? (
|
| 91 |
+
<TrendingUp className="h-5 w-5" />
|
| 92 |
+
) : (
|
| 93 |
+
<TrendingDown className="h-5 w-5" />
|
| 94 |
+
)}
|
| 95 |
+
<span className="text-lg font-semibold">
|
| 96 |
+
{formatPercent(changePct)}
|
| 97 |
+
</span>
|
| 98 |
+
</div>
|
| 99 |
+
)}
|
| 100 |
+
</div>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
+
{/* Key stats */}
|
| 104 |
+
<div className="grid grid-cols-4 gap-4 mt-6 pt-6 border-t">
|
| 105 |
+
<StatItem label="Açılış" value={latestPrice?.last_price} format="currency" />
|
| 106 |
+
<StatItem label="Yüksek" value={latestPrice?.day_high} format="currency" />
|
| 107 |
+
<StatItem label="Düşük" value={latestPrice?.day_low} format="currency" />
|
| 108 |
+
<StatItem label="Hacim" value={latestPrice?.volume} format="number" />
|
| 109 |
+
</div>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
{/* Price Chart */}
|
| 113 |
+
<PriceChart symbol={symbol} />
|
| 114 |
+
|
| 115 |
+
{/* Technical Indicators */}
|
| 116 |
+
{data.indicators && (
|
| 117 |
+
<TechnicalIndicators indicators={data.indicators} />
|
| 118 |
+
)}
|
| 119 |
+
|
| 120 |
+
{/* ML Prediction Analysis */}
|
| 121 |
+
<MLPredictionCard symbol={symbol} />
|
| 122 |
+
|
| 123 |
+
{/* ML Predictions (Old) */}
|
| 124 |
+
{data.predictions && data.predictions.length > 0 && (
|
| 125 |
+
<MLPredictions predictions={data.predictions} />
|
| 126 |
+
)}
|
| 127 |
+
</div>
|
| 128 |
+
)
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
function StatItem({
|
| 132 |
+
label,
|
| 133 |
+
value,
|
| 134 |
+
format
|
| 135 |
+
}: {
|
| 136 |
+
label: string
|
| 137 |
+
value: any
|
| 138 |
+
format: 'currency' | 'number' | 'percent'
|
| 139 |
+
}) {
|
| 140 |
+
const formatValue = () => {
|
| 141 |
+
if (value === null || value === undefined) return '-'
|
| 142 |
+
|
| 143 |
+
switch (format) {
|
| 144 |
+
case 'currency':
|
| 145 |
+
return formatCurrency(value)
|
| 146 |
+
case 'number':
|
| 147 |
+
return new Intl.NumberFormat('tr-TR').format(value)
|
| 148 |
+
case 'percent':
|
| 149 |
+
return formatPercent(value)
|
| 150 |
+
default:
|
| 151 |
+
return value
|
| 152 |
+
}
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
return (
|
| 156 |
+
<div>
|
| 157 |
+
<div className="text-sm text-gray-500">{label}</div>
|
| 158 |
+
<div className="text-lg font-semibold text-gray-900 mt-1">
|
| 159 |
+
{formatValue()}
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
)
|
| 163 |
+
}
|
nextjs-app/src/components/StockList.tsx
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import Link from 'next/link'
|
| 5 |
+
import { StockSummary } from '@/types'
|
| 6 |
+
import { formatCurrency, formatPercent } from '@/lib/utils'
|
| 7 |
+
import { TrendingUp, TrendingDown } from 'lucide-react'
|
| 8 |
+
|
| 9 |
+
export function StockList() {
|
| 10 |
+
const [stocks, setStocks] = useState<StockSummary[]>([])
|
| 11 |
+
const [loading, setLoading] = useState(true)
|
| 12 |
+
const [filter, setFilter] = useState('')
|
| 13 |
+
|
| 14 |
+
useEffect(() => {
|
| 15 |
+
async function fetchStocks() {
|
| 16 |
+
try {
|
| 17 |
+
const response = await fetch('/api/stocks')
|
| 18 |
+
const data = await response.json()
|
| 19 |
+
setStocks(Array.isArray(data) ? data : [])
|
| 20 |
+
} catch (error) {
|
| 21 |
+
console.error('Failed to fetch stocks:', error)
|
| 22 |
+
setStocks([])
|
| 23 |
+
} finally {
|
| 24 |
+
setLoading(false)
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
fetchStocks()
|
| 29 |
+
}, [])
|
| 30 |
+
|
| 31 |
+
const filteredStocks = (stocks || []).filter(stock =>
|
| 32 |
+
stock.symbol.toLowerCase().includes(filter.toLowerCase()) ||
|
| 33 |
+
stock.name.toLowerCase().includes(filter.toLowerCase())
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
if (loading) {
|
| 37 |
+
return (
|
| 38 |
+
<div className="bg-white rounded-lg shadow">
|
| 39 |
+
<div className="p-6 border-b">
|
| 40 |
+
<div className="h-8 bg-gray-200 rounded w-1/4 animate-pulse"></div>
|
| 41 |
+
</div>
|
| 42 |
+
<div className="p-6 space-y-4">
|
| 43 |
+
{[...Array(10)].map((_, i) => (
|
| 44 |
+
<div key={i} className="h-16 bg-gray-100 rounded animate-pulse"></div>
|
| 45 |
+
))}
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
return (
|
| 52 |
+
<div className="bg-white rounded-lg shadow">
|
| 53 |
+
<div className="p-6 border-b">
|
| 54 |
+
<h2 className="text-2xl font-bold text-gray-900 mb-4">Hisse Senetleri</h2>
|
| 55 |
+
<input
|
| 56 |
+
type="text"
|
| 57 |
+
placeholder="Hisse ara (sembol veya isim)..."
|
| 58 |
+
value={filter}
|
| 59 |
+
onChange={(e) => setFilter(e.target.value)}
|
| 60 |
+
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
|
| 61 |
+
/>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div className="overflow-x-auto">
|
| 65 |
+
<table className="w-full">
|
| 66 |
+
<thead className="bg-gray-50 border-b">
|
| 67 |
+
<tr>
|
| 68 |
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 69 |
+
Sembol
|
| 70 |
+
</th>
|
| 71 |
+
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 72 |
+
Şirket
|
| 73 |
+
</th>
|
| 74 |
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 75 |
+
Fiyat
|
| 76 |
+
</th>
|
| 77 |
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 78 |
+
Değişim
|
| 79 |
+
</th>
|
| 80 |
+
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 81 |
+
Trend
|
| 82 |
+
</th>
|
| 83 |
+
<th className="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
| 84 |
+
RSI
|
| 85 |
+
</th>
|
| 86 |
+
</tr>
|
| 87 |
+
</thead>
|
| 88 |
+
<tbody className="bg-white divide-y divide-gray-200">
|
| 89 |
+
{filteredStocks.map((stock) => (
|
| 90 |
+
<tr key={stock.id} className="hover:bg-gray-50 transition-colors">
|
| 91 |
+
<td className="px-6 py-4 whitespace-nowrap">
|
| 92 |
+
<Link
|
| 93 |
+
href={`/stocks/${stock.symbol}`}
|
| 94 |
+
className="text-primary-600 font-semibold hover:text-primary-800"
|
| 95 |
+
>
|
| 96 |
+
{stock.symbol}
|
| 97 |
+
</Link>
|
| 98 |
+
</td>
|
| 99 |
+
<td className="px-6 py-4">
|
| 100 |
+
<div className="text-sm text-gray-900">{stock.name}</div>
|
| 101 |
+
<div className="text-xs text-gray-500">{stock.sector}</div>
|
| 102 |
+
</td>
|
| 103 |
+
<td className="px-6 py-4 whitespace-nowrap text-right">
|
| 104 |
+
<div className="text-sm font-medium text-gray-900">
|
| 105 |
+
{stock.last_price ? formatCurrency(stock.last_price) : '-'}
|
| 106 |
+
</div>
|
| 107 |
+
</td>
|
| 108 |
+
<td className="px-6 py-4 whitespace-nowrap text-right">
|
| 109 |
+
{stock.change_pct !== null && stock.change_pct !== undefined ? (
|
| 110 |
+
<div className="flex items-center justify-end space-x-1">
|
| 111 |
+
{stock.change_pct > 0 ? (
|
| 112 |
+
<>
|
| 113 |
+
<TrendingUp className="h-4 w-4 text-success" />
|
| 114 |
+
<span className="text-sm font-medium text-success">
|
| 115 |
+
{formatPercent(stock.change_pct)}
|
| 116 |
+
</span>
|
| 117 |
+
</>
|
| 118 |
+
) : stock.change_pct < 0 ? (
|
| 119 |
+
<>
|
| 120 |
+
<TrendingDown className="h-4 w-4 text-danger" />
|
| 121 |
+
<span className="text-sm font-medium text-danger">
|
| 122 |
+
{formatPercent(stock.change_pct)}
|
| 123 |
+
</span>
|
| 124 |
+
</>
|
| 125 |
+
) : (
|
| 126 |
+
<span className="text-sm text-gray-500">0.00%</span>
|
| 127 |
+
)}
|
| 128 |
+
</div>
|
| 129 |
+
) : (
|
| 130 |
+
<span className="text-sm text-gray-400">-</span>
|
| 131 |
+
)}
|
| 132 |
+
</td>
|
| 133 |
+
<td className="px-6 py-4 whitespace-nowrap text-center">
|
| 134 |
+
{stock.trend ? (
|
| 135 |
+
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
| 136 |
+
stock.trend === 'bullish'
|
| 137 |
+
? 'bg-green-100 text-green-800'
|
| 138 |
+
: stock.trend === 'bearish'
|
| 139 |
+
? 'bg-red-100 text-red-800'
|
| 140 |
+
: 'bg-gray-100 text-gray-800'
|
| 141 |
+
}`}>
|
| 142 |
+
{stock.trend === 'bullish' ? 'Yükseliş' : stock.trend === 'bearish' ? 'Düşüş' : 'Nötr'}
|
| 143 |
+
</span>
|
| 144 |
+
) : (
|
| 145 |
+
<span className="text-sm text-gray-400">-</span>
|
| 146 |
+
)}
|
| 147 |
+
</td>
|
| 148 |
+
<td className="px-6 py-4 whitespace-nowrap text-center">
|
| 149 |
+
{stock.rsi_14 !== null ? (
|
| 150 |
+
<span className={`text-sm font-medium ${
|
| 151 |
+
stock.rsi_14 > 70
|
| 152 |
+
? 'text-red-600'
|
| 153 |
+
: stock.rsi_14 < 30
|
| 154 |
+
? 'text-green-600'
|
| 155 |
+
: 'text-gray-600'
|
| 156 |
+
}`}>
|
| 157 |
+
{stock.rsi_14.toFixed(1)}
|
| 158 |
+
</span>
|
| 159 |
+
) : (
|
| 160 |
+
<span className="text-sm text-gray-400">-</span>
|
| 161 |
+
)}
|
| 162 |
+
</td>
|
| 163 |
+
</tr>
|
| 164 |
+
))}
|
| 165 |
+
</tbody>
|
| 166 |
+
</table>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
{filteredStocks.length === 0 && (
|
| 170 |
+
<div className="p-12 text-center text-gray-500">
|
| 171 |
+
Hiç hisse bulunamadı
|
| 172 |
+
</div>
|
| 173 |
+
)}
|
| 174 |
+
</div>
|
| 175 |
+
)
|
| 176 |
+
}
|
nextjs-app/src/components/TechnicalIndicators.tsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { TechnicalIndicator } from '@/types'
|
| 4 |
+
|
| 5 |
+
export function TechnicalIndicators({ indicators }: { indicators: TechnicalIndicator }) {
|
| 6 |
+
return (
|
| 7 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 8 |
+
<h2 className="text-xl font-bold text-gray-900 mb-6">Teknik Göstergeler</h2>
|
| 9 |
+
|
| 10 |
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
| 11 |
+
{/* Moving Averages */}
|
| 12 |
+
<IndicatorGroup title="Hareketli Ortalamalar">
|
| 13 |
+
<IndicatorItem label="SMA 5" value={indicators.sma_5} />
|
| 14 |
+
<IndicatorItem label="SMA 10" value={indicators.sma_10} />
|
| 15 |
+
<IndicatorItem label="SMA 20" value={indicators.sma_20} />
|
| 16 |
+
<IndicatorItem label="SMA 50" value={indicators.sma_50} />
|
| 17 |
+
<IndicatorItem label="SMA 200" value={indicators.sma_200} />
|
| 18 |
+
<IndicatorItem label="EMA 12" value={indicators.ema_12} />
|
| 19 |
+
<IndicatorItem label="EMA 26" value={indicators.ema_26} />
|
| 20 |
+
</IndicatorGroup>
|
| 21 |
+
|
| 22 |
+
{/* Oscillators */}
|
| 23 |
+
<IndicatorGroup title="Osilatörler">
|
| 24 |
+
<IndicatorItem
|
| 25 |
+
label="RSI (14)"
|
| 26 |
+
value={indicators.rsi_14}
|
| 27 |
+
colorize={(val) =>
|
| 28 |
+
val > 70 ? 'text-red-600' :
|
| 29 |
+
val < 30 ? 'text-green-600' :
|
| 30 |
+
'text-gray-900'
|
| 31 |
+
}
|
| 32 |
+
/>
|
| 33 |
+
<IndicatorItem label="MACD" value={indicators.macd} precision={4} />
|
| 34 |
+
<IndicatorItem label="MACD Signal" value={indicators.macd_signal} precision={4} />
|
| 35 |
+
<IndicatorItem label="MACD Histogram" value={indicators.macd_histogram} precision={4} />
|
| 36 |
+
<IndicatorItem label="Stochastic K" value={indicators.stochastic_k} />
|
| 37 |
+
<IndicatorItem label="Stochastic D" value={indicators.stochastic_d} />
|
| 38 |
+
<IndicatorItem label="Williams %R" value={indicators.williams_r} />
|
| 39 |
+
<IndicatorItem label="CCI (20)" value={indicators.cci_20} precision={2} />
|
| 40 |
+
</IndicatorGroup>
|
| 41 |
+
|
| 42 |
+
{/* Volatility */}
|
| 43 |
+
<IndicatorGroup title="Volatilite">
|
| 44 |
+
<IndicatorItem label="Bollinger Üst" value={indicators.bollinger_upper} />
|
| 45 |
+
<IndicatorItem label="Bollinger Orta" value={indicators.bollinger_middle} />
|
| 46 |
+
<IndicatorItem label="Bollinger Alt" value={indicators.bollinger_lower} />
|
| 47 |
+
<IndicatorItem label="ATR (14)" value={indicators.atr_14} precision={4} />
|
| 48 |
+
</IndicatorGroup>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div className="mt-4 pt-4 border-t text-sm text-gray-500">
|
| 52 |
+
Son güncelleme: {new Date(indicators.date).toLocaleDateString('tr-TR')}
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
)
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
function IndicatorGroup({
|
| 59 |
+
title,
|
| 60 |
+
children
|
| 61 |
+
}: {
|
| 62 |
+
title: string
|
| 63 |
+
children: React.ReactNode
|
| 64 |
+
}) {
|
| 65 |
+
return (
|
| 66 |
+
<div className="space-y-3">
|
| 67 |
+
<h3 className="font-semibold text-gray-700 text-sm uppercase tracking-wide">
|
| 68 |
+
{title}
|
| 69 |
+
</h3>
|
| 70 |
+
<div className="space-y-2">
|
| 71 |
+
{children}
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
)
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
function IndicatorItem({
|
| 78 |
+
label,
|
| 79 |
+
value,
|
| 80 |
+
precision = 2,
|
| 81 |
+
colorize
|
| 82 |
+
}: {
|
| 83 |
+
label: string
|
| 84 |
+
value: number | null
|
| 85 |
+
precision?: number
|
| 86 |
+
colorize?: (value: number) => string
|
| 87 |
+
}) {
|
| 88 |
+
const displayValue = value !== null && value !== undefined
|
| 89 |
+
? value.toFixed(precision)
|
| 90 |
+
: '-'
|
| 91 |
+
|
| 92 |
+
const colorClass = value !== null && colorize
|
| 93 |
+
? colorize(value)
|
| 94 |
+
: 'text-gray-900'
|
| 95 |
+
|
| 96 |
+
return (
|
| 97 |
+
<div className="flex items-center justify-between py-1">
|
| 98 |
+
<span className="text-sm text-gray-600">{label}</span>
|
| 99 |
+
<span className={`text-sm font-semibold ${colorClass}`}>
|
| 100 |
+
{displayValue}
|
| 101 |
+
</span>
|
| 102 |
+
</div>
|
| 103 |
+
)
|
| 104 |
+
}
|
nextjs-app/src/components/TopMLPredictions.tsx
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client';
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react';
|
| 4 |
+
import { Brain, TrendingUp, TrendingDown, Loader2 } from 'lucide-react';
|
| 5 |
+
import Link from 'next/link';
|
| 6 |
+
|
| 7 |
+
interface TopPrediction {
|
| 8 |
+
symbol: string;
|
| 9 |
+
name: string;
|
| 10 |
+
current_price: number;
|
| 11 |
+
predicted_price: number;
|
| 12 |
+
prediction_change: number;
|
| 13 |
+
confidence: number;
|
| 14 |
+
signal: 'BUY' | 'SELL' | 'HOLD';
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export default function TopMLPredictions() {
|
| 18 |
+
const [predictions, setPredictions] = useState<TopPrediction[]>([]);
|
| 19 |
+
const [loading, setLoading] = useState(true);
|
| 20 |
+
|
| 21 |
+
useEffect(() => {
|
| 22 |
+
fetchTopPredictions();
|
| 23 |
+
}, []);
|
| 24 |
+
|
| 25 |
+
const fetchTopPredictions = async () => {
|
| 26 |
+
try {
|
| 27 |
+
setLoading(true);
|
| 28 |
+
|
| 29 |
+
// Get top stocks
|
| 30 |
+
const stocksRes = await fetch('/api/stocks?limit=21');
|
| 31 |
+
const stocks = await stocksRes.json();
|
| 32 |
+
|
| 33 |
+
// Get predictions for all stocks
|
| 34 |
+
const symbols = stocks.map((s: any) => s.symbol);
|
| 35 |
+
const predictionsRes = await fetch('/api/ml/predict', {
|
| 36 |
+
method: 'POST',
|
| 37 |
+
headers: { 'Content-Type': 'application/json' },
|
| 38 |
+
body: JSON.stringify({ symbols })
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
const data = await predictionsRes.json();
|
| 42 |
+
|
| 43 |
+
// Combine stock info with predictions
|
| 44 |
+
const combined = data.predictions.map((pred: any) => {
|
| 45 |
+
const stock = stocks.find((s: any) => s.symbol === pred.symbol);
|
| 46 |
+
return {
|
| 47 |
+
...pred,
|
| 48 |
+
name: stock?.name || pred.symbol
|
| 49 |
+
};
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
// Sort by signal (BUY first) and confidence
|
| 53 |
+
const sorted = combined.sort((a: TopPrediction, b: TopPrediction) => {
|
| 54 |
+
if (a.signal === 'BUY' && b.signal !== 'BUY') return -1;
|
| 55 |
+
if (a.signal !== 'BUY' && b.signal === 'BUY') return 1;
|
| 56 |
+
return b.confidence - a.confidence;
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
setPredictions(sorted.slice(0, 5)); // Top 5
|
| 60 |
+
} catch (error) {
|
| 61 |
+
console.error('Failed to fetch predictions:', error);
|
| 62 |
+
} finally {
|
| 63 |
+
setLoading(false);
|
| 64 |
+
}
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
if (loading) {
|
| 68 |
+
return (
|
| 69 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 70 |
+
<div className="flex items-center gap-2 mb-4">
|
| 71 |
+
<Brain className="w-5 h-5 text-purple-600" />
|
| 72 |
+
<h2 className="text-lg font-semibold">Top ML Tahminleri</h2>
|
| 73 |
+
</div>
|
| 74 |
+
<div className="flex items-center justify-center py-8">
|
| 75 |
+
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return (
|
| 82 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 83 |
+
<div className="flex items-center justify-between mb-4">
|
| 84 |
+
<div className="flex items-center gap-2">
|
| 85 |
+
<Brain className="w-5 h-5 text-purple-600" />
|
| 86 |
+
<h2 className="text-lg font-semibold">Top ML Tahminleri</h2>
|
| 87 |
+
</div>
|
| 88 |
+
<button
|
| 89 |
+
onClick={fetchTopPredictions}
|
| 90 |
+
className="text-sm text-purple-600 hover:text-purple-800"
|
| 91 |
+
>
|
| 92 |
+
Yenile
|
| 93 |
+
</button>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<div className="space-y-3">
|
| 97 |
+
{predictions.map((pred) => (
|
| 98 |
+
<Link
|
| 99 |
+
key={pred.symbol}
|
| 100 |
+
href={`/stocks/${pred.symbol}`}
|
| 101 |
+
className="block p-4 rounded-lg border hover:border-purple-300 hover:bg-purple-50 transition-colors"
|
| 102 |
+
>
|
| 103 |
+
<div className="flex items-start justify-between">
|
| 104 |
+
<div className="flex-1">
|
| 105 |
+
<div className="flex items-center gap-2">
|
| 106 |
+
<span className="font-semibold text-gray-900">{pred.symbol}</span>
|
| 107 |
+
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
| 108 |
+
pred.signal === 'BUY'
|
| 109 |
+
? 'bg-green-100 text-green-800'
|
| 110 |
+
: pred.signal === 'SELL'
|
| 111 |
+
? 'bg-red-100 text-red-800'
|
| 112 |
+
: 'bg-gray-100 text-gray-800'
|
| 113 |
+
}`}>
|
| 114 |
+
{pred.signal}
|
| 115 |
+
</span>
|
| 116 |
+
</div>
|
| 117 |
+
<div className="text-sm text-gray-600 mt-1">{pred.name}</div>
|
| 118 |
+
</div>
|
| 119 |
+
|
| 120 |
+
<div className="text-right">
|
| 121 |
+
<div className="text-sm text-gray-600">
|
| 122 |
+
Güven: <span className="font-semibold">{pred.confidence}%</span>
|
| 123 |
+
</div>
|
| 124 |
+
<div className={`text-sm font-semibold mt-1 flex items-center justify-end gap-1 ${
|
| 125 |
+
pred.prediction_change >= 0 ? 'text-green-600' : 'text-red-600'
|
| 126 |
+
}`}>
|
| 127 |
+
{pred.prediction_change >= 0 ? (
|
| 128 |
+
<TrendingUp className="w-4 h-4" />
|
| 129 |
+
) : (
|
| 130 |
+
<TrendingDown className="w-4 h-4" />
|
| 131 |
+
)}
|
| 132 |
+
<span>
|
| 133 |
+
{pred.prediction_change >= 0 ? '+' : ''}
|
| 134 |
+
{pred.prediction_change.toFixed(2)}%
|
| 135 |
+
</span>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<div className="mt-3 pt-3 border-t border-gray-100 flex justify-between text-xs text-gray-500">
|
| 141 |
+
<span>Mevcut: ₺{pred.current_price.toFixed(2)}</span>
|
| 142 |
+
<span>Tahmin: ₺{pred.predicted_price.toFixed(2)}</span>
|
| 143 |
+
</div>
|
| 144 |
+
</Link>
|
| 145 |
+
))}
|
| 146 |
+
</div>
|
| 147 |
+
|
| 148 |
+
<div className="mt-4 text-xs text-gray-500 text-center">
|
| 149 |
+
Teknik gösterge bazlı ML analizi ile oluşturulmuştur
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
);
|
| 153 |
+
}
|
nextjs-app/src/components/TopMovers.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import { TrendingUp, TrendingDown } from 'lucide-react'
|
| 5 |
+
import { formatPercent } from '@/lib/utils'
|
| 6 |
+
|
| 7 |
+
interface Mover {
|
| 8 |
+
symbol: string
|
| 9 |
+
name: string
|
| 10 |
+
change_pct: number
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function TopMovers() {
|
| 14 |
+
const [gainers, setGainers] = useState<Mover[]>([])
|
| 15 |
+
const [losers, setLosers] = useState<Mover[]>([])
|
| 16 |
+
const [loading, setLoading] = useState(true)
|
| 17 |
+
|
| 18 |
+
useEffect(() => {
|
| 19 |
+
async function fetchMovers() {
|
| 20 |
+
try {
|
| 21 |
+
const response = await fetch('/api/movers')
|
| 22 |
+
const data = await response.json()
|
| 23 |
+
setGainers(Array.isArray(data?.gainers) ? data.gainers : [])
|
| 24 |
+
setLosers(Array.isArray(data?.losers) ? data.losers : [])
|
| 25 |
+
} catch (error) {
|
| 26 |
+
console.error('Failed to fetch movers:', error)
|
| 27 |
+
setGainers([])
|
| 28 |
+
setLosers([])
|
| 29 |
+
} finally {
|
| 30 |
+
setLoading(false)
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
fetchMovers()
|
| 35 |
+
}, [])
|
| 36 |
+
|
| 37 |
+
if (loading) {
|
| 38 |
+
return (
|
| 39 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 40 |
+
<div className="h-6 bg-gray-200 rounded w-1/2 mb-4 animate-pulse"></div>
|
| 41 |
+
<div className="space-y-3">
|
| 42 |
+
{[...Array(5)].map((_, i) => (
|
| 43 |
+
<div key={i} className="h-12 bg-gray-100 rounded animate-pulse"></div>
|
| 44 |
+
))}
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return (
|
| 51 |
+
<div className="space-y-6">
|
| 52 |
+
{/* Gainers */}
|
| 53 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 54 |
+
<div className="flex items-center space-x-2 mb-4">
|
| 55 |
+
<TrendingUp className="h-5 w-5 text-success" />
|
| 56 |
+
<h3 className="text-lg font-semibold text-gray-900">En Çok Yükselenler</h3>
|
| 57 |
+
</div>
|
| 58 |
+
<div className="space-y-3">
|
| 59 |
+
{gainers.slice(0, 5).map((stock) => (
|
| 60 |
+
<div key={stock.symbol} className="flex items-center justify-between">
|
| 61 |
+
<div>
|
| 62 |
+
<div className="font-semibold text-gray-900">{stock.symbol}</div>
|
| 63 |
+
<div className="text-xs text-gray-500 truncate max-w-[120px]">
|
| 64 |
+
{stock.name}
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
<div className="text-success font-semibold">
|
| 68 |
+
{formatPercent(stock.change_pct)}
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
))}
|
| 72 |
+
</div>
|
| 73 |
+
{gainers.length === 0 && (
|
| 74 |
+
<p className="text-sm text-gray-500 text-center py-4">
|
| 75 |
+
Veri bulunamadı
|
| 76 |
+
</p>
|
| 77 |
+
)}
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
{/* Losers */}
|
| 81 |
+
<div className="bg-white rounded-lg shadow p-6">
|
| 82 |
+
<div className="flex items-center space-x-2 mb-4">
|
| 83 |
+
<TrendingDown className="h-5 w-5 text-danger" />
|
| 84 |
+
<h3 className="text-lg font-semibold text-gray-900">En Çok Düşenler</h3>
|
| 85 |
+
</div>
|
| 86 |
+
<div className="space-y-3">
|
| 87 |
+
{losers.slice(0, 5).map((stock) => (
|
| 88 |
+
<div key={stock.symbol} className="flex items-center justify-between">
|
| 89 |
+
<div>
|
| 90 |
+
<div className="font-semibold text-gray-900">{stock.symbol}</div>
|
| 91 |
+
<div className="text-xs text-gray-500 truncate max-w-[120px]">
|
| 92 |
+
{stock.name}
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
<div className="text-danger font-semibold">
|
| 96 |
+
{formatPercent(stock.change_pct)}
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
))}
|
| 100 |
+
</div>
|
| 101 |
+
{losers.length === 0 && (
|
| 102 |
+
<p className="text-sm text-gray-500 text-center py-4">
|
| 103 |
+
Veri bulunamadı
|
| 104 |
+
</p>
|
| 105 |
+
)}
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
)
|
| 109 |
+
}
|