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
@@ -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">