gg
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 3m34s

This commit is contained in:
2026-06-23 03:06:44 +03:00
parent 5dc3d7e3a3
commit 206be10941
10 changed files with 394 additions and 5 deletions
+27 -2
View File
@@ -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"
} }
} }
+27 -2
View File
@@ -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>
);
}
+1
View File
@@ -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 },
+21
View File
@@ -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,
};
+35
View File
@@ -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;
}
+15
View File
@@ -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),
});
};
+1 -1
View File
File diff suppressed because one or more lines are too long