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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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) */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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",
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user