This commit is contained in:
+90
-4
@@ -235,14 +235,21 @@
|
|||||||
"HTFT": "Half Time / Full Time",
|
"HTFT": "Half Time / Full Time",
|
||||||
"HT/FT": "Half Time / Full Time",
|
"HT/FT": "Half Time / Full Time",
|
||||||
"OE": "Odd / Even",
|
"OE": "Odd / Even",
|
||||||
"HT_OU05": "First Half 0.5 Goals"
|
"HT_OU05": "First Half 0.5 Goals",
|
||||||
|
"HT_OU15": "First Half 1.5 Goals",
|
||||||
|
"CARDS": "Cards 4.5",
|
||||||
|
"HCAP": "Handicap Result"
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"summary-title": "Prediction Summary",
|
"summary-title": "Prediction Summary",
|
||||||
"summary-info": "Shows model signals and uncertainty in a conservative summary.",
|
"summary-info": "Shows model signals and uncertainty in a conservative summary.",
|
||||||
|
"model-signal-disclaimer": "This is a model signal; it is not a guaranteed result, guarantee, or hit-rate promise. Signal score can be wrong because of in-match variance, lineups, and data quality.",
|
||||||
"main-recommendation": "Highlighted Signal",
|
"main-recommendation": "Highlighted Signal",
|
||||||
"best-market-copy": "is the strongest option in this market.",
|
"best-market-copy": "is the strongest option in this market.",
|
||||||
"confidence-label": "Confidence",
|
"confidence-label": "Confidence",
|
||||||
|
"confidence-interval": "Confidence Interval",
|
||||||
|
"confidence-interval-warning": "The confidence interval is wide. Even with a signal, it is not recommended as a standalone pick.",
|
||||||
|
"confidence-band": "Band",
|
||||||
"odds-label": "Odds",
|
"odds-label": "Odds",
|
||||||
"edge-label": "Theoretical Edge",
|
"edge-label": "Theoretical Edge",
|
||||||
"edge-info": "The theoretical gap between model probability and market probability; it is not a guarantee or a certain profit expectation.",
|
"edge-info": "The theoretical gap between model probability and market probability; it is not a guarantee or a certain profit expectation.",
|
||||||
@@ -253,8 +260,22 @@
|
|||||||
"playability-label": "Model signal",
|
"playability-label": "Model signal",
|
||||||
"quick-read": "Quick read",
|
"quick-read": "Quick read",
|
||||||
"lineup-source": "Lineup Source",
|
"lineup-source": "Lineup Source",
|
||||||
|
"lineup-confirmed-live": "Confirmed starting XI",
|
||||||
|
"lineup-probable-xi": "Probable starting XI",
|
||||||
|
"unknown": "Unknown",
|
||||||
"model-label": "Model",
|
"model-label": "Model",
|
||||||
"engine-info": "Shows which components influence the prediction the most.",
|
"engine-info": "Shows which components influence the prediction the most.",
|
||||||
|
"engine-team-football": "Team Strength",
|
||||||
|
"engine-team-basketball": "Team Form",
|
||||||
|
"engine-player-football": "Player Impact",
|
||||||
|
"engine-player-basketball": "Lineup Impact",
|
||||||
|
"engine-odds": "Odds Analysis",
|
||||||
|
"engine-referee-football": "Referee Impact",
|
||||||
|
"engine-referee-basketball": "Supporting Signals",
|
||||||
|
"engine-label-high": "High",
|
||||||
|
"engine-label-medium": "Medium",
|
||||||
|
"engine-label-low": "Low",
|
||||||
|
"engine-label-very-low": "Very Low",
|
||||||
"best-single-pick": "Strongest Signal",
|
"best-single-pick": "Strongest Signal",
|
||||||
"alternative-markets": "Alternative Markets",
|
"alternative-markets": "Alternative Markets",
|
||||||
"alternative-markets-info": "Options outside the main recommendation.",
|
"alternative-markets-info": "Options outside the main recommendation.",
|
||||||
@@ -264,7 +285,48 @@
|
|||||||
"all-markets-info": "Compares every option in a single table.",
|
"all-markets-info": "Compares every option in a single table.",
|
||||||
"market-board-info": "The probability distribution the model sees for each market.",
|
"market-board-info": "The probability distribution the model sees for each market.",
|
||||||
"bet-advice-info": "The model's final action recommendation.",
|
"bet-advice-info": "The model's final action recommendation.",
|
||||||
"recommended-stake-inline": "Suggested size"
|
"recommended-stake-inline": "Suggested size",
|
||||||
|
"model-probability-short": "Model",
|
||||||
|
"market-probability-short": "Market",
|
||||||
|
"theoretical-edge-inline": "Theoretical edge",
|
||||||
|
"playable": "Playable",
|
||||||
|
"risky": "Risky",
|
||||||
|
"hit-probability": "Hit Probability",
|
||||||
|
"calibrated-confidence": "Calibrated Confidence",
|
||||||
|
"score-scenario-football": "Score Scenario",
|
||||||
|
"score-scenario-basketball": "Points Scenario",
|
||||||
|
"score-scenario-info-football": "Expected score and the most likely scenarios.",
|
||||||
|
"score-scenario-info-basketball": "Expected points distribution and the most likely match scenarios.",
|
||||||
|
"full-time-football": "Full Time",
|
||||||
|
"full-time-basketball": "Full-Time Points",
|
||||||
|
"half-time-football": "Half Time",
|
||||||
|
"half-time-basketball": "Half-Time Points",
|
||||||
|
"expected-total-football": "Total xG",
|
||||||
|
"expected-total-basketball": "Expected Total Points",
|
||||||
|
"live": "LIVE",
|
||||||
|
"pre-match-prediction": "Pre-match prediction",
|
||||||
|
"prediction-contradictions": "Prediction Contradictions",
|
||||||
|
"data-quality": "Data Quality",
|
||||||
|
"data-quality-info": "How reliable the lineup, odds, and match data are.",
|
||||||
|
"risk-info": "Upset probability and uncertainty level.",
|
||||||
|
"risk-commentary": "Risk Commentary",
|
||||||
|
"risk-default-comment": "The model asks for extra caution on this match.",
|
||||||
|
"surprise-score": "Upset score",
|
||||||
|
"match-commentary-title": "Match Commentary",
|
||||||
|
"match-commentary-info": "The model's human-readable summary of the match.",
|
||||||
|
"reasoning-info": "High-level summary of why the model reads this match this way.",
|
||||||
|
"bet-advice-play": "PLAY",
|
||||||
|
"bet-advice-pass": "PASS",
|
||||||
|
"signal-tier-core": "Core",
|
||||||
|
"signal-tier-value": "Value",
|
||||||
|
"signal-tier-lean": "Lean",
|
||||||
|
"signal-tier-longshot": "Longshot",
|
||||||
|
"signal-tier-pass": "Pass",
|
||||||
|
"confidence-high": "High",
|
||||||
|
"confidence-medium": "Medium",
|
||||||
|
"confidence-low": "Low",
|
||||||
|
"confidence-unknown": "Unknown",
|
||||||
|
"info": "Info"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"coupons": {
|
"coupons": {
|
||||||
@@ -324,6 +386,9 @@
|
|||||||
"candidate-pool-help": "Only football matches that have not started yet are listed here. Finished and live matches are excluded.",
|
"candidate-pool-help": "Only football matches that have not started yet are listed here. Finished and live matches are excluded.",
|
||||||
"candidate-pool-subtitle": "Source: live_matches table • sport: football • status: not started",
|
"candidate-pool-subtitle": "Source: live_matches table • sport: football • status: not started",
|
||||||
"match-count-suffix": "matches",
|
"match-count-suffix": "matches",
|
||||||
|
"match-count-label": "Coupon Match Count",
|
||||||
|
"match-count-help": "How many matches should the AI coupon include? You can choose between 2 and 15. If you do not select any matches, the full bulletin is scanned.",
|
||||||
|
"match-count-auto": "Full bulletin ({count} matches)",
|
||||||
"upcoming-badge": "Upcoming",
|
"upcoming-badge": "Upcoming",
|
||||||
"upcoming-reference": "Upcoming pool",
|
"upcoming-reference": "Upcoming pool",
|
||||||
"finished-badge": "Finished",
|
"finished-badge": "Finished",
|
||||||
@@ -419,7 +484,8 @@
|
|||||||
"countries": "Countries",
|
"countries": "Countries",
|
||||||
"leagues": "Leagues",
|
"leagues": "Leagues",
|
||||||
"countries-leagues": "Countries & Leagues",
|
"countries-leagues": "Countries & Leagues",
|
||||||
"search-at-least-2": "Type at least 2 characters to search teams."
|
"search-at-least-2": "Type at least 2 characters to search teams.",
|
||||||
|
"all": "All"
|
||||||
},
|
},
|
||||||
"h2h": {
|
"h2h": {
|
||||||
"title": "Head to Head",
|
"title": "Head to Head",
|
||||||
@@ -475,7 +541,9 @@
|
|||||||
"analytics": "Analytics Overview",
|
"analytics": "Analytics Overview",
|
||||||
"user-management": "User Management",
|
"user-management": "User Management",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
|
"premium-users": "Premium Users",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
|
"subscription": "Subscription",
|
||||||
"usage-limits": "Usage Limits",
|
"usage-limits": "Usage Limits",
|
||||||
"total-users": "Total Users",
|
"total-users": "Total Users",
|
||||||
"active-users": "Active Users",
|
"active-users": "Active Users",
|
||||||
@@ -495,7 +563,25 @@
|
|||||||
"user-email": "Email",
|
"user-email": "Email",
|
||||||
"user-role": "Role",
|
"user-role": "Role",
|
||||||
"user-status": "Status",
|
"user-status": "Status",
|
||||||
"no-users": "No users found."
|
"no-users": "No users found.",
|
||||||
|
"restricted": "Restricted",
|
||||||
|
"admin-access-required": "Admin access required",
|
||||||
|
"admin-access-description": "This area is only available to superadmin accounts.",
|
||||||
|
"search-users-placeholder": "Search by email or name...",
|
||||||
|
"all-roles": "View All Roles",
|
||||||
|
"standard-user": "Standard User",
|
||||||
|
"superadmin": "System Administrator (Admin)",
|
||||||
|
"all-plans": "View All Plans",
|
||||||
|
"plan-free": "Free",
|
||||||
|
"plan-plus": "Plus Plan",
|
||||||
|
"plan-premium": "Premium Plan",
|
||||||
|
"plan-past-due": "Past Due",
|
||||||
|
"plan-cancelled": "Cancelled",
|
||||||
|
"edit-user-title": "Edit User: {email}",
|
||||||
|
"user-role-field": "User Role",
|
||||||
|
"subscription-plan-field": "Subscription Plan",
|
||||||
|
"subscription-end-date": "Subscription End Date (Optional)",
|
||||||
|
"account-active-question": "Is the account active?"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"limits": {
|
"limits": {
|
||||||
|
|||||||
+99
-3
@@ -235,14 +235,21 @@
|
|||||||
"HTFT": "İlk Yarı / Maç Sonu",
|
"HTFT": "İlk Yarı / Maç Sonu",
|
||||||
"HT/FT": "İlk Yarı / Maç Sonu",
|
"HT/FT": "İlk Yarı / Maç Sonu",
|
||||||
"OE": "Tek / Çift",
|
"OE": "Tek / Çift",
|
||||||
"HT_OU05": "İlk Yarı 0.5 Gol"
|
"HT_OU05": "İlk Yarı 0.5 Gol",
|
||||||
|
"HT_OU15": "İlk Yarı 1.5 Gol",
|
||||||
|
"CARDS": "Kartlar 4.5",
|
||||||
|
"HCAP": "Handikap Sonucu"
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"summary-title": "Tahmin Özeti",
|
"summary-title": "Tahmin Özeti",
|
||||||
"summary-info": "Model sinyallerini ve belirsizlikleri sade şekilde gösterir.",
|
"summary-info": "Model sinyallerini ve belirsizlikleri sade şekilde gösterir.",
|
||||||
|
"model-signal-disclaimer": "Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi nedeniyle yanılabilir.",
|
||||||
"main-recommendation": "Öne Çıkan Sinyal",
|
"main-recommendation": "Öne Çıkan Sinyal",
|
||||||
"best-market-copy": "marketinde en güçlü seçim.",
|
"best-market-copy": "marketinde en güçlü seçim.",
|
||||||
"confidence-label": "Güven",
|
"confidence-label": "Güven",
|
||||||
|
"confidence-interval": "Güven Aralığı",
|
||||||
|
"confidence-interval-warning": "Güven aralığı geniş. Sinyal olsa bile tek başına oynanması önerilmez.",
|
||||||
|
"confidence-band": "Band",
|
||||||
"odds-label": "Oran",
|
"odds-label": "Oran",
|
||||||
"edge-label": "Teorik Avantaj",
|
"edge-label": "Teorik Avantaj",
|
||||||
"edge-info": "Model olasılığı ile piyasa olasılığı arasındaki teorik farktır; tutma garantisi veya kesin kazanç beklentisi değildir.",
|
"edge-info": "Model olasılığı ile piyasa olasılığı arasındaki teorik farktır; tutma garantisi veya kesin kazanç beklentisi değildir.",
|
||||||
@@ -253,8 +260,22 @@
|
|||||||
"playability-label": "Model sinyali",
|
"playability-label": "Model sinyali",
|
||||||
"quick-read": "Hızlı yorum",
|
"quick-read": "Hızlı yorum",
|
||||||
"lineup-source": "Kadronun Kaynağı",
|
"lineup-source": "Kadronun Kaynağı",
|
||||||
|
"lineup-confirmed-live": "Onaylı ilk 11",
|
||||||
|
"lineup-probable-xi": "Muhtemel ilk 11",
|
||||||
|
"unknown": "Bilinmiyor",
|
||||||
"model-label": "Model",
|
"model-label": "Model",
|
||||||
"engine-info": "Tahmini en çok hangi bileşenlerin etkilediğini gösterir.",
|
"engine-info": "Tahmini en çok hangi bileşenlerin etkilediğini gösterir.",
|
||||||
|
"engine-team-football": "Takım Gücü",
|
||||||
|
"engine-team-basketball": "Takım Formu",
|
||||||
|
"engine-player-football": "Oyuncu Etkisi",
|
||||||
|
"engine-player-basketball": "Kadro Etkisi",
|
||||||
|
"engine-odds": "Oran Analizi",
|
||||||
|
"engine-referee-football": "Hakem Etkisi",
|
||||||
|
"engine-referee-basketball": "Yardımcı Sinyaller",
|
||||||
|
"engine-label-high": "Yüksek",
|
||||||
|
"engine-label-medium": "Orta",
|
||||||
|
"engine-label-low": "Düşük",
|
||||||
|
"engine-label-very-low": "Çok Düşük",
|
||||||
"best-single-pick": "En Güçlü Sinyal",
|
"best-single-pick": "En Güçlü Sinyal",
|
||||||
"alternative-markets": "Alternatif Marketler",
|
"alternative-markets": "Alternatif Marketler",
|
||||||
"alternative-markets-info": "Ana tahmin dışındaki seçenekler.",
|
"alternative-markets-info": "Ana tahmin dışındaki seçenekler.",
|
||||||
@@ -264,7 +285,48 @@
|
|||||||
"all-markets-info": "Bütün seçenekleri tek tabloda karşılaştırır.",
|
"all-markets-info": "Bütün seçenekleri tek tabloda karşılaştırır.",
|
||||||
"market-board-info": "Modelin her markette gördüğü olasılık dağılımı.",
|
"market-board-info": "Modelin her markette gördüğü olasılık dağılımı.",
|
||||||
"bet-advice-info": "Modelin nihai aksiyon önerisi.",
|
"bet-advice-info": "Modelin nihai aksiyon önerisi.",
|
||||||
"recommended-stake-inline": "Önerilen miktar"
|
"recommended-stake-inline": "Önerilen miktar",
|
||||||
|
"model-probability-short": "Model",
|
||||||
|
"market-probability-short": "Piyasa",
|
||||||
|
"theoretical-edge-inline": "Teorik avantaj",
|
||||||
|
"playable": "Oynanabilir",
|
||||||
|
"risky": "Riskli",
|
||||||
|
"hit-probability": "Tutma Olasılığı",
|
||||||
|
"calibrated-confidence": "Kalibre Güven",
|
||||||
|
"score-scenario-football": "Skor Senaryosu",
|
||||||
|
"score-scenario-basketball": "Sayı Senaryosu",
|
||||||
|
"score-scenario-info-football": "Beklenen skor ve en olası senaryolar.",
|
||||||
|
"score-scenario-info-basketball": "Beklenen sayı dağılımı ve en olası maç senaryoları.",
|
||||||
|
"full-time-football": "Maç Sonu",
|
||||||
|
"full-time-basketball": "Maç Sonu Sayı",
|
||||||
|
"half-time-football": "İlk Yarı",
|
||||||
|
"half-time-basketball": "İlk Yarı Sayı",
|
||||||
|
"expected-total-football": "Toplam xG",
|
||||||
|
"expected-total-basketball": "Beklenen Toplam Sayı",
|
||||||
|
"live": "CANLI",
|
||||||
|
"pre-match-prediction": "Maç öncesi tahmin",
|
||||||
|
"prediction-contradictions": "Tahmin Çelişkileri",
|
||||||
|
"data-quality": "Veri Kalitesi",
|
||||||
|
"data-quality-info": "Kadro, oran ve maç verisinin ne kadar güvenilir olduğu.",
|
||||||
|
"risk-info": "Sürpriz ihtimali ve belirsizlik seviyesi.",
|
||||||
|
"risk-commentary": "Risk Yorumu",
|
||||||
|
"risk-default-comment": "Model bu maçta ekstra dikkat istiyor.",
|
||||||
|
"surprise-score": "Sürpriz skoru",
|
||||||
|
"match-commentary-title": "Maç Yorumu",
|
||||||
|
"match-commentary-info": "Modelin maç hakkındaki insan okunabilir özeti.",
|
||||||
|
"reasoning-info": "Modelin bu maçı neden bu şekilde okuduğunun üst seviye özeti.",
|
||||||
|
"bet-advice-play": "OYNA",
|
||||||
|
"bet-advice-pass": "OYNAMA",
|
||||||
|
"signal-tier-core": "Çekirdek",
|
||||||
|
"signal-tier-value": "Değer",
|
||||||
|
"signal-tier-lean": "Yorum",
|
||||||
|
"signal-tier-longshot": "Sürpriz",
|
||||||
|
"signal-tier-pass": "Pas",
|
||||||
|
"confidence-high": "Yüksek",
|
||||||
|
"confidence-medium": "Orta",
|
||||||
|
"confidence-low": "Düşük",
|
||||||
|
"confidence-unknown": "Belirsiz",
|
||||||
|
"info": "Bilgi"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"coupons": {
|
"coupons": {
|
||||||
@@ -312,6 +374,8 @@
|
|||||||
"coupon": "Kupon",
|
"coupon": "Kupon",
|
||||||
"candidate-match-count": "Aday Maç",
|
"candidate-match-count": "Aday Maç",
|
||||||
"candidate-match-count-help": "Kupon oluşturmak için şu anda uygun olan yaklaşan futbol maçı sayısı.",
|
"candidate-match-count-help": "Kupon oluşturmak için şu anda uygun olan yaklaşan futbol maçı sayısı.",
|
||||||
|
"finished-match-count": "Biten Maç",
|
||||||
|
"finished-match-count-help": "Biten futbol maçları için isteğe bağlı referans listesi. Bunlar kupon tahmininde asla kullanılmaz.",
|
||||||
"selected-match-count": "Seçilen Maç",
|
"selected-match-count": "Seçilen Maç",
|
||||||
"selected-match-count-help": "Maçları siz seçerseniz AI kuponu sadece bu havuzdan üretir.",
|
"selected-match-count-help": "Maçları siz seçerseniz AI kuponu sadece bu havuzdan üretir.",
|
||||||
"suggested-bet-count": "Önerilen Bahis",
|
"suggested-bet-count": "Önerilen Bahis",
|
||||||
@@ -323,12 +387,24 @@
|
|||||||
"candidate-pool-subtitle": "Kaynak: live_matches tablosu - spor: futbol - durum: başlamamış",
|
"candidate-pool-subtitle": "Kaynak: live_matches tablosu - spor: futbol - durum: başlamamış",
|
||||||
"match-count-suffix": "maç",
|
"match-count-suffix": "maç",
|
||||||
"upcoming-badge": "Yaklaşan",
|
"upcoming-badge": "Yaklaşan",
|
||||||
|
"upcoming-reference": "Yaklaşan havuz",
|
||||||
|
"finished-badge": "Bitti",
|
||||||
|
"prediction-locked": "Tahmine Kapalı",
|
||||||
|
"read-only-short": "Salt okunur",
|
||||||
"selected-short": "Seçildi",
|
"selected-short": "Seçildi",
|
||||||
"select-match": "Seç",
|
"select-match": "Seç",
|
||||||
|
"match-state": "Maç Durumu",
|
||||||
"selection-mode": "AI Havuzu",
|
"selection-mode": "AI Havuzu",
|
||||||
"manual-pool": "Manuel havuz",
|
"manual-pool": "Manuel havuz",
|
||||||
"auto-pool": "Otomatik havuz",
|
"auto-pool": "Otomatik havuz",
|
||||||
|
"finished-reference-only": "Sadece referans",
|
||||||
"no-upcoming-matches": "Şu anda kupon oluşturmaya uygun yaklaşan futbol maçı bulunmuyor.",
|
"no-upcoming-matches": "Şu anda kupon oluşturmaya uygun yaklaşan futbol maçı bulunmuyor.",
|
||||||
|
"finished-matches-title": "Biten Maçlar",
|
||||||
|
"finished-matches-help": "Bu maçlar sadece referans için gösterilir. Seçilemezler ve kupon tahmini oluşturulmadan önce backend tarafından filtrelenirler.",
|
||||||
|
"finished-matches-subtitle": "İsteğe bağlı arşiv görünümü. Skorlar ve maç sonu istatistikleri kupon tahmin akışına gönderilmez.",
|
||||||
|
"show-finished-matches": "Biten maçları göster",
|
||||||
|
"hide-finished-matches": "Biten maçları gizle",
|
||||||
|
"no-finished-matches": "Geçerli görünüm için biten futbol maçı bulunamadı.",
|
||||||
"manual-selection-active": "AI yalnızca aşağıda seçtiğiniz maçları kullanacak.",
|
"manual-selection-active": "AI yalnızca aşağıda seçtiğiniz maçları kullanacak.",
|
||||||
"automatic-selection-active": "Henüz manuel seçim yok. AI tüm yaklaşan maç havuzundan seçecek.",
|
"automatic-selection-active": "Henüz manuel seçim yok. AI tüm yaklaşan maç havuzundan seçecek.",
|
||||||
"selected-matches-panel-title": "Seçili Maç Havuzu",
|
"selected-matches-panel-title": "Seçili Maç Havuzu",
|
||||||
@@ -465,7 +541,9 @@
|
|||||||
"analytics": "Analitik Genel Bakış",
|
"analytics": "Analitik Genel Bakış",
|
||||||
"user-management": "Kullanıcı Yönetimi",
|
"user-management": "Kullanıcı Yönetimi",
|
||||||
"users": "Kullanıcılar",
|
"users": "Kullanıcılar",
|
||||||
|
"premium-users": "Premium Kullanıcı",
|
||||||
"settings": "Ayarlar",
|
"settings": "Ayarlar",
|
||||||
|
"subscription": "Abonelik",
|
||||||
"usage-limits": "Kullanım Limitleri",
|
"usage-limits": "Kullanım Limitleri",
|
||||||
"total-users": "Toplam Kullanıcı",
|
"total-users": "Toplam Kullanıcı",
|
||||||
"active-users": "Aktif Kullanıcı",
|
"active-users": "Aktif Kullanıcı",
|
||||||
@@ -485,7 +563,25 @@
|
|||||||
"user-email": "E-Posta",
|
"user-email": "E-Posta",
|
||||||
"user-role": "Rol",
|
"user-role": "Rol",
|
||||||
"user-status": "Durum",
|
"user-status": "Durum",
|
||||||
"no-users": "Kullanıcı bulunamadı."
|
"no-users": "Kullanıcı bulunamadı.",
|
||||||
|
"restricted": "Kısıtlı",
|
||||||
|
"admin-access-required": "Admin erişimi gerekli",
|
||||||
|
"admin-access-description": "Bu alan yalnızca superadmin hesapları tarafından kullanılabilir.",
|
||||||
|
"search-users-placeholder": "E-posta veya isim ara...",
|
||||||
|
"all-roles": "Tüm Rolleri Gör",
|
||||||
|
"standard-user": "Standart Kullanıcı",
|
||||||
|
"superadmin": "Sistem Yöneticisi (Admin)",
|
||||||
|
"all-plans": "Tüm Paketleri Gör",
|
||||||
|
"plan-free": "Ücretsiz (Free)",
|
||||||
|
"plan-plus": "Plus Paketi",
|
||||||
|
"plan-premium": "Premium Paketi",
|
||||||
|
"plan-past-due": "Ödeme Gecikti (Past Due)",
|
||||||
|
"plan-cancelled": "İptal Edildi (Cancelled)",
|
||||||
|
"edit-user-title": "Kullanıcı Düzenle: {email}",
|
||||||
|
"user-role-field": "Kullanıcı Rolü",
|
||||||
|
"subscription-plan-field": "Abonelik Paketi",
|
||||||
|
"subscription-end-date": "Abonelik Bitiş Tarihi (Opsiyonel)",
|
||||||
|
"account-active-question": "Hesap Aktif mi?"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"limits": {
|
"limits": {
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import {
|
|||||||
Separator,
|
Separator,
|
||||||
Input,
|
Input,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { NativeSelectRoot, NativeSelectField } from "@/components/ui/forms/native-select";
|
import {
|
||||||
|
NativeSelectRoot,
|
||||||
|
NativeSelectField,
|
||||||
|
} from "@/components/ui/forms/native-select";
|
||||||
import { useTranslations, useFormatter } from "next-intl";
|
import { useTranslations, useFormatter } from "next-intl";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
import {
|
import {
|
||||||
@@ -27,7 +30,13 @@ import {
|
|||||||
import { useAdminAnalytics, useAdminUsers } from "@/lib/api/admin/use-hooks";
|
import { useAdminAnalytics, useAdminUsers } from "@/lib/api/admin/use-hooks";
|
||||||
import type { AdminUserDto, AnalyticsOverviewDto } from "@/lib/api/admin/types";
|
import type { AdminUserDto, AnalyticsOverviewDto } from "@/lib/api/admin/types";
|
||||||
import { formatRoleLabel, isAdminRole } from "@/lib/auth/roles";
|
import { formatRoleLabel, isAdminRole } from "@/lib/auth/roles";
|
||||||
import { LuUsers, LuChartBar, LuActivity, LuShield, LuPencil } from "react-icons/lu";
|
import {
|
||||||
|
LuUsers,
|
||||||
|
LuChartBar,
|
||||||
|
LuActivity,
|
||||||
|
LuShield,
|
||||||
|
LuPencil,
|
||||||
|
} from "react-icons/lu";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { EditUserModal } from "./edit-user-modal";
|
import { EditUserModal } from "./edit-user-modal";
|
||||||
@@ -88,13 +97,19 @@ export default function AdminContent() {
|
|||||||
const format = useFormatter();
|
const format = useFormatter();
|
||||||
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
|
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
|
||||||
const [editingUser, setEditingUser] = useState<AdminUserDto | null>(null);
|
const [editingUser, setEditingUser] = useState<AdminUserDto | null>(null);
|
||||||
const [searchParams, setSearchParams] = useState({ search: "", role: "", subscriptionStatus: "", page: 1, limit: 10 });
|
const [searchParams, setSearchParams] = useState({
|
||||||
|
search: "",
|
||||||
|
role: "",
|
||||||
|
subscriptionStatus: "",
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = setTimeout(() => {
|
const handler = setTimeout(() => {
|
||||||
setDebouncedSearch(searchParams.search);
|
setDebouncedSearch(searchParams.search);
|
||||||
setSearchParams(prev => ({ ...prev, page: 1 }));
|
setSearchParams((prev) => ({ ...prev, page: 1 }));
|
||||||
}, 500);
|
}, 500);
|
||||||
return () => clearTimeout(handler);
|
return () => clearTimeout(handler);
|
||||||
}, [searchParams.search]);
|
}, [searchParams.search]);
|
||||||
@@ -113,7 +128,7 @@ export default function AdminContent() {
|
|||||||
role: searchParams.role,
|
role: searchParams.role,
|
||||||
subscriptionStatus: searchParams.subscriptionStatus,
|
subscriptionStatus: searchParams.subscriptionStatus,
|
||||||
page: searchParams.page,
|
page: searchParams.page,
|
||||||
limit: searchParams.limit
|
limit: searchParams.limit,
|
||||||
},
|
},
|
||||||
canAccessAdmin,
|
canAccessAdmin,
|
||||||
);
|
);
|
||||||
@@ -150,13 +165,13 @@ export default function AdminContent() {
|
|||||||
<VStack gap={3}>
|
<VStack gap={3}>
|
||||||
<Badge colorPalette="red" variant="subtle" borderRadius="full">
|
<Badge colorPalette="red" variant="subtle" borderRadius="full">
|
||||||
<LuShield />
|
<LuShield />
|
||||||
Restricted
|
{t("restricted")}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Heading as="h2" size="md">
|
<Heading as="h2" size="md">
|
||||||
Admin access required
|
{t("admin-access-required")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text color="fg.muted" textAlign="center" maxW="md">
|
<Text color="fg.muted" textAlign="center" maxW="md">
|
||||||
This area is only available to superadmin accounts.
|
{t("admin-access-description")}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
@@ -236,7 +251,7 @@ export default function AdminContent() {
|
|||||||
</StaggerItem>
|
</StaggerItem>
|
||||||
<StaggerItem>
|
<StaggerItem>
|
||||||
<AdminStat
|
<AdminStat
|
||||||
label={t("premium-users", { fallback: "Premium Users" })}
|
label={t("premium-users")}
|
||||||
value={analytics?.users?.premium ?? 0}
|
value={analytics?.users?.premium ?? 0}
|
||||||
icon={<LuShield />}
|
icon={<LuShield />}
|
||||||
colorPalette="purple"
|
colorPalette="purple"
|
||||||
@@ -272,32 +287,49 @@ export default function AdminContent() {
|
|||||||
<Card.Body py={4}>
|
<Card.Body py={4}>
|
||||||
<SimpleGrid columns={{ base: 1, md: 3 }} gap={4}>
|
<SimpleGrid columns={{ base: 1, md: 3 }} gap={4}>
|
||||||
<Input
|
<Input
|
||||||
placeholder="E-posta veya isim ara..."
|
placeholder={t("search-users-placeholder")}
|
||||||
value={searchParams.search}
|
value={searchParams.search}
|
||||||
onChange={(e) => setSearchParams({ ...searchParams, search: e.target.value })}
|
onChange={(e) =>
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
search: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<NativeSelectRoot>
|
<NativeSelectRoot>
|
||||||
<NativeSelectField
|
<NativeSelectField
|
||||||
placeholder="Tüm Rolleri Gör"
|
placeholder={t("all-roles")}
|
||||||
value={searchParams.role}
|
value={searchParams.role}
|
||||||
onChange={(e) => setSearchParams({ ...searchParams, role: e.target.value, page: 1 })}
|
onChange={(e) =>
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
role: e.target.value,
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
items={[
|
items={[
|
||||||
{ label: "Standart Kullanıcı", value: "user" },
|
{ label: t("standard-user"), value: "user" },
|
||||||
{ label: "Admin", value: "superadmin" }
|
{ label: t("superadmin"), value: "superadmin" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</NativeSelectRoot>
|
</NativeSelectRoot>
|
||||||
<NativeSelectRoot>
|
<NativeSelectRoot>
|
||||||
<NativeSelectField
|
<NativeSelectField
|
||||||
placeholder="Tüm Paketleri Gör"
|
placeholder={t("all-plans")}
|
||||||
value={searchParams.subscriptionStatus}
|
value={searchParams.subscriptionStatus}
|
||||||
onChange={(e) => setSearchParams({ ...searchParams, subscriptionStatus: e.target.value, page: 1 })}
|
onChange={(e) =>
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
subscriptionStatus: e.target.value,
|
||||||
|
page: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
items={[
|
items={[
|
||||||
{ label: "Ücretsiz (Free)", value: "free" },
|
{ label: t("plan-free"), value: "free" },
|
||||||
{ label: "Plus", value: "plus" },
|
{ label: "Plus", value: "plus" },
|
||||||
{ label: "Premium", value: "premium" },
|
{ label: "Premium", value: "premium" },
|
||||||
{ label: "Gecikmiş", value: "past_due" },
|
{ label: t("plan-past-due"), value: "past_due" },
|
||||||
{ label: "İptal", value: "cancelled" }
|
{ label: t("plan-cancelled"), value: "cancelled" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</NativeSelectRoot>
|
</NativeSelectRoot>
|
||||||
@@ -310,7 +342,11 @@ export default function AdminContent() {
|
|||||||
<Spinner size="lg" color="primary.500" />
|
<Spinner size="lg" color="primary.500" />
|
||||||
</Flex>
|
</Flex>
|
||||||
) : users.length > 0 ? (
|
) : users.length > 0 ? (
|
||||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
<Card.Root
|
||||||
|
bg={cardBg}
|
||||||
|
borderColor={borderColor}
|
||||||
|
borderRadius="xl"
|
||||||
|
>
|
||||||
<Card.Body>
|
<Card.Body>
|
||||||
<VStack gap={0} align="stretch">
|
<VStack gap={0} align="stretch">
|
||||||
{/* Table Header */}
|
{/* Table Header */}
|
||||||
@@ -330,7 +366,7 @@ export default function AdminContent() {
|
|||||||
{t("user-role")}
|
{t("user-role")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text flex={1} textAlign="center">
|
<Text flex={1} textAlign="center">
|
||||||
{t("subscription", { fallback: "Subscription" })}
|
{t("subscription")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text flex={1} textAlign="center">
|
<Text flex={1} textAlign="center">
|
||||||
{t("user-status")}
|
{t("user-status")}
|
||||||
@@ -357,7 +393,12 @@ export default function AdminContent() {
|
|||||||
>
|
>
|
||||||
{getUserDisplayName(user)}
|
{getUserDisplayName(user)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
|
<Text
|
||||||
|
flex={2}
|
||||||
|
fontSize="sm"
|
||||||
|
color="fg.muted"
|
||||||
|
truncate
|
||||||
|
>
|
||||||
{user.email}
|
{user.email}
|
||||||
</Text>
|
</Text>
|
||||||
<Flex flex={1} justify="center">
|
<Flex flex={1} justify="center">
|
||||||
@@ -372,9 +413,20 @@ export default function AdminContent() {
|
|||||||
{formatRoleLabel(user.role)}
|
{formatRoleLabel(user.role)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex flex={1} justify="center" direction="column" align="center" gap={1}>
|
<Flex
|
||||||
|
flex={1}
|
||||||
|
justify="center"
|
||||||
|
direction="column"
|
||||||
|
align="center"
|
||||||
|
gap={1}
|
||||||
|
>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={user.subscriptionStatus === "premium" || user.subscriptionStatus === "plus" ? "purple" : "gray"}
|
colorPalette={
|
||||||
|
user.subscriptionStatus === "premium" ||
|
||||||
|
user.subscriptionStatus === "plus"
|
||||||
|
? "purple"
|
||||||
|
: "gray"
|
||||||
|
}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
@@ -384,7 +436,14 @@ export default function AdminContent() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
{user.subscriptionExpiresAt ? (
|
{user.subscriptionExpiresAt ? (
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
{format.dateTime(new Date(user.subscriptionExpiresAt), { year: 'numeric', month: '2-digit', day: '2-digit' })}
|
{format.dateTime(
|
||||||
|
new Date(user.subscriptionExpiresAt),
|
||||||
|
{
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
},
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
@@ -436,25 +495,45 @@ export default function AdminContent() {
|
|||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
{meta && meta.totalPages > 1 && (
|
{meta && meta.totalPages > 1 && (
|
||||||
<Flex justify="center" pt={4} pb={2} gap={2} borderTopWidth="1px" borderColor={borderColor} mt={2}>
|
<Flex
|
||||||
|
justify="center"
|
||||||
|
pt={4}
|
||||||
|
pb={2}
|
||||||
|
gap={2}
|
||||||
|
borderTopWidth="1px"
|
||||||
|
borderColor={borderColor}
|
||||||
|
mt={2}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!meta.hasPreviousPage}
|
disabled={!meta.hasPreviousPage}
|
||||||
onClick={() => setSearchParams({ ...searchParams, page: meta.page - 1 })}
|
onClick={() =>
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
page: meta.page - 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Önceki
|
{tCommon("previous")}
|
||||||
</Button>
|
</Button>
|
||||||
<Flex align="center" gap={2} fontSize="sm">
|
<Flex align="center" gap={2} fontSize="sm">
|
||||||
<Text>Sayfa {meta.page} / {meta.totalPages}</Text>
|
<Text>
|
||||||
|
{tCommon("page")} {meta.page} / {meta.totalPages}
|
||||||
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={!meta.hasNextPage}
|
disabled={!meta.hasNextPage}
|
||||||
onClick={() => setSearchParams({ ...searchParams, page: meta.page + 1 })}
|
onClick={() =>
|
||||||
|
setSearchParams({
|
||||||
|
...searchParams,
|
||||||
|
page: meta.page + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Sonraki
|
{tCommon("next")}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Button, VStack, Input } from "@chakra-ui/react";
|
||||||
Button,
|
|
||||||
VStack,
|
|
||||||
Input,
|
|
||||||
} from "@chakra-ui/react";
|
|
||||||
import {
|
import {
|
||||||
DialogRoot,
|
DialogRoot,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -15,11 +11,14 @@ import {
|
|||||||
DialogCloseTrigger,
|
DialogCloseTrigger,
|
||||||
} from "@/components/ui/overlays/dialog";
|
} from "@/components/ui/overlays/dialog";
|
||||||
import { Field } from "@/components/ui/forms/field";
|
import { Field } from "@/components/ui/forms/field";
|
||||||
import { NativeSelectRoot, NativeSelectField } from "@/components/ui/forms/native-select";
|
import {
|
||||||
|
NativeSelectRoot,
|
||||||
|
NativeSelectField,
|
||||||
|
} from "@/components/ui/forms/native-select";
|
||||||
import { Switch } from "@/components/ui/forms/switch";
|
import { Switch } from "@/components/ui/forms/switch";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { AdminUserDto } from "@/lib/api/admin/types";
|
import { AdminUserDto } from "@/lib/api/admin/types";
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
useUpdateUserRole,
|
useUpdateUserRole,
|
||||||
useUpdateUserSubscription,
|
useUpdateUserSubscription,
|
||||||
@@ -33,52 +32,73 @@ interface EditUserModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) {
|
export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) {
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditUserModalContent
|
||||||
|
key={user.id}
|
||||||
|
user={user}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateInputValue(value?: string | null): string {
|
||||||
|
if (!value) return "";
|
||||||
|
try {
|
||||||
|
return new Date(value).toISOString().split("T")[0];
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditUserModalContent({
|
||||||
|
user,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
user: AdminUserDto;
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
const t = useTranslations("admin");
|
const t = useTranslations("admin");
|
||||||
const tCommon = useTranslations("common");
|
const tCommon = useTranslations("common");
|
||||||
|
|
||||||
const [role, setRole] = useState("user");
|
const [role, setRole] = useState(user.role || "user");
|
||||||
const [plan, setPlan] = useState("free");
|
const [plan, setPlan] = useState(user.subscriptionStatus || "free");
|
||||||
const [expiresAt, setExpiresAt] = useState<string>("");
|
const [expiresAt, setExpiresAt] = useState<string>(
|
||||||
const [isActive, setIsActive] = useState(true);
|
formatDateInputValue(user.subscriptionExpiresAt),
|
||||||
|
);
|
||||||
|
const [isActive, setIsActive] = useState(user.isActive);
|
||||||
|
|
||||||
const { mutateAsync: updateRole, isPending: rolePending } = useUpdateUserRole();
|
const { mutateAsync: updateRole, isPending: rolePending } =
|
||||||
const { mutateAsync: updateSub, isPending: subPending } = useUpdateUserSubscription();
|
useUpdateUserRole();
|
||||||
const { mutateAsync: toggleActive, isPending: togglePending } = useToggleUserActive();
|
const { mutateAsync: updateSub, isPending: subPending } =
|
||||||
|
useUpdateUserSubscription();
|
||||||
useEffect(() => {
|
const { mutateAsync: toggleActive, isPending: togglePending } =
|
||||||
if (user) {
|
useToggleUserActive();
|
||||||
setRole(user.role || "user");
|
|
||||||
setPlan(user.subscriptionStatus || "free");
|
|
||||||
setIsActive(user.isActive);
|
|
||||||
if (user.subscriptionExpiresAt) {
|
|
||||||
try {
|
|
||||||
const date = new Date(user.subscriptionExpiresAt);
|
|
||||||
setExpiresAt(date.toISOString().split('T')[0]);
|
|
||||||
} catch(e) { setExpiresAt(""); }
|
|
||||||
} else {
|
|
||||||
setExpiresAt("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [user]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!user) return;
|
|
||||||
try {
|
try {
|
||||||
if (role !== user.role) {
|
if (role !== user.role) {
|
||||||
await updateRole({ id: user.id, dto: { role } });
|
await updateRole({ id: user.id, dto: { role } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentExpiresAtStr = user.subscriptionExpiresAt
|
const currentExpiresAtStr = user.subscriptionExpiresAt
|
||||||
? new Date(user.subscriptionExpiresAt).toISOString().split('T')[0]
|
? new Date(user.subscriptionExpiresAt).toISOString().split("T")[0]
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
if (plan !== user.subscriptionStatus || expiresAt !== currentExpiresAtStr) {
|
if (
|
||||||
|
plan !== user.subscriptionStatus ||
|
||||||
|
expiresAt !== currentExpiresAtStr
|
||||||
|
) {
|
||||||
await updateSub({
|
await updateSub({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
dto: {
|
dto: {
|
||||||
plan,
|
plan,
|
||||||
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null
|
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (isActive !== user.isActive) {
|
if (isActive !== user.isActive) {
|
||||||
@@ -92,57 +112,64 @@ export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) {
|
|||||||
|
|
||||||
const isPending = rolePending || subPending || togglePending;
|
const isPending = rolePending || subPending || togglePending;
|
||||||
|
|
||||||
if (!user) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogRoot open={isOpen} onOpenChange={(e) => !e.open && onClose()}>
|
<DialogRoot open={isOpen} onOpenChange={(e) => !e.open && onClose()}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Kullanıcı Düzenle: {user.email}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{t("edit-user-title", { email: user.email })}
|
||||||
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<VStack gap={4} align="stretch">
|
<VStack gap={4} align="stretch">
|
||||||
<Field label="Kullanıcı Rolü">
|
<Field label={t("user-role-field")}>
|
||||||
<NativeSelectRoot>
|
<NativeSelectRoot>
|
||||||
<NativeSelectField
|
<NativeSelectField
|
||||||
value={role}
|
value={role}
|
||||||
onChange={(e) => setRole(e.target.value)}
|
onChange={(e) => setRole(e.target.value)}
|
||||||
items={[
|
items={[
|
||||||
{ label: "Standart Kullanıcı", value: "user" },
|
{ label: t("standard-user"), value: "user" },
|
||||||
{ label: "Sistem Yöneticisi (Admin)", value: "superadmin" },
|
{ label: t("superadmin"), value: "superadmin" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</NativeSelectRoot>
|
</NativeSelectRoot>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field label="Abonelik Paketi">
|
<Field label={t("subscription-plan-field")}>
|
||||||
<NativeSelectRoot>
|
<NativeSelectRoot>
|
||||||
<NativeSelectField
|
<NativeSelectField
|
||||||
value={plan}
|
value={plan}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newPlan = e.target.value;
|
const newPlan = e.target.value;
|
||||||
setPlan(newPlan);
|
setPlan(newPlan);
|
||||||
if ((newPlan === "premium" || newPlan === "plus") && !expiresAt) {
|
if (
|
||||||
|
(newPlan === "premium" || newPlan === "plus") &&
|
||||||
|
!expiresAt
|
||||||
|
) {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
d.setDate(d.getDate() + 30);
|
d.setDate(d.getDate() + 30);
|
||||||
setExpiresAt(d.toISOString().split('T')[0]);
|
setExpiresAt(d.toISOString().split("T")[0]);
|
||||||
} else if (newPlan === "free" || newPlan === "cancelled" || newPlan === "past_due") {
|
} else if (
|
||||||
|
newPlan === "free" ||
|
||||||
|
newPlan === "cancelled" ||
|
||||||
|
newPlan === "past_due"
|
||||||
|
) {
|
||||||
setExpiresAt("");
|
setExpiresAt("");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
items={[
|
items={[
|
||||||
{ label: "Ücretsiz (Free)", value: "free" },
|
{ label: t("plan-free"), value: "free" },
|
||||||
{ label: "Plus Paketi", value: "plus" },
|
{ label: t("plan-plus"), value: "plus" },
|
||||||
{ label: "Premium Paketi", value: "premium" },
|
{ label: t("plan-premium"), value: "premium" },
|
||||||
{ label: "Ödeme Gecikti (Past Due)", value: "past_due" },
|
{ label: t("plan-past-due"), value: "past_due" },
|
||||||
{ label: "İptal Edildi (Cancelled)", value: "cancelled" },
|
{ label: t("plan-cancelled"), value: "cancelled" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</NativeSelectRoot>
|
</NativeSelectRoot>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{plan !== "free" && (
|
{plan !== "free" && (
|
||||||
<Field label="Abonelik Bitiş Tarihi (Opsiyonel)">
|
<Field label={t("subscription-end-date")}>
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
value={expiresAt}
|
value={expiresAt}
|
||||||
@@ -151,19 +178,26 @@ export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) {
|
|||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Field label="Hesap Aktif mi?">
|
<Field label={t("account-active-question")}>
|
||||||
<Switch checked={isActive} onCheckedChange={(e) => setIsActive(e.checked)}>
|
<Switch
|
||||||
{isActive ? "Aktif" : "Pasif"}
|
checked={isActive}
|
||||||
|
onCheckedChange={(e) => setIsActive(e.checked)}
|
||||||
|
>
|
||||||
|
{isActive ? tCommon("active") : tCommon("inactive")}
|
||||||
</Switch>
|
</Switch>
|
||||||
</Field>
|
</Field>
|
||||||
</VStack>
|
</VStack>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={onClose} disabled={isPending}>
|
<Button variant="outline" onClick={onClose} disabled={isPending}>
|
||||||
İptal
|
{tCommon("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button colorPalette="primary" onClick={handleSave} loading={isPending}>
|
<Button
|
||||||
Kaydet
|
colorPalette="primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
loading={isPending}
|
||||||
|
>
|
||||||
|
{tCommon("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
<DialogCloseTrigger />
|
<DialogCloseTrigger />
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import { useTranslations } from "next-intl";
|
|||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
import { SlideUp } from "@/components/motion";
|
import { SlideUp } from "@/components/motion";
|
||||||
import { useSearchTeams, useHeadToHead } from "@/lib/api/leagues/use-hooks";
|
import { useSearchTeams, useHeadToHead } from "@/lib/api/leagues/use-hooks";
|
||||||
import type { TeamDto, HeadToHeadDto } from "@/lib/api/leagues/types";
|
import type { TeamDto } from "@/lib/api/leagues/types";
|
||||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||||
import { LuSearch, LuArrowLeftRight } from "react-icons/lu";
|
import { LuSearch, LuArrowLeftRight } from "react-icons/lu";
|
||||||
import { useState, useEffect } from "react";
|
import { useState } from "react";
|
||||||
import { useDebounce } from "@/hooks/use-debounce";
|
import { useDebounce } from "@/hooks/use-debounce";
|
||||||
|
|
||||||
function TeamSearchInput({
|
function TeamSearchInput({
|
||||||
@@ -134,7 +134,7 @@ export default function H2HContent() {
|
|||||||
?.data
|
?.data
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
label: team1?.name || t("team1"),
|
label: team1?.name || t("team-1"),
|
||||||
value: h2h.data.data.team1Wins,
|
value: h2h.data.data.team1Wins,
|
||||||
color: "green",
|
color: "green",
|
||||||
},
|
},
|
||||||
@@ -144,7 +144,7 @@ export default function H2HContent() {
|
|||||||
color: "gray",
|
color: "gray",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: team2?.name || t("team2"),
|
label: team2?.name || t("team-2"),
|
||||||
value: h2h.data.data.team2Wins,
|
value: h2h.data.data.team2Wins,
|
||||||
color: "blue",
|
color: "blue",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,6 +46,16 @@ interface PredictionCardProps {
|
|||||||
prediction: MatchPredictionDto;
|
prediction: MatchPredictionDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PredictionUiMessages = Record<string, string>;
|
||||||
|
|
||||||
|
function getUiText(
|
||||||
|
ui: PredictionUiMessages | undefined,
|
||||||
|
key: string,
|
||||||
|
fallback: string,
|
||||||
|
): string {
|
||||||
|
return ui?.[key] || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function formatReasonFallback(reason: string): string {
|
function formatReasonFallback(reason: string): string {
|
||||||
if (reason.startsWith("risk:")) return formatReasonFallback(reason.slice(5));
|
if (reason.startsWith("risk:")) return formatReasonFallback(reason.slice(5));
|
||||||
const evMatch = reason.match(/^ev_edge_([+\-][\d.]+%)_grade_(\w)$/);
|
const evMatch = reason.match(/^ev_edge_([+\-][\d.]+%)_grade_(\w)$/);
|
||||||
@@ -158,16 +168,16 @@ function getEngineLabelPalette(label?: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEngineLabelText(label?: string): string {
|
function getEngineLabelText(label?: string, ui?: PredictionUiMessages): string {
|
||||||
switch ((label || "").toUpperCase()) {
|
switch ((label || "").toUpperCase()) {
|
||||||
case "YUKSEK":
|
case "YUKSEK":
|
||||||
return "Yüksek";
|
return getUiText(ui, "engine-label-high", "Yüksek");
|
||||||
case "ORTA":
|
case "ORTA":
|
||||||
return "Orta";
|
return getUiText(ui, "engine-label-medium", "Orta");
|
||||||
case "DUSUK":
|
case "DUSUK":
|
||||||
return "Düşük";
|
return getUiText(ui, "engine-label-low", "Düşük");
|
||||||
case "COK_DUSUK":
|
case "COK_DUSUK":
|
||||||
return "Çok Düşük";
|
return getUiText(ui, "engine-label-very-low", "Çok Düşük");
|
||||||
default:
|
default:
|
||||||
return label || "";
|
return label || "";
|
||||||
}
|
}
|
||||||
@@ -214,23 +224,30 @@ function getConfidenceBandPalette(band?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfidenceBandLabel(band?: string) {
|
function getConfidenceBandLabel(band?: string, ui?: PredictionUiMessages) {
|
||||||
switch ((band || "").toUpperCase()) {
|
switch ((band || "").toUpperCase()) {
|
||||||
case "HIGH":
|
case "HIGH":
|
||||||
return "Yüksek";
|
return getUiText(ui, "confidence-high", "Yüksek");
|
||||||
case "MEDIUM":
|
case "MEDIUM":
|
||||||
return "Orta";
|
return getUiText(ui, "confidence-medium", "Orta");
|
||||||
case "LOW":
|
case "LOW":
|
||||||
return "Düşük";
|
return getUiText(ui, "confidence-low", "Düşük");
|
||||||
default:
|
default:
|
||||||
return "Belirsiz";
|
return getUiText(ui, "confidence-unknown", "Belirsiz");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLineupSourceLabel(source?: string): string {
|
function getLineupSourceLabel(
|
||||||
if (source === "confirmed_live") return "Onayli ilk 11";
|
source?: string,
|
||||||
if (source === "probable_xi") return "Muhtemel ilk 11";
|
ui?: PredictionUiMessages,
|
||||||
return source ? formatReasonFallback(source) : "Bilinmiyor";
|
): string {
|
||||||
|
if (source === "confirmed_live")
|
||||||
|
return getUiText(ui, "lineup-confirmed-live", "Onaylı ilk 11");
|
||||||
|
if (source === "probable_xi")
|
||||||
|
return getUiText(ui, "lineup-probable-xi", "Muhtemel ilk 11");
|
||||||
|
return source
|
||||||
|
? formatReasonFallback(source)
|
||||||
|
: getUiText(ui, "unknown", "Bilinmiyor");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatInterval(
|
function formatInterval(
|
||||||
@@ -359,22 +376,28 @@ function getSignalTierPalette(tier?: SignalTier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSignalTierLabel(tier?: SignalTier) {
|
function getSignalTierLabel(tier?: SignalTier, ui?: PredictionUiMessages) {
|
||||||
switch (tier) {
|
switch (tier) {
|
||||||
case "CORE":
|
case "CORE":
|
||||||
return "Çekirdek";
|
return getUiText(ui, "signal-tier-core", "Çekirdek");
|
||||||
case "VALUE":
|
case "VALUE":
|
||||||
return "Değer";
|
return getUiText(ui, "signal-tier-value", "Değer");
|
||||||
case "LEAN":
|
case "LEAN":
|
||||||
return "Yorum";
|
return getUiText(ui, "signal-tier-lean", "Yorum");
|
||||||
case "LONGSHOT":
|
case "LONGSHOT":
|
||||||
return "Sürpriz";
|
return getUiText(ui, "signal-tier-longshot", "Sürpriz");
|
||||||
default:
|
default:
|
||||||
return "Pas";
|
return getUiText(ui, "signal-tier-pass", "Pas");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TooltipIcon({ content }: { content: string }) {
|
function TooltipIcon({
|
||||||
|
content,
|
||||||
|
ariaLabel = "Bilgi",
|
||||||
|
}: {
|
||||||
|
content: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={content}
|
content={content}
|
||||||
@@ -383,7 +406,7 @@ function TooltipIcon({ content }: { content: string }) {
|
|||||||
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
|
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="Bilgi"
|
aria-label={ariaLabel}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="2xs"
|
size="2xs"
|
||||||
colorPalette="gray"
|
colorPalette="gray"
|
||||||
@@ -498,9 +521,14 @@ function ReasonList({
|
|||||||
function ProbabilitySplit({
|
function ProbabilitySplit({
|
||||||
modelProb,
|
modelProb,
|
||||||
impliedProb,
|
impliedProb,
|
||||||
|
labels,
|
||||||
}: {
|
}: {
|
||||||
modelProb: number;
|
modelProb: number;
|
||||||
impliedProb: number;
|
impliedProb: number;
|
||||||
|
labels: {
|
||||||
|
model: string;
|
||||||
|
market: string;
|
||||||
|
};
|
||||||
}) {
|
}) {
|
||||||
const trackBg = useColorModeValue("gray.100", "gray.700");
|
const trackBg = useColorModeValue("gray.100", "gray.700");
|
||||||
if (!impliedProb || impliedProb <= 0) return null;
|
if (!impliedProb || impliedProb <= 0) return null;
|
||||||
@@ -508,10 +536,10 @@ function ProbabilitySplit({
|
|||||||
<VStack align="stretch" gap={2}>
|
<VStack align="stretch" gap={2}>
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Text fontSize="xs" color="blue.600" fontWeight="semibold">
|
<Text fontSize="xs" color="blue.600" fontWeight="semibold">
|
||||||
Model {formatProbability(modelProb, 0)}
|
{labels.model} {formatProbability(modelProb, 0)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="orange.500" fontWeight="semibold">
|
<Text fontSize="xs" color="orange.500" fontWeight="semibold">
|
||||||
Piyasa {formatProbability(impliedProb, 0)}
|
{labels.market} {formatProbability(impliedProb, 0)}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box position="relative">
|
<Box position="relative">
|
||||||
@@ -538,6 +566,7 @@ function PickCard({
|
|||||||
palette,
|
palette,
|
||||||
marketLabels,
|
marketLabels,
|
||||||
labels,
|
labels,
|
||||||
|
ui,
|
||||||
}: {
|
}: {
|
||||||
pick: MatchPickDto;
|
pick: MatchPickDto;
|
||||||
stakeFallback?: number;
|
stakeFallback?: number;
|
||||||
@@ -545,12 +574,19 @@ function PickCard({
|
|||||||
resolveReason: (reason: string) => string;
|
resolveReason: (reason: string) => string;
|
||||||
palette: string;
|
palette: string;
|
||||||
marketLabels?: Record<string, string>;
|
marketLabels?: Record<string, string>;
|
||||||
|
ui?: PredictionUiMessages;
|
||||||
labels: {
|
labels: {
|
||||||
confidence: string;
|
confidence: string;
|
||||||
odds: string;
|
odds: string;
|
||||||
recommendedStake: string;
|
recommendedStake: string;
|
||||||
playScore: string;
|
playScore: string;
|
||||||
playability: string;
|
playability: string;
|
||||||
|
confidenceInterval: string;
|
||||||
|
confidenceBand: string;
|
||||||
|
confidenceIntervalWarning: string;
|
||||||
|
theoreticalEdgeInline: string;
|
||||||
|
modelProbability: string;
|
||||||
|
marketProbability: string;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const bg = useColorModeValue(`${palette}.50`, `${palette}.950`);
|
const bg = useColorModeValue(`${palette}.50`, `${palette}.950`);
|
||||||
@@ -591,16 +627,16 @@ function PickCard({
|
|||||||
colorPalette={getSignalTierPalette(pick.signal_tier)}
|
colorPalette={getSignalTierPalette(pick.signal_tier)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{getSignalTierLabel(pick.signal_tier)}
|
{getSignalTierLabel(pick.signal_tier, ui)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorPalette={confidenceBandPalette} variant="subtle">
|
<Badge colorPalette={confidenceBandPalette} variant="subtle">
|
||||||
{getConfidenceBandLabel(pick.confidence_interval?.band)}
|
{getConfidenceBandLabel(pick.confidence_interval?.band, ui)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={getEdgePalette(pick.ev_edge)}
|
colorPalette={getEdgePalette(pick.ev_edge)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
Teorik avantaj {formatEdgeSignal(pick.ev_edge)}
|
{labels.theoreticalEdgeInline} {formatEdgeSignal(pick.ev_edge)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -619,12 +655,12 @@ function PickCard({
|
|||||||
value={formatSignalScore(pick.play_score)}
|
value={formatSignalScore(pick.play_score)}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Guven Araligi"
|
label={labels.confidenceInterval}
|
||||||
value={formatInterval(pick.confidence_interval)}
|
value={formatInterval(pick.confidence_interval)}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Band"
|
label={labels.confidenceBand}
|
||||||
value={getConfidenceBandLabel(pick.confidence_interval?.band)}
|
value={getConfidenceBandLabel(pick.confidence_interval?.band, ui)}
|
||||||
accent={`${confidenceBandPalette}.500`}
|
accent={`${confidenceBandPalette}.500`}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -632,6 +668,10 @@ function PickCard({
|
|||||||
<ProbabilitySplit
|
<ProbabilitySplit
|
||||||
modelProb={pick.probability}
|
modelProb={pick.probability}
|
||||||
impliedProb={pick.implied_prob}
|
impliedProb={pick.implied_prob}
|
||||||
|
labels={{
|
||||||
|
model: labels.modelProbability,
|
||||||
|
market: labels.marketProbability,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<HStack justify="space-between" mb={1.5}>
|
<HStack justify="space-between" mb={1.5}>
|
||||||
@@ -661,8 +701,7 @@ function PickCard({
|
|||||||
borderColor={intervalWarningBorder}
|
borderColor={intervalWarningBorder}
|
||||||
>
|
>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi
|
{labels.confidenceIntervalWarning}
|
||||||
onerilmez.
|
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -676,11 +715,13 @@ function SummaryTable({
|
|||||||
marketLabels,
|
marketLabels,
|
||||||
title,
|
title,
|
||||||
info,
|
info,
|
||||||
|
ui,
|
||||||
}: {
|
}: {
|
||||||
items: MatchBetSummaryItemDto[];
|
items: MatchBetSummaryItemDto[];
|
||||||
marketLabels?: Record<string, string>;
|
marketLabels?: Record<string, string>;
|
||||||
title: string;
|
title: string;
|
||||||
info: string;
|
info: string;
|
||||||
|
ui?: PredictionUiMessages;
|
||||||
}) {
|
}) {
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||||
@@ -728,7 +769,7 @@ function SummaryTable({
|
|||||||
colorPalette={getSignalTierPalette(item.signal_tier)}
|
colorPalette={getSignalTierPalette(item.signal_tier)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{getSignalTierLabel(item.signal_tier)}
|
{getSignalTierLabel(item.signal_tier, ui)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text fontWeight="semibold">
|
<Text fontWeight="semibold">
|
||||||
{getMarketLabel(item.market, marketLabels)}
|
{getMarketLabel(item.market, marketLabels)}
|
||||||
@@ -753,7 +794,7 @@ function SummaryTable({
|
|||||||
)}
|
)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{getConfidenceBandLabel(item.confidence_interval?.band)}
|
{getConfidenceBandLabel(item.confidence_interval?.band, ui)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="surface">
|
<Badge variant="surface">
|
||||||
{formatUnits(item.stake_units)}
|
{formatUnits(item.stake_units)}
|
||||||
@@ -812,7 +853,6 @@ function SummaryTable({
|
|||||||
<Badge variant="surface">{formatUnits(item.stake_units)}</Badge>
|
<Badge variant="surface">{formatUnits(item.stake_units)}</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Flex> */}
|
</Flex> */}
|
||||||
|
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
@@ -825,12 +865,14 @@ function MarketBoardSection({
|
|||||||
marketLabels,
|
marketLabels,
|
||||||
title,
|
title,
|
||||||
info,
|
info,
|
||||||
|
ui,
|
||||||
}: {
|
}: {
|
||||||
marketBoard?: Record<string, MarketBoardEntryDto>;
|
marketBoard?: Record<string, MarketBoardEntryDto>;
|
||||||
betSummary?: MatchBetSummaryItemDto[];
|
betSummary?: MatchBetSummaryItemDto[];
|
||||||
marketLabels?: Record<string, string>;
|
marketLabels?: Record<string, string>;
|
||||||
title: string;
|
title: string;
|
||||||
info: string;
|
info: string;
|
||||||
|
ui?: PredictionUiMessages;
|
||||||
}) {
|
}) {
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||||
@@ -881,7 +923,9 @@ function MarketBoardSection({
|
|||||||
colorPalette={summary.playable ? "green" : "gray"}
|
colorPalette={summary.playable ? "green" : "gray"}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{summary.playable ? "Oynanabilir" : "Riskli"}
|
{summary.playable
|
||||||
|
? getUiText(ui, "playable", "Oynanabilir")
|
||||||
|
: getUiText(ui, "risky", "Riskli")}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{summary?.signal_tier ? (
|
{summary?.signal_tier ? (
|
||||||
@@ -891,7 +935,7 @@ function MarketBoardSection({
|
|||||||
)}
|
)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{getSignalTierLabel(summary.signal_tier)}
|
{getSignalTierLabel(summary.signal_tier, ui)}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{summary?.bet_grade ? (
|
{summary?.bet_grade ? (
|
||||||
@@ -913,12 +957,16 @@ function MarketBoardSection({
|
|||||||
</Flex>
|
</Flex>
|
||||||
<SimpleGrid columns={3} gap={2} mb={3}>
|
<SimpleGrid columns={3} gap={2} mb={3}>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Tutma Olasiligi"
|
label={getUiText(ui, "hit-probability", "Tutma Olasılığı")}
|
||||||
value={formatPercent(entry.confidence, 0)}
|
value={formatPercent(entry.confidence, 0)}
|
||||||
accent="green.500"
|
accent="green.500"
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Kalibre Guven"
|
label={getUiText(
|
||||||
|
ui,
|
||||||
|
"calibrated-confidence",
|
||||||
|
"Kalibre Güven",
|
||||||
|
)}
|
||||||
value={
|
value={
|
||||||
summary
|
summary
|
||||||
? formatPercent(summary.calibrated_confidence, 0)
|
? formatPercent(summary.calibrated_confidence, 0)
|
||||||
@@ -933,7 +981,8 @@ function MarketBoardSection({
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
{interval ? (
|
{interval ? (
|
||||||
<Text fontSize="xs" color="fg.muted" mb={3}>
|
<Text fontSize="xs" color="fg.muted" mb={3}>
|
||||||
Guven araligi: {formatInterval(interval)}
|
{getUiText(ui, "confidence-interval", "Güven Aralığı")}:{" "}
|
||||||
|
{formatInterval(interval)}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
<VStack align="stretch" gap={2.5}>
|
<VStack align="stretch" gap={2.5}>
|
||||||
@@ -973,9 +1022,11 @@ function MarketBoardSection({
|
|||||||
function ScoreCard({
|
function ScoreCard({
|
||||||
prediction,
|
prediction,
|
||||||
sport,
|
sport,
|
||||||
|
ui,
|
||||||
}: {
|
}: {
|
||||||
prediction: MatchPredictionDto;
|
prediction: MatchPredictionDto;
|
||||||
sport: SportType;
|
sport: SportType;
|
||||||
|
ui?: PredictionUiMessages;
|
||||||
}) {
|
}) {
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||||
@@ -987,24 +1038,52 @@ function ScoreCard({
|
|||||||
<Card.Body gap={4}>
|
<Card.Body gap={4}>
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
icon={LuTarget}
|
icon={LuTarget}
|
||||||
title={isBasketball ? "Sayi Senaryosu" : "Skor Senaryosu"}
|
title={
|
||||||
|
isBasketball
|
||||||
|
? getUiText(ui, "score-scenario-basketball", "Sayı Senaryosu")
|
||||||
|
: getUiText(ui, "score-scenario-football", "Skor Senaryosu")
|
||||||
|
}
|
||||||
info={
|
info={
|
||||||
isBasketball
|
isBasketball
|
||||||
? "Beklenen sayi dagilimi ve en olasi mac senaryolari."
|
? getUiText(
|
||||||
: "Beklenen skor ve en olasi senaryolar."
|
ui,
|
||||||
|
"score-scenario-info-basketball",
|
||||||
|
"Beklenen sayı dağılımı ve en olası maç senaryoları.",
|
||||||
|
)
|
||||||
|
: getUiText(
|
||||||
|
ui,
|
||||||
|
"score-scenario-info-football",
|
||||||
|
"Beklenen skor ve en olası senaryolar.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SimpleGrid columns={{ base: 1, md: 3 }} gap={3}>
|
<SimpleGrid columns={{ base: 1, md: 3 }} gap={3}>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={isBasketball ? "Mac Sonu Sayi" : "Mac Sonu"}
|
label={
|
||||||
|
isBasketball
|
||||||
|
? getUiText(ui, "full-time-basketball", "Maç Sonu Sayı")
|
||||||
|
: getUiText(ui, "full-time-football", "Maç Sonu")
|
||||||
|
}
|
||||||
value={prediction.score_prediction.ft}
|
value={prediction.score_prediction.ft}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={isBasketball ? "Ilk Yari Sayi" : "Ilk Yari"}
|
label={
|
||||||
|
isBasketball
|
||||||
|
? getUiText(ui, "half-time-basketball", "İlk Yarı Sayı")
|
||||||
|
: getUiText(ui, "half-time-football", "İlk Yarı")
|
||||||
|
}
|
||||||
value={prediction.score_prediction.ht}
|
value={prediction.score_prediction.ht}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={isBasketball ? "Beklenen Toplam Sayi" : "Toplam xG"}
|
label={
|
||||||
|
isBasketball
|
||||||
|
? getUiText(
|
||||||
|
ui,
|
||||||
|
"expected-total-basketball",
|
||||||
|
"Beklenen Toplam Sayı",
|
||||||
|
)
|
||||||
|
: getUiText(ui, "expected-total-football", "Toplam xG")
|
||||||
|
}
|
||||||
value={prediction.score_prediction.xg_total.toFixed(2)}
|
value={prediction.score_prediction.xg_total.toFixed(2)}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -1053,6 +1132,16 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
const pageBg = useColorModeValue("gray.50", "gray.900");
|
const pageBg = useColorModeValue("gray.50", "gray.900");
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||||
|
const liveBg = useColorModeValue("red.50", "red.950");
|
||||||
|
const liveBorderColor = useColorModeValue("red.300", "red.800");
|
||||||
|
const warningBg = useColorModeValue("yellow.50", "yellow.950");
|
||||||
|
const warningBorderColor = useColorModeValue("yellow.300", "yellow.800");
|
||||||
|
const orangeBg = useColorModeValue("orange.50", "orange.950");
|
||||||
|
const orangeBorderColor = useColorModeValue("orange.200", "orange.800");
|
||||||
|
const greenBg = useColorModeValue("green.50", "green.950");
|
||||||
|
const greenBorderColor = useColorModeValue("green.200", "green.800");
|
||||||
|
const statCardBg = useColorModeValue("gray.50", "whiteAlpha.50");
|
||||||
|
const trackBgColor = useColorModeValue("gray.100", "gray.700");
|
||||||
const riskPalette = getRiskPalette(prediction.risk.level);
|
const riskPalette = getRiskPalette(prediction.risk.level);
|
||||||
const qualityPalette = getQualityPalette(prediction.data_quality.label);
|
const qualityPalette = getQualityPalette(prediction.data_quality.label);
|
||||||
const recommendedPick = prediction.main_pick;
|
const recommendedPick = prediction.main_pick;
|
||||||
@@ -1067,7 +1156,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
{
|
{
|
||||||
key: "team",
|
key: "team",
|
||||||
icon: LuGauge,
|
icon: LuGauge,
|
||||||
label: isBasketball ? "Takim Formu" : "Takim Gucu",
|
label: isBasketball
|
||||||
|
? uiText("engine-team-basketball", "Takım Formu")
|
||||||
|
: uiText("engine-team-football", "Takım Gücü"),
|
||||||
value: prediction.engine_breakdown.team,
|
value: prediction.engine_breakdown.team,
|
||||||
color: "blue.400",
|
color: "blue.400",
|
||||||
detail: engineDetail?.team,
|
detail: engineDetail?.team,
|
||||||
@@ -1075,7 +1166,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
{
|
{
|
||||||
key: "player",
|
key: "player",
|
||||||
icon: LuSparkles,
|
icon: LuSparkles,
|
||||||
label: isBasketball ? "Kadro Etkisi" : "Oyuncu Etkisi",
|
label: isBasketball
|
||||||
|
? uiText("engine-player-basketball", "Kadro Etkisi")
|
||||||
|
: uiText("engine-player-football", "Oyuncu Etkisi"),
|
||||||
value: prediction.engine_breakdown.player,
|
value: prediction.engine_breakdown.player,
|
||||||
color: "green.400",
|
color: "green.400",
|
||||||
detail: engineDetail?.player,
|
detail: engineDetail?.player,
|
||||||
@@ -1083,14 +1176,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
{
|
{
|
||||||
key: "odds",
|
key: "odds",
|
||||||
icon: LuTrendingUp,
|
icon: LuTrendingUp,
|
||||||
label: "Oran Analizi",
|
label: uiText("engine-odds", "Oran Analizi"),
|
||||||
value: prediction.engine_breakdown.odds,
|
|
||||||
color: "orange.400",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "odds",
|
|
||||||
icon: LuTrendingUp,
|
|
||||||
label: "Oran Analizi",
|
|
||||||
value: prediction.engine_breakdown.odds,
|
value: prediction.engine_breakdown.odds,
|
||||||
color: "orange.400",
|
color: "orange.400",
|
||||||
detail: engineDetail?.odds,
|
detail: engineDetail?.odds,
|
||||||
@@ -1098,7 +1184,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
{
|
{
|
||||||
key: "referee",
|
key: "referee",
|
||||||
icon: LuShieldAlert,
|
icon: LuShieldAlert,
|
||||||
label: isBasketball ? "Yardimci Sinyaller" : "Hakem Etkisi",
|
label: isBasketball
|
||||||
|
? uiText("engine-referee-basketball", "Yardımcı Sinyaller")
|
||||||
|
: uiText("engine-referee-football", "Hakem Etkisi"),
|
||||||
value: prediction.engine_breakdown.referee,
|
value: prediction.engine_breakdown.referee,
|
||||||
color: "purple.400",
|
color: "purple.400",
|
||||||
detail: engineDetail?.referee,
|
detail: engineDetail?.referee,
|
||||||
@@ -1110,33 +1198,49 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
const isLive = Boolean(prediction.match_info?.is_live);
|
const isLive = Boolean(prediction.match_info?.is_live);
|
||||||
const isStale = Boolean(prediction.prediction_freshness?.is_stale_for_live);
|
const isStale = Boolean(prediction.prediction_freshness?.is_stale_for_live);
|
||||||
const contradictions = prediction.match_commentary?.contradictions || [];
|
const contradictions = prediction.match_commentary?.contradictions || [];
|
||||||
|
const pickCardLabels = {
|
||||||
|
confidence: uiText("confidence-label", "Güven"),
|
||||||
|
odds: uiText("odds-label", "Oran"),
|
||||||
|
recommendedStake: uiText("stake-label-short", "Stake"),
|
||||||
|
playScore: uiText("play-score-label", "Model Sinyali"),
|
||||||
|
playability: uiText("playability-label", "Model sinyali"),
|
||||||
|
confidenceInterval: uiText("confidence-interval", "Güven Aralığı"),
|
||||||
|
confidenceBand: uiText("confidence-band", "Band"),
|
||||||
|
confidenceIntervalWarning: uiText(
|
||||||
|
"confidence-interval-warning",
|
||||||
|
"Güven aralığı geniş. Sinyal olsa bile tek başına oynanması önerilmez.",
|
||||||
|
),
|
||||||
|
theoreticalEdgeInline: uiText("theoretical-edge-inline", "Teorik avantaj"),
|
||||||
|
modelProbability: uiText("model-probability-short", "Model"),
|
||||||
|
marketProbability: uiText("market-probability-short", "Piyasa"),
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack align="stretch" gap={5}>
|
<VStack align="stretch" gap={5}>
|
||||||
{isLive ? (
|
{isLive ? (
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
bg={useColorModeValue("red.50", "red.950")}
|
bg={liveBg}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={useColorModeValue("red.300", "red.800")}
|
borderColor={liveBorderColor}
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
>
|
>
|
||||||
<HStack justify="space-between" align="center">
|
<HStack justify="space-between" align="center">
|
||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<Icon as={LuFlame} color="red.500" />
|
<Icon as={LuFlame} color="red.500" />
|
||||||
<Text fontWeight="bold" color="red.600">
|
<Text fontWeight="bold" color="red.600">
|
||||||
🔴 CANLI
|
🔴 {uiText("live", "CANLI")}
|
||||||
</Text>
|
</Text>
|
||||||
{liveScoreHome != null && liveScoreAway != null ? (
|
{liveScoreHome != null && liveScoreAway != null ? (
|
||||||
<Text fontWeight="semibold">
|
<Text fontWeight="semibold">
|
||||||
{prediction.match_info.home_team} {liveScoreHome} - {liveScoreAway}{" "}
|
{prediction.match_info.home_team} {liveScoreHome} -{" "}
|
||||||
{prediction.match_info.away_team}
|
{liveScoreAway} {prediction.match_info.away_team}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</HStack>
|
</HStack>
|
||||||
{isStale ? (
|
{isStale ? (
|
||||||
<Badge colorPalette="orange" variant="solid">
|
<Badge colorPalette="orange" variant="solid">
|
||||||
Maç öncesi tahmin
|
{uiText("pre-match-prediction", "Maç öncesi tahmin")}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -1146,15 +1250,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
{contradictions.length ? (
|
{contradictions.length ? (
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
bg={useColorModeValue("yellow.50", "yellow.950")}
|
bg={warningBg}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={useColorModeValue("yellow.300", "yellow.800")}
|
borderColor={warningBorderColor}
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
>
|
>
|
||||||
<HStack align="start" gap={2}>
|
<HStack align="start" gap={2}>
|
||||||
<Icon as={LuTriangleAlert} color="yellow.600" mt={0.5} />
|
<Icon as={LuTriangleAlert} color="yellow.600" mt={0.5} />
|
||||||
<VStack align="start" gap={1}>
|
<VStack align="start" gap={1}>
|
||||||
<Text fontWeight="semibold">Tahmin Çelişkileri</Text>
|
<Text fontWeight="semibold">
|
||||||
|
{uiText("prediction-contradictions", "Tahmin Çelişkileri")}
|
||||||
|
</Text>
|
||||||
{contradictions.map((text, idx) => (
|
{contradictions.map((text, idx) => (
|
||||||
<Text key={idx} fontSize="sm" color="fg.muted">
|
<Text key={idx} fontSize="sm" color="fg.muted">
|
||||||
• {text}
|
• {text}
|
||||||
@@ -1169,17 +1275,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<Card.Body gap={5}>
|
<Card.Body gap={5}>
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
icon={LuBrain}
|
icon={LuBrain}
|
||||||
title={uiText("summary-title", "Tahmin Ozeti")}
|
title={uiText("summary-title", "Tahmin Özeti")}
|
||||||
info={uiText(
|
info={uiText(
|
||||||
"summary-info",
|
"summary-info",
|
||||||
"Model sinyallerini ve belirsizlikleri sade sekilde gosterir.",
|
"Model sinyallerini ve belirsizlikleri sade şekilde gösterir.",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Box
|
<Box
|
||||||
p={3}
|
p={3}
|
||||||
bg={useColorModeValue("orange.50", "orange.950")}
|
bg={orangeBg}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={useColorModeValue("orange.200", "orange.800")}
|
borderColor={orangeBorderColor}
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
>
|
>
|
||||||
<HStack align="start" gap={2}>
|
<HStack align="start" gap={2}>
|
||||||
@@ -1190,9 +1296,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
mt={0.5}
|
mt={0.5}
|
||||||
/>
|
/>
|
||||||
<Text fontSize="sm" color="fg.muted" lineHeight="tall">
|
<Text fontSize="sm" color="fg.muted" lineHeight="tall">
|
||||||
Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi
|
{uiText(
|
||||||
değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi
|
"model-signal-disclaimer",
|
||||||
nedeniyle yanılabilir.
|
"Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi nedeniyle yanılabilir.",
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -1201,9 +1308,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<Grid templateColumns={{ base: "1fr", xl: "1.4fr 1fr" }} gap={4}>
|
<Grid templateColumns={{ base: "1fr", xl: "1.4fr 1fr" }} gap={4}>
|
||||||
<Box
|
<Box
|
||||||
p={4}
|
p={4}
|
||||||
bg={useColorModeValue("green.50", "green.950")}
|
bg={greenBg}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={useColorModeValue("green.200", "green.800")}
|
borderColor={greenBorderColor}
|
||||||
borderRadius="2xl"
|
borderRadius="2xl"
|
||||||
>
|
>
|
||||||
<HStack justify="space-between" align="start" mb={4}>
|
<HStack justify="space-between" align="start" mb={4}>
|
||||||
@@ -1220,12 +1327,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
{getMarketLabel(recommendedPick.market, marketLabels)}{" "}
|
{getMarketLabel(recommendedPick.market, marketLabels)}{" "}
|
||||||
{uiText("best-market-copy", "marketinde en guclu secim.")}
|
{uiText("best-market-copy", "marketinde en güçlü seçim.")}
|
||||||
</Text>
|
</Text>
|
||||||
<HStack gap={2} flexWrap="wrap">
|
<HStack gap={2} flexWrap="wrap">
|
||||||
<Badge colorPalette={mainBandPalette} variant="subtle">
|
<Badge colorPalette={mainBandPalette} variant="subtle">
|
||||||
{getConfidenceBandLabel(
|
{getConfidenceBandLabel(
|
||||||
prediction.bet_advice.confidence_band,
|
prediction.bet_advice.confidence_band,
|
||||||
|
ui,
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
{recommendedPick.confidence_interval ? (
|
{recommendedPick.confidence_interval ? (
|
||||||
@@ -1239,7 +1347,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
</HStack>
|
</HStack>
|
||||||
<SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
|
<SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={uiText("confidence-label", "Guven")}
|
label={uiText("confidence-label", "Güven")}
|
||||||
value={formatPercent(
|
value={formatPercent(
|
||||||
recommendedPick.calibrated_confidence,
|
recommendedPick.calibrated_confidence,
|
||||||
0,
|
0,
|
||||||
@@ -1250,7 +1358,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
value={formatOdds(recommendedPick.odds)}
|
value={formatOdds(recommendedPick.odds)}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Guven Araligi"
|
label={uiText("confidence-interval", "Güven Aralığı")}
|
||||||
value={formatInterval(recommendedPick.confidence_interval)}
|
value={formatInterval(recommendedPick.confidence_interval)}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
@@ -1258,19 +1366,19 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
value={formatEdgeSignal(recommendedPick.ev_edge)}
|
value={formatEdgeSignal(recommendedPick.ev_edge)}
|
||||||
helper={uiText(
|
helper={uiText(
|
||||||
"edge-info",
|
"edge-info",
|
||||||
"Model olasiligi ile piyasa olasiligi arasindaki teorik farktir; tutma garantisi veya kesin kazanc beklentisi degildir.",
|
"Model olasılığı ile piyasa olasılığı arasındaki teorik farktır; tutma garantisi veya kesin kazanç beklentisi değildir.",
|
||||||
)}
|
)}
|
||||||
accent={`${getEdgePalette(recommendedPick.ev_edge)}.500`}
|
accent={`${getEdgePalette(recommendedPick.ev_edge)}.500`}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={uiText("stake-label", "Onerilen Miktar (Stake)")}
|
label={uiText("stake-label", "Önerilen Miktar (Stake)")}
|
||||||
value={formatUnits(
|
value={formatUnits(
|
||||||
recommendedPick.stake_units ||
|
recommendedPick.stake_units ||
|
||||||
prediction.bet_advice.suggested_stake_units,
|
prediction.bet_advice.suggested_stake_units,
|
||||||
)}
|
)}
|
||||||
helper={uiText(
|
helper={uiText(
|
||||||
"stake-info",
|
"stake-info",
|
||||||
"Stake, bu bahis icin onerilen bahis birimidir. 2.0u demek, kendi bankroll planinizdaki 2 birimlik bahis anlamina gelir.",
|
"Stake, bu bahis için önerilen bahis birimidir. 2.0u, kendi bankroll planınızdaki 2 birimlik bahis anlamına gelir.",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -1284,7 +1392,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
borderRadius="2xl"
|
borderRadius="2xl"
|
||||||
>
|
>
|
||||||
<Text fontSize="sm" fontWeight="semibold" mb={3}>
|
<Text fontSize="sm" fontWeight="semibold" mb={3}>
|
||||||
{uiText("quick-read", "Hizli yorum")}
|
{uiText("quick-read", "Hızlı yorum")}
|
||||||
</Text>
|
</Text>
|
||||||
<ReasonList
|
<ReasonList
|
||||||
items={[
|
items={[
|
||||||
@@ -1299,21 +1407,28 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
|
|
||||||
<SimpleGrid columns={{ base: 1, md: 2, xl: 4 }} gap={3}>
|
<SimpleGrid columns={{ base: 1, md: 2, xl: 4 }} gap={3}>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label="Veri Kalitesi"
|
label={uiText("data-quality", "Veri Kalitesi")}
|
||||||
value={formatPercent(prediction.data_quality.score * 100, 0)}
|
value={formatPercent(prediction.data_quality.score * 100, 0)}
|
||||||
helper="Kadro, oran ve mac verisinin ne kadar guvenilir oldugu."
|
helper={uiText(
|
||||||
|
"data-quality-info",
|
||||||
|
"Kadro, oran ve maç verisinin ne kadar güvenilir olduğu.",
|
||||||
|
)}
|
||||||
accent={`${qualityPalette}.500`}
|
accent={`${qualityPalette}.500`}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={t("risk-level")}
|
label={t("risk-level")}
|
||||||
value={`${prediction.risk.level} (${prediction.risk.score}/100)`}
|
value={`${prediction.risk.level} (${prediction.risk.score}/100)`}
|
||||||
helper="Surpriz ihtimali ve belirsizlik seviyesi."
|
helper={uiText(
|
||||||
|
"risk-info",
|
||||||
|
"Sürpriz ihtimali ve belirsizlik seviyesi.",
|
||||||
|
)}
|
||||||
accent={`${riskPalette}.500`}
|
accent={`${riskPalette}.500`}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
label={uiText("lineup-source", "Lineup Kaynagi")}
|
label={uiText("lineup-source", "Kadronun Kaynağı")}
|
||||||
value={getLineupSourceLabel(
|
value={getLineupSourceLabel(
|
||||||
prediction.data_quality.lineup_source,
|
prediction.data_quality.lineup_source,
|
||||||
|
ui,
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<MetricTile
|
<MetricTile
|
||||||
@@ -1326,9 +1441,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
prediction.risk.warnings?.length ? (
|
prediction.risk.warnings?.length ? (
|
||||||
<Box
|
<Box
|
||||||
p={4}
|
p={4}
|
||||||
bg={useColorModeValue("orange.50", "orange.950")}
|
bg={orangeBg}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={useColorModeValue("orange.200", "orange.800")}
|
borderColor={orangeBorderColor}
|
||||||
borderRadius="2xl"
|
borderRadius="2xl"
|
||||||
>
|
>
|
||||||
<HStack align="start" gap={3}>
|
<HStack align="start" gap={3}>
|
||||||
@@ -1339,12 +1454,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
mt={0.5}
|
mt={0.5}
|
||||||
/>
|
/>
|
||||||
<VStack align="start" gap={1.5}>
|
<VStack align="start" gap={1.5}>
|
||||||
<Text fontWeight="semibold">Risk Yorumu</Text>
|
<Text fontWeight="semibold">
|
||||||
|
{uiText("risk-commentary", "Risk Yorumu")}
|
||||||
|
</Text>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
{prediction.risk.surprise_comment ||
|
{prediction.risk.surprise_comment ||
|
||||||
(prediction.risk.surprise_type
|
(prediction.risk.surprise_type
|
||||||
? `${resolveReason(prediction.risk.surprise_type)}`
|
? `${resolveReason(prediction.risk.surprise_type)}`
|
||||||
: "Model bu maçta ekstra dikkat istiyor.")}
|
: uiText(
|
||||||
|
"risk-default-comment",
|
||||||
|
"Model bu maçta ekstra dikkat istiyor.",
|
||||||
|
))}
|
||||||
</Text>
|
</Text>
|
||||||
{prediction.risk.surprise_score !== undefined ? (
|
{prediction.risk.surprise_score !== undefined ? (
|
||||||
<Text
|
<Text
|
||||||
@@ -1352,7 +1472,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
fontWeight="semibold"
|
fontWeight="semibold"
|
||||||
color="orange.600"
|
color="orange.600"
|
||||||
>
|
>
|
||||||
Sürpriz skoru:{" "}
|
{uiText("surprise-score", "Sürpriz skoru")}:{" "}
|
||||||
{formatPercent(prediction.risk.surprise_score, 0)}
|
{formatPercent(prediction.risk.surprise_score, 0)}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -1361,7 +1481,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
{prediction.risk.surprise_breakdown.map((entry) => (
|
{prediction.risk.surprise_breakdown.map((entry) => (
|
||||||
<HStack key={entry.code} gap={2}>
|
<HStack key={entry.code} gap={2}>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={entry.points >= 15 ? "red" : entry.points >= 8 ? "orange" : "yellow"}
|
colorPalette={
|
||||||
|
entry.points >= 15
|
||||||
|
? "red"
|
||||||
|
: entry.points >= 8
|
||||||
|
? "orange"
|
||||||
|
: "yellow"
|
||||||
|
}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
+{entry.points.toFixed(0)}
|
+{entry.points.toFixed(0)}
|
||||||
@@ -1395,7 +1521,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
title={t("engine-breakdown-title")}
|
title={t("engine-breakdown-title")}
|
||||||
info={uiText(
|
info={uiText(
|
||||||
"engine-info",
|
"engine-info",
|
||||||
"Tahmini en cok hangi bilesenlerin etkiledigini gosterir.",
|
"Tahmini en çok hangi bileşenlerin etkilediğini gösterir.",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
|
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
|
||||||
@@ -1403,7 +1529,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<Box
|
<Box
|
||||||
key={item.key}
|
key={item.key}
|
||||||
p={4}
|
p={4}
|
||||||
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
|
bg={statCardBg}
|
||||||
borderWidth="1px"
|
borderWidth="1px"
|
||||||
borderColor={borderColor}
|
borderColor={borderColor}
|
||||||
borderRadius="xl"
|
borderRadius="xl"
|
||||||
@@ -1421,7 +1547,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
colorPalette={getEngineLabelPalette(item.detail.label)}
|
colorPalette={getEngineLabelPalette(item.detail.label)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{getEngineLabelText(item.detail.label)}
|
{getEngineLabelText(item.detail.label, ui)}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
<Text fontSize="sm" fontWeight="bold">
|
<Text fontSize="sm" fontWeight="bold">
|
||||||
@@ -1432,9 +1558,8 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<Bar
|
<Bar
|
||||||
value={Math.min(item.value, 100)}
|
value={Math.min(item.value, 100)}
|
||||||
color={item.color}
|
color={item.color}
|
||||||
trackBg={useColorModeValue("gray.100", "gray.700")}
|
trackBg={trackBgColor}
|
||||||
/>
|
/>
|
||||||
<Bar value={Math.min(item.value, 100)} color={item.color} trackBg={useColorModeValue("gray.100", "gray.700")} />
|
|
||||||
{item.detail?.interpretation ? (
|
{item.detail?.interpretation ? (
|
||||||
<Text fontSize="xs" color="fg.muted" mt={2}>
|
<Text fontSize="xs" color="fg.muted" mt={2}>
|
||||||
{item.detail.interpretation}
|
{item.detail.interpretation}
|
||||||
@@ -1454,13 +1579,8 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
palette="green"
|
palette="green"
|
||||||
stakeFallback={prediction.bet_advice.suggested_stake_units}
|
stakeFallback={prediction.bet_advice.suggested_stake_units}
|
||||||
marketLabels={marketLabels}
|
marketLabels={marketLabels}
|
||||||
labels={{
|
labels={pickCardLabels}
|
||||||
confidence: uiText("confidence-label", "Guven"),
|
ui={ui}
|
||||||
odds: uiText("odds-label", "Oran"),
|
|
||||||
recommendedStake: uiText("stake-label-short", "Stake"),
|
|
||||||
playScore: uiText("play-score-label", "Model Sinyali"),
|
|
||||||
playability: uiText("playability-label", "Model sinyali"),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -1472,7 +1592,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
title={uiText("alternative-markets", "Alternatif Marketler")}
|
title={uiText("alternative-markets", "Alternatif Marketler")}
|
||||||
info={uiText(
|
info={uiText(
|
||||||
"alternative-markets-info",
|
"alternative-markets-info",
|
||||||
"Ana tahmin disindaki secenekler.",
|
"Ana tahmin dışındaki seçenekler.",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
||||||
@@ -1483,18 +1603,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
title={
|
title={
|
||||||
pick.playable
|
pick.playable
|
||||||
? uiText("alternative", "Alternatif")
|
? uiText("alternative", "Alternatif")
|
||||||
: uiText("pass-market", "PASS market")
|
: uiText("pass-market", "Elenen Market")
|
||||||
}
|
}
|
||||||
resolveReason={resolveReason}
|
resolveReason={resolveReason}
|
||||||
palette={pick.ev_edge > 0 ? "blue" : "orange"}
|
palette={pick.ev_edge > 0 ? "blue" : "orange"}
|
||||||
marketLabels={marketLabels}
|
marketLabels={marketLabels}
|
||||||
labels={{
|
labels={pickCardLabels}
|
||||||
confidence: uiText("confidence-label", "Guven"),
|
ui={ui}
|
||||||
odds: uiText("odds-label", "Oran"),
|
|
||||||
recommendedStake: uiText("stake-label-short", "Stake"),
|
|
||||||
playScore: uiText("play-score-label", "Model Sinyali"),
|
|
||||||
playability: uiText("playability-label", "Model sinyali"),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
@@ -1505,19 +1620,24 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<SummaryTable
|
<SummaryTable
|
||||||
items={prediction.bet_summary || []}
|
items={prediction.bet_summary || []}
|
||||||
marketLabels={marketLabels}
|
marketLabels={marketLabels}
|
||||||
title={uiText("all-markets-title", "Tum Marketler")}
|
title={uiText("all-markets-title", "Tüm Marketler")}
|
||||||
info={uiText(
|
info={uiText(
|
||||||
"all-markets-info",
|
"all-markets-info",
|
||||||
"Butun secenekleri tek tabloda karsilastir.",
|
"Bütün seçenekleri tek tabloda karşılaştırır.",
|
||||||
)}
|
)}
|
||||||
|
ui={ui}
|
||||||
/>
|
/>
|
||||||
{prediction.match_commentary?.headline || prediction.match_commentary?.summary ? (
|
{prediction.match_commentary?.headline ||
|
||||||
|
prediction.match_commentary?.summary ? (
|
||||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||||
<Card.Body gap={3}>
|
<Card.Body gap={3}>
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
icon={LuBrain}
|
icon={LuBrain}
|
||||||
title="Maç Yorumu"
|
title={uiText("match-commentary-title", "Maç Yorumu")}
|
||||||
info="Modelin maç hakkındaki insan-okunabilir özeti"
|
info={uiText(
|
||||||
|
"match-commentary-info",
|
||||||
|
"Modelin maç hakkındaki insan okunabilir özeti.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{prediction.match_commentary.headline ? (
|
{prediction.match_commentary.headline ? (
|
||||||
<Text fontSize="md" fontWeight="bold">
|
<Text fontSize="md" fontWeight="bold">
|
||||||
@@ -1541,7 +1661,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
</Card.Body>
|
</Card.Body>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
) : null}
|
) : null}
|
||||||
<ScoreCard prediction={prediction} sport={sport} />
|
<ScoreCard prediction={prediction} sport={sport} ui={ui} />
|
||||||
<MarketBoardSection
|
<MarketBoardSection
|
||||||
marketBoard={prediction.market_board}
|
marketBoard={prediction.market_board}
|
||||||
betSummary={prediction.bet_summary || []}
|
betSummary={prediction.bet_summary || []}
|
||||||
@@ -1549,8 +1669,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
title={t("market-board")}
|
title={t("market-board")}
|
||||||
info={uiText(
|
info={uiText(
|
||||||
"market-board-info",
|
"market-board-info",
|
||||||
"Modelin her markette gordugu olasilik dagilimi.",
|
"Modelin her markette gördüğü olasılık dağılımı.",
|
||||||
)}
|
)}
|
||||||
|
ui={ui}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{prediction.v27_engine ? (
|
{prediction.v27_engine ? (
|
||||||
@@ -1562,7 +1683,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<SectionTitle
|
<SectionTitle
|
||||||
icon={LuSparkles}
|
icon={LuSparkles}
|
||||||
title={t("bet-advice")}
|
title={t("bet-advice")}
|
||||||
info={uiText("bet-advice-info", "Modelin nihai aksiyon onerisi.")}
|
info={uiText("bet-advice-info", "Modelin nihai aksiyon önerisi.")}
|
||||||
/>
|
/>
|
||||||
<HStack
|
<HStack
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
@@ -1579,7 +1700,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
px={3}
|
px={3}
|
||||||
py={1}
|
py={1}
|
||||||
>
|
>
|
||||||
{prediction.bet_advice.playable ? "OYNA" : "OYNAMA"}
|
{prediction.bet_advice.playable
|
||||||
|
? uiText("bet-advice-play", "OYNA")
|
||||||
|
: uiText("bet-advice-pass", "OYNAMA")}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={mainBandPalette}
|
colorPalette={mainBandPalette}
|
||||||
@@ -1589,7 +1712,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
px={3}
|
px={3}
|
||||||
py={1}
|
py={1}
|
||||||
>
|
>
|
||||||
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
|
{getConfidenceBandLabel(
|
||||||
|
prediction.bet_advice.confidence_band,
|
||||||
|
ui,
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette={getSignalTierPalette(
|
colorPalette={getSignalTierPalette(
|
||||||
@@ -1601,14 +1727,14 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
px={3}
|
px={3}
|
||||||
py={1}
|
py={1}
|
||||||
>
|
>
|
||||||
{getSignalTierLabel(prediction.bet_advice.signal_tier)}
|
{getSignalTierLabel(prediction.bet_advice.signal_tier, ui)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Text color="fg.muted">
|
<Text color="fg.muted">
|
||||||
{resolveReason(prediction.bet_advice.reason)}
|
{resolveReason(prediction.bet_advice.reason)}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Badge variant="surface" fontSize="sm" px={3} py={1}>
|
<Badge variant="surface" fontSize="sm" px={3} py={1}>
|
||||||
{uiText("recommended-stake-inline", "Onerilen miktar")}:{" "}
|
{uiText("recommended-stake-inline", "Önerilen miktar")}:{" "}
|
||||||
{formatUnits(prediction.bet_advice.suggested_stake_units)}
|
{formatUnits(prediction.bet_advice.suggested_stake_units)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -1616,7 +1742,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
|||||||
<SectionTitle
|
<SectionTitle
|
||||||
icon={LuBrain}
|
icon={LuBrain}
|
||||||
title={t("reasoning")}
|
title={t("reasoning")}
|
||||||
info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti."
|
info={uiText(
|
||||||
|
"reasoning-info",
|
||||||
|
"Modelin bu maçı neden bu şekilde okuduğunun üst seviye özeti.",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<ReasonList
|
<ReasonList
|
||||||
items={prediction.reasoning_factors}
|
items={prediction.reasoning_factors}
|
||||||
|
|||||||
Reference in New Issue
Block a user