onerozbey commited on
Commit
991ee9f
·
1 Parent(s): 6a126bf

Add Next.js components

Browse files
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
+ }