@@ -1,13 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
Badge,
|
||||
Image,
|
||||
ScrollArea,
|
||||
} from "@chakra-ui/react";
|
||||
import { Box, Flex, Text, Badge, Image, ScrollArea } from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import type { ActiveLeagueDto } from "@/lib/api/matches/types";
|
||||
@@ -68,7 +61,9 @@ export default function LeagueFilterBar({
|
||||
py={2}
|
||||
borderRadius="full"
|
||||
borderWidth="1.5px"
|
||||
borderColor={selectedLeagueId === null ? activeBorder : chipBorder}
|
||||
borderColor={
|
||||
selectedLeagueId === null ? activeBorder : chipBorder
|
||||
}
|
||||
bg={selectedLeagueId === null ? activeBg : chipBg}
|
||||
cursor="pointer"
|
||||
flexShrink={0}
|
||||
@@ -133,7 +128,12 @@ export default function LeagueFilterBar({
|
||||
) : null}
|
||||
|
||||
{/* League name + country */}
|
||||
<Flex direction="column" align="flex-start" gap={0} lineHeight="1">
|
||||
<Flex
|
||||
direction="column"
|
||||
align="flex-start"
|
||||
gap={0}
|
||||
lineHeight="1"
|
||||
>
|
||||
<Text
|
||||
fontSize="xs"
|
||||
fontWeight={isActive ? "bold" : "medium"}
|
||||
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
Icon,
|
||||
} from "@chakra-ui/react";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import { LuUsers, LuUser, LuInfo, LuShieldCheck, LuClock } from "react-icons/lu";
|
||||
import {
|
||||
LuUsers,
|
||||
LuUser,
|
||||
LuInfo,
|
||||
LuShieldCheck,
|
||||
LuClock,
|
||||
} from "react-icons/lu";
|
||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||
import type { MatchPredictionDto } from "@/lib/api/predictions/types";
|
||||
|
||||
@@ -79,10 +85,18 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
||||
const meta = getLineupSourceMeta(source);
|
||||
|
||||
// Fallback: If no starting players are marked, but we have players, treat them as probable XI
|
||||
if (homeLineups.length === 0 && match.lineups?.home && match.lineups.home.length > 0) {
|
||||
if (
|
||||
homeLineups.length === 0 &&
|
||||
match.lineups?.home &&
|
||||
match.lineups.home.length > 0
|
||||
) {
|
||||
homeLineups = match.lineups.home.slice(0, 11);
|
||||
}
|
||||
if (awayLineups.length === 0 && match.lineups?.away && match.lineups.away.length > 0) {
|
||||
if (
|
||||
awayLineups.length === 0 &&
|
||||
match.lineups?.away &&
|
||||
match.lineups.away.length > 0
|
||||
) {
|
||||
awayLineups = match.lineups.away.slice(0, 11);
|
||||
}
|
||||
|
||||
@@ -99,10 +113,7 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
||||
{meta.title}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Badge
|
||||
colorPalette={meta.badgeColor}
|
||||
variant="subtle"
|
||||
>
|
||||
<Badge colorPalette={meta.badgeColor} variant="subtle">
|
||||
{meta.badge}
|
||||
</Badge>
|
||||
</Flex>
|
||||
@@ -271,10 +282,15 @@ export default function LineupsCard({ match, prediction }: LineupsCardProps) {
|
||||
<Text fontWeight="semibold" color="fg.muted">
|
||||
Kadro Henüz Açıklanmadı
|
||||
</Text>
|
||||
<Text fontSize="sm" color="fg.subtle" 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.
|
||||
<Text
|
||||
fontSize="sm"
|
||||
color="fg.subtle"
|
||||
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.
|
||||
</Text>
|
||||
</VStack>
|
||||
</Flex>
|
||||
|
||||
@@ -21,10 +21,20 @@ import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import { SlideUp, FadeIn } from "@/components/motion";
|
||||
import { useMatchDetails } from "@/lib/api/matches/use-hooks";
|
||||
import { usePrediction } from "@/lib/api/predictions/use-hooks";
|
||||
import { useGetMe } from "@/lib/api/users/use-hooks";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { UsersQueryKeys } from "@/lib/api/users/use-hooks";
|
||||
import PredictionCard from "@/components/matches/prediction-card";
|
||||
import OddsCard from "@/components/matches/odds-card";
|
||||
import LineupsCard from "@/components/matches/lineups-card";
|
||||
import { LuArrowLeft, LuRefreshCw, LuShield, LuFlag, LuUser } from "react-icons/lu";
|
||||
import {
|
||||
LuArrowLeft,
|
||||
LuRefreshCw,
|
||||
LuShield,
|
||||
LuFlag,
|
||||
LuUser,
|
||||
LuSparkles,
|
||||
} from "react-icons/lu";
|
||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
@@ -60,6 +70,10 @@ interface SidelinedData {
|
||||
export default function MatchDetailContent() {
|
||||
const t = useTranslations("matches");
|
||||
const tPred = useTranslations("predictions");
|
||||
const queryClient = useQueryClient();
|
||||
const { data: meData } = useGetMe();
|
||||
const usageLimit = meData?.data?.usageLimit;
|
||||
const hasLimit = usageLimit ? (usageLimit.maxAnalyses - usageLimit.analysisCount > 0) : true;
|
||||
const tCommon = useTranslations("common");
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
@@ -70,9 +84,16 @@ export default function MatchDetailContent() {
|
||||
const {
|
||||
data: predictionData,
|
||||
isLoading: predLoading,
|
||||
refetch: refetchPrediction,
|
||||
refetch: refetchPredictionRaw,
|
||||
isFetching: isPredFetching,
|
||||
} = usePrediction(matchId);
|
||||
|
||||
const refetchPrediction = async () => {
|
||||
await refetchPredictionRaw();
|
||||
// After refetching, update the limits in the header
|
||||
queryClient.invalidateQueries({ queryKey: UsersQueryKeys.me() });
|
||||
};
|
||||
|
||||
const headerBg = useColorModeValue("white", "gray.800");
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||
@@ -139,7 +160,13 @@ export default function MatchDetailContent() {
|
||||
>
|
||||
{/* League Banner */}
|
||||
{match.league && (
|
||||
<Box bg={subtleBg} px={4} py={2.5} borderBottomWidth="1px" borderColor={borderColor}>
|
||||
<Box
|
||||
bg={subtleBg}
|
||||
px={4}
|
||||
py={2.5}
|
||||
borderBottomWidth="1px"
|
||||
borderColor={borderColor}
|
||||
>
|
||||
<Flex justify="center" align="center" gap={2}>
|
||||
{match.league.country?.flag && (
|
||||
<Image
|
||||
@@ -151,7 +178,8 @@ export default function MatchDetailContent() {
|
||||
/>
|
||||
)}
|
||||
<Text fontSize="sm" fontWeight="semibold" color="fg.muted">
|
||||
{match.league.country?.name && `${match.league.country.name} • `}
|
||||
{match.league.country?.name &&
|
||||
`${match.league.country.name} • `}
|
||||
{match.league.name}
|
||||
</Text>
|
||||
<Badge
|
||||
@@ -300,7 +328,10 @@ export default function MatchDetailContent() {
|
||||
>
|
||||
<LuUser size={14} />
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
{t("referee")}: <Text as="span" fontWeight="semibold" color="fg">{match.refereeName}</Text>
|
||||
{t("referee")}:{" "}
|
||||
<Text as="span" fontWeight="semibold" color="fg">
|
||||
{match.refereeName}
|
||||
</Text>
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
@@ -312,7 +343,12 @@ export default function MatchDetailContent() {
|
||||
{/* ═══════════════════════════════════════════ */}
|
||||
{hasSidelined && (
|
||||
<FadeIn>
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl" mb={6}>
|
||||
<Card.Root
|
||||
bg={cardBg}
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
mb={6}
|
||||
>
|
||||
<Card.Body>
|
||||
<Heading as="h2" size="md" mb={4}>
|
||||
🏥 {t("sidelined")}
|
||||
@@ -361,6 +397,7 @@ export default function MatchDetailContent() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => refetchPrediction()}
|
||||
disabled={!hasLimit}
|
||||
gap={1.5}
|
||||
>
|
||||
<LuRefreshCw />
|
||||
@@ -368,7 +405,7 @@ export default function MatchDetailContent() {
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{predLoading ? (
|
||||
{predLoading || isPredFetching ? (
|
||||
<Flex justify="center" py={10}>
|
||||
<Spinner size="md" color="primary.500" />
|
||||
</Flex>
|
||||
@@ -377,8 +414,21 @@ export default function MatchDetailContent() {
|
||||
) : (
|
||||
<Card.Root borderColor={borderColor} borderRadius="xl">
|
||||
<Card.Body>
|
||||
<Flex justify="center" align="center" py={8}>
|
||||
<Text color="fg.muted">{tPred("no-predictions")}</Text>
|
||||
<Flex direction="column" justify="center" align="center" py={8} gap={4}>
|
||||
<Text color="fg.muted">{tPred("no-predictions", { defaultValue: "Tahmin bulunmuyor." })}</Text>
|
||||
<Button
|
||||
colorPalette="primary"
|
||||
onClick={() => refetchPrediction()}
|
||||
disabled={!hasLimit}
|
||||
loading={isPredFetching}
|
||||
>
|
||||
<LuSparkles /> {tPred("generate", { defaultValue: "Yapay Zeka ile Analiz Et" })}
|
||||
</Button>
|
||||
{!hasLimit && (
|
||||
<Text fontSize="sm" color="red.500">
|
||||
{tCommon("limits.out_of_analysis", { defaultValue: "Günlük analiz limitiniz doldu." })}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
@@ -409,15 +459,31 @@ interface SidelinedColumnProps {
|
||||
t: ReturnType<typeof useTranslations>;
|
||||
}
|
||||
|
||||
function SidelinedColumn({ team, teamName, teamLogo, injuryBg, injuryBorder, t }: SidelinedColumnProps) {
|
||||
function SidelinedColumn({
|
||||
team,
|
||||
teamName,
|
||||
teamLogo,
|
||||
injuryBg,
|
||||
injuryBorder,
|
||||
t,
|
||||
}: SidelinedColumnProps) {
|
||||
const players = team?.players || [];
|
||||
|
||||
if (players.length === 0) {
|
||||
return (
|
||||
<Box>
|
||||
<HStack gap={2} mb={3}>
|
||||
{teamLogo && <Image src={teamLogo} alt={teamName} boxSize="20px" objectFit="contain" />}
|
||||
<Text fontSize="sm" fontWeight="bold">{teamName}</Text>
|
||||
{teamLogo && (
|
||||
<Image
|
||||
src={teamLogo}
|
||||
alt={teamName}
|
||||
boxSize="20px"
|
||||
objectFit="contain"
|
||||
/>
|
||||
)}
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
{teamName}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color="fg.muted" fontStyle="italic">
|
||||
{t("no-sidelined")}
|
||||
@@ -429,9 +495,23 @@ function SidelinedColumn({ team, teamName, teamLogo, injuryBg, injuryBorder, t }
|
||||
return (
|
||||
<Box>
|
||||
<HStack gap={2} mb={3}>
|
||||
{teamLogo && <Image src={teamLogo} alt={teamName} boxSize="20px" objectFit="contain" />}
|
||||
<Text fontSize="sm" fontWeight="bold">{teamName}</Text>
|
||||
<Badge colorPalette="red" variant="subtle" fontSize="2xs" borderRadius="full">
|
||||
{teamLogo && (
|
||||
<Image
|
||||
src={teamLogo}
|
||||
alt={teamName}
|
||||
boxSize="20px"
|
||||
objectFit="contain"
|
||||
/>
|
||||
)}
|
||||
<Text fontSize="sm" fontWeight="bold">
|
||||
{teamName}
|
||||
</Text>
|
||||
<Badge
|
||||
colorPalette="red"
|
||||
variant="subtle"
|
||||
fontSize="2xs"
|
||||
borderRadius="full"
|
||||
>
|
||||
{players.length}
|
||||
</Badge>
|
||||
</HStack>
|
||||
@@ -458,21 +538,21 @@ function SidelinedColumn({ team, teamName, teamLogo, injuryBg, injuryBorder, t }
|
||||
</Badge>
|
||||
)}
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
{player.description || (
|
||||
player.type === "injury"
|
||||
{player.description ||
|
||||
(player.type === "injury"
|
||||
? t("injury")
|
||||
: player.type === "suspended"
|
||||
? t("suspended")
|
||||
: t("other-reason")
|
||||
)}
|
||||
: t("other-reason"))}
|
||||
</Text>
|
||||
</HStack>
|
||||
</VStack>
|
||||
{player.matchesMissed !== undefined && player.matchesMissed > 0 && (
|
||||
<Badge colorPalette="red" variant="subtle" fontSize="2xs">
|
||||
{player.matchesMissed} {t("matches-missed")}
|
||||
</Badge>
|
||||
)}
|
||||
{player.matchesMissed !== undefined &&
|
||||
player.matchesMissed > 0 && (
|
||||
<Badge colorPalette="red" variant="subtle" fontSize="2xs">
|
||||
{player.matchesMissed} {t("matches-missed")}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import { Box, Grid, Text, Flex, Image, HStack, VStack } from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import { StaggerContainer, StaggerItem, ScrollSlideUp } from "@/components/motion";
|
||||
import {
|
||||
StaggerContainer,
|
||||
StaggerItem,
|
||||
ScrollSlideUp,
|
||||
} from "@/components/motion";
|
||||
import { Skeleton } from "@/components/ui/feedback/skeleton";
|
||||
import MatchCard from "./match-card";
|
||||
import type {
|
||||
@@ -53,7 +57,13 @@ function MatchCardSkeleton() {
|
||||
</HStack>
|
||||
|
||||
{/* League */}
|
||||
<Flex mt={3} pt={2} borderTopWidth="1px" borderColor={border} justify="center">
|
||||
<Flex
|
||||
mt={3}
|
||||
pt={2}
|
||||
borderTopWidth="1px"
|
||||
borderColor={border}
|
||||
justify="center"
|
||||
>
|
||||
<Skeleton height="12px" width="120px" />
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
@@ -4,7 +4,12 @@ import { Box, Flex, Heading, Group, Button } from "@chakra-ui/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { SlideUp } from "@/components/motion";
|
||||
import { SportFilter, LeagueSidebar, LeagueFilterBar, MatchList } from "@/components/matches";
|
||||
import {
|
||||
SportFilter,
|
||||
LeagueSidebar,
|
||||
LeagueFilterBar,
|
||||
MatchList,
|
||||
} from "@/components/matches";
|
||||
import { useQueryMatches, useActiveLeagues } from "@/lib/api/matches/use-hooks";
|
||||
import { useMatchStore } from "@/lib/stores/match-store";
|
||||
|
||||
@@ -17,7 +22,7 @@ export default function MatchesContent() {
|
||||
const leagueFilter = useMatchStore((s) => s.leagueFilter);
|
||||
const setSport = useMatchStore((s) => s.setSport);
|
||||
const setLeague = useMatchStore((s) => s.setLeague);
|
||||
|
||||
|
||||
const [quickFilter, setQuickFilter] = useState<QuickFilter>("all");
|
||||
const [dateFilter, setDateFilter] = useState<string>("");
|
||||
|
||||
@@ -37,7 +42,12 @@ export default function MatchesContent() {
|
||||
};
|
||||
})();
|
||||
|
||||
const triggerQuery = (currentSport: typeof sport, currentLeague: string | null, currentFilter: QuickFilter, currentDate?: string) => {
|
||||
const triggerQuery = (
|
||||
currentSport: typeof sport,
|
||||
currentLeague: string | null,
|
||||
currentFilter: QuickFilter,
|
||||
currentDate?: string,
|
||||
) => {
|
||||
const payload: any = {
|
||||
sport: currentSport,
|
||||
leagueId: currentLeague || undefined,
|
||||
@@ -107,35 +117,42 @@ export default function MatchesContent() {
|
||||
</Flex>
|
||||
|
||||
{/* Quick Filters */}
|
||||
<Flex mb={6} overflowX="auto" pb={2} css={{ "&::-webkit-scrollbar": { display: "none" } }} gap={4} align="center">
|
||||
<Flex
|
||||
mb={6}
|
||||
overflowX="auto"
|
||||
pb={2}
|
||||
css={{ "&::-webkit-scrollbar": { display: "none" } }}
|
||||
gap={4}
|
||||
align="center"
|
||||
>
|
||||
<Group attached>
|
||||
<Button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleQuickFilterChange("all")}
|
||||
onClick={() => handleQuickFilterChange("all")}
|
||||
colorPalette={quickFilter === "all" ? "primary" : "gray"}
|
||||
variant={quickFilter === "all" ? "solid" : "outline"}
|
||||
>
|
||||
{t("all-matches")}
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleQuickFilterChange("today")}
|
||||
onClick={() => handleQuickFilterChange("today")}
|
||||
colorPalette={quickFilter === "today" ? "primary" : "gray"}
|
||||
variant={quickFilter === "today" ? "solid" : "outline"}
|
||||
>
|
||||
{t("today-matches")}
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleQuickFilterChange("live")}
|
||||
onClick={() => handleQuickFilterChange("live")}
|
||||
colorPalette={quickFilter === "live" ? "primary" : "gray"}
|
||||
variant={quickFilter === "live" ? "solid" : "outline"}
|
||||
>
|
||||
{t("live")}
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => handleQuickFilterChange("next_1_hour")}
|
||||
onClick={() => handleQuickFilterChange("next_1_hour")}
|
||||
colorPalette={quickFilter === "next_1_hour" ? "primary" : "gray"}
|
||||
variant={quickFilter === "next_1_hour" ? "solid" : "outline"}
|
||||
>
|
||||
@@ -163,7 +180,7 @@ export default function MatchesContent() {
|
||||
fontSize: "0.875rem",
|
||||
background: "transparent",
|
||||
color: "inherit",
|
||||
outline: "none"
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -52,30 +52,54 @@ function formatReasonFallback(reason: string): string {
|
||||
if (evMatch) return `Teorik avantaj sinyali: Not ${evMatch[2]}`;
|
||||
const negMatch = reason.match(/^negative_model_edge_([+\-][\d.]+)$/);
|
||||
if (negMatch) return `Model avantajı negatif (${negMatch[1]})`;
|
||||
const thresholdMatch = reason.match(/^below_market_edge_threshold_([+\-]?[\d.]+)$/);
|
||||
if (thresholdMatch) return `Piyasa avantaj eşiğinin altında (${thresholdMatch[1]})`;
|
||||
if (reason === "confidence_interval_too_wide") return "Güven aralığı fazla geniş.";
|
||||
const thresholdMatch = reason.match(
|
||||
/^below_market_edge_threshold_([+\-]?[\d.]+)$/,
|
||||
);
|
||||
if (thresholdMatch)
|
||||
return `Piyasa avantaj eşiğinin altında (${thresholdMatch[1]})`;
|
||||
if (reason === "confidence_interval_too_wide")
|
||||
return "Güven aralığı fazla geniş.";
|
||||
if (reason === "confidence_band_low") return "Güven bandı düşük.";
|
||||
if (reason === "draw_probability_elevated") return "Beraberlik olasılığı yükselmiş görünüyor.";
|
||||
if (reason === "balanced_match_risk") return "Maç dengeli görünüyor, sürpriz riski var.";
|
||||
if (reason === "high_total_goal_volatility") return "Yüksek gol temposu sürpriz riskini artırıyor.";
|
||||
if (reason === "mutual_goal_pressure") return "İki takım da gol tehdidi üretiyor.";
|
||||
if (reason === "late_goal_swing_risk") return "Geç gol veya skor kırılması riski yüksek.";
|
||||
if (reason === "live_match_open_state") return "Canlı maç tamamen açık oyuna dönmüş durumda.";
|
||||
if (reason === "live_match_active_state") return "Canlı maç beklenenden daha hareketli ilerliyor.";
|
||||
if (reason === "live_state_impossible_market") return "Canlı maç durumu bu marketi geçersiz kılıyor.";
|
||||
if (reason === "live_score_exceeds_under_line") return "Canlı skor, alt seçeneğinin üst sınırına çok yaklaştı veya geçti.";
|
||||
if (reason === "score_model_conflicts_with_under_pick") return "Skor ve xG modeli bu alt seçeneğiyle çelişiyor.";
|
||||
if (reason === "score_model_conflicts_with_over_pick") return "Skor ve xG modeli bu üst seçeneğiyle çelişiyor.";
|
||||
if (reason === "market_stack_conflict_over25") return "2.5 üst sinyali bu marketle çelişiyor.";
|
||||
if (reason === "market_stack_conflict_btts") return "KG Var sinyali bu marketle çelişiyor.";
|
||||
if (reason === "live_total_goals_close_to_line") return "Canlı toplam gol sayısı bu çizgiye fazla yaklaştı.";
|
||||
if (reason === "score_model_conflicts_with_btts_no") return "Skor ve xG modeli KG Yok seçeneğiyle çelişiyor.";
|
||||
if (reason === "score_model_conflicts_with_draw_pick") return "Skor modeli beraberlik seçeneğini desteklemiyor.";
|
||||
if (reason === "score_model_conflicts_with_home_pick") return "Skor modeli ev sahibi seçeneğini desteklemiyor.";
|
||||
if (reason === "score_model_conflicts_with_away_pick") return "Skor modeli deplasman seçeneğini desteklemiyor.";
|
||||
if (reason === "draw_probability_elevated")
|
||||
return "Beraberlik olasılığı yükselmiş görünüyor.";
|
||||
if (reason === "balanced_match_risk")
|
||||
return "Maç dengeli görünüyor, sürpriz riski var.";
|
||||
if (reason === "high_total_goal_volatility")
|
||||
return "Yüksek gol temposu sürpriz riskini artırıyor.";
|
||||
if (reason === "mutual_goal_pressure")
|
||||
return "İki takım da gol tehdidi üretiyor.";
|
||||
if (reason === "late_goal_swing_risk")
|
||||
return "Geç gol veya skor kırılması riski yüksek.";
|
||||
if (reason === "live_match_open_state")
|
||||
return "Canlı maç tamamen açık oyuna dönmüş durumda.";
|
||||
if (reason === "live_match_active_state")
|
||||
return "Canlı maç beklenenden daha hareketli ilerliyor.";
|
||||
if (reason === "live_state_impossible_market")
|
||||
return "Canlı maç durumu bu marketi geçersiz kılıyor.";
|
||||
if (reason === "live_score_exceeds_under_line")
|
||||
return "Canlı skor, alt seçeneğinin üst sınırına çok yaklaştı veya geçti.";
|
||||
if (reason === "score_model_conflicts_with_under_pick")
|
||||
return "Skor ve xG modeli bu alt seçeneğiyle çelişiyor.";
|
||||
if (reason === "score_model_conflicts_with_over_pick")
|
||||
return "Skor ve xG modeli bu üst seçeneğiyle çelişiyor.";
|
||||
if (reason === "market_stack_conflict_over25")
|
||||
return "2.5 üst sinyali bu marketle çelişiyor.";
|
||||
if (reason === "market_stack_conflict_btts")
|
||||
return "KG Var sinyali bu marketle çelişiyor.";
|
||||
if (reason === "live_total_goals_close_to_line")
|
||||
return "Canlı toplam gol sayısı bu çizgiye fazla yaklaştı.";
|
||||
if (reason === "score_model_conflicts_with_btts_no")
|
||||
return "Skor ve xG modeli KG Yok seçeneğiyle çelişiyor.";
|
||||
if (reason === "score_model_conflicts_with_draw_pick")
|
||||
return "Skor modeli beraberlik seçeneğini desteklemiyor.";
|
||||
if (reason === "score_model_conflicts_with_home_pick")
|
||||
return "Skor modeli ev sahibi seçeneğini desteklemiyor.";
|
||||
if (reason === "score_model_conflicts_with_away_pick")
|
||||
return "Skor modeli deplasman seçeneğini desteklemiyor.";
|
||||
if (/^[a-z0-9_]+$/i.test(reason)) {
|
||||
return reason.replace(/_/g, " ").replace(/^\w/, (char) => char.toUpperCase());
|
||||
return reason
|
||||
.replace(/_/g, " ")
|
||||
.replace(/^\w/, (char) => char.toUpperCase());
|
||||
}
|
||||
return reason;
|
||||
}
|
||||
@@ -101,7 +125,8 @@ function formatEdgeSignal(value?: number): string {
|
||||
}
|
||||
|
||||
function getEdgePalette(value?: number): string {
|
||||
if (value === undefined || value === null || Number.isNaN(value)) return "gray";
|
||||
if (value === undefined || value === null || Number.isNaN(value))
|
||||
return "gray";
|
||||
if (value <= 0) return "red";
|
||||
if (value < 0.08) return "yellow";
|
||||
if (value < 0.15) return "orange";
|
||||
@@ -211,7 +236,10 @@ function getMarketLabel(
|
||||
market: string,
|
||||
marketLabels?: Record<string, string>,
|
||||
): string {
|
||||
if (marketLabels && Object.prototype.hasOwnProperty.call(marketLabels, market)) {
|
||||
if (
|
||||
marketLabels &&
|
||||
Object.prototype.hasOwnProperty.call(marketLabels, market)
|
||||
) {
|
||||
return marketLabels[market];
|
||||
}
|
||||
|
||||
@@ -278,7 +306,13 @@ function getPredictionSport(prediction: MatchPredictionDto): SportType {
|
||||
return "football";
|
||||
}
|
||||
|
||||
const SIGNAL_TIER_ORDER: SignalTier[] = ["CORE", "VALUE", "LEAN", "LONGSHOT", "PASS"];
|
||||
const SIGNAL_TIER_ORDER: SignalTier[] = [
|
||||
"CORE",
|
||||
"VALUE",
|
||||
"LEAN",
|
||||
"LONGSHOT",
|
||||
"PASS",
|
||||
];
|
||||
|
||||
function getSignalTierPalette(tier?: SignalTier) {
|
||||
switch (tier) {
|
||||
@@ -318,7 +352,12 @@ function TooltipIcon({ content }: { content: string }) {
|
||||
positioning={{ placement: "top" }}
|
||||
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
|
||||
>
|
||||
<IconButton aria-label="Bilgi" variant="ghost" size="2xs" colorPalette="gray">
|
||||
<IconButton
|
||||
aria-label="Bilgi"
|
||||
variant="ghost"
|
||||
size="2xs"
|
||||
colorPalette="gray"
|
||||
>
|
||||
<LuCircleHelp />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -361,7 +400,13 @@ function MetricTile({
|
||||
const bg = useColorModeValue("gray.50", "whiteAlpha.50");
|
||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||
return (
|
||||
<Box p={3.5} bg={bg} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
|
||||
<Box
|
||||
p={3.5}
|
||||
bg={bg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack justify="space-between" mb={1.5}>
|
||||
<Text fontSize="xs" color="fg.muted" fontWeight="medium">
|
||||
{label}
|
||||
@@ -388,7 +433,12 @@ function Bar({
|
||||
}) {
|
||||
return (
|
||||
<Box h={height} w="full" bg={trackBg} borderRadius="full" overflow="hidden">
|
||||
<Box h="full" w={`${Math.max(0, Math.min(100, value))}%`} bg={color} borderRadius="full" />
|
||||
<Box
|
||||
h="full"
|
||||
w={`${Math.max(0, Math.min(100, value))}%`}
|
||||
bg={color}
|
||||
borderRadius="full"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -484,7 +534,12 @@ function PickCard({
|
||||
return (
|
||||
<Card.Root bg={bg} borderColor={borderColor} borderRadius="2xl">
|
||||
<Card.Body gap={4}>
|
||||
<Flex justify="space-between" align={{ base: "start", md: "center" }} direction={{ base: "column", md: "row" }} gap={3}>
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align={{ base: "start", md: "center" }}
|
||||
direction={{ base: "column", md: "row" }}
|
||||
gap={3}
|
||||
>
|
||||
<VStack align="start" gap={2}>
|
||||
<Badge colorPalette={palette} variant="solid" borderRadius="full">
|
||||
{title}
|
||||
@@ -493,30 +548,50 @@ function PickCard({
|
||||
{pick.pick}
|
||||
</Text>
|
||||
<HStack gap={2} flexWrap="wrap">
|
||||
<Badge variant="subtle">{getMarketLabel(pick.market, marketLabels)}</Badge>
|
||||
<Badge colorPalette={pick.playable ? "green" : "gray"} variant="subtle">
|
||||
<Badge variant="subtle">
|
||||
{getMarketLabel(pick.market, marketLabels)}
|
||||
</Badge>
|
||||
<Badge
|
||||
colorPalette={pick.playable ? "green" : "gray"}
|
||||
variant="subtle"
|
||||
>
|
||||
{pick.bet_grade}
|
||||
</Badge>
|
||||
<Badge colorPalette={getSignalTierPalette(pick.signal_tier)} variant="subtle">
|
||||
<Badge
|
||||
colorPalette={getSignalTierPalette(pick.signal_tier)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getSignalTierLabel(pick.signal_tier)}
|
||||
</Badge>
|
||||
<Badge colorPalette={confidenceBandPalette} variant="subtle">
|
||||
{getConfidenceBandLabel(pick.confidence_interval?.band)}
|
||||
</Badge>
|
||||
<Badge colorPalette={getEdgePalette(pick.ev_edge)} variant="subtle">
|
||||
<Badge
|
||||
colorPalette={getEdgePalette(pick.ev_edge)}
|
||||
variant="subtle"
|
||||
>
|
||||
Teorik avantaj {formatEdgeSignal(pick.ev_edge)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<SimpleGrid columns={2} gap={3} minW={{ base: "full", md: "320px" }}>
|
||||
<MetricTile label={labels.confidence} value={formatPercent(pick.calibrated_confidence, 0)} />
|
||||
<MetricTile
|
||||
label={labels.confidence}
|
||||
value={formatPercent(pick.calibrated_confidence, 0)}
|
||||
/>
|
||||
<MetricTile label={labels.odds} value={formatOdds(pick.odds)} />
|
||||
<MetricTile
|
||||
label={labels.recommendedStake}
|
||||
value={formatUnits(pick.stake_units || stakeFallback)}
|
||||
/>
|
||||
<MetricTile label={labels.playScore} value={formatSignalScore(pick.play_score)} />
|
||||
<MetricTile label="Guven Araligi" value={formatInterval(pick.confidence_interval)} />
|
||||
<MetricTile
|
||||
label={labels.playScore}
|
||||
value={formatSignalScore(pick.play_score)}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Guven Araligi"
|
||||
value={formatInterval(pick.confidence_interval)}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Band"
|
||||
value={getConfidenceBandLabel(pick.confidence_interval?.band)}
|
||||
@@ -524,7 +599,10 @@ function PickCard({
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</Flex>
|
||||
<ProbabilitySplit modelProb={pick.probability} impliedProb={pick.implied_prob} />
|
||||
<ProbabilitySplit
|
||||
modelProb={pick.probability}
|
||||
impliedProb={pick.implied_prob}
|
||||
/>
|
||||
<Box>
|
||||
<HStack justify="space-between" mb={1.5}>
|
||||
<Text fontSize="sm" fontWeight="semibold">
|
||||
@@ -540,7 +618,10 @@ function PickCard({
|
||||
trackBg={trackBg}
|
||||
/>
|
||||
</Box>
|
||||
<ReasonList items={pick.decision_reasons} resolveReason={resolveReason} />
|
||||
<ReasonList
|
||||
items={pick.decision_reasons}
|
||||
resolveReason={resolveReason}
|
||||
/>
|
||||
{pick.confidence_interval && !pick.confidence_interval.threshold_met ? (
|
||||
<Box
|
||||
p={3}
|
||||
@@ -550,7 +631,8 @@ function PickCard({
|
||||
borderColor={intervalWarningBorder}
|
||||
>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi onerilmez.
|
||||
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi
|
||||
onerilmez.
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
@@ -582,48 +664,73 @@ function SummaryTable({
|
||||
{items
|
||||
.slice()
|
||||
.sort((left, right) => {
|
||||
const leftIndex = SIGNAL_TIER_ORDER.indexOf(left.signal_tier || "PASS");
|
||||
const rightIndex = SIGNAL_TIER_ORDER.indexOf(right.signal_tier || "PASS");
|
||||
const leftIndex = SIGNAL_TIER_ORDER.indexOf(
|
||||
left.signal_tier || "PASS",
|
||||
);
|
||||
const rightIndex = SIGNAL_TIER_ORDER.indexOf(
|
||||
right.signal_tier || "PASS",
|
||||
);
|
||||
if (leftIndex !== rightIndex) return leftIndex - rightIndex;
|
||||
return right.calibrated_confidence - left.calibrated_confidence;
|
||||
})
|
||||
.map((item) => (
|
||||
<Flex
|
||||
key={`${item.market}-${item.pick}`}
|
||||
justify="space-between"
|
||||
align={{ base: "start", md: "center" }}
|
||||
direction={{ base: "column", md: "row" }}
|
||||
gap={3}
|
||||
px={3}
|
||||
py={3}
|
||||
borderRadius="xl"
|
||||
bg={item.playable ? highlightBg : "transparent"}
|
||||
borderWidth="1px"
|
||||
borderColor={item.playable ? "green.200" : borderColor}
|
||||
>
|
||||
<HStack gap={2} flexWrap="wrap">
|
||||
<Badge colorPalette={item.playable ? "green" : "gray"} variant="subtle">
|
||||
{item.bet_grade}
|
||||
</Badge>
|
||||
<Badge colorPalette={getSignalTierPalette(item.signal_tier)} variant="subtle">
|
||||
{getSignalTierLabel(item.signal_tier)}
|
||||
</Badge>
|
||||
<Text fontWeight="semibold">{getMarketLabel(item.market, marketLabels)}</Text>
|
||||
<Text color="fg.muted">{item.pick}</Text>
|
||||
</HStack>
|
||||
<HStack gap={5} fontSize="sm">
|
||||
<Text minW="48px">{formatOdds(item.odds)}</Text>
|
||||
<Text minW="96px" color={`${getEdgePalette(item.ev_edge)}.500`} fontWeight="semibold">
|
||||
{formatEdgeSignal(item.ev_edge)}
|
||||
</Text>
|
||||
<Text minW="48px">{formatPercent(item.calibrated_confidence, 0)}</Text>
|
||||
<Badge colorPalette={getConfidenceBandPalette(item.confidence_interval?.band)} variant="subtle">
|
||||
{getConfidenceBandLabel(item.confidence_interval?.band)}
|
||||
</Badge>
|
||||
<Badge variant="surface">{formatUnits(item.stake_units)}</Badge>
|
||||
</HStack>
|
||||
</Flex>
|
||||
))}
|
||||
<Flex
|
||||
key={`${item.market}-${item.pick}`}
|
||||
justify="space-between"
|
||||
align={{ base: "start", md: "center" }}
|
||||
direction={{ base: "column", md: "row" }}
|
||||
gap={3}
|
||||
px={3}
|
||||
py={3}
|
||||
borderRadius="xl"
|
||||
bg={item.playable ? highlightBg : "transparent"}
|
||||
borderWidth="1px"
|
||||
borderColor={item.playable ? "green.200" : borderColor}
|
||||
>
|
||||
<HStack gap={2} flexWrap="wrap">
|
||||
<Badge
|
||||
colorPalette={item.playable ? "green" : "gray"}
|
||||
variant="subtle"
|
||||
>
|
||||
{item.bet_grade}
|
||||
</Badge>
|
||||
<Badge
|
||||
colorPalette={getSignalTierPalette(item.signal_tier)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getSignalTierLabel(item.signal_tier)}
|
||||
</Badge>
|
||||
<Text fontWeight="semibold">
|
||||
{getMarketLabel(item.market, marketLabels)}
|
||||
</Text>
|
||||
<Text color="fg.muted">{item.pick}</Text>
|
||||
</HStack>
|
||||
<HStack gap={5} fontSize="sm">
|
||||
<Text minW="48px">{formatOdds(item.odds)}</Text>
|
||||
<Text
|
||||
minW="96px"
|
||||
color={`${getEdgePalette(item.ev_edge)}.500`}
|
||||
fontWeight="semibold"
|
||||
>
|
||||
{formatEdgeSignal(item.ev_edge)}
|
||||
</Text>
|
||||
<Text minW="48px">
|
||||
{formatPercent(item.calibrated_confidence, 0)}
|
||||
</Text>
|
||||
<Badge
|
||||
colorPalette={getConfidenceBandPalette(
|
||||
item.confidence_interval?.band,
|
||||
)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getConfidenceBandLabel(item.confidence_interval?.band)}
|
||||
</Badge>
|
||||
<Badge variant="surface">
|
||||
{formatUnits(item.stake_units)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
</Flex>
|
||||
))}
|
||||
</VStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
@@ -650,7 +757,9 @@ function MarketBoardSection({
|
||||
|
||||
if (!marketBoard || !Object.keys(marketBoard).length) return null;
|
||||
|
||||
const summaryByMarket = new Map((betSummary || []).map((item) => [item.market, item]));
|
||||
const summaryByMarket = new Map(
|
||||
(betSummary || []).map((item) => [item.market, item]),
|
||||
);
|
||||
const orderedEntries = Object.entries(marketBoard).sort(([left], [right]) => {
|
||||
const leftIndex = MARKET_ORDER.indexOf(left);
|
||||
const rightIndex = MARKET_ORDER.indexOf(right);
|
||||
@@ -662,11 +771,7 @@ function MarketBoardSection({
|
||||
return (
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
|
||||
<Card.Body gap={4}>
|
||||
<SectionTitle
|
||||
icon={LuChartNoAxesCombined}
|
||||
title={title}
|
||||
info={info}
|
||||
/>
|
||||
<SectionTitle icon={LuChartNoAxesCombined} title={title} info={info} />
|
||||
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
||||
{orderedEntries.map(([market, entry]) => {
|
||||
if (!entry?.probs) return null;
|
||||
@@ -690,21 +795,33 @@ function MarketBoardSection({
|
||||
</Text>
|
||||
<HStack gap={2} flexWrap="wrap">
|
||||
{summary ? (
|
||||
<Badge colorPalette={summary.playable ? "green" : "gray"} variant="subtle">
|
||||
<Badge
|
||||
colorPalette={summary.playable ? "green" : "gray"}
|
||||
variant="subtle"
|
||||
>
|
||||
{summary.playable ? "Oynanabilir" : "Riskli"}
|
||||
</Badge>
|
||||
) : null}
|
||||
{summary?.signal_tier ? (
|
||||
<Badge colorPalette={getSignalTierPalette(summary.signal_tier)} variant="subtle">
|
||||
<Badge
|
||||
colorPalette={getSignalTierPalette(
|
||||
summary.signal_tier,
|
||||
)}
|
||||
variant="subtle"
|
||||
>
|
||||
{getSignalTierLabel(summary.signal_tier)}
|
||||
</Badge>
|
||||
) : null}
|
||||
{summary?.bet_grade ? <Badge variant="outline">{summary.bet_grade}</Badge> : null}
|
||||
{summary?.bet_grade ? (
|
||||
<Badge variant="outline">{summary.bet_grade}</Badge>
|
||||
) : null}
|
||||
</HStack>
|
||||
</VStack>
|
||||
{entry.pick ? (
|
||||
<Badge
|
||||
colorPalette={getConfidenceBandPalette(entry.confidence_band || interval?.band)}
|
||||
colorPalette={getConfidenceBandPalette(
|
||||
entry.confidence_band || interval?.band,
|
||||
)}
|
||||
variant="subtle"
|
||||
borderRadius="full"
|
||||
>
|
||||
@@ -720,7 +837,11 @@ function MarketBoardSection({
|
||||
/>
|
||||
<MetricTile
|
||||
label="Kalibre Guven"
|
||||
value={summary ? formatPercent(summary.calibrated_confidence, 0) : "-"}
|
||||
value={
|
||||
summary
|
||||
? formatPercent(summary.calibrated_confidence, 0)
|
||||
: "-"
|
||||
}
|
||||
accent={summary?.playable ? "green.500" : "orange.500"}
|
||||
/>
|
||||
<MetricTile
|
||||
@@ -807,7 +928,14 @@ function ScoreCard({
|
||||
</SimpleGrid>
|
||||
<SimpleGrid columns={{ base: 2, md: 5 }} gap={2}>
|
||||
{prediction.scenario_top5.map((scenario) => (
|
||||
<Box key={`${scenario.score}-${scenario.prob}`} p={3} bg={subBg} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
|
||||
<Box
|
||||
key={`${scenario.score}-${scenario.prob}`}
|
||||
p={3}
|
||||
bg={subBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<Text fontSize="lg" fontWeight="bold">
|
||||
{scenario.score}
|
||||
</Text>
|
||||
@@ -835,7 +963,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
const ui = messages.predictions?.ui;
|
||||
const uiText = (key: string, fallback: string) => ui?.[key] || fallback;
|
||||
const resolveReason = (reason: string) =>
|
||||
getPredictionReasonText(reason, messages.predictions?.["prediction-reasons"]);
|
||||
getPredictionReasonText(
|
||||
reason,
|
||||
messages.predictions?.["prediction-reasons"],
|
||||
);
|
||||
|
||||
const pageBg = useColorModeValue("gray.50", "gray.900");
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
@@ -864,7 +995,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
value: prediction.engine_breakdown.player,
|
||||
color: "green.400",
|
||||
},
|
||||
{ key: "odds", icon: LuTrendingUp, label: "Oran Analizi", value: prediction.engine_breakdown.odds, color: "orange.400" },
|
||||
{
|
||||
key: "odds",
|
||||
icon: LuTrendingUp,
|
||||
label: "Oran Analizi",
|
||||
value: prediction.engine_breakdown.odds,
|
||||
color: "orange.400",
|
||||
},
|
||||
{
|
||||
key: "referee",
|
||||
icon: LuShieldAlert,
|
||||
@@ -894,30 +1031,50 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack align="start" gap={2}>
|
||||
<Icon as={LuShieldAlert} boxSize={4.5} color="orange.500" mt={0.5} />
|
||||
<Icon
|
||||
as={LuShieldAlert}
|
||||
boxSize={4.5}
|
||||
color="orange.500"
|
||||
mt={0.5}
|
||||
/>
|
||||
<Text fontSize="sm" color="fg.muted" lineHeight="tall">
|
||||
Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi nedeniyle yanılabilir.
|
||||
Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi
|
||||
değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi
|
||||
nedeniyle yanılabilir.
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
{recommendedPick ? (
|
||||
<Grid templateColumns={{ base: "1fr", xl: "1.4fr 1fr" }} gap={4}>
|
||||
<Box p={4} bg={useColorModeValue("green.50", "green.950")} borderWidth="1px" borderColor={useColorModeValue("green.200", "green.800")} borderRadius="2xl">
|
||||
<Box
|
||||
p={4}
|
||||
bg={useColorModeValue("green.50", "green.950")}
|
||||
borderWidth="1px"
|
||||
borderColor={useColorModeValue("green.200", "green.800")}
|
||||
borderRadius="2xl"
|
||||
>
|
||||
<HStack justify="space-between" align="start" mb={4}>
|
||||
<VStack align="start" gap={2}>
|
||||
<Badge colorPalette="green" variant="solid" borderRadius="full">
|
||||
<Badge
|
||||
colorPalette="green"
|
||||
variant="solid"
|
||||
borderRadius="full"
|
||||
>
|
||||
{uiText("main-recommendation", "Öne Çıkan Sinyal")}
|
||||
</Badge>
|
||||
<Text fontSize="2xl" fontWeight="bold">
|
||||
{recommendedPick.pick}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
{getMarketLabel(recommendedPick.market, marketLabels)} {uiText("best-market-copy", "marketinde en guclu secim.")}
|
||||
{getMarketLabel(recommendedPick.market, marketLabels)}{" "}
|
||||
{uiText("best-market-copy", "marketinde en guclu secim.")}
|
||||
</Text>
|
||||
<HStack gap={2} flexWrap="wrap">
|
||||
<Badge colorPalette={mainBandPalette} variant="subtle">
|
||||
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
|
||||
{getConfidenceBandLabel(
|
||||
prediction.bet_advice.confidence_band,
|
||||
)}
|
||||
</Badge>
|
||||
{recommendedPick.confidence_interval ? (
|
||||
<Badge variant="outline">
|
||||
@@ -929,9 +1086,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<Icon as={LuBadgeCheck} boxSize={8} color="green.500" />
|
||||
</HStack>
|
||||
<SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
|
||||
<MetricTile label={uiText("confidence-label", "Guven")} value={formatPercent(recommendedPick.calibrated_confidence, 0)} />
|
||||
<MetricTile label={uiText("odds-label", "Oran")} value={formatOdds(recommendedPick.odds)} />
|
||||
<MetricTile label="Guven Araligi" value={formatInterval(recommendedPick.confidence_interval)} />
|
||||
<MetricTile
|
||||
label={uiText("confidence-label", "Guven")}
|
||||
value={formatPercent(
|
||||
recommendedPick.calibrated_confidence,
|
||||
0,
|
||||
)}
|
||||
/>
|
||||
<MetricTile
|
||||
label={uiText("odds-label", "Oran")}
|
||||
value={formatOdds(recommendedPick.odds)}
|
||||
/>
|
||||
<MetricTile
|
||||
label="Guven Araligi"
|
||||
value={formatInterval(recommendedPick.confidence_interval)}
|
||||
/>
|
||||
<MetricTile
|
||||
label={uiText("edge-label", "Teorik Avantaj")}
|
||||
value={formatEdgeSignal(recommendedPick.ev_edge)}
|
||||
@@ -955,7 +1124,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
|
||||
<Box p={4} bg={cardBg} borderWidth="1px" borderColor={borderColor} borderRadius="2xl">
|
||||
<Box
|
||||
p={4}
|
||||
bg={cardBg}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="2xl"
|
||||
>
|
||||
<Text fontSize="sm" fontWeight="semibold" mb={3}>
|
||||
{uiText("quick-read", "Hizli yorum")}
|
||||
</Text>
|
||||
@@ -985,12 +1160,18 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
/>
|
||||
<MetricTile
|
||||
label={uiText("lineup-source", "Lineup Kaynagi")}
|
||||
value={getLineupSourceLabel(prediction.data_quality.lineup_source)}
|
||||
value={getLineupSourceLabel(
|
||||
prediction.data_quality.lineup_source,
|
||||
)}
|
||||
/>
|
||||
<MetricTile
|
||||
label={uiText("model-label", "Model")}
|
||||
value={prediction.model_version}
|
||||
/>
|
||||
<MetricTile label={uiText("model-label", "Model")} value={prediction.model_version} />
|
||||
</SimpleGrid>
|
||||
|
||||
{prediction.risk.is_surprise_risk || prediction.risk.warnings?.length ? (
|
||||
{prediction.risk.is_surprise_risk ||
|
||||
prediction.risk.warnings?.length ? (
|
||||
<Box
|
||||
p={4}
|
||||
bg={useColorModeValue("orange.50", "orange.950")}
|
||||
@@ -999,7 +1180,12 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
borderRadius="2xl"
|
||||
>
|
||||
<HStack align="start" gap={3}>
|
||||
<Icon as={LuTriangleAlert} boxSize={5} color="orange.500" mt={0.5} />
|
||||
<Icon
|
||||
as={LuTriangleAlert}
|
||||
boxSize={5}
|
||||
color="orange.500"
|
||||
mt={0.5}
|
||||
/>
|
||||
<VStack align="start" gap={1.5}>
|
||||
<Text fontWeight="semibold">Risk Yorumu</Text>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
@@ -1009,8 +1195,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
: "Model bu maçta ekstra dikkat istiyor.")}
|
||||
</Text>
|
||||
{prediction.risk.surprise_score !== undefined ? (
|
||||
<Text fontSize="sm" fontWeight="semibold" color="orange.600">
|
||||
Sürpriz skoru: {formatPercent(prediction.risk.surprise_score, 0)}
|
||||
<Text
|
||||
fontSize="sm"
|
||||
fontWeight="semibold"
|
||||
color="orange.600"
|
||||
>
|
||||
Sürpriz skoru:{" "}
|
||||
{formatPercent(prediction.risk.surprise_score, 0)}
|
||||
</Text>
|
||||
) : null}
|
||||
<ReasonList
|
||||
@@ -1032,11 +1223,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<SectionTitle
|
||||
icon={LuChartColumn}
|
||||
title={t("engine-breakdown-title")}
|
||||
info={uiText("engine-info", "Tahmini en cok hangi bilesenlerin etkiledigini gosterir.")}
|
||||
info={uiText(
|
||||
"engine-info",
|
||||
"Tahmini en cok hangi bilesenlerin etkiledigini gosterir.",
|
||||
)}
|
||||
/>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
|
||||
{engineItems.map((item) => (
|
||||
<Box key={item.key} p={4} bg={useColorModeValue("gray.50", "whiteAlpha.50")} borderWidth="1px" borderColor={borderColor} borderRadius="xl">
|
||||
<Box
|
||||
key={item.key}
|
||||
p={4}
|
||||
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
|
||||
borderWidth="1px"
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
>
|
||||
<HStack justify="space-between" mb={2}>
|
||||
<HStack gap={2}>
|
||||
<Icon as={item.icon} boxSize={4} color={item.color} />
|
||||
@@ -1048,7 +1249,11 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
+{item.value.toFixed(1)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Bar value={Math.min(item.value, 100)} color={item.color} trackBg={useColorModeValue("gray.100", "gray.700")} />
|
||||
<Bar
|
||||
value={Math.min(item.value, 100)}
|
||||
color={item.color}
|
||||
trackBg={useColorModeValue("gray.100", "gray.700")}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
@@ -1079,14 +1284,21 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
<SectionTitle
|
||||
icon={LuFlame}
|
||||
title={uiText("alternative-markets", "Alternatif Marketler")}
|
||||
info={uiText("alternative-markets-info", "Ana tahmin disindaki secenekler.")}
|
||||
info={uiText(
|
||||
"alternative-markets-info",
|
||||
"Ana tahmin disindaki secenekler.",
|
||||
)}
|
||||
/>
|
||||
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
|
||||
{prediction.supporting_picks.map((pick) => (
|
||||
<PickCard
|
||||
key={`${pick.market}-${pick.pick}`}
|
||||
pick={pick}
|
||||
title={pick.playable ? uiText("alternative", "Alternatif") : uiText("pass-market", "PASS market")}
|
||||
title={
|
||||
pick.playable
|
||||
? uiText("alternative", "Alternatif")
|
||||
: uiText("pass-market", "PASS market")
|
||||
}
|
||||
resolveReason={resolveReason}
|
||||
palette={pick.ev_edge > 0 ? "blue" : "orange"}
|
||||
marketLabels={marketLabels}
|
||||
@@ -1108,7 +1320,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
items={prediction.bet_summary || []}
|
||||
marketLabels={marketLabels}
|
||||
title={uiText("all-markets-title", "Tum Marketler")}
|
||||
info={uiText("all-markets-info", "Butun secenekleri tek tabloda karsilastir.")}
|
||||
info={uiText(
|
||||
"all-markets-info",
|
||||
"Butun secenekleri tek tabloda karsilastir.",
|
||||
)}
|
||||
/>
|
||||
<ScoreCard prediction={prediction} sport={sport} />
|
||||
<MarketBoardSection
|
||||
@@ -1116,7 +1331,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
betSummary={prediction.bet_summary || []}
|
||||
marketLabels={marketLabels}
|
||||
title={t("market-board")}
|
||||
info={uiText("market-board-info", "Modelin her markette gordugu olasilik dagilimi.")}
|
||||
info={uiText(
|
||||
"market-board-info",
|
||||
"Modelin her markette gordugu olasilik dagilimi.",
|
||||
)}
|
||||
/>
|
||||
|
||||
{prediction.v27_engine ? (
|
||||
@@ -1130,16 +1348,37 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
title={t("bet-advice")}
|
||||
info={uiText("bet-advice-info", "Modelin nihai aksiyon onerisi.")}
|
||||
/>
|
||||
<HStack justify="space-between" align={{ base: "start", md: "center" }} flexDir={{ base: "column", md: "row" }} gap={3}>
|
||||
<HStack
|
||||
justify="space-between"
|
||||
align={{ base: "start", md: "center" }}
|
||||
flexDir={{ base: "column", md: "row" }}
|
||||
gap={3}
|
||||
>
|
||||
<HStack gap={3}>
|
||||
<Badge colorPalette={prediction.bet_advice.playable ? "green" : "red"} variant="solid" borderRadius="full" fontSize="sm" px={3} py={1}>
|
||||
<Badge
|
||||
colorPalette={prediction.bet_advice.playable ? "green" : "red"}
|
||||
variant="solid"
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
{prediction.bet_advice.playable ? "OYNA" : "OYNAMA"}
|
||||
</Badge>
|
||||
<Badge colorPalette={mainBandPalette} variant="subtle" borderRadius="full" fontSize="sm" px={3} py={1}>
|
||||
<Badge
|
||||
colorPalette={mainBandPalette}
|
||||
variant="subtle"
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
px={3}
|
||||
py={1}
|
||||
>
|
||||
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
|
||||
</Badge>
|
||||
<Badge
|
||||
colorPalette={getSignalTierPalette(prediction.bet_advice.signal_tier)}
|
||||
colorPalette={getSignalTierPalette(
|
||||
prediction.bet_advice.signal_tier,
|
||||
)}
|
||||
variant="subtle"
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
@@ -1148,15 +1387,25 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
|
||||
>
|
||||
{getSignalTierLabel(prediction.bet_advice.signal_tier)}
|
||||
</Badge>
|
||||
<Text color="fg.muted">{resolveReason(prediction.bet_advice.reason)}</Text>
|
||||
<Text color="fg.muted">
|
||||
{resolveReason(prediction.bet_advice.reason)}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Badge variant="surface" fontSize="sm" px={3} py={1}>
|
||||
{uiText("recommended-stake-inline", "Onerilen miktar")}: {formatUnits(prediction.bet_advice.suggested_stake_units)}
|
||||
{uiText("recommended-stake-inline", "Onerilen miktar")}:{" "}
|
||||
{formatUnits(prediction.bet_advice.suggested_stake_units)}
|
||||
</Badge>
|
||||
</HStack>
|
||||
<Separator />
|
||||
<SectionTitle icon={LuBrain} title={t("reasoning")} info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti." />
|
||||
<ReasonList items={prediction.reasoning_factors} resolveReason={resolveReason} />
|
||||
<SectionTitle
|
||||
icon={LuBrain}
|
||||
title={t("reasoning")}
|
||||
info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti."
|
||||
/>
|
||||
<ReasonList
|
||||
items={prediction.reasoning_factors}
|
||||
resolveReason={resolveReason}
|
||||
/>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
</VStack>
|
||||
|
||||
@@ -153,7 +153,12 @@ function TripleValueCard({
|
||||
isValue ? "green.300" : "gray.200",
|
||||
isValue ? "green.700" : "gray.700",
|
||||
);
|
||||
const edgeColor = entry.edge > 0.03 ? "green.500" : entry.edge < -0.03 ? "red.400" : "fg.muted";
|
||||
const edgeColor =
|
||||
entry.edge > 0.03
|
||||
? "green.500"
|
||||
: entry.edge < -0.03
|
||||
? "red.400"
|
||||
: "fg.muted";
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -249,13 +254,7 @@ function ProgressBar({
|
||||
const trackBg = useColorModeValue("gray.100", "gray.700");
|
||||
const w = max > 0 ? Math.min(100, (value / max) * 100) : 0;
|
||||
return (
|
||||
<Box
|
||||
h="10px"
|
||||
w="full"
|
||||
bg={trackBg}
|
||||
borderRadius="full"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box h="10px" w="full" bg={trackBg} borderRadius="full" overflow="hidden">
|
||||
<Box
|
||||
h="full"
|
||||
w={`${w}%`}
|
||||
@@ -460,13 +459,28 @@ function HtftGrid({
|
||||
{/* Column headers */}
|
||||
<Grid templateColumns="50px repeat(3, 1fr)" gap={1.5} mb={1.5}>
|
||||
<Box />
|
||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
||||
<Text
|
||||
fontSize="2xs"
|
||||
fontWeight="bold"
|
||||
textAlign="center"
|
||||
color="fg.muted"
|
||||
>
|
||||
MS 1
|
||||
</Text>
|
||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
||||
<Text
|
||||
fontSize="2xs"
|
||||
fontWeight="bold"
|
||||
textAlign="center"
|
||||
color="fg.muted"
|
||||
>
|
||||
MS X
|
||||
</Text>
|
||||
<Text fontSize="2xs" fontWeight="bold" textAlign="center" color="fg.muted">
|
||||
<Text
|
||||
fontSize="2xs"
|
||||
fontWeight="bold"
|
||||
textAlign="center"
|
||||
color="fg.muted"
|
||||
>
|
||||
MS 2
|
||||
</Text>
|
||||
</Grid>
|
||||
@@ -611,7 +625,12 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
||||
|
||||
{/* Engine version badge */}
|
||||
<HStack>
|
||||
<Badge colorPalette="purple" variant="subtle" borderRadius="full" fontSize="2xs">
|
||||
<Badge
|
||||
colorPalette="purple"
|
||||
variant="subtle"
|
||||
borderRadius="full"
|
||||
fontSize="2xs"
|
||||
>
|
||||
{engine.version}
|
||||
</Badge>
|
||||
{engine.consensus && (
|
||||
@@ -621,11 +640,18 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
||||
borderRadius="full"
|
||||
fontSize="2xs"
|
||||
>
|
||||
{engine.consensus === "AGREE" ? "Motorlar Uyumlu" : "Motorlar Farklı"}
|
||||
{engine.consensus === "AGREE"
|
||||
? "Motorlar Uyumlu"
|
||||
: "Motorlar Farklı"}
|
||||
</Badge>
|
||||
)}
|
||||
{valueHits.length > 0 && (
|
||||
<Badge colorPalette="green" variant="outline" borderRadius="full" fontSize="2xs">
|
||||
<Badge
|
||||
colorPalette="green"
|
||||
variant="outline"
|
||||
borderRadius="full"
|
||||
fontSize="2xs"
|
||||
>
|
||||
{valueHits.length} Değer Sinyali
|
||||
</Badge>
|
||||
)}
|
||||
@@ -656,7 +682,10 @@ export default function V28OddsBandPanel({ engine }: V28OddsBandPanelProps) {
|
||||
{/* Cards + HTFT side by side on large screens */}
|
||||
{(hasCards || hasHtft) && (
|
||||
<Grid
|
||||
templateColumns={{ base: "1fr", xl: hasCards && hasHtft ? "1fr 1fr" : "1fr" }}
|
||||
templateColumns={{
|
||||
base: "1fr",
|
||||
xl: hasCards && hasHtft ? "1fr 1fr" : "1fr",
|
||||
}}
|
||||
gap={4}
|
||||
>
|
||||
{hasCards && <CardsSection cards={cards} />}
|
||||
|
||||
Reference in New Issue
Block a user