UI/UX pass: i18n completion, responsive fixes, cleanup
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m37s
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m37s
- Fix flex 'align=flex-start' shrinking content to min width in column
mode on matches, dashboard, and coupon-builder layouts (mobile/tablet
cards now span full width)
- Localize all user-facing hardcoded Turkish strings: lineups card,
odds card, v28 odds band panel, team/league pages, teams search,
auth toasts, league headers (new keys in tr/en messages)
- Populate empty predictions.prediction-reasons group (20 veto reasons)
and missing predictions.ui keys so the EN locale gets full coverage
- Use the active locale for all date formatting instead of hardcoded
tr-TR (match card, match detail, team detail)
- Replace raw native date input with Chakra Input (dark mode + theming)
- Rename match-detail first tab from 'Tüm Maçlar' to 'Genel Bakış'
- Fix '4 maçlar' grammar with a proper {count} maç message
- Delete dead 3300-line Chakra showcase component (home-card.tsx)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+126
-7
@@ -38,7 +38,9 @@
|
|||||||
"login-success": "Login successful!",
|
"login-success": "Login successful!",
|
||||||
"register-success": "Registration successful!",
|
"register-success": "Registration successful!",
|
||||||
"login-required-title": "Login Required",
|
"login-required-title": "Login Required",
|
||||||
"login-required-message": "Please sign in or create an account to view match analysis."
|
"login-required-message": "Please sign in or create an account to view match analysis.",
|
||||||
|
"login-error": "Login failed!",
|
||||||
|
"register-error": "Registration failed!"
|
||||||
},
|
},
|
||||||
"all-right-reserved": "All rights reserved.",
|
"all-right-reserved": "All rights reserved.",
|
||||||
"privacy-policy": "Privacy Policy",
|
"privacy-policy": "Privacy Policy",
|
||||||
@@ -155,7 +157,26 @@
|
|||||||
"penalty": "Penalty",
|
"penalty": "Penalty",
|
||||||
"half-time": "1st Half",
|
"half-time": "1st Half",
|
||||||
"second-half": "2nd Half",
|
"second-half": "2nd Half",
|
||||||
"assist": "Assist"
|
"assist": "Assist",
|
||||||
|
"live-odds": "Live Betting Odds",
|
||||||
|
"lineup-official-xi": "Official Starting XI",
|
||||||
|
"lineup-confirmed-squad": "Confirmed Squad",
|
||||||
|
"lineup-confirmed": "Confirmed",
|
||||||
|
"lineup-confirmed-live-desc": "The lineup has been officially confirmed.",
|
||||||
|
"lineup-participation-desc": "Lineup taken from match participation data.",
|
||||||
|
"lineup-probable-squad": "Probable Squad",
|
||||||
|
"lineup-probable": "Probable",
|
||||||
|
"lineup-probable-desc": "Probable lineup based on starting XI data from recent matches. The AI analysis is based on this squad.",
|
||||||
|
"lineup-info": "Lineup Info",
|
||||||
|
"lineup-pending": "Awaiting Lineup",
|
||||||
|
"lineup-pending-desc": "The lineup has not been announced yet. The AI analysis is based on overall team strength and statistics.",
|
||||||
|
"lineup-tbd": "Lineup not yet known",
|
||||||
|
"lineup-tbd-sub": "Will be updated close to kick-off",
|
||||||
|
"lineup-not-announced": "Lineup Not Announced Yet",
|
||||||
|
"lineup-not-announced-desc": "{home} and {away} lineups will be updated close to kick-off. The AI analysis is based on team statistics and balance of strength.",
|
||||||
|
"overview": "Overview",
|
||||||
|
"match-count": "{count} matches",
|
||||||
|
"odds-legend": "% = model's calibrated hit probability · ↓↑ = opening→closing movement"
|
||||||
},
|
},
|
||||||
"predictions": {
|
"predictions": {
|
||||||
"title": "Predictions",
|
"title": "Predictions",
|
||||||
@@ -223,7 +244,27 @@
|
|||||||
"no_bet_conditions_met": "The algorithm could not find a safe/valuable bet for this match.",
|
"no_bet_conditions_met": "The algorithm could not find a safe/valuable bet for this match.",
|
||||||
"insufficient_play_score": "Model signal is below the threshold.",
|
"insufficient_play_score": "Model signal is below the threshold.",
|
||||||
"no_ev_edge_minimum_stake": "Passed safety gates but no mathematical edge — minimum stake applied.",
|
"no_ev_edge_minimum_stake": "Passed safety gates but no mathematical edge — minimum stake applied.",
|
||||||
"upset_risk_detected": "High upset risk detected, proceed with caution."
|
"upset_risk_detected": "High upset risk detected, proceed with caution.",
|
||||||
|
"confidence_interval_too_wide": "The confidence interval is too wide.",
|
||||||
|
"confidence_band_low": "The confidence band is low.",
|
||||||
|
"draw_probability_elevated": "The draw probability looks elevated.",
|
||||||
|
"balanced_match_risk": "The match looks balanced; there is upset risk.",
|
||||||
|
"high_total_goal_volatility": "High goal tempo increases the upset risk.",
|
||||||
|
"mutual_goal_pressure": "Both teams are generating goal threat.",
|
||||||
|
"late_goal_swing_risk": "High risk of a late goal or score swing.",
|
||||||
|
"live_match_open_state": "The live match has turned completely open.",
|
||||||
|
"live_match_active_state": "The live match is more active than expected.",
|
||||||
|
"live_state_impossible_market": "The live match state invalidates this market.",
|
||||||
|
"live_score_exceeds_under_line": "The live score is very close to or past the under line.",
|
||||||
|
"score_model_conflicts_with_under_pick": "The score and xG model conflict with this under pick.",
|
||||||
|
"score_model_conflicts_with_over_pick": "The score and xG model conflict with this over pick.",
|
||||||
|
"market_stack_conflict_over25": "The over 2.5 signal conflicts with this market.",
|
||||||
|
"market_stack_conflict_btts": "The BTTS signal conflicts with this market.",
|
||||||
|
"live_total_goals_close_to_line": "The live total goals are too close to this line.",
|
||||||
|
"score_model_conflicts_with_btts_no": "The score and xG model conflict with the BTTS-No pick.",
|
||||||
|
"score_model_conflicts_with_draw_pick": "The score model does not support the draw pick.",
|
||||||
|
"score_model_conflicts_with_home_pick": "The score model does not support the home pick.",
|
||||||
|
"score_model_conflicts_with_away_pick": "The score model does not support the away pick."
|
||||||
},
|
},
|
||||||
"ev-edge": "Theoretical Edge",
|
"ev-edge": "Theoretical Edge",
|
||||||
"implied-prob": "Market Probability",
|
"implied-prob": "Market Probability",
|
||||||
@@ -360,7 +401,11 @@
|
|||||||
"confidence-medium": "Medium",
|
"confidence-medium": "Medium",
|
||||||
"confidence-low": "Low",
|
"confidence-low": "Low",
|
||||||
"confidence-unknown": "Unknown",
|
"confidence-unknown": "Unknown",
|
||||||
"info": "Info"
|
"info": "Info",
|
||||||
|
"unified-confidence": "Confidence Score",
|
||||||
|
"underdog-ref-tooltip": "Model probability of the underdog side (informational)",
|
||||||
|
"trap-tooltip": "The market is overconfident",
|
||||||
|
"no-value-tooltip": "Model agrees with the favourite but the odds are too low"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"coupons": {
|
"coupons": {
|
||||||
@@ -487,7 +532,8 @@
|
|||||||
"freq-mode-active": "Frequency Engine active",
|
"freq-mode-active": "Frequency Engine active",
|
||||||
"ai-mode-active": "AI Engine active",
|
"ai-mode-active": "AI Engine active",
|
||||||
"engine-mode-label": "Engine Mode",
|
"engine-mode-label": "Engine Mode",
|
||||||
"engine-mode-help": "AI: Gemini-based AI prediction. Frequency: Database-driven statistical analysis."
|
"engine-mode-help": "AI: Gemini-based AI prediction. Frequency: Database-driven statistical analysis.",
|
||||||
|
"all-markets-scanned": "All markets will be scanned"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profile",
|
"title": "Profile",
|
||||||
@@ -519,7 +565,14 @@
|
|||||||
"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"
|
"all": "All",
|
||||||
|
"back-to-leagues": "Back to Leagues",
|
||||||
|
"league-not-found": "League Not Found",
|
||||||
|
"past-matches": "Past Matches",
|
||||||
|
"not-found": "Not Found",
|
||||||
|
"no-leagues-found": "No leagues match the selected criteria.",
|
||||||
|
"team-not-found": "Team Not Found",
|
||||||
|
"no-teams-found": "No team found matching \"{query}\"."
|
||||||
},
|
},
|
||||||
"h2h": {
|
"h2h": {
|
||||||
"title": "Head to Head",
|
"title": "Head to Head",
|
||||||
@@ -809,5 +862,71 @@
|
|||||||
"remaining": "remaining"
|
"remaining": "remaining"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"refund-policy": "Refund Policy"
|
"refund-policy": "Refund Policy",
|
||||||
|
"teams": {
|
||||||
|
"search-placeholder": "Type a team name... (min 2 characters)",
|
||||||
|
"search-hint": "Type the name of the team you are looking for",
|
||||||
|
"search-example": "E.g.: Galatasaray, Barcelona, Manchester City",
|
||||||
|
"no-results": "No results found",
|
||||||
|
"upcoming": "Upcoming",
|
||||||
|
"not-found": "Team not found",
|
||||||
|
"matches-count": "Matches",
|
||||||
|
"upcoming-matches": "Upcoming Matches",
|
||||||
|
"past-matches": "Past Matches",
|
||||||
|
"page-info": "Page {page}/{totalPages} • {total} matches in total",
|
||||||
|
"no-past-matches-season": "No past matches found for this season"
|
||||||
|
},
|
||||||
|
"v28": {
|
||||||
|
"avg-cards": "Avg: {avg} cards",
|
||||||
|
"band": "Band",
|
||||||
|
"cards-analysis": "Cards Analysis",
|
||||||
|
"combined-over-rate": "Combined OVER Rate",
|
||||||
|
"combined-weighting": "Weighted 60% referee + 40% teams",
|
||||||
|
"confirmations": "Confirm",
|
||||||
|
"engines-agree": "Engines Agree",
|
||||||
|
"engines-disagree": "Engines Disagree",
|
||||||
|
"ft": "FT",
|
||||||
|
"ht": "HT",
|
||||||
|
"htft-info": "Historical hit rates of half-time/full-time combinations within a similar odds band.",
|
||||||
|
"htft-title": "HT/FT Combinations",
|
||||||
|
"insufficient": "LOW DATA",
|
||||||
|
"insufficient-data": "Insufficient data — not enough match samples yet.",
|
||||||
|
"label-away": "FT Away",
|
||||||
|
"label-btts_yes": "BTTS Yes",
|
||||||
|
"label-cards_over": "Cards Over",
|
||||||
|
"label-dc_12": "DC 12",
|
||||||
|
"label-dc_1x": "DC 1X",
|
||||||
|
"label-dc_x2": "DC X2",
|
||||||
|
"label-home": "FT Home",
|
||||||
|
"label-ht_away": "HT Away",
|
||||||
|
"label-ht_home": "HT Home",
|
||||||
|
"label-ht_ou05_over": "HT Over 0.5",
|
||||||
|
"label-ht_ou15_over": "HT Over 1.5",
|
||||||
|
"label-htft_11": "HT/FT 1/1",
|
||||||
|
"label-htft_12": "HT/FT 1/2",
|
||||||
|
"label-htft_1x": "HT/FT 1/X",
|
||||||
|
"label-htft_21": "HT/FT 2/1",
|
||||||
|
"label-htft_22": "HT/FT 2/2",
|
||||||
|
"label-htft_2x": "HT/FT 2/X",
|
||||||
|
"label-htft_x1": "HT/FT X/1",
|
||||||
|
"label-htft_x2": "HT/FT X/2",
|
||||||
|
"label-htft_xx": "HT/FT X/X",
|
||||||
|
"label-oe_odd": "Odd",
|
||||||
|
"label-ou15_over": "Over 1.5",
|
||||||
|
"label-ou25_over": "Over 2.5",
|
||||||
|
"label-ou35_over": "Over 3.5",
|
||||||
|
"matches-count": "{count} matches",
|
||||||
|
"odds": "Odds",
|
||||||
|
"over-rate": "Over rate",
|
||||||
|
"panel-info": "Statistical analysis of results that occurred at similar odds in past matches. Includes Triple Value, Cards Profile, and HT/FT combinations.",
|
||||||
|
"panel-title": "V28 Odds Band Analysis",
|
||||||
|
"pass": "PASS",
|
||||||
|
"referee-profile": "Referee Profile",
|
||||||
|
"strongest": "Strongest",
|
||||||
|
"team-profile": "Team Profile",
|
||||||
|
"triple-value-info": "Comparison of model probability, odds-band statistics, and market odds. A positive edge means the model sees an advantage.",
|
||||||
|
"triple-value-title": "Value Detection (Triple Value)",
|
||||||
|
"value": "VALUE",
|
||||||
|
"value-signals": "{count} Value Signals"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+126
-7
@@ -38,7 +38,9 @@
|
|||||||
"login-success": "Giriş başarılı!",
|
"login-success": "Giriş başarılı!",
|
||||||
"register-success": "Kayıt başarılı!",
|
"register-success": "Kayıt başarılı!",
|
||||||
"login-required-title": "Giriş Yapmanız Gerekiyor",
|
"login-required-title": "Giriş Yapmanız Gerekiyor",
|
||||||
"login-required-message": "Maç analizlerini görüntülemek için lütfen giriş yapın veya hesap oluşturun."
|
"login-required-message": "Maç analizlerini görüntülemek için lütfen giriş yapın veya hesap oluşturun.",
|
||||||
|
"login-error": "Giriş yapılamadı!",
|
||||||
|
"register-error": "Kayıt yapılamadı!"
|
||||||
},
|
},
|
||||||
"all-right-reserved": "Tüm hakları saklıdır.",
|
"all-right-reserved": "Tüm hakları saklıdır.",
|
||||||
"privacy-policy": "Gizlilik Politikası",
|
"privacy-policy": "Gizlilik Politikası",
|
||||||
@@ -155,7 +157,26 @@
|
|||||||
"penalty": "Penaltı",
|
"penalty": "Penaltı",
|
||||||
"half-time": "İlk Yarı",
|
"half-time": "İlk Yarı",
|
||||||
"second-half": "İkinci Yarı",
|
"second-half": "İkinci Yarı",
|
||||||
"assist": "Asist"
|
"assist": "Asist",
|
||||||
|
"live-odds": "Canlı İddaa Oranları",
|
||||||
|
"lineup-official-xi": "Resmi İlk 11",
|
||||||
|
"lineup-confirmed-squad": "Onaylı Kadro",
|
||||||
|
"lineup-confirmed": "Onaylı",
|
||||||
|
"lineup-confirmed-live-desc": "Kadro resmi olarak onaylandı.",
|
||||||
|
"lineup-participation-desc": "Kadro maç katılım verilerinden alındı.",
|
||||||
|
"lineup-probable-squad": "Muhtemel Kadro",
|
||||||
|
"lineup-probable": "Muhtemel",
|
||||||
|
"lineup-probable-desc": "Son maçlardaki ilk 11 verilerine dayalı muhtemel kadro. AI analizi bu kadro üzerinden yapılmaktadır.",
|
||||||
|
"lineup-info": "Kadro Bilgisi",
|
||||||
|
"lineup-pending": "Kadro Bekleniyor",
|
||||||
|
"lineup-pending-desc": "Kadro henüz açıklanmadı. AI analizi, takımların genel güç dengesi ve istatistiklerine dayalı olarak üretilmiştir.",
|
||||||
|
"lineup-tbd": "Kadro henüz belli değil",
|
||||||
|
"lineup-tbd-sub": "Maç saatine yakın güncellenecek",
|
||||||
|
"lineup-not-announced": "Kadro Henüz Açıklanmadı",
|
||||||
|
"lineup-not-announced-desc": "{home} ve {away} kadroları maç saatine yakın güncellenecektir. AI analizi, takım istatistikleri ve güç dengesi üzerinden yapılmaktadır.",
|
||||||
|
"overview": "Genel Bakış",
|
||||||
|
"match-count": "{count} maç",
|
||||||
|
"odds-legend": "%= modelin tutma olasılığı (kalibre) · ↓↑ = açılış→kapanış hareketi"
|
||||||
},
|
},
|
||||||
"predictions": {
|
"predictions": {
|
||||||
"title": "Tahminler",
|
"title": "Tahminler",
|
||||||
@@ -223,7 +244,27 @@
|
|||||||
"missing_spread_odds": "Handikap oranları eksik.",
|
"missing_spread_odds": "Handikap oranları eksik.",
|
||||||
"no_bet_conditions_met": "Algoritma bu maç için güvenli/değerli bir bahis önerisi bulamadı.",
|
"no_bet_conditions_met": "Algoritma bu maç için güvenli/değerli bir bahis önerisi bulamadı.",
|
||||||
"insufficient_play_score": "Model sinyali eşiğin altında kaldı.",
|
"insufficient_play_score": "Model sinyali eşiğin altında kaldı.",
|
||||||
"no_ev_edge_minimum_stake": "Güvenlik kontrollerini geçti ancak matematik avantaj yok — minimum bahis uygulandı."
|
"no_ev_edge_minimum_stake": "Güvenlik kontrollerini geçti ancak matematik avantaj yok — minimum bahis uygulandı.",
|
||||||
|
"confidence_interval_too_wide": "Güven aralığı fazla geniş.",
|
||||||
|
"confidence_band_low": "Güven bandı düşük.",
|
||||||
|
"draw_probability_elevated": "Beraberlik olasılığı yükselmiş görünüyor.",
|
||||||
|
"balanced_match_risk": "Maç dengeli görünüyor, sürpriz riski var.",
|
||||||
|
"high_total_goal_volatility": "Yüksek gol temposu sürpriz riskini artırıyor.",
|
||||||
|
"mutual_goal_pressure": "İki takım da gol tehdidi üretiyor.",
|
||||||
|
"late_goal_swing_risk": "Geç gol veya skor kırılması riski yüksek.",
|
||||||
|
"live_match_open_state": "Canlı maç tamamen açık oyuna dönmüş durumda.",
|
||||||
|
"live_match_active_state": "Canlı maç beklenenden daha hareketli ilerliyor.",
|
||||||
|
"live_state_impossible_market": "Canlı maç durumu bu marketi geçersiz kılıyor.",
|
||||||
|
"live_score_exceeds_under_line": "Canlı skor, alt seçeneğinin üst sınırına çok yaklaştı veya geçti.",
|
||||||
|
"score_model_conflicts_with_under_pick": "Skor ve xG modeli bu alt seçeneğiyle çelişiyor.",
|
||||||
|
"score_model_conflicts_with_over_pick": "Skor ve xG modeli bu üst seçeneğiyle çelişiyor.",
|
||||||
|
"market_stack_conflict_over25": "2.5 üst sinyali bu marketle çelişiyor.",
|
||||||
|
"market_stack_conflict_btts": "KG Var sinyali bu marketle çelişiyor.",
|
||||||
|
"live_total_goals_close_to_line": "Canlı toplam gol sayısı bu çizgiye fazla yaklaştı.",
|
||||||
|
"score_model_conflicts_with_btts_no": "Skor ve xG modeli KG Yok seçeneğiyle çelişiyor.",
|
||||||
|
"score_model_conflicts_with_draw_pick": "Skor modeli beraberlik seçeneğini desteklemiyor.",
|
||||||
|
"score_model_conflicts_with_home_pick": "Skor modeli ev sahibi seçeneğini desteklemiyor.",
|
||||||
|
"score_model_conflicts_with_away_pick": "Skor modeli deplasman seçeneğini desteklemiyor."
|
||||||
},
|
},
|
||||||
"ev-edge": "Teorik Avantaj",
|
"ev-edge": "Teorik Avantaj",
|
||||||
"implied-prob": "Piyasa Olasılığı",
|
"implied-prob": "Piyasa Olasılığı",
|
||||||
@@ -360,7 +401,11 @@
|
|||||||
"confidence-medium": "Orta",
|
"confidence-medium": "Orta",
|
||||||
"confidence-low": "Düşük",
|
"confidence-low": "Düşük",
|
||||||
"confidence-unknown": "Belirsiz",
|
"confidence-unknown": "Belirsiz",
|
||||||
"info": "Bilgi"
|
"info": "Bilgi",
|
||||||
|
"unified-confidence": "Güven Skoru",
|
||||||
|
"underdog-ref-tooltip": "Underdog tarafının model olasılığı (bilgi amaçlı)",
|
||||||
|
"trap-tooltip": "Piyasa aşırı güveniyor",
|
||||||
|
"no-value-tooltip": "Model favoriyle hemfikir ama oran çok düşük"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"coupons": {
|
"coupons": {
|
||||||
@@ -487,7 +532,8 @@
|
|||||||
"freq-mode-active": "Frekans Motoru aktif",
|
"freq-mode-active": "Frekans Motoru aktif",
|
||||||
"ai-mode-active": "AI Motoru aktif",
|
"ai-mode-active": "AI Motoru aktif",
|
||||||
"engine-mode-label": "Motor Seçimi",
|
"engine-mode-label": "Motor Seçimi",
|
||||||
"engine-mode-help": "AI: Gemini tabanlı yapay zeka tahmini. Frekans: Veritabanı tabanlı istatistiksel analiz."
|
"engine-mode-help": "AI: Gemini tabanlı yapay zeka tahmini. Frekans: Veritabanı tabanlı istatistiksel analiz.",
|
||||||
|
"all-markets-scanned": "Tüm marketler taranacak"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profil",
|
"title": "Profil",
|
||||||
@@ -519,7 +565,14 @@
|
|||||||
"leagues": "Ligler",
|
"leagues": "Ligler",
|
||||||
"countries-leagues": "Ülkeler & Ligler",
|
"countries-leagues": "Ülkeler & Ligler",
|
||||||
"search-at-least-2": "Takım aramak için en az 2 karakter yazın.",
|
"search-at-least-2": "Takım aramak için en az 2 karakter yazın.",
|
||||||
"all": "Tümü"
|
"all": "Tümü",
|
||||||
|
"back-to-leagues": "Liglere Dön",
|
||||||
|
"league-not-found": "Lig Bulunamadı",
|
||||||
|
"past-matches": "Geçmiş Maçlar",
|
||||||
|
"not-found": "Bulunamadı",
|
||||||
|
"no-leagues-found": "Seçili kriterlere uygun lig bulunamadı.",
|
||||||
|
"team-not-found": "Takım Bulunamadı",
|
||||||
|
"no-teams-found": "\"{query}\" aramasıyla eşleşen bir takım bulunamadı."
|
||||||
},
|
},
|
||||||
"h2h": {
|
"h2h": {
|
||||||
"title": "Karşılıklı Karşılaşma",
|
"title": "Karşılıklı Karşılaşma",
|
||||||
@@ -809,5 +862,71 @@
|
|||||||
"remaining": "kalan"
|
"remaining": "kalan"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"refund-policy": "İade Politikası"
|
"refund-policy": "İade Politikası",
|
||||||
|
"teams": {
|
||||||
|
"search-placeholder": "Takım adı yazın... (min 2 karakter)",
|
||||||
|
"search-hint": "Aramak istediğiniz takımın adını yazın",
|
||||||
|
"search-example": "Örnek: Galatasaray, Barcelona, Manchester City",
|
||||||
|
"no-results": "Sonuç bulunamadı",
|
||||||
|
"upcoming": "Yaklaşan",
|
||||||
|
"not-found": "Takım bulunamadı",
|
||||||
|
"matches-count": "Maç",
|
||||||
|
"upcoming-matches": "Yaklaşan Maçlar",
|
||||||
|
"past-matches": "Geçmiş Maçlar",
|
||||||
|
"page-info": "Sayfa {page}/{totalPages} • Toplam {total} maç",
|
||||||
|
"no-past-matches-season": "Bu sezonda geçmiş maç bulunamadı"
|
||||||
|
},
|
||||||
|
"v28": {
|
||||||
|
"avg-cards": "Ort: {avg} kart",
|
||||||
|
"band": "Band",
|
||||||
|
"cards-analysis": "Kart Analizi",
|
||||||
|
"combined-over-rate": "Kombine ÜST Oranı",
|
||||||
|
"combined-weighting": "%60 Hakem + %40 Takım ağırlıklı",
|
||||||
|
"confirmations": "Onay",
|
||||||
|
"engines-agree": "Motorlar Uyumlu",
|
||||||
|
"engines-disagree": "Motorlar Farklı",
|
||||||
|
"ft": "MS",
|
||||||
|
"ht": "İY",
|
||||||
|
"htft-info": "İlk yarı sonucu ve maç sonucu kombinasyonlarının tarihsel oran bandındaki gerçekleşme oranları.",
|
||||||
|
"htft-title": "İY/MS Kombinasyonları",
|
||||||
|
"insufficient": "YETERSİZ",
|
||||||
|
"insufficient-data": "Yetersiz veri — henüz yeterli maç örneği bulunamadı.",
|
||||||
|
"label-away": "MS Dep",
|
||||||
|
"label-btts_yes": "KG Var",
|
||||||
|
"label-cards_over": "Kart ÜST",
|
||||||
|
"label-dc_12": "ÇŞ 12",
|
||||||
|
"label-dc_1x": "ÇŞ 1X",
|
||||||
|
"label-dc_x2": "ÇŞ X2",
|
||||||
|
"label-home": "MS Ev",
|
||||||
|
"label-ht_away": "İY Dep",
|
||||||
|
"label-ht_home": "İY Ev",
|
||||||
|
"label-ht_ou05_over": "İY ÜST 0.5",
|
||||||
|
"label-ht_ou15_over": "İY ÜST 1.5",
|
||||||
|
"label-htft_11": "İY/MS 1/1",
|
||||||
|
"label-htft_12": "İY/MS 1/2",
|
||||||
|
"label-htft_1x": "İY/MS 1/X",
|
||||||
|
"label-htft_21": "İY/MS 2/1",
|
||||||
|
"label-htft_22": "İY/MS 2/2",
|
||||||
|
"label-htft_2x": "İY/MS 2/X",
|
||||||
|
"label-htft_x1": "İY/MS X/1",
|
||||||
|
"label-htft_x2": "İY/MS X/2",
|
||||||
|
"label-htft_xx": "İY/MS X/X",
|
||||||
|
"label-oe_odd": "Tek",
|
||||||
|
"label-ou15_over": "ÜST 1.5",
|
||||||
|
"label-ou25_over": "ÜST 2.5",
|
||||||
|
"label-ou35_over": "ÜST 3.5",
|
||||||
|
"matches-count": "{count} maç",
|
||||||
|
"odds": "Oran",
|
||||||
|
"over-rate": "Üst oranı",
|
||||||
|
"panel-info": "Geçmiş maçlarda benzer oranlarda gerçekleşen sonuçların istatistiksel analizi. Triple Value, Kart Profili ve İY/MS kombinasyonlarını içerir.",
|
||||||
|
"panel-title": "V28 Oran Bandı Analizi",
|
||||||
|
"pass": "PAS",
|
||||||
|
"referee-profile": "Hakem Profili",
|
||||||
|
"strongest": "En güçlü",
|
||||||
|
"team-profile": "Takım Profili",
|
||||||
|
"triple-value-info": "Model olasılığı, oran bandı istatistiği ve piyasa oranı karşılaştırması. Edge pozitifse model avantaj görüyor demektir.",
|
||||||
|
"triple-value-title": "Değer Tespiti (Triple Value)",
|
||||||
|
"value": "DEĞER",
|
||||||
|
"value-signals": "{count} Değer Sinyali"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ export default function SignInForm() {
|
|||||||
router.replace("/home");
|
router.replace("/home");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toaster.error({
|
toaster.error({
|
||||||
title: (error as Error).message || "Giriş yaparken hata oluştu!",
|
title: (error as Error).message || t("auth.login-error"),
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -101,12 +101,12 @@ export function LoginModal({
|
|||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
toaster.success({
|
toaster.success({
|
||||||
title: t("auth.login-success") || "Giriş başarılı!",
|
title: t("auth.login-success"),
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toaster.error({
|
toaster.error({
|
||||||
title: (error as Error).message || "Giriş yapılamadı!",
|
title: (error as Error).message || t("auth.login-error"),
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -138,12 +138,12 @@ export function LoginModal({
|
|||||||
|
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
toaster.success({
|
toaster.success({
|
||||||
title: t("auth.register-success") || "Kayıt başarılı!",
|
title: t("auth.register-success"),
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toaster.error({
|
toaster.error({
|
||||||
title: (error as Error).message || "Kayıt yapılamadı!",
|
title: (error as Error).message || t("auth.register-error"),
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -569,7 +569,7 @@ export default function CouponBuilderContent() {
|
|||||||
<Flex
|
<Flex
|
||||||
gap={6}
|
gap={6}
|
||||||
direction={{ base: "column", xl: "row" }}
|
direction={{ base: "column", xl: "row" }}
|
||||||
align="flex-start"
|
align={{ base: "stretch", xl: "flex-start" }}
|
||||||
>
|
>
|
||||||
<Box flex={1.7} minW={0}>
|
<Box flex={1.7} minW={0}>
|
||||||
<Card.Root
|
<Card.Root
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default function FrequencyPanel() {
|
|||||||
</HStack>
|
</HStack>
|
||||||
{selectedMarkets.length === 0 && (
|
{selectedMarkets.length === 0 && (
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Text fontSize="xs" color="fg.muted">
|
||||||
Tüm marketler taranacak
|
{t("all-markets-scanned")}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ export default function DashboardContent() {
|
|||||||
<Flex
|
<Flex
|
||||||
gap={6}
|
gap={6}
|
||||||
direction={{ base: "column", lg: "row" }}
|
direction={{ base: "column", lg: "row" }}
|
||||||
align="flex-start"
|
align={{ base: "stretch", lg: "flex-start" }}
|
||||||
>
|
>
|
||||||
{/* Left Column — Today's Matches */}
|
{/* Left Column — Today's Matches */}
|
||||||
<Box flex={2} minW={0}>
|
<Box flex={2} minW={0}>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export default function LeagueDetailContent({
|
|||||||
mb={2}
|
mb={2}
|
||||||
fontWeight="medium"
|
fontWeight="medium"
|
||||||
>
|
>
|
||||||
<LuArrowLeft /> Liglere Dön
|
<LuArrowLeft /> {t("back-to-leagues")}
|
||||||
</ChakraLink>
|
</ChakraLink>
|
||||||
|
|
||||||
{leagueQuery.isLoading ? (
|
{leagueQuery.isLoading ? (
|
||||||
@@ -134,7 +134,7 @@ export default function LeagueDetailContent({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Heading>Lig Bulunamadı</Heading>
|
<Heading>{t("league-not-found")}</Heading>
|
||||||
)}
|
)}
|
||||||
</VStack>
|
</VStack>
|
||||||
</SlideUp>
|
</SlideUp>
|
||||||
@@ -163,7 +163,7 @@ export default function LeagueDetailContent({
|
|||||||
borderColor={useColorModeValue("gray.200", "gray.800")}
|
borderColor={useColorModeValue("gray.200", "gray.800")}
|
||||||
>
|
>
|
||||||
<Heading size="md" mb={6}>
|
<Heading size="md" mb={6}>
|
||||||
Geçmiş Maçlar
|
{t("past-matches")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<MatchList
|
<MatchList
|
||||||
flatMatches={flatMatches}
|
flatMatches={flatMatches}
|
||||||
|
|||||||
@@ -514,10 +514,10 @@ export default function LeaguesContent() {
|
|||||||
<LuTrophy size={40} color="gray" />
|
<LuTrophy size={40} color="gray" />
|
||||||
</Box>
|
</Box>
|
||||||
<Heading size="md" mb={2}>
|
<Heading size="md" mb={2}>
|
||||||
Bulunamadı
|
{t("not-found")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text color="fg.muted">
|
<Text color="fg.muted">
|
||||||
Seçili kriterlere uygun lig bulunamadı.
|
{t("no-leagues-found")}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
@@ -709,11 +709,10 @@ export default function LeaguesContent() {
|
|||||||
textAlign="center"
|
textAlign="center"
|
||||||
>
|
>
|
||||||
<Heading size="md" mb={2}>
|
<Heading size="md" mb={2}>
|
||||||
Takım Bulunamadı
|
{t("team-not-found")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Text color="fg.muted">
|
<Text color="fg.muted">
|
||||||
"{debouncedTeamQuery}" aramasıyla eşleşen bir takım
|
{t("no-teams-found", { query: debouncedTeamQuery })}
|
||||||
bulunamadı.
|
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Icon,
|
Icon,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import {
|
import {
|
||||||
LuUsers,
|
LuUsers,
|
||||||
LuUser,
|
LuUser,
|
||||||
@@ -30,47 +31,49 @@ interface LineupsCardProps {
|
|||||||
/**
|
/**
|
||||||
* Lineup source metadata used for title, badge, and informational banners.
|
* Lineup source metadata used for title, badge, and informational banners.
|
||||||
*/
|
*/
|
||||||
function getLineupSourceMeta(source?: string) {
|
function getLineupSourceMeta(
|
||||||
|
source: string | undefined,
|
||||||
|
t: ReturnType<typeof useTranslations>,
|
||||||
|
) {
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case "confirmed_live":
|
case "confirmed_live":
|
||||||
return {
|
return {
|
||||||
title: "Resmi İlk 11",
|
title: t("lineup-official-xi"),
|
||||||
badge: "Onaylı Kadro",
|
badge: t("lineup-confirmed-squad"),
|
||||||
badgeColor: "green" as const,
|
badgeColor: "green" as const,
|
||||||
icon: LuShieldCheck,
|
icon: LuShieldCheck,
|
||||||
description: "Kadro resmi olarak onaylandı.",
|
description: t("lineup-confirmed-live-desc"),
|
||||||
};
|
};
|
||||||
case "confirmed_participation":
|
case "confirmed_participation":
|
||||||
return {
|
return {
|
||||||
title: "Onaylı Kadro",
|
title: t("lineup-confirmed-squad"),
|
||||||
badge: "Onaylı",
|
badge: t("lineup-confirmed"),
|
||||||
badgeColor: "green" as const,
|
badgeColor: "green" as const,
|
||||||
icon: LuShieldCheck,
|
icon: LuShieldCheck,
|
||||||
description: "Kadro maç katılım verilerinden alındı.",
|
description: t("lineup-participation-desc"),
|
||||||
};
|
};
|
||||||
case "probable_xi":
|
case "probable_xi":
|
||||||
return {
|
return {
|
||||||
title: "Muhtemel Kadro",
|
title: t("lineup-probable-squad"),
|
||||||
badge: "Muhtemel",
|
badge: t("lineup-probable"),
|
||||||
badgeColor: "orange" as const,
|
badgeColor: "orange" as const,
|
||||||
icon: LuUsers,
|
icon: LuUsers,
|
||||||
description:
|
description: t("lineup-probable-desc"),
|
||||||
"Son maçlardaki ilk 11 verilerine dayalı muhtemel kadro. AI analizi bu kadro üzerinden yapılmaktadır.",
|
|
||||||
};
|
};
|
||||||
case "none":
|
case "none":
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
title: "Kadro Bilgisi",
|
title: t("lineup-info"),
|
||||||
badge: "Kadro Bekleniyor",
|
badge: t("lineup-pending"),
|
||||||
badgeColor: "gray" as const,
|
badgeColor: "gray" as const,
|
||||||
icon: LuClock,
|
icon: LuClock,
|
||||||
description:
|
description: t("lineup-pending-desc"),
|
||||||
"Kadro henüz açıklanmadı. AI analizi, takımların genel güç dengesi ve istatistiklerine dayalı olarak üretilmiştir.",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
||||||
|
const t = useTranslations("matches");
|
||||||
const cardBg = useColorModeValue("white", "gray.800");
|
const cardBg = useColorModeValue("white", "gray.800");
|
||||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||||
const headerBg = useColorModeValue("gray.50", "whiteAlpha.50");
|
const headerBg = useColorModeValue("gray.50", "whiteAlpha.50");
|
||||||
@@ -82,7 +85,7 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|||||||
|
|
||||||
// Determine lineup source from prediction data quality
|
// Determine lineup source from prediction data quality
|
||||||
const source = prediction?.data_quality?.lineup_source;
|
const source = prediction?.data_quality?.lineup_source;
|
||||||
const meta = getLineupSourceMeta(source);
|
const meta = getLineupSourceMeta(source, t);
|
||||||
|
|
||||||
// Fallback: If no starting players are marked, but we have players, treat them as probable XI
|
// Fallback: If no starting players are marked, but we have players, treat them as probable XI
|
||||||
if (
|
if (
|
||||||
@@ -195,10 +198,10 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|||||||
gap={1}
|
gap={1}
|
||||||
>
|
>
|
||||||
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
|
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
|
||||||
Kadro henüz belli değil
|
{t("lineup-tbd")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="fg.subtle">
|
<Text fontSize="xs" color="fg.subtle">
|
||||||
Maç saatine yakın güncellenecek
|
{t("lineup-tbd-sub")}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
@@ -259,10 +262,10 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|||||||
gap={1}
|
gap={1}
|
||||||
>
|
>
|
||||||
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
|
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
|
||||||
Kadro henüz belli değil
|
{t("lineup-tbd")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="fg.subtle">
|
<Text fontSize="xs" color="fg.subtle">
|
||||||
Maç saatine yakın güncellenecek
|
{t("lineup-tbd-sub")}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
@@ -280,7 +283,7 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|||||||
<Icon as={LuClock} boxSize={8} color="fg.subtle" />
|
<Icon as={LuClock} boxSize={8} color="fg.subtle" />
|
||||||
<VStack gap={1}>
|
<VStack gap={1}>
|
||||||
<Text fontWeight="semibold" color="fg.muted">
|
<Text fontWeight="semibold" color="fg.muted">
|
||||||
Kadro Henüz Açıklanmadı
|
{t("lineup-not-announced")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
@@ -288,9 +291,10 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
|||||||
textAlign="center"
|
textAlign="center"
|
||||||
maxW="sm"
|
maxW="sm"
|
||||||
>
|
>
|
||||||
{match.homeTeamName} ve {match.awayTeamName} kadroları maç
|
{t("lineup-not-announced-desc", {
|
||||||
saatine yakın güncellenecektir. AI analizi, takım istatistikleri
|
home: match.homeTeamName,
|
||||||
ve güç dengesi üzerinden yapılmaktadır.
|
away: match.awayTeamName,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
VStack,
|
VStack,
|
||||||
Image,
|
Image,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
@@ -28,6 +28,7 @@ const MotionBox = motion.create(Box);
|
|||||||
export default function MatchCard({ match }: MatchCardProps) {
|
export default function MatchCard({ match }: MatchCardProps) {
|
||||||
const t = useTranslations("matches");
|
const t = useTranslations("matches");
|
||||||
const tAuth = useTranslations("auth");
|
const tAuth = useTranslations("auth");
|
||||||
|
const locale = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
@@ -107,7 +108,7 @@ export default function MatchCard({ match }: MatchCardProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Text fontSize="xs" color="fg.muted">
|
||||||
{matchDate.toLocaleDateString("tr-TR", {
|
{matchDate.toLocaleDateString(locale, {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
@@ -123,6 +123,7 @@ function MatchDetailSkeleton() {
|
|||||||
export default function MatchDetailContent() {
|
export default function MatchDetailContent() {
|
||||||
const t = useTranslations("matches");
|
const t = useTranslations("matches");
|
||||||
const tPred = useTranslations("predictions");
|
const tPred = useTranslations("predictions");
|
||||||
|
const locale = useLocale();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { data: meData } = useGetMe();
|
const { data: meData } = useGetMe();
|
||||||
const usageLimit = meData?.data?.usageLimit;
|
const usageLimit = meData?.data?.usageLimit;
|
||||||
@@ -224,7 +225,7 @@ export default function MatchDetailContent() {
|
|||||||
const show = (key: string) => activeTab === "all" || activeTab === key;
|
const show = (key: string) => activeTab === "all" || activeTab === key;
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: "all", label: t("all-matches", { defaultValue: "Hepsi" }) },
|
{ key: "all", label: t("overview") },
|
||||||
...(matchEvents.length > 0 ? [{ key: "events", label: t("match-events") }] : []),
|
...(matchEvents.length > 0 ? [{ key: "events", label: t("match-events") }] : []),
|
||||||
...(hasStats ? [{ key: "stats", label: t("statistics") }] : []),
|
...(hasStats ? [{ key: "stats", label: t("statistics") }] : []),
|
||||||
{ key: "lineups", label: t("lineups") },
|
{ key: "lineups", label: t("lineups") },
|
||||||
@@ -391,7 +392,7 @@ export default function MatchDetailContent() {
|
|||||||
<HStack gap={1} mt={1}>
|
<HStack gap={1} mt={1}>
|
||||||
<LuCalendar size={11} />
|
<LuCalendar size={11} />
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
{new Date(match.mstUtc).toLocaleDateString("tr-TR", {
|
{new Date(match.mstUtc).toLocaleDateString(locale, {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export default function MatchList({
|
|||||||
{league.name}
|
{league.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="fg.muted" ml="auto">
|
<Text fontSize="xs" color="fg.muted" ml="auto">
|
||||||
{league.matches.length} {t("title").toLowerCase()}
|
{t("match-count", { count: league.matches.length })}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Box, Flex, Heading, Group, Button } from "@chakra-ui/react";
|
import { Box, Flex, Heading, Group, Button, Input } from "@chakra-ui/react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { SlideUp } from "@/components/motion";
|
import { SlideUp } from "@/components/motion";
|
||||||
@@ -160,8 +160,13 @@ export default function MatchesContent() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
|
size="sm"
|
||||||
|
w="auto"
|
||||||
|
minW="150px"
|
||||||
|
flexShrink={0}
|
||||||
|
borderRadius="md"
|
||||||
value={dateFilter}
|
value={dateFilter}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const dateVal = e.target.value;
|
const dateVal = e.target.value;
|
||||||
@@ -173,15 +178,6 @@ export default function MatchesContent() {
|
|||||||
handleQuickFilterChange("all");
|
handleQuickFilterChange("all");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
style={{
|
|
||||||
padding: "0.25rem 0.5rem",
|
|
||||||
borderRadius: "0.375rem",
|
|
||||||
border: "1px solid var(--chakra-colors-gray-200)",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
background: "transparent",
|
|
||||||
color: "inherit",
|
|
||||||
outline: "none",
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@@ -198,7 +194,7 @@ export default function MatchesContent() {
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<Flex
|
<Flex
|
||||||
gap={6}
|
gap={6}
|
||||||
align="flex-start"
|
align={{ base: "stretch", lg: "flex-start" }}
|
||||||
direction={{ base: "column", lg: "row" }}
|
direction={{ base: "column", lg: "row" }}
|
||||||
>
|
>
|
||||||
{/* League Sidebar (Desktop only) */}
|
{/* League Sidebar (Desktop only) */}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Box, SimpleGrid, Text, Card, VStack, Flex } from "@chakra-ui/react";
|
import { Box, SimpleGrid, Text, Card, VStack, Flex } from "@chakra-ui/react";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
import type { MatchPredictionDto } from "@/lib/api/predictions/types";
|
import type { MatchPredictionDto } from "@/lib/api/predictions/types";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
// movement: per market (Turkish name) -> per selection -> {open, close}
|
// movement: per market (Turkish name) -> per selection -> {open, close}
|
||||||
export type OddsMovement = Record<
|
export type OddsMovement = Record<
|
||||||
@@ -145,6 +146,7 @@ function MarketBlock({ title, selections, prediction, movement }: MarketBlockPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function OddsCard({ odds, prediction, movement }: OddsCardProps) {
|
export default function OddsCard({ odds, prediction, movement }: OddsCardProps) {
|
||||||
|
const t = useTranslations("matches");
|
||||||
const cardBg = useColorModeValue("white", "gray.900");
|
const cardBg = useColorModeValue("white", "gray.900");
|
||||||
const borderColor = useColorModeValue("gray.100", "gray.800");
|
const borderColor = useColorModeValue("gray.100", "gray.800");
|
||||||
|
|
||||||
@@ -176,10 +178,10 @@ export default function OddsCard({ odds, prediction, movement }: OddsCardProps)
|
|||||||
<VStack align="stretch" gap={4}>
|
<VStack align="stretch" gap={4}>
|
||||||
<Flex justify="space-between" align="baseline" wrap="wrap" gap={1}>
|
<Flex justify="space-between" align="baseline" wrap="wrap" gap={1}>
|
||||||
<Text fontSize="lg" fontWeight="bold">
|
<Text fontSize="lg" fontWeight="bold">
|
||||||
Canlı İddaa Oranları
|
{t("live-odds")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Text fontSize="xs" color="fg.muted">
|
||||||
%= modelin tutma olasılığı (kalibre) · ↓↑ = açılış→kapanış hareketi
|
{t("odds-legend")}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
|
|||||||
@@ -837,17 +837,17 @@ function SummaryTable({
|
|||||||
{getSignalTierLabel(item.signal_tier)}
|
{getSignalTierLabel(item.signal_tier)}
|
||||||
</Badge>
|
</Badge>
|
||||||
{item.is_underdog_reference ? (
|
{item.is_underdog_reference ? (
|
||||||
<Badge colorPalette="gray" variant="outline" title="Underdog tarafının model olasılığı (bilgi amaçlı)">
|
<Badge colorPalette="gray" variant="outline" title={getUiText(ui, "underdog-ref-tooltip", "Underdog tarafının model olasılığı (bilgi amaçlı)")}>
|
||||||
Underdog ref.
|
Underdog ref.
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{item.betting_brain?.trap_market_flag ? (
|
{item.betting_brain?.trap_market_flag ? (
|
||||||
<Badge colorPalette="red" variant="subtle" title={`Piyasa aşırı güveniyor (gap ${(item.betting_brain.trap_market_gap || 0) * 100 | 0}pp)`}>
|
<Badge colorPalette="red" variant="subtle" title={`${getUiText(ui, "trap-tooltip", "Piyasa aşırı güveniyor")} (gap ${((item.betting_brain.trap_market_gap || 0) * 100) | 0}pp)`}>
|
||||||
Trap
|
Trap
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
{item.betting_brain?.action === "WATCH_NO_VALUE" ? (
|
{item.betting_brain?.action === "WATCH_NO_VALUE" ? (
|
||||||
<Badge colorPalette="orange" variant="subtle" title="Model favoriyle hemfikir ama oran çok düşük">
|
<Badge colorPalette="orange" variant="subtle" title={getUiText(ui, "no-value-tooltip", "Model favoriyle hemfikir ama oran çok düşük")}>
|
||||||
No-value
|
No-value
|
||||||
</Badge>
|
</Badge>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
LuTrendingUp,
|
LuTrendingUp,
|
||||||
} from "react-icons/lu";
|
} from "react-icons/lu";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { Tooltip } from "@/components/ui/overlays/tooltip";
|
import { Tooltip } from "@/components/ui/overlays/tooltip";
|
||||||
import type {
|
import type {
|
||||||
HtftComboKey,
|
HtftComboKey,
|
||||||
@@ -44,32 +45,32 @@ function edgeStr(edge: number): string {
|
|||||||
return `${sign}${(edge * 100).toFixed(1)}%`;
|
return `${sign}${(edge * 100).toFixed(1)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRIPLE_VALUE_LABELS: Record<string, string> = {
|
const TRIPLE_VALUE_KEYS = new Set([
|
||||||
home: "MS Ev",
|
"home",
|
||||||
away: "MS Dep",
|
"away",
|
||||||
ou25_over: "ÜST 2.5",
|
"ou25_over",
|
||||||
btts_yes: "KG Var",
|
"btts_yes",
|
||||||
ou15_over: "ÜST 1.5",
|
"ou15_over",
|
||||||
ou35_over: "ÜST 3.5",
|
"ou35_over",
|
||||||
dc_1x: "ÇŞ 1X",
|
"dc_1x",
|
||||||
dc_x2: "ÇŞ X2",
|
"dc_x2",
|
||||||
dc_12: "ÇŞ 12",
|
"dc_12",
|
||||||
ht_home: "İY Ev",
|
"ht_home",
|
||||||
ht_away: "İY Dep",
|
"ht_away",
|
||||||
ht_ou05_over: "İY ÜST 0.5",
|
"ht_ou05_over",
|
||||||
ht_ou15_over: "İY ÜST 1.5",
|
"ht_ou15_over",
|
||||||
oe_odd: "Tek",
|
"oe_odd",
|
||||||
cards_over: "Kart ÜST",
|
"cards_over",
|
||||||
htft_11: "İY/MS 1/1",
|
"htft_11",
|
||||||
htft_1x: "İY/MS 1/X",
|
"htft_1x",
|
||||||
htft_12: "İY/MS 1/2",
|
"htft_12",
|
||||||
htft_x1: "İY/MS X/1",
|
"htft_x1",
|
||||||
htft_xx: "İY/MS X/X",
|
"htft_xx",
|
||||||
htft_x2: "İY/MS X/2",
|
"htft_x2",
|
||||||
htft_21: "İY/MS 2/1",
|
"htft_21",
|
||||||
htft_2x: "İY/MS 2/X",
|
"htft_2x",
|
||||||
htft_22: "İY/MS 2/2",
|
"htft_22",
|
||||||
};
|
]);
|
||||||
|
|
||||||
const HTFT_DISPLAY: Record<HtftComboKey, string> = {
|
const HTFT_DISPLAY: Record<HtftComboKey, string> = {
|
||||||
"11": "1/1",
|
"11": "1/1",
|
||||||
@@ -98,7 +99,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="info"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="2xs"
|
size="2xs"
|
||||||
colorPalette="gray"
|
colorPalette="gray"
|
||||||
@@ -142,6 +143,7 @@ function TripleValueCard({
|
|||||||
label: string;
|
label: string;
|
||||||
entry: TripleValueEntryDto;
|
entry: TripleValueEntryDto;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("v28");
|
||||||
const isValue = entry.is_value;
|
const isValue = entry.is_value;
|
||||||
const hasSample = entry.band_sample >= 5;
|
const hasSample = entry.band_sample >= 5;
|
||||||
|
|
||||||
@@ -194,11 +196,11 @@ function TripleValueCard({
|
|||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
>
|
>
|
||||||
DEĞER
|
{t("value")}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : hasSample ? (
|
) : hasSample ? (
|
||||||
<Badge variant="outline" fontSize="2xs" borderRadius="full">
|
<Badge variant="outline" fontSize="2xs" borderRadius="full">
|
||||||
PAS
|
{t("pass")}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -207,7 +209,7 @@ function TripleValueCard({
|
|||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
>
|
>
|
||||||
YETERSİZ
|
{t("insufficient")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -218,20 +220,20 @@ function TripleValueCard({
|
|||||||
|
|
||||||
<HStack gap={2} flexWrap="wrap">
|
<HStack gap={2} flexWrap="wrap">
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
Band: {pct(entry.band_rate, 1)}
|
{t("band")}: {pct(entry.band_rate, 1)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
Oran: {pct(entry.implied_prob, 1)}
|
{t("odds")}: {pct(entry.implied_prob, 1)}
|
||||||
</Text>
|
</Text>
|
||||||
{entry.confirmations !== undefined && (
|
{entry.confirmations !== undefined && (
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
Onay: {entry.confirmations}/2
|
{t("confirmations")}: {entry.confirmations}/2
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
{entry.band_sample} maç
|
{t("matches-count", { count: entry.band_sample })}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -267,6 +269,7 @@ function ProgressBar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
||||||
|
const t = useTranslations("v28");
|
||||||
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 hasData = cards.sample >= 3;
|
const hasData = cards.sample >= 3;
|
||||||
@@ -283,11 +286,11 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
|||||||
<HStack gap={2} mb={2}>
|
<HStack gap={2} mb={2}>
|
||||||
<Icon as={LuRectangleVertical} boxSize={4} color="yellow.500" />
|
<Icon as={LuRectangleVertical} boxSize={4} color="yellow.500" />
|
||||||
<Text fontSize="sm" fontWeight="semibold">
|
<Text fontSize="sm" fontWeight="semibold">
|
||||||
Kart Analizi
|
{t("cards-analysis")}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Text fontSize="sm" color="fg.muted">
|
<Text fontSize="sm" color="fg.muted">
|
||||||
Yetersiz veri — henüz yeterli maç örneği bulunamadı.
|
{t("insufficient-data")}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -309,11 +312,11 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
|||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<Icon as={LuRectangleVertical} boxSize={4} color="yellow.500" />
|
<Icon as={LuRectangleVertical} boxSize={4} color="yellow.500" />
|
||||||
<Text fontSize="sm" fontWeight="semibold">
|
<Text fontSize="sm" fontWeight="semibold">
|
||||||
Kart Analizi
|
{t("cards-analysis")}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Badge variant="outline" fontSize="2xs" borderRadius="full">
|
<Badge variant="outline" fontSize="2xs" borderRadius="full">
|
||||||
{cards.sample} maç
|
{t("matches-count", { count: cards.sample })}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
@@ -322,10 +325,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
|||||||
<Box>
|
<Box>
|
||||||
<HStack justify="space-between" mb={1}>
|
<HStack justify="space-between" mb={1}>
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Text fontSize="xs" color="fg.muted">
|
||||||
Hakem Profili
|
{t("referee-profile")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" fontWeight="semibold">
|
<Text fontSize="xs" fontWeight="semibold">
|
||||||
Ort: {cards.referee_avg.toFixed(1)} kart
|
{t("avg-cards", { avg: cards.referee_avg.toFixed(1) })}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@@ -335,10 +338,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
|||||||
/>
|
/>
|
||||||
<HStack justify="space-between" mt={0.5}>
|
<HStack justify="space-between" mt={0.5}>
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
Üst oranı: {pct(cards.referee_over_rate, 0)}
|
{t("over-rate")}: {pct(cards.referee_over_rate, 0)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
{cards.referee_sample} maç
|
{t("matches-count", { count: cards.referee_sample })}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -347,10 +350,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
|||||||
<Box>
|
<Box>
|
||||||
<HStack justify="space-between" mb={1}>
|
<HStack justify="space-between" mb={1}>
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Text fontSize="xs" color="fg.muted">
|
||||||
Takım Profili
|
{t("team-profile")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="xs" fontWeight="semibold">
|
<Text fontSize="xs" fontWeight="semibold">
|
||||||
Ort: {cards.team_avg.toFixed(1)} kart
|
{t("avg-cards", { avg: cards.team_avg.toFixed(1) })}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@@ -360,10 +363,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
|||||||
/>
|
/>
|
||||||
<HStack justify="space-between" mt={0.5}>
|
<HStack justify="space-between" mt={0.5}>
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
Üst oranı: {pct(cards.team_over_rate, 0)}
|
{t("over-rate")}: {pct(cards.team_over_rate, 0)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
{cards.team_sample} maç
|
{t("matches-count", { count: cards.team_sample })}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -377,10 +380,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
|
|||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<VStack align="start" gap={0}>
|
<VStack align="start" gap={0}>
|
||||||
<Text fontSize="xs" fontWeight="semibold">
|
<Text fontSize="xs" fontWeight="semibold">
|
||||||
Kombine ÜST Oranı
|
{t("combined-over-rate")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
%60 Hakem + %40 Takım ağırlıklı
|
{t("combined-weighting")}
|
||||||
</Text>
|
</Text>
|
||||||
</VStack>
|
</VStack>
|
||||||
<Text fontSize="2xl" fontWeight="bold" color={overColor}>
|
<Text fontSize="2xl" fontWeight="bold" color={overColor}>
|
||||||
@@ -402,6 +405,7 @@ function HtftGrid({
|
|||||||
}: {
|
}: {
|
||||||
htft: Record<HtftComboKey, OddsBandHtftComboDto>;
|
htft: Record<HtftComboKey, OddsBandHtftComboDto>;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("v28");
|
||||||
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");
|
||||||
|
|
||||||
@@ -450,10 +454,10 @@ function HtftGrid({
|
|||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<Icon as={LuTarget} boxSize={4} color="teal.500" />
|
<Icon as={LuTarget} boxSize={4} color="teal.500" />
|
||||||
<Text fontSize="sm" fontWeight="semibold">
|
<Text fontSize="sm" fontWeight="semibold">
|
||||||
İY/MS Kombinasyonları
|
{t("htft-title")}
|
||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
<TooltipIcon content="İlk yarı sonucu ve maç sonucu kombinasyonlarının tarihsel oran bandındaki gerçekleşme oranları." />
|
<TooltipIcon content={t("htft-info")} />
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
{/* Column headers */}
|
{/* Column headers */}
|
||||||
@@ -465,7 +469,7 @@ function HtftGrid({
|
|||||||
textAlign="center"
|
textAlign="center"
|
||||||
color="fg.muted"
|
color="fg.muted"
|
||||||
>
|
>
|
||||||
MS 1
|
{t("ft")} 1
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
@@ -473,7 +477,7 @@ function HtftGrid({
|
|||||||
textAlign="center"
|
textAlign="center"
|
||||||
color="fg.muted"
|
color="fg.muted"
|
||||||
>
|
>
|
||||||
MS X
|
{t("ft")} X
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
@@ -481,13 +485,13 @@ function HtftGrid({
|
|||||||
textAlign="center"
|
textAlign="center"
|
||||||
color="fg.muted"
|
color="fg.muted"
|
||||||
>
|
>
|
||||||
MS 2
|
{t("ft")} 2
|
||||||
</Text>
|
</Text>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* Grid rows */}
|
{/* Grid rows */}
|
||||||
{HTFT_ROWS.map((row, rowIdx) => {
|
{HTFT_ROWS.map((row, rowIdx) => {
|
||||||
const rowLabels = ["İY 1", "İY X", "İY 2"];
|
const rowLabels = [`${t("ht")} 1`, `${t("ht")} X`, `${t("ht")} 2`];
|
||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
key={rowIdx}
|
key={rowIdx}
|
||||||
@@ -538,7 +542,7 @@ function HtftGrid({
|
|||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
color={isMax ? "whiteAlpha.800" : "fg.muted"}
|
color={isMax ? "whiteAlpha.800" : "fg.muted"}
|
||||||
>
|
>
|
||||||
{data.sample} maç
|
{t("matches-count", { count: data.sample })}
|
||||||
</Text>
|
</Text>
|
||||||
{isMax && (
|
{isMax && (
|
||||||
<Icon
|
<Icon
|
||||||
@@ -568,7 +572,7 @@ function HtftGrid({
|
|||||||
<HStack gap={2}>
|
<HStack gap={2}>
|
||||||
<Icon as={LuTrendingUp} boxSize={4} color="green.500" />
|
<Icon as={LuTrendingUp} boxSize={4} color="green.500" />
|
||||||
<Text fontSize="xs" fontWeight="semibold">
|
<Text fontSize="xs" fontWeight="semibold">
|
||||||
En güçlü:{" "}
|
{t("strongest")}:{" "}
|
||||||
<Text as="span" color="green.500">
|
<Text as="span" color="green.500">
|
||||||
{HTFT_DISPLAY[maxKey]} ({pct(maxRate, 0)})
|
{HTFT_DISPLAY[maxKey]} ({pct(maxRate, 0)})
|
||||||
</Text>
|
</Text>
|
||||||
@@ -589,6 +593,7 @@ interface V28OddsBandPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
||||||
|
const t = useTranslations("v28");
|
||||||
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");
|
||||||
|
|
||||||
@@ -619,8 +624,8 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
|||||||
<Card.Body gap={5}>
|
<Card.Body gap={5}>
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
icon={LuShieldAlert}
|
icon={LuShieldAlert}
|
||||||
title="V28 Oran Bandı Analizi"
|
title={t("panel-title")}
|
||||||
info="Geçmiş maçlarda benzer oranlarda gerçekleşen sonuçların istatistiksel analizi. Triple Value, Kart Profili ve İY/MS kombinasyonlarını içerir."
|
info={t("panel-info")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Engine version badge */}
|
{/* Engine version badge */}
|
||||||
@@ -641,8 +646,8 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
|||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
>
|
>
|
||||||
{engine.consensus === "AGREE"
|
{engine.consensus === "AGREE"
|
||||||
? "Motorlar Uyumlu"
|
? t("engines-agree")
|
||||||
: "Motorlar Farklı"}
|
: t("engines-disagree")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{valueHits.length > 0 && (
|
{valueHits.length > 0 && (
|
||||||
@@ -652,7 +657,7 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
|||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
fontSize="2xs"
|
fontSize="2xs"
|
||||||
>
|
>
|
||||||
{valueHits.length} Değer Sinyali
|
{t("value-signals", { count: valueHits.length })}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
@@ -663,15 +668,15 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
|||||||
<HStack mb={3} gap={2}>
|
<HStack mb={3} gap={2}>
|
||||||
<Icon as={LuTarget} boxSize={4} color="blue.500" />
|
<Icon as={LuTarget} boxSize={4} color="blue.500" />
|
||||||
<Text fontSize="sm" fontWeight="semibold">
|
<Text fontSize="sm" fontWeight="semibold">
|
||||||
Değer Tespiti (Triple Value)
|
{t("triple-value-title")}
|
||||||
</Text>
|
</Text>
|
||||||
<TooltipIcon content="Model olasılığı, oran bandı istatistiği ve piyasa oranı karşılaştırması. Edge pozitifse model avantaj görüyor demektir." />
|
<TooltipIcon content={t("triple-value-info")} />
|
||||||
</HStack>
|
</HStack>
|
||||||
<SimpleGrid columns={{ base: 2, md: 3, xl: 4 }} gap={2.5}>
|
<SimpleGrid columns={{ base: 2, md: 3, xl: 4 }} gap={2.5}>
|
||||||
{orderedEntries.map(([key, entry]) => (
|
{orderedEntries.map(([key, entry]) => (
|
||||||
<TripleValueCard
|
<TripleValueCard
|
||||||
key={key}
|
key={key}
|
||||||
label={TRIPLE_VALUE_LABELS[key] || key}
|
label={TRIPLE_VALUE_KEYS.has(key) ? t(`label-${key}`) : key}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||||
@@ -178,18 +178,18 @@ export default function TeamDetailContent() {
|
|||||||
if (isMatchLive(match))
|
if (isMatchLive(match))
|
||||||
return (
|
return (
|
||||||
<Badge colorPalette="red" variant="subtle" fontSize="xs">
|
<Badge colorPalette="red" variant="subtle" fontSize="xs">
|
||||||
Canlı
|
{t("matches.live")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
if (isMatchFinished(match))
|
if (isMatchFinished(match))
|
||||||
return (
|
return (
|
||||||
<Badge colorPalette="gray" variant="subtle" fontSize="xs">
|
<Badge colorPalette="gray" variant="subtle" fontSize="xs">
|
||||||
Bitti
|
{t("matches.finished")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Badge colorPalette="green" variant="subtle" fontSize="xs">
|
<Badge colorPalette="green" variant="subtle" fontSize="xs">
|
||||||
Yaklaşan
|
{t("teams.upcoming")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -206,10 +206,10 @@ export default function TeamDetailContent() {
|
|||||||
return (
|
return (
|
||||||
<Flex justify="center" py={20} direction="column" align="center" gap={4}>
|
<Flex justify="center" py={20} direction="column" align="center" gap={4}>
|
||||||
<Text color="fg.muted" fontSize="lg">
|
<Text color="fg.muted" fontSize="lg">
|
||||||
Takım bulunamadı
|
{t("teams.not-found")}
|
||||||
</Text>
|
</Text>
|
||||||
<Button variant="outline" onClick={() => router.back()}>
|
<Button variant="outline" onClick={() => router.back()}>
|
||||||
<LuArrowLeft /> Geri
|
<LuArrowLeft /> {t("common.back")}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@@ -273,11 +273,11 @@ export default function TeamDetailContent() {
|
|||||||
<HStack gap={4} mt={1}>
|
<HStack gap={4} mt={1}>
|
||||||
<Badge colorPalette="blue" variant="subtle">
|
<Badge colorPalette="blue" variant="subtle">
|
||||||
<LuTrophy style={{ width: 12, height: 12 }} />
|
<LuTrophy style={{ width: 12, height: 12 }} />
|
||||||
{totalMatches} Maç
|
{totalMatches} {t("teams.matches-count")}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorPalette="green" variant="subtle">
|
<Badge colorPalette="green" variant="subtle">
|
||||||
<LuCalendar style={{ width: 12, height: 12 }} />
|
<LuCalendar style={{ width: 12, height: 12 }} />
|
||||||
{upcomingMatches.length} Yaklaşan
|
{upcomingMatches.length} {t("teams.upcoming")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</HStack>
|
</HStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
@@ -290,7 +290,7 @@ export default function TeamDetailContent() {
|
|||||||
<FadeIn>
|
<FadeIn>
|
||||||
<Box mb={6}>
|
<Box mb={6}>
|
||||||
<Heading as="h2" size="lg" mb={4}>
|
<Heading as="h2" size="lg" mb={4}>
|
||||||
📅 Yaklaşan Maçlar
|
📅 {t("teams.upcoming-matches")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<VStack gap={2} align="stretch">
|
<VStack gap={2} align="stretch">
|
||||||
{upcomingMatches.map((match: MatchResponseDto) => (
|
{upcomingMatches.map((match: MatchResponseDto) => (
|
||||||
@@ -323,11 +323,11 @@ export default function TeamDetailContent() {
|
|||||||
gap={2}
|
gap={2}
|
||||||
>
|
>
|
||||||
<Heading as="h2" size="lg">
|
<Heading as="h2" size="lg">
|
||||||
📊 Geçmiş Maçlar
|
📊 {t("teams.past-matches")}
|
||||||
</Heading>
|
</Heading>
|
||||||
{/* Pagination Info */}
|
{/* Pagination Info */}
|
||||||
<Text fontSize="xs" color="fg.muted">
|
<Text fontSize="xs" color="fg.muted">
|
||||||
Sayfa {currentPage}/{totalPages} • Toplam {totalMatches} maç
|
{t("teams.page-info", { page: currentPage, totalPages, total: totalMatches })}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ export default function TeamDetailContent() {
|
|||||||
</Flex>
|
</Flex>
|
||||||
) : pastMatches.length === 0 ? (
|
) : pastMatches.length === 0 ? (
|
||||||
<Text color="fg.muted" textAlign="center" py={8}>
|
<Text color="fg.muted" textAlign="center" py={8}>
|
||||||
Bu sezonda geçmiş maç bulunamadı
|
{t("teams.no-past-matches-season")}
|
||||||
</Text>
|
</Text>
|
||||||
) : (
|
) : (
|
||||||
<VStack gap={2} align="stretch">
|
<VStack gap={2} align="stretch">
|
||||||
@@ -401,7 +401,7 @@ export default function TeamDetailContent() {
|
|||||||
disabled={currentPage <= 1}
|
disabled={currentPage <= 1}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
>
|
>
|
||||||
← Önceki
|
← {t("common.previous")}
|
||||||
</Button>
|
</Button>
|
||||||
<HStack gap={1}>
|
<HStack gap={1}>
|
||||||
{Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
|
{Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
|
||||||
@@ -444,7 +444,7 @@ export default function TeamDetailContent() {
|
|||||||
disabled={currentPage >= totalPages}
|
disabled={currentPage >= totalPages}
|
||||||
borderRadius="full"
|
borderRadius="full"
|
||||||
>
|
>
|
||||||
Sonraki →
|
{t("common.next")} →
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
@@ -481,6 +481,7 @@ function MatchRow({
|
|||||||
statusBadge,
|
statusBadge,
|
||||||
onClick,
|
onClick,
|
||||||
}: MatchRowProps) {
|
}: MatchRowProps) {
|
||||||
|
const locale = useLocale();
|
||||||
const hoverBg = useColorModeValue("gray.50", "gray.700");
|
const hoverBg = useColorModeValue("gray.50", "gray.700");
|
||||||
const matchTimestamp = getMatchTimestamp(match);
|
const matchTimestamp = getMatchTimestamp(match);
|
||||||
const homeTeamName = getTeamSideName(match.homeTeam, match.homeTeamName);
|
const homeTeamName = getTeamSideName(match.homeTeam, match.homeTeamName);
|
||||||
@@ -547,7 +548,7 @@ function MatchRow({
|
|||||||
)}
|
)}
|
||||||
<Text fontSize="2xs" color="fg.muted">
|
<Text fontSize="2xs" color="fg.muted">
|
||||||
{matchTimestamp
|
{matchTimestamp
|
||||||
? new Date(matchTimestamp).toLocaleDateString("tr-TR", {
|
? new Date(matchTimestamp).toLocaleDateString(locale, {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function TeamsContent() {
|
|||||||
<Input
|
<Input
|
||||||
value={query}
|
value={query}
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
placeholder="Takım adı yazın... (min 2 karakter)"
|
placeholder={t("teams.search-placeholder")}
|
||||||
variant="flushed"
|
variant="flushed"
|
||||||
size="lg"
|
size="lg"
|
||||||
fontSize="md"
|
fontSize="md"
|
||||||
@@ -78,15 +78,15 @@ export default function TeamsContent() {
|
|||||||
>
|
>
|
||||||
<Text fontSize="5xl">⚽</Text>
|
<Text fontSize="5xl">⚽</Text>
|
||||||
<Text color="fg.muted" fontSize="lg">
|
<Text color="fg.muted" fontSize="lg">
|
||||||
Aramak istediğiniz takımın adını yazın
|
{t("teams.search-hint")}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color="fg.muted" fontSize="sm">
|
<Text color="fg.muted" fontSize="sm">
|
||||||
Örnek: Galatasaray, Barcelona, Manchester City
|
{t("teams.search-example")}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : teams.length === 0 ? (
|
) : teams.length === 0 ? (
|
||||||
<Flex justify="center" py={10}>
|
<Flex justify="center" py={10}>
|
||||||
<Text color="fg.muted">Sonuç bulunamadı</Text>
|
<Text color="fg.muted">{t("teams.no-results")}</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
) : (
|
) : (
|
||||||
<VStack gap={3} align="stretch">
|
<VStack gap={3} align="stretch">
|
||||||
|
|||||||
Reference in New Issue
Block a user