oran
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 3m12s

This commit is contained in:
2026-06-07 23:50:29 +03:00
parent fbf279d2d0
commit 095992ad26
6 changed files with 172 additions and 80 deletions
@@ -19,7 +19,7 @@ import { useParams, useRouter } from "next/navigation";
import { useState } from "react";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp, FadeIn } from "@/components/motion";
import { useMatchDetails } from "@/lib/api/matches/use-hooks";
import { useMatchDetails, useOddsMovement } from "@/lib/api/matches/use-hooks";
import { usePrediction, useAiCommentary } from "@/lib/api/predictions/use-hooks";
import { useGetMe } from "@/lib/api/users/use-hooks";
import { useQueryClient } from "@tanstack/react-query";
@@ -170,6 +170,8 @@ export default function MatchDetailContent() {
const match = matchData?.data as MatchResponseDto | undefined;
const prediction = predictionData?.data;
const { data: movementData } = useOddsMovement(matchId);
const oddsMovement = movementData?.data;
if (matchLoading) return <MatchDetailSkeleton />;
@@ -830,7 +832,7 @@ export default function MatchDetailContent() {
{/* ══════════════════════════════════════════════ */}
{match.odds && Object.keys(match.odds).length > 0 && show("odds") && (
<Box mt={6}>
<OddsCard odds={match.odds} />
<OddsCard odds={match.odds} prediction={prediction} movement={oddsMovement} />
</Box>
)}
</Box>
+142 -76
View File
@@ -1,110 +1,163 @@
"use client";
import {
Box,
SimpleGrid,
Text,
Badge,
Card,
VStack,
Flex,
} from "@chakra-ui/react";
import { Box, SimpleGrid, Text, Card, VStack, Flex } from "@chakra-ui/react";
import { useColorModeValue } from "@/components/ui/color-mode";
import type { MatchPredictionDto } from "@/lib/api/predictions/types";
// movement: per market (Turkish name) -> per selection -> {open, close}
export type OddsMovement = Record<
string,
Record<string, { open: number; close: number }>
>;
interface OddsCardProps {
odds?: Record<string, Record<string, { odd: string }>>;
prediction?: MatchPredictionDto;
movement?: OddsMovement;
}
// Turkish odds-market name -> prediction market_board code
const MARKET_CODE: Record<string, string> = {
"Maç Sonucu": "MS",
"1. Yarı Sonucu": "HT",
"İlk Yarı Sonucu": "HT",
"Karşılıklı Gol": "BTTS",
"Çifte Şans": "DC",
"1,5 Alt/Üst": "OU15",
"1.5 Alt/Üst": "OU15",
"2,5 Alt/Üst": "OU25",
"2.5 Alt/Üst": "OU25",
"3,5 Alt/Üst": "OU35",
"3.5 Alt/Üst": "OU35",
};
// odds selection label -> market_board probs key
function probKey(code: string, sel: string): string | null {
if (code === "MS" || code === "HT") {
return ["1", "X", "2"].includes(sel) ? sel : null;
}
if (code.startsWith("OU")) {
if (sel === "Üst" || sel === "Ust") return "over";
if (sel === "Alt") return "under";
return null;
}
if (code === "BTTS") {
if (sel === "Var") return "yes";
if (sel === "Yok") return "no";
return null;
}
if (code === "DC") {
return { "1-X": "1X", "X-2": "X2", "1-2": "12" }[sel] ?? null;
}
return null;
}
interface MarketBlockProps {
title: string;
selections: Record<string, { odd: string }>;
prediction?: MatchPredictionDto;
movement?: OddsMovement;
}
function MarketBlock({ title, selections }: MarketBlockProps) {
function MarketBlock({ title, selections, prediction, movement }: MarketBlockProps) {
const bg = useColorModeValue("gray.50", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
const selectionBg = useColorModeValue("white", "gray.700");
// Sort selections based on common market patterns
const sortedKeys = Object.keys(selections).sort((a, b) => {
// MS: 1, X, 2
if (["1", "X", "2"].includes(a) && ["1", "X", "2"].includes(b)) {
const order = ["1", "X", "2"];
return order.indexOf(a) - order.indexOf(b);
}
// Alt/Üst: Alt, Üst
if (["Alt", "Üst"].includes(a) && ["Alt", "Üst"].includes(b)) {
return a === "Alt" ? -1 : 1; // Alt first
}
// KG: Var, Yok
if (["Var", "Yok"].includes(a) && ["Var", "Yok"].includes(b)) {
return a === "Var" ? -1 : 1; // Var first
}
const code = MARKET_CODE[title];
const probs = code ? prediction?.market_board?.[code]?.probs : undefined;
const moveMkt = movement?.[title];
const order = (a: string, b: string) => {
if (["1", "X", "2"].includes(a) && ["1", "X", "2"].includes(b))
return ["1", "X", "2"].indexOf(a) - ["1", "X", "2"].indexOf(b);
if (["Alt", "Üst"].includes(a) && ["Alt", "Üst"].includes(b))
return a === "Alt" ? -1 : 1;
if (["Var", "Yok"].includes(a) && ["Var", "Yok"].includes(b))
return a === "Var" ? -1 : 1;
return a.localeCompare(b);
});
};
const sortedKeys = Object.keys(selections).sort(order);
return (
<Box
bg={bg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
overflow="hidden"
>
<Box bg={bg} borderWidth="1px" borderColor={borderColor} borderRadius="md" overflow="hidden">
<Box px={3} py={1.5} borderBottomWidth="1px" borderColor={borderColor}>
<Text
fontSize="xs"
fontWeight="bold"
color="fg.muted"
textTransform="uppercase"
>
<Text fontSize="xs" fontWeight="bold" color="fg.muted" textTransform="uppercase">
{title}
</Text>
</Box>
<Flex p={2} gap={2} wrap="wrap">
{sortedKeys.map((key) => (
<Flex
key={key}
direction="column"
align="center"
justify="center"
bg={selectionBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="sm"
minW="50px"
py={1}
px={2}
flex={1}
>
<Text fontSize="xs" color="fg.muted" mb={0.5}>
{key}
</Text>
<Text fontSize="sm" fontWeight="bold" color="primary.500">
{Number(selections[key].odd).toFixed(2)}
</Text>
</Flex>
))}
{sortedKeys.map((key) => {
const pk = code ? probKey(code, key) : null;
const prob = pk && probs ? probs[pk] : undefined; // 0..1
const mv = moveMkt?.[key];
let movePct: number | null = null;
if (mv && mv.open > 0) movePct = ((mv.close - mv.open) / mv.open) * 100;
return (
<Flex
key={key}
direction="column"
align="center"
justify="center"
bg={selectionBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="sm"
minW="64px"
py={1}
px={2}
flex={1}
>
<Text fontSize="xs" color="fg.muted" mb={0.5}>
{key}
</Text>
<Text fontSize="sm" fontWeight="bold" color="primary.500">
{Number(selections[key].odd).toFixed(2)}
</Text>
{/* Model hit probability (calibrated) */}
{prob !== undefined && (
<Text
fontSize="xs"
fontWeight="semibold"
color={prob >= 0.5 ? "green.500" : "fg.muted"}
>
%{Math.round(prob * 100)}
</Text>
)}
{/* Opening→closing total movement */}
{movePct !== null && Math.abs(movePct) >= 0.5 && (
<Text
fontSize="10px"
color={movePct < 0 ? "red.400" : "blue.400"}
title={`Açılış ${mv!.open.toFixed(2)} → Kapanış ${mv!.close.toFixed(2)}`}
>
{movePct < 0 ? "↓" : "↑"}
{Math.abs(movePct).toFixed(1)}%
</Text>
)}
</Flex>
);
})}
</Flex>
</Box>
);
}
export default function OddsCard({ odds }: OddsCardProps) {
export default function OddsCard({ odds, prediction, movement }: OddsCardProps) {
const cardBg = useColorModeValue("white", "gray.900");
const borderColor = useColorModeValue("gray.100", "gray.800");
if (!odds || Object.keys(odds).length === 0) {
return null;
}
if (!odds || Object.keys(odds).length === 0) return null;
// Define priority markets to show at the top
const PRIORITY_MARKETS = [
"Maç Sonucu",
"2.5 Alt/Üst",
"2,5 Alt/Üst",
"Karşılıklı Gol",
"İlk Yarı Sonucu",
"1. Yarı Sonucu",
"Çifte Şans",
"Kart",
"Korner",
];
@@ -117,26 +170,33 @@ export default function OddsCard({ odds }: OddsCardProps) {
(k) => !PRIORITY_MARKETS.some((pm) => k.includes(pm)),
);
// Group similar markets if needed, but simple list for now
return (
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl" mb={6}>
<Card.Body>
<VStack align="stretch" gap={4}>
<Text fontSize="lg" fontWeight="bold">
Canlı İddaa Oranları
</Text>
<Flex justify="space-between" align="baseline" wrap="wrap" gap={1}>
<Text fontSize="lg" fontWeight="bold">
Canlı İddaa Oranları
</Text>
<Text fontSize="xs" color="fg.muted">
%= modelin tutma olasılığı (kalibre) · = açılışkapanış hareketi
</Text>
</Flex>
{/* Priority Markets Grid */}
{priorityKeys.length > 0 && (
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
{priorityKeys.map((key) => (
<MarketBlock key={key} title={key} selections={odds[key]} />
<MarketBlock
key={key}
title={key}
selections={odds[key]}
prediction={prediction}
movement={movement}
/>
))}
</SimpleGrid>
)}
{/* Other Markets - Show ALL */}
{otherKeys.length > 0 && (
<SimpleGrid
columns={{ base: 1, md: 2, lg: 3 }}
@@ -144,7 +204,13 @@ export default function OddsCard({ odds }: OddsCardProps) {
mt={priorityKeys.length > 0 ? 2 : 0}
>
{otherKeys.map((key) => (
<MarketBlock key={key} title={key} selections={odds[key]} />
<MarketBlock
key={key}
title={key}
selections={odds[key]}
prediction={prediction}
movement={movement}
/>
))}
</SimpleGrid>
)}
+14
View File
@@ -48,9 +48,23 @@ const getActiveLeagues = (sport?: string) => {
});
};
export type OddsMovementDto = Record<
string,
Record<string, { open: number; close: number }>
>;
const getOddsMovement = (id: string) => {
return apiRequest<ApiResponse<OddsMovementDto>>({
url: `/matches/${id}/odds-movement`,
client: "core",
method: "get",
});
};
export const matchesService = {
listMatches,
getMatchDetails,
queryMatches,
getActiveLeagues,
getOddsMovement,
};
+10
View File
@@ -7,6 +7,8 @@ export const MatchesQueryKeys = {
list: (params?: MatchListParams) =>
[...MatchesQueryKeys.all, "list", params] as const,
detail: (id: string) => [...MatchesQueryKeys.all, "detail", id] as const,
oddsMovement: (id: string) =>
[...MatchesQueryKeys.all, "oddsMovement", id] as const,
activeLeagues: (sport?: string) =>
[...MatchesQueryKeys.all, "activeLeagues", sport] as const,
};
@@ -26,6 +28,14 @@ export const useMatchDetails = (id: string) => {
});
};
export const useOddsMovement = (id: string) => {
return useQuery({
queryKey: MatchesQueryKeys.oddsMovement(id),
queryFn: () => matchesService.getOddsMovement(id),
enabled: !!id,
});
};
export const useQueryMatches = () => {
return useMutation({
mutationFn: (queryDto: MatchQueryDto) =>
+1 -1
View File
File diff suppressed because one or more lines are too long