This commit is contained in:
+26
-1
@@ -67,7 +67,8 @@
|
|||||||
"pricing": "Pricing",
|
"pricing": "Pricing",
|
||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"coupons": "Coupons",
|
"coupons": "Coupons",
|
||||||
"tools": "Tools"
|
"tools": "Tools",
|
||||||
|
"value-board": "Value Board"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"hero-title": "AI-Powered Betting Predictions",
|
"hero-title": "AI-Powered Betting Predictions",
|
||||||
@@ -781,6 +782,10 @@
|
|||||||
"pricing": {
|
"pricing": {
|
||||||
"title": "Pricing — iddaai",
|
"title": "Pricing — iddaai",
|
||||||
"description": "Explore iddaai AI-powered betting analysis plans. Free, Plus, and Premium plans available."
|
"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": {
|
"pricing": {
|
||||||
@@ -928,5 +933,25 @@
|
|||||||
"triple-value-title": "Value Detection (Triple Value)",
|
"triple-value-title": "Value Detection (Triple Value)",
|
||||||
"value": "VALUE",
|
"value": "VALUE",
|
||||||
"value-signals": "{count} Value Signals"
|
"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",
|
"pricing": "Fiyatlandırma",
|
||||||
"contact": "İletişim",
|
"contact": "İletişim",
|
||||||
"coupons": "Kuponlar",
|
"coupons": "Kuponlar",
|
||||||
"tools": "Araçlar"
|
"tools": "Araçlar",
|
||||||
|
"value-board": "Değer Panosu"
|
||||||
},
|
},
|
||||||
"landing": {
|
"landing": {
|
||||||
"hero-title": "Yapay Zeka Destekli Bahis Tahminleri",
|
"hero-title": "Yapay Zeka Destekli Bahis Tahminleri",
|
||||||
@@ -781,6 +782,10 @@
|
|||||||
"pricing": {
|
"pricing": {
|
||||||
"title": "Fiyatlandırma — iddaai",
|
"title": "Fiyatlandırma — iddaai",
|
||||||
"description": "iddaai AI destekli iddaa analiz planlarını keşfedin. Ücretsiz, Plus ve Premium planlar."
|
"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": {
|
"pricing": {
|
||||||
@@ -928,5 +933,25 @@
|
|||||||
"triple-value-title": "Değer Tespiti (Triple Value)",
|
"triple-value-title": "Değer Tespiti (Triple Value)",
|
||||||
"value": "DEĞER",
|
"value": "DEĞER",
|
||||||
"value-signals": "{count} Değer Sinyali"
|
"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
|
// Public — always visible in nav
|
||||||
{ label: "home", href: "/home", public: true },
|
{ label: "home", href: "/home", public: true },
|
||||||
{ label: "matches", href: "/matches", public: true },
|
{ label: "matches", href: "/matches", public: true },
|
||||||
|
{ label: "value-board", href: "/value-board", public: true },
|
||||||
{ label: "leagues", href: "/leagues", public: true },
|
{ label: "leagues", href: "/leagues", public: true },
|
||||||
{ label: "h2h", href: "/h2h", 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