UI/UX pass: i18n completion, responsive fixes, cleanup
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:
2026-06-10 16:55:52 +03:00
parent 095992ad26
commit 5dc3d7e3a3
20 changed files with 407 additions and 3463 deletions
+126 -7
View File
@@ -38,7 +38,9 @@
"login-success": "Login successful!",
"register-success": "Registration successful!",
"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.",
"privacy-policy": "Privacy Policy",
@@ -155,7 +157,26 @@
"penalty": "Penalty",
"half-time": "1st 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": {
"title": "Predictions",
@@ -223,7 +244,27 @@
"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.",
"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",
"implied-prob": "Market Probability",
@@ -360,7 +401,11 @@
"confidence-medium": "Medium",
"confidence-low": "Low",
"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": {
@@ -487,7 +532,8 @@
"freq-mode-active": "Frequency Engine active",
"ai-mode-active": "AI Engine active",
"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": {
"title": "Profile",
@@ -519,7 +565,14 @@
"leagues": "Leagues",
"countries-leagues": "Countries & Leagues",
"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": {
"title": "Head to Head",
@@ -809,5 +862,71 @@
"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
View File
@@ -38,7 +38,9 @@
"login-success": "Giriş başarılı!",
"register-success": "Kayıt başarılı!",
"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.",
"privacy-policy": "Gizlilik Politikası",
@@ -155,7 +157,26 @@
"penalty": "Penaltı",
"half-time": "İlk 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": {
"title": "Tahminler",
@@ -223,7 +244,27 @@
"missing_spread_odds": "Handikap oranları eksik.",
"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ı.",
"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",
"implied-prob": "Piyasa Olasılığı",
@@ -360,7 +401,11 @@
"confidence-medium": "Orta",
"confidence-low": "Düşük",
"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": {
@@ -487,7 +532,8 @@
"freq-mode-active": "Frekans Motoru aktif",
"ai-mode-active": "AI Motoru aktif",
"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": {
"title": "Profil",
@@ -519,7 +565,14 @@
"leagues": "Ligler",
"countries-leagues": "Ülkeler & Ligler",
"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": {
"title": "Karşılıklı Karşılaşma",
@@ -809,5 +862,71 @@
"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");
} catch (error) {
toaster.error({
title: (error as Error).message || "Giriş yaparken hata oluştu!",
title: (error as Error).message || t("auth.login-error"),
type: "error",
});
} finally {
+4 -4
View File
@@ -101,12 +101,12 @@ export function LoginModal({
onOpenChange(false);
toaster.success({
title: t("auth.login-success") || "Giriş başarılı!",
title: t("auth.login-success"),
type: "success",
});
} catch (error) {
toaster.error({
title: (error as Error).message || "Giriş yapılamadı!",
title: (error as Error).message || t("auth.login-error"),
type: "error",
});
} finally {
@@ -138,12 +138,12 @@ export function LoginModal({
onOpenChange(false);
toaster.success({
title: t("auth.register-success") || "Kayıt başarılı!",
title: t("auth.register-success"),
type: "success",
});
} catch (error) {
toaster.error({
title: (error as Error).message || "Kayıt yapılamadı!",
title: (error as Error).message || t("auth.register-error"),
type: "error",
});
} finally {
@@ -569,7 +569,7 @@ export default function CouponBuilderContent() {
<Flex
gap={6}
direction={{ base: "column", xl: "row" }}
align="flex-start"
align={{ base: "stretch", xl: "flex-start" }}
>
<Box flex={1.7} minW={0}>
<Card.Root
+1 -1
View File
@@ -247,7 +247,7 @@ export default function FrequencyPanel() {
</HStack>
{selectedMarkets.length === 0 && (
<Text fontSize="xs" color="fg.muted">
Tüm marketler taranacak
{t("all-markets-scanned")}
</Text>
)}
</VStack>
@@ -265,7 +265,7 @@ export default function DashboardContent() {
<Flex
gap={6}
direction={{ base: "column", lg: "row" }}
align="flex-start"
align={{ base: "stretch", lg: "flex-start" }}
>
{/* Left Column — Today's Matches */}
<Box flex={2} minW={0}>
@@ -85,7 +85,7 @@ export default function LeagueDetailContent({
mb={2}
fontWeight="medium"
>
<LuArrowLeft /> Liglere Dön
<LuArrowLeft /> {t("back-to-leagues")}
</ChakraLink>
{leagueQuery.isLoading ? (
@@ -134,7 +134,7 @@ export default function LeagueDetailContent({
</HStack>
</>
) : (
<Heading>Lig Bulunamadı</Heading>
<Heading>{t("league-not-found")}</Heading>
)}
</VStack>
</SlideUp>
@@ -163,7 +163,7 @@ export default function LeagueDetailContent({
borderColor={useColorModeValue("gray.200", "gray.800")}
>
<Heading size="md" mb={6}>
Geçmiş Maçlar
{t("past-matches")}
</Heading>
<MatchList
flatMatches={flatMatches}
+4 -5
View File
@@ -514,10 +514,10 @@ export default function LeaguesContent() {
<LuTrophy size={40} color="gray" />
</Box>
<Heading size="md" mb={2}>
Bulunamadı
{t("not-found")}
</Heading>
<Text color="fg.muted">
Seçili kriterlere uygun lig bulunamadı.
{t("no-leagues-found")}
</Text>
</Flex>
) : (
@@ -709,11 +709,10 @@ export default function LeaguesContent() {
textAlign="center"
>
<Heading size="md" mb={2}>
Takım Bulunamadı
{t("team-not-found")}
</Heading>
<Text color="fg.muted">
"{debouncedTeamQuery}" aramasıyla eşleşen bir takım
bulunamadı.
{t("no-teams-found", { query: debouncedTeamQuery })}
</Text>
</Flex>
) : (
+28 -24
View File
@@ -12,6 +12,7 @@ import {
Icon,
} from "@chakra-ui/react";
import { useColorModeValue } from "@/components/ui/color-mode";
import { useTranslations } from "next-intl";
import {
LuUsers,
LuUser,
@@ -30,47 +31,49 @@ interface LineupsCardProps {
/**
* 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) {
case "confirmed_live":
return {
title: "Resmi İlk 11",
badge: "Onaylı Kadro",
title: t("lineup-official-xi"),
badge: t("lineup-confirmed-squad"),
badgeColor: "green" as const,
icon: LuShieldCheck,
description: "Kadro resmi olarak onaylandı.",
description: t("lineup-confirmed-live-desc"),
};
case "confirmed_participation":
return {
title: "Onaylı Kadro",
badge: "Onaylı",
title: t("lineup-confirmed-squad"),
badge: t("lineup-confirmed"),
badgeColor: "green" as const,
icon: LuShieldCheck,
description: "Kadro maç katılım verilerinden alındı.",
description: t("lineup-participation-desc"),
};
case "probable_xi":
return {
title: "Muhtemel Kadro",
badge: "Muhtemel",
title: t("lineup-probable-squad"),
badge: t("lineup-probable"),
badgeColor: "orange" as const,
icon: LuUsers,
description:
"Son maçlardaki ilk 11 verilerine dayalı muhtemel kadro. AI analizi bu kadro üzerinden yapılmaktadır.",
description: t("lineup-probable-desc"),
};
case "none":
default:
return {
title: "Kadro Bilgisi",
badge: "Kadro Bekleniyor",
title: t("lineup-info"),
badge: t("lineup-pending"),
badgeColor: "gray" as const,
icon: LuClock,
description:
"Kadro henüz açıklanmadı. AI analizi, takımların genel güç dengesi ve istatistiklerine dayalı olarak üretilmiştir.",
description: t("lineup-pending-desc"),
};
}
}
export default function LineupsCard({ match, prediction }: LineupsCardProps) {
const t = useTranslations("matches");
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
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
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
if (
@@ -195,10 +198,10 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
gap={1}
>
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
Kadro henüz belli değil
{t("lineup-tbd")}
</Text>
<Text fontSize="xs" color="fg.subtle">
Maç saatine yakın güncellenecek
{t("lineup-tbd-sub")}
</Text>
</Flex>
)}
@@ -259,10 +262,10 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
gap={1}
>
<Text fontSize="sm" color="fg.muted" fontWeight="medium">
Kadro henüz belli değil
{t("lineup-tbd")}
</Text>
<Text fontSize="xs" color="fg.subtle">
Maç saatine yakın güncellenecek
{t("lineup-tbd-sub")}
</Text>
</Flex>
)}
@@ -280,7 +283,7 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
<Icon as={LuClock} boxSize={8} color="fg.subtle" />
<VStack gap={1}>
<Text fontWeight="semibold" color="fg.muted">
Kadro Henüz Açıklanmadı
{t("lineup-not-announced")}
</Text>
<Text
fontSize="sm"
@@ -288,9 +291,10 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
textAlign="center"
maxW="sm"
>
{match.homeTeamName} ve {match.awayTeamName} kadroları maç
saatine yakın güncellenecektir. AI analizi, takım istatistikleri
ve güç dengesi üzerinden yapılmaktadır.
{t("lineup-not-announced-desc", {
home: match.homeTeamName,
away: match.awayTeamName,
})}
</Text>
</VStack>
</Flex>
+3 -2
View File
@@ -9,7 +9,7 @@ import {
VStack,
Image,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { motion } from "framer-motion";
@@ -28,6 +28,7 @@ const MotionBox = motion.create(Box);
export default function MatchCard({ match }: MatchCardProps) {
const t = useTranslations("matches");
const tAuth = useTranslations("auth");
const locale = useLocale();
const router = useRouter();
const { data: session } = useSession();
const [loginModalOpen, setLoginModalOpen] = useState(false);
@@ -107,7 +108,7 @@ export default function MatchCard({ match }: MatchCardProps) {
</Badge>
<Text fontSize="xs" color="fg.muted">
{matchDate.toLocaleDateString("tr-TR", {
{matchDate.toLocaleDateString(locale, {
day: "2-digit",
month: "short",
hour: "2-digit",
@@ -14,7 +14,7 @@ import {
SimpleGrid,
Skeleton,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useColorModeValue } from "@/components/ui/color-mode";
@@ -123,6 +123,7 @@ function MatchDetailSkeleton() {
export default function MatchDetailContent() {
const t = useTranslations("matches");
const tPred = useTranslations("predictions");
const locale = useLocale();
const queryClient = useQueryClient();
const { data: meData } = useGetMe();
const usageLimit = meData?.data?.usageLimit;
@@ -224,7 +225,7 @@ export default function MatchDetailContent() {
const show = (key: string) => activeTab === "all" || activeTab === key;
const tabs = [
{ key: "all", label: t("all-matches", { defaultValue: "Hepsi" }) },
{ key: "all", label: t("overview") },
...(matchEvents.length > 0 ? [{ key: "events", label: t("match-events") }] : []),
...(hasStats ? [{ key: "stats", label: t("statistics") }] : []),
{ key: "lineups", label: t("lineups") },
@@ -391,7 +392,7 @@ export default function MatchDetailContent() {
<HStack gap={1} mt={1}>
<LuCalendar size={11} />
<Text fontSize="2xs" color="fg.muted">
{new Date(match.mstUtc).toLocaleDateString("tr-TR", {
{new Date(match.mstUtc).toLocaleDateString(locale, {
day: "2-digit",
month: "short",
year: "numeric",
+1 -1
View File
@@ -206,7 +206,7 @@ export default function MatchList({
{league.name}
</Text>
<Text fontSize="xs" color="fg.muted" ml="auto">
{league.matches.length} {t("title").toLowerCase()}
{t("match-count", { count: league.matches.length })}
</Text>
</Flex>
+8 -12
View File
@@ -1,6 +1,6 @@
"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 { useTranslations } from "next-intl";
import { SlideUp } from "@/components/motion";
@@ -160,8 +160,13 @@ export default function MatchesContent() {
</Button>
</Group>
<input
<Input
type="date"
size="sm"
w="auto"
minW="150px"
flexShrink={0}
borderRadius="md"
value={dateFilter}
onChange={(e) => {
const dateVal = e.target.value;
@@ -173,15 +178,6 @@ export default function MatchesContent() {
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>
@@ -198,7 +194,7 @@ export default function MatchesContent() {
{/* Main Content */}
<Flex
gap={6}
align="flex-start"
align={{ base: "stretch", lg: "flex-start" }}
direction={{ base: "column", lg: "row" }}
>
{/* League Sidebar (Desktop only) */}
+4 -2
View File
@@ -3,6 +3,7 @@
import { Box, SimpleGrid, Text, Card, VStack, Flex } from "@chakra-ui/react";
import { useColorModeValue } from "@/components/ui/color-mode";
import type { MatchPredictionDto } from "@/lib/api/predictions/types";
import { useTranslations } from "next-intl";
// movement: per market (Turkish name) -> per selection -> {open, close}
export type OddsMovement = Record<
@@ -145,6 +146,7 @@ function MarketBlock({ title, selections, prediction, movement }: MarketBlockPro
}
export default function OddsCard({ odds, prediction, movement }: OddsCardProps) {
const t = useTranslations("matches");
const cardBg = useColorModeValue("white", "gray.900");
const borderColor = useColorModeValue("gray.100", "gray.800");
@@ -176,10 +178,10 @@ export default function OddsCard({ odds, prediction, movement }: OddsCardProps)
<VStack align="stretch" gap={4}>
<Flex justify="space-between" align="baseline" wrap="wrap" gap={1}>
<Text fontSize="lg" fontWeight="bold">
Canlı İddaa Oranları
{t("live-odds")}
</Text>
<Text fontSize="xs" color="fg.muted">
%= modelin tutma olasılığı (kalibre) · = açılışkapanış hareketi
{t("odds-legend")}
</Text>
</Flex>
+3 -3
View File
@@ -837,17 +837,17 @@ function SummaryTable({
{getSignalTierLabel(item.signal_tier)}
</Badge>
{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.
</Badge>
) : null}
{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
</Badge>
) : null}
{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
</Badge>
) : null}
+69 -64
View File
@@ -21,6 +21,7 @@ import {
LuTrendingUp,
} from "react-icons/lu";
import { useColorModeValue } from "@/components/ui/color-mode";
import { useTranslations } from "next-intl";
import { Tooltip } from "@/components/ui/overlays/tooltip";
import type {
HtftComboKey,
@@ -44,32 +45,32 @@ function edgeStr(edge: number): string {
return `${sign}${(edge * 100).toFixed(1)}%`;
}
const TRIPLE_VALUE_LABELS: Record<string, string> = {
home: "MS Ev",
away: "MS Dep",
ou25_over: "ÜST 2.5",
btts_yes: "KG Var",
ou15_over: "ÜST 1.5",
ou35_over: "ÜST 3.5",
dc_1x: "ÇŞ 1X",
dc_x2: "ÇŞ X2",
dc_12: "ÇŞ 12",
ht_home: "İY Ev",
ht_away: "İY Dep",
ht_ou05_over: "İY ÜST 0.5",
ht_ou15_over: "İY ÜST 1.5",
oe_odd: "Tek",
cards_over: "Kart ÜST",
htft_11: "İY/MS 1/1",
htft_1x: "İY/MS 1/X",
htft_12: "İY/MS 1/2",
htft_x1: "İY/MS X/1",
htft_xx: "İY/MS X/X",
htft_x2: "İY/MS X/2",
htft_21: "İY/MS 2/1",
htft_2x: "İY/MS 2/X",
htft_22: "İY/MS 2/2",
};
const TRIPLE_VALUE_KEYS = new Set([
"home",
"away",
"ou25_over",
"btts_yes",
"ou15_over",
"ou35_over",
"dc_1x",
"dc_x2",
"dc_12",
"ht_home",
"ht_away",
"ht_ou05_over",
"ht_ou15_over",
"oe_odd",
"cards_over",
"htft_11",
"htft_1x",
"htft_12",
"htft_x1",
"htft_xx",
"htft_x2",
"htft_21",
"htft_2x",
"htft_22",
]);
const HTFT_DISPLAY: Record<HtftComboKey, string> = {
"11": "1/1",
@@ -98,7 +99,7 @@ function TooltipIcon({ content }: { content: string }) {
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
>
<IconButton
aria-label="Bilgi"
aria-label="info"
variant="ghost"
size="2xs"
colorPalette="gray"
@@ -142,6 +143,7 @@ function TripleValueCard({
label: string;
entry: TripleValueEntryDto;
}) {
const t = useTranslations("v28");
const isValue = entry.is_value;
const hasSample = entry.band_sample >= 5;
@@ -194,11 +196,11 @@ function TripleValueCard({
fontSize="2xs"
borderRadius="full"
>
DEĞER
{t("value")}
</Badge>
) : hasSample ? (
<Badge variant="outline" fontSize="2xs" borderRadius="full">
PAS
{t("pass")}
</Badge>
) : (
<Badge
@@ -207,7 +209,7 @@ function TripleValueCard({
fontSize="2xs"
borderRadius="full"
>
YETERSİZ
{t("insufficient")}
</Badge>
)}
</HStack>
@@ -218,20 +220,20 @@ function TripleValueCard({
<HStack gap={2} flexWrap="wrap">
<Text fontSize="2xs" color="fg.muted">
Band: {pct(entry.band_rate, 1)}
{t("band")}: {pct(entry.band_rate, 1)}
</Text>
<Text fontSize="2xs" color="fg.muted">
Oran: {pct(entry.implied_prob, 1)}
{t("odds")}: {pct(entry.implied_prob, 1)}
</Text>
{entry.confirmations !== undefined && (
<Text fontSize="2xs" color="fg.muted">
Onay: {entry.confirmations}/2
{t("confirmations")}: {entry.confirmations}/2
</Text>
)}
</HStack>
<Text fontSize="2xs" color="fg.muted">
{entry.band_sample} maç
{t("matches-count", { count: entry.band_sample })}
</Text>
</VStack>
</Box>
@@ -267,6 +269,7 @@ function ProgressBar({
}
function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
const t = useTranslations("v28");
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
const hasData = cards.sample >= 3;
@@ -283,11 +286,11 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
<HStack gap={2} mb={2}>
<Icon as={LuRectangleVertical} boxSize={4} color="yellow.500" />
<Text fontSize="sm" fontWeight="semibold">
Kart Analizi
{t("cards-analysis")}
</Text>
</HStack>
<Text fontSize="sm" color="fg.muted">
Yetersiz veri henüz yeterli maç örneği bulunamadı.
{t("insufficient-data")}
</Text>
</Box>
);
@@ -309,11 +312,11 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
<HStack gap={2}>
<Icon as={LuRectangleVertical} boxSize={4} color="yellow.500" />
<Text fontSize="sm" fontWeight="semibold">
Kart Analizi
{t("cards-analysis")}
</Text>
</HStack>
<Badge variant="outline" fontSize="2xs" borderRadius="full">
{cards.sample} maç
{t("matches-count", { count: cards.sample })}
</Badge>
</HStack>
@@ -322,10 +325,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
<Box>
<HStack justify="space-between" mb={1}>
<Text fontSize="xs" color="fg.muted">
Hakem Profili
{t("referee-profile")}
</Text>
<Text fontSize="xs" fontWeight="semibold">
Ort: {cards.referee_avg.toFixed(1)} kart
{t("avg-cards", { avg: cards.referee_avg.toFixed(1) })}
</Text>
</HStack>
<ProgressBar
@@ -335,10 +338,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
/>
<HStack justify="space-between" mt={0.5}>
<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 fontSize="2xs" color="fg.muted">
{cards.referee_sample} maç
{t("matches-count", { count: cards.referee_sample })}
</Text>
</HStack>
</Box>
@@ -347,10 +350,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
<Box>
<HStack justify="space-between" mb={1}>
<Text fontSize="xs" color="fg.muted">
Takım Profili
{t("team-profile")}
</Text>
<Text fontSize="xs" fontWeight="semibold">
Ort: {cards.team_avg.toFixed(1)} kart
{t("avg-cards", { avg: cards.team_avg.toFixed(1) })}
</Text>
</HStack>
<ProgressBar
@@ -360,10 +363,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
/>
<HStack justify="space-between" mt={0.5}>
<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 fontSize="2xs" color="fg.muted">
{cards.team_sample} maç
{t("matches-count", { count: cards.team_sample })}
</Text>
</HStack>
</Box>
@@ -377,10 +380,10 @@ function CardsSection({ cards }: { cards: OddsBandCardsDto }) {
<HStack justify="space-between">
<VStack align="start" gap={0}>
<Text fontSize="xs" fontWeight="semibold">
Kombine ÜST Oranı
{t("combined-over-rate")}
</Text>
<Text fontSize="2xs" color="fg.muted">
%60 Hakem + %40 Takım ağırlıklı
{t("combined-weighting")}
</Text>
</VStack>
<Text fontSize="2xl" fontWeight="bold" color={overColor}>
@@ -402,6 +405,7 @@ function HtftGrid({
}: {
htft: Record<HtftComboKey, OddsBandHtftComboDto>;
}) {
const t = useTranslations("v28");
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
@@ -450,10 +454,10 @@ function HtftGrid({
<HStack gap={2}>
<Icon as={LuTarget} boxSize={4} color="teal.500" />
<Text fontSize="sm" fontWeight="semibold">
İY/MS Kombinasyonları
{t("htft-title")}
</Text>
</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>
{/* Column headers */}
@@ -465,7 +469,7 @@ function HtftGrid({
textAlign="center"
color="fg.muted"
>
MS 1
{t("ft")} 1
</Text>
<Text
fontSize="2xs"
@@ -473,7 +477,7 @@ function HtftGrid({
textAlign="center"
color="fg.muted"
>
MS X
{t("ft")} X
</Text>
<Text
fontSize="2xs"
@@ -481,13 +485,13 @@ function HtftGrid({
textAlign="center"
color="fg.muted"
>
MS 2
{t("ft")} 2
</Text>
</Grid>
{/* Grid rows */}
{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 (
<Grid
key={rowIdx}
@@ -538,7 +542,7 @@ function HtftGrid({
fontSize="2xs"
color={isMax ? "whiteAlpha.800" : "fg.muted"}
>
{data.sample} maç
{t("matches-count", { count: data.sample })}
</Text>
{isMax && (
<Icon
@@ -568,7 +572,7 @@ function HtftGrid({
<HStack gap={2}>
<Icon as={LuTrendingUp} boxSize={4} color="green.500" />
<Text fontSize="xs" fontWeight="semibold">
En güçlü:{" "}
{t("strongest")}:{" "}
<Text as="span" color="green.500">
{HTFT_DISPLAY[maxKey]} ({pct(maxRate, 0)})
</Text>
@@ -589,6 +593,7 @@ interface V28OddsBandPanelProps {
}
export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
const t = useTranslations("v28");
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
@@ -619,8 +624,8 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
<Card.Body gap={5}>
<SectionTitle
icon={LuShieldAlert}
title="V28 Oran Bandı Analizi"
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."
title={t("panel-title")}
info={t("panel-info")}
/>
{/* Engine version badge */}
@@ -641,8 +646,8 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
fontSize="2xs"
>
{engine.consensus === "AGREE"
? "Motorlar Uyumlu"
: "Motorlar Farklı"}
? t("engines-agree")
: t("engines-disagree")}
</Badge>
)}
{valueHits.length > 0 && (
@@ -652,7 +657,7 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
borderRadius="full"
fontSize="2xs"
>
{valueHits.length} Değer Sinyali
{t("value-signals", { count: valueHits.length })}
</Badge>
)}
</HStack>
@@ -663,15 +668,15 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
<HStack mb={3} gap={2}>
<Icon as={LuTarget} boxSize={4} color="blue.500" />
<Text fontSize="sm" fontWeight="semibold">
Değer Tespiti (Triple Value)
{t("triple-value-title")}
</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>
<SimpleGrid columns={{ base: 2, md: 3, xl: 4 }} gap={2.5}>
{orderedEntries.map(([key, entry]) => (
<TripleValueCard
key={key}
label={TRIPLE_VALUE_LABELS[key] || key}
label={TRIPLE_VALUE_KEYS.has(key) ? t(`label-${key}`) : key}
entry={entry}
/>
))}
File diff suppressed because it is too large Load Diff
+16 -15
View File
@@ -13,7 +13,7 @@ import {
Button,
Card,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useColorModeValue } from "@/components/ui/color-mode";
@@ -178,18 +178,18 @@ export default function TeamDetailContent() {
if (isMatchLive(match))
return (
<Badge colorPalette="red" variant="subtle" fontSize="xs">
Canlı
{t("matches.live")}
</Badge>
);
if (isMatchFinished(match))
return (
<Badge colorPalette="gray" variant="subtle" fontSize="xs">
Bitti
{t("matches.finished")}
</Badge>
);
return (
<Badge colorPalette="green" variant="subtle" fontSize="xs">
Yaklaşan
{t("teams.upcoming")}
</Badge>
);
};
@@ -206,10 +206,10 @@ export default function TeamDetailContent() {
return (
<Flex justify="center" py={20} direction="column" align="center" gap={4}>
<Text color="fg.muted" fontSize="lg">
Takım bulunamadı
{t("teams.not-found")}
</Text>
<Button variant="outline" onClick={() => router.back()}>
<LuArrowLeft /> Geri
<LuArrowLeft /> {t("common.back")}
</Button>
</Flex>
);
@@ -273,11 +273,11 @@ export default function TeamDetailContent() {
<HStack gap={4} mt={1}>
<Badge colorPalette="blue" variant="subtle">
<LuTrophy style={{ width: 12, height: 12 }} />
{totalMatches} Maç
{totalMatches} {t("teams.matches-count")}
</Badge>
<Badge colorPalette="green" variant="subtle">
<LuCalendar style={{ width: 12, height: 12 }} />
{upcomingMatches.length} Yaklaşan
{upcomingMatches.length} {t("teams.upcoming")}
</Badge>
</HStack>
</VStack>
@@ -290,7 +290,7 @@ export default function TeamDetailContent() {
<FadeIn>
<Box mb={6}>
<Heading as="h2" size="lg" mb={4}>
📅 Yaklaşan Maçlar
📅 {t("teams.upcoming-matches")}
</Heading>
<VStack gap={2} align="stretch">
{upcomingMatches.map((match: MatchResponseDto) => (
@@ -323,11 +323,11 @@ export default function TeamDetailContent() {
gap={2}
>
<Heading as="h2" size="lg">
📊 Geçmiş Maçlar
📊 {t("teams.past-matches")}
</Heading>
{/* Pagination Info */}
<Text fontSize="xs" color="fg.muted">
Sayfa {currentPage}/{totalPages} Toplam {totalMatches} maç
{t("teams.page-info", { page: currentPage, totalPages, total: totalMatches })}
</Text>
</Flex>
@@ -370,7 +370,7 @@ export default function TeamDetailContent() {
</Flex>
) : pastMatches.length === 0 ? (
<Text color="fg.muted" textAlign="center" py={8}>
Bu sezonda geçmiş maç bulunamadı
{t("teams.no-past-matches-season")}
</Text>
) : (
<VStack gap={2} align="stretch">
@@ -401,7 +401,7 @@ export default function TeamDetailContent() {
disabled={currentPage <= 1}
borderRadius="full"
>
Önceki
{t("common.previous")}
</Button>
<HStack gap={1}>
{Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
@@ -444,7 +444,7 @@ export default function TeamDetailContent() {
disabled={currentPage >= totalPages}
borderRadius="full"
>
Sonraki
{t("common.next")}
</Button>
</Flex>
)}
@@ -481,6 +481,7 @@ function MatchRow({
statusBadge,
onClick,
}: MatchRowProps) {
const locale = useLocale();
const hoverBg = useColorModeValue("gray.50", "gray.700");
const matchTimestamp = getMatchTimestamp(match);
const homeTeamName = getTeamSideName(match.homeTeam, match.homeTeamName);
@@ -547,7 +548,7 @@ function MatchRow({
)}
<Text fontSize="2xs" color="fg.muted">
{matchTimestamp
? new Date(matchTimestamp).toLocaleDateString("tr-TR", {
? new Date(matchTimestamp).toLocaleDateString(locale, {
day: "2-digit",
month: "short",
})
+4 -4
View File
@@ -56,7 +56,7 @@ export default function TeamsContent() {
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Takım adı yazın... (min 2 karakter)"
placeholder={t("teams.search-placeholder")}
variant="flushed"
size="lg"
fontSize="md"
@@ -78,15 +78,15 @@ export default function TeamsContent() {
>
<Text fontSize="5xl"></Text>
<Text color="fg.muted" fontSize="lg">
Aramak istediğiniz takımın adını yazın
{t("teams.search-hint")}
</Text>
<Text color="fg.muted" fontSize="sm">
Örnek: Galatasaray, Barcelona, Manchester City
{t("teams.search-example")}
</Text>
</Flex>
) : teams.length === 0 ? (
<Flex justify="center" py={10}>
<Text color="fg.muted">Sonuç bulunamadı</Text>
<Text color="fg.muted">{t("teams.no-results")}</Text>
</Flex>
) : (
<VStack gap={3} align="stretch">