This commit is contained in:
+26
-1
@@ -67,7 +67,8 @@
|
||||
"pricing": "Pricing",
|
||||
"contact": "Contact",
|
||||
"coupons": "Coupons",
|
||||
"tools": "Tools"
|
||||
"tools": "Tools",
|
||||
"value-board": "Value Board"
|
||||
},
|
||||
"landing": {
|
||||
"hero-title": "AI-Powered Betting Predictions",
|
||||
@@ -781,6 +782,10 @@
|
||||
"pricing": {
|
||||
"title": "Pricing — iddaai",
|
||||
"description": "Explore iddaai AI-powered betting analysis plans. Free, Plus, and Premium plans available."
|
||||
},
|
||||
"value-board": {
|
||||
"title": "Value Board",
|
||||
"description": "De-vigged, calibrated true probabilities for upcoming matches. No profit promise — transparent information."
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
@@ -928,5 +933,25 @@
|
||||
"triple-value-title": "Value Detection (Triple Value)",
|
||||
"value": "VALUE",
|
||||
"value-signals": "{count} Value Signals"
|
||||
},
|
||||
"value-board": {
|
||||
"title": "Value Board",
|
||||
"tagline": "Margin-removed TRUE probabilities (de-vig). Not a win guarantee — transparent, calibrated info. You decide.",
|
||||
"proof": "Calibration: under 2% error on real-odds matches",
|
||||
"error": "Failed to load data.",
|
||||
"empty": "No upcoming matches found.",
|
||||
"disclaimer": "Probabilities are computed by removing the bookmaker margin (~19-24%) from the odds. There is no \"sure bet\"; this board transparently shows the market's true view.",
|
||||
"home": "Home",
|
||||
"draw": "Draw",
|
||||
"away": "Away",
|
||||
"over": "Over",
|
||||
"btts": "BTTS",
|
||||
"ht": "HT",
|
||||
"cup": "Cup · home strong",
|
||||
"calibrated": "calibrated",
|
||||
"topScore": "Most likely score",
|
||||
"expectedGoals": "Expected goals",
|
||||
"vig": "margin",
|
||||
"lean": "Top pick"
|
||||
}
|
||||
}
|
||||
+26
-1
@@ -67,7 +67,8 @@
|
||||
"pricing": "Fiyatlandırma",
|
||||
"contact": "İletişim",
|
||||
"coupons": "Kuponlar",
|
||||
"tools": "Araçlar"
|
||||
"tools": "Araçlar",
|
||||
"value-board": "Değer Panosu"
|
||||
},
|
||||
"landing": {
|
||||
"hero-title": "Yapay Zeka Destekli Bahis Tahminleri",
|
||||
@@ -781,6 +782,10 @@
|
||||
"pricing": {
|
||||
"title": "Fiyatlandırma — iddaai",
|
||||
"description": "iddaai AI destekli iddaa analiz planlarını keşfedin. Ücretsiz, Plus ve Premium planlar."
|
||||
},
|
||||
"value-board": {
|
||||
"title": "Değer Panosu",
|
||||
"description": "Yaklaşan maçlar için marjı çıkarılmış, kalibre gerçek olasılıklar. Kazandırma vaadi değil — şeffaf bilgi."
|
||||
}
|
||||
},
|
||||
"pricing": {
|
||||
@@ -928,5 +933,25 @@
|
||||
"triple-value-title": "Değer Tespiti (Triple Value)",
|
||||
"value": "DEĞER",
|
||||
"value-signals": "{count} Değer Sinyali"
|
||||
},
|
||||
"value-board": {
|
||||
"title": "Değer Panosu",
|
||||
"tagline": "Marjı çıkarılmış GERÇEK olasılıklar (de-vig). Kazandırma garantisi değil — şeffaf, kalibre bilgi. Kararı sen ver.",
|
||||
"proof": "Kalibrasyon: gerçek-oranlı maçlarda hata < %2",
|
||||
"error": "Veriler yüklenemedi.",
|
||||
"empty": "Yaklaşan maç bulunamadı.",
|
||||
"disclaimer": "Olasılıklar bahisçi oranlarından marj (~%19-24) çıkarılarak hesaplanır. \"Garanti bahis\" yoktur; bu pano piyasanın gerçek görüşünü şeffafça gösterir.",
|
||||
"home": "Ev",
|
||||
"draw": "Ber",
|
||||
"away": "Dep",
|
||||
"over": "Üst",
|
||||
"btts": "KG",
|
||||
"ht": "İY",
|
||||
"cup": "Kupa · ev güçlü",
|
||||
"calibrated": "kalibre",
|
||||
"topScore": "En olası skor",
|
||||
"expectedGoals": "Beklenen gol",
|
||||
"vig": "marj",
|
||||
"lean": "Öne çıkan"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { Metadata } from "next";
|
||||
import ValueBoardContent from "@/components/value-board/value-board-content";
|
||||
|
||||
export async function generateMetadata(props: {
|
||||
params: Promise<{ locale: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { locale } = await props.params;
|
||||
const t = await getTranslations({ locale, namespace: "seo" });
|
||||
const siteUrl = process.env.NEXT_PUBLIC_APP_URL || "https://iddaai.com";
|
||||
|
||||
return {
|
||||
title: t("value-board.title"),
|
||||
description: t("value-board.description"),
|
||||
alternates: {
|
||||
canonical: `${siteUrl}/${locale}/value-board`,
|
||||
languages: {
|
||||
en: `${siteUrl}/en/value-board`,
|
||||
tr: `${siteUrl}/tr/value-board`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function ValueBoardPage() {
|
||||
return <ValueBoardContent />;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { Box, Card, Flex, HStack, Text, Badge } from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import type { ValueBoardMatch } from "@/lib/api/value-board/types";
|
||||
|
||||
function pct(p: number): string {
|
||||
return `%${Math.round(p * 100)}`;
|
||||
}
|
||||
|
||||
function formatKickoff(ms: number): string {
|
||||
if (!ms) return "";
|
||||
const d = new Date(ms);
|
||||
const p = (n: number) => String(n).padStart(2, "0");
|
||||
return `${p(d.getDate())}.${p(d.getMonth() + 1)} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function ProbBar({
|
||||
label,
|
||||
value,
|
||||
color,
|
||||
track,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
track: string;
|
||||
}) {
|
||||
return (
|
||||
<HStack gap={2}>
|
||||
<Text w="62px" fontSize="xs" color="fg.muted" flexShrink={0}>
|
||||
{label}
|
||||
</Text>
|
||||
<Box flex="1" h="8px" bg={track} borderRadius="full" overflow="hidden">
|
||||
<Box
|
||||
h="full"
|
||||
w={`${(value * 100).toFixed(1)}%`}
|
||||
bg={color}
|
||||
borderRadius="full"
|
||||
/>
|
||||
</Box>
|
||||
<Text
|
||||
w="42px"
|
||||
textAlign="right"
|
||||
fontSize="xs"
|
||||
fontWeight="semibold"
|
||||
flexShrink={0}
|
||||
>
|
||||
{pct(value)}
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ValueBoardCard({ item }: { item: ValueBoardMatch }) {
|
||||
const t = useTranslations("value-board");
|
||||
const cardBg = useColorModeValue("white", "gray.900");
|
||||
const track = useColorModeValue("gray.100", "gray.800");
|
||||
const leanLabel =
|
||||
item.lean &&
|
||||
{ "1": t("home"), X: t("draw"), "2": t("away") }[item.lean.key];
|
||||
|
||||
return (
|
||||
<Card.Root bg={cardBg} borderWidth="1px" borderRadius="2xl" overflow="hidden">
|
||||
<Card.Body p={5}>
|
||||
<Flex justify="space-between" align="flex-start" gap={2} mb={2}>
|
||||
<Box>
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
{item.league}
|
||||
</Text>
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
{formatKickoff(item.kickoff)}
|
||||
</Text>
|
||||
</Box>
|
||||
<Badge
|
||||
colorPalette={item.isCup ? "orange" : "green"}
|
||||
variant="subtle"
|
||||
fontSize="0.65rem"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{item.isCup ? t("cup") : t("calibrated")}
|
||||
</Badge>
|
||||
</Flex>
|
||||
|
||||
<Text fontSize="md" fontWeight="bold" mb={3}>
|
||||
{item.matchName}
|
||||
</Text>
|
||||
|
||||
{item.ms && (
|
||||
<Box mb={3}>
|
||||
<ProbBar label={t("home")} value={item.ms.home} color="green.500" track={track} />
|
||||
<Box h="4px" />
|
||||
<ProbBar label={t("draw")} value={item.ms.draw} color="gray.400" track={track} />
|
||||
<Box h="4px" />
|
||||
<ProbBar label={t("away")} value={item.ms.away} color="blue.500" track={track} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{item.ou25 && (
|
||||
<ProbBar
|
||||
label={`${t("over")} 2.5`}
|
||||
value={item.ou25.over}
|
||||
color="orange.400"
|
||||
track={track}
|
||||
/>
|
||||
)}
|
||||
{item.btts && (
|
||||
<>
|
||||
<Box h="4px" />
|
||||
<ProbBar
|
||||
label={`${t("btts")} ✓`}
|
||||
value={item.btts.yes}
|
||||
color="purple.400"
|
||||
track={track}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{item.htResult && (
|
||||
<>
|
||||
<Box h="4px" />
|
||||
<ProbBar
|
||||
label={`${t("ht")} 1`}
|
||||
value={item.htResult.home}
|
||||
color="teal.400"
|
||||
track={track}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Flex
|
||||
mt={4}
|
||||
pt={3}
|
||||
borderTopWidth="1px"
|
||||
justify="space-between"
|
||||
align="center"
|
||||
wrap="wrap"
|
||||
gap={2}
|
||||
>
|
||||
<HStack gap={3} fontSize="xs">
|
||||
{item.topScores[0] && (
|
||||
<Text color="fg.muted">
|
||||
{t("topScore")}:{" "}
|
||||
<Text as="span" fontWeight="semibold" color="fg">
|
||||
{item.topScores[0].score} ({pct(item.topScores[0].prob)})
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
{item.expectedGoals != null && (
|
||||
<Text color="fg.muted">
|
||||
{t("expectedGoals")}:{" "}
|
||||
<Text as="span" fontWeight="semibold" color="fg">
|
||||
{item.expectedGoals.toFixed(1)}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
{item.vigPct != null && (
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
{t("vig")} %{item.vigPct.toFixed(1)}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
{item.lean && leanLabel && (
|
||||
<Text fontSize="sm" mt={2} fontWeight="medium">
|
||||
→ {t("lean")}: {leanLabel} {pct(item.lean.prob)}
|
||||
</Text>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Center,
|
||||
Heading,
|
||||
SimpleGrid,
|
||||
Spinner,
|
||||
Text,
|
||||
Badge,
|
||||
Stack,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useValueBoard } from "@/lib/api/value-board/use-hooks";
|
||||
import ValueBoardCard from "./value-board-card";
|
||||
|
||||
export default function ValueBoardContent() {
|
||||
const t = useTranslations("value-board");
|
||||
const { data, isLoading, error } = useValueBoard("football");
|
||||
const items = data?.data ?? [];
|
||||
|
||||
return (
|
||||
<Box maxW="1200px" mx="auto" px={{ base: 4, md: 6 }} py={8}>
|
||||
<Heading as="h1" size="xl" fontWeight="bold">
|
||||
{t("title")}
|
||||
</Heading>
|
||||
<Text color="fg.muted" mt={2} maxW="640px">
|
||||
{t("tagline")}
|
||||
</Text>
|
||||
<Badge colorPalette="green" variant="subtle" mt={3}>
|
||||
✓ {t("proof")}
|
||||
</Badge>
|
||||
|
||||
{isLoading && (
|
||||
<Center minH="320px">
|
||||
<Spinner size="lg" />
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{error && !isLoading && (
|
||||
<Center minH="200px">
|
||||
<Text color="fg.muted">{t("error")}</Text>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && items.length === 0 && (
|
||||
<Center minH="200px">
|
||||
<Text color="fg.muted">{t("empty")}</Text>
|
||||
</Center>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && items.length > 0 && (
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} gap={5} mt={6}>
|
||||
{items.map((item) => (
|
||||
<ValueBoardCard key={item.id} item={item} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
<Stack mt={10} gap={1}>
|
||||
<Text fontSize="xs" color="fg.muted">
|
||||
{t("disclaimer")}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -42,6 +42,7 @@ export const NAV_ITEMS: NavItem[] = [
|
||||
// Public — always visible in nav
|
||||
{ label: "home", href: "/home", public: true },
|
||||
{ label: "matches", href: "/matches", public: true },
|
||||
{ label: "value-board", href: "/value-board", public: true },
|
||||
{ label: "leagues", href: "/leagues", public: true },
|
||||
{ label: "h2h", href: "/h2h", public: true },
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import type { ValueBoardMatch } from "./types";
|
||||
|
||||
/**
|
||||
* Value Board Service
|
||||
* Backend: /api/value-board
|
||||
*/
|
||||
|
||||
const getValueBoard = (sport?: string) => {
|
||||
return apiRequest<ApiResponse<ValueBoardMatch[]>>({
|
||||
url: "/value-board",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params: sport ? { sport } : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export const valueBoardService = {
|
||||
getValueBoard,
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
export interface ThreeWayProbs {
|
||||
home: number;
|
||||
draw: number;
|
||||
away: number;
|
||||
}
|
||||
|
||||
export interface TwoWayProbs {
|
||||
over: number;
|
||||
under: number;
|
||||
}
|
||||
|
||||
export interface ScoreProb {
|
||||
score: string;
|
||||
prob: number;
|
||||
}
|
||||
|
||||
export interface ValueBoardMatch {
|
||||
id: string;
|
||||
matchName: string;
|
||||
homeTeam: string;
|
||||
awayTeam: string;
|
||||
league: string;
|
||||
country?: string;
|
||||
kickoff: number; // epoch ms
|
||||
isCup: boolean;
|
||||
vigPct: number | null; // bookmaker margin on the MS market, %
|
||||
ms: ThreeWayProbs | null;
|
||||
ou25: TwoWayProbs | null;
|
||||
btts: { yes: number; no: number } | null;
|
||||
htResult: ThreeWayProbs | null;
|
||||
htOu15: TwoWayProbs | null;
|
||||
topScores: ScoreProb[];
|
||||
expectedGoals: number | null;
|
||||
lean: { key: "1" | "X" | "2"; prob: number } | null;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { valueBoardService } from "./service";
|
||||
|
||||
export const ValueBoardQueryKeys = {
|
||||
all: ["value-board"] as const,
|
||||
list: (sport?: string) =>
|
||||
[...ValueBoardQueryKeys.all, "list", sport] as const,
|
||||
};
|
||||
|
||||
export const useValueBoard = (sport?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ValueBoardQueryKeys.list(sport),
|
||||
queryFn: () => valueBoardService.getValueBoard(sport),
|
||||
});
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user