gg
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m41s

This commit is contained in:
2026-05-12 03:03:49 +03:00
parent e6e58b4433
commit b2ccc98226
24 changed files with 814 additions and 164 deletions
+28 -21
View File
@@ -246,27 +246,34 @@ export default function MatchDetailContent() {
{/* Score */}
<VStack gap={1} flexShrink={0}>
{match.score && (isLive || isFinished) ? (
<HStack gap={3}>
<Text
fontSize="5xl"
fontWeight="900"
lineHeight="1"
color={isLive ? "red.500" : "fg"}
>
{match.score.home}
</Text>
<Text fontSize="2xl" color="fg.muted" fontWeight="300">
:
</Text>
<Text
fontSize="5xl"
fontWeight="900"
lineHeight="1"
color={isLive ? "red.500" : "fg"}
>
{match.score.away}
</Text>
</HStack>
<>
<HStack gap={3}>
<Text
fontSize="4xl"
fontWeight="900"
color={isLive ? "red.500" : "fg"}
>
{match.score.home}
</Text>
<Text fontSize="2xl" color="fg.muted">
-
</Text>
<Text
fontSize="4xl"
fontWeight="900"
color={isLive ? "red.500" : "fg"}
>
{match.score.away}
</Text>
</HStack>
{match.score.htHome != null &&
match.score.htAway != null && (
<Text fontSize="xs" color="fg.muted">
(HT: {match.score.htHome}-{match.score.htAway})
</Text>
)}
</>
) : (
<Text fontSize="xl" fontWeight="bold" color="fg.muted">
{t("vs")}
+229 -13
View File
@@ -143,6 +143,36 @@ function formatUnits(value?: number): string {
return `${value.toFixed(1)}u`;
}
function getEngineLabelPalette(label?: string): string {
switch ((label || "").toUpperCase()) {
case "YUKSEK":
return "green";
case "ORTA":
return "yellow";
case "DUSUK":
return "orange";
case "COK_DUSUK":
return "red";
default:
return "gray";
}
}
function getEngineLabelText(label?: string): string {
switch ((label || "").toUpperCase()) {
case "YUKSEK":
return "Yüksek";
case "ORTA":
return "Orta";
case "DUSUK":
return "Düşük";
case "COK_DUSUK":
return "Çok Düşük";
default:
return label || "";
}
}
function getRiskPalette(level: string) {
switch (level.toUpperCase()) {
case "LOW":
@@ -731,6 +761,58 @@ function SummaryTable({
</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>
{item.is_underdog_reference ? (
<Badge colorPalette="gray" variant="outline" title="Underdog tarafının model olasılığı (bilgi amaçlı)">
Underdog ref.
</Badge>
) : null}
{item.betting_brain?.trap_market_flag ? (
<Badge colorPalette="red" variant="subtle" title={`Piyasa aşırı güveniyor (gap ${(item.betting_brain.trap_market_gap || 0) * 100 | 0}pp)`}>
Trap
</Badge>
) : null}
{item.betting_brain?.action === "WATCH_NO_VALUE" ? (
<Badge colorPalette="orange" variant="subtle" title="Model favoriyle hemfikir ama oran çok düşük">
No-value
</Badge>
) : null}
<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="68px" color={item.ev_edge > 0 ? "green.500" : "red.500"} fontWeight="semibold">
{item.ev_edge > 0 ? "+" : ""}
{formatPercent(item.ev_edge * 100, 1)}
</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>
@@ -869,7 +951,7 @@ function MarketBoardSection({
value={probability * 100}
color={
entry.pick === outcome ||
entry.pick?.toUpperCase() === outcome.toUpperCase()
entry.pick?.toUpperCase() === outcome.toUpperCase()
? "green.400"
: "blue.400"
}
@@ -980,6 +1062,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
const sport = getPredictionSport(prediction);
const isBasketball = sport === "basketball";
const engineDetail = prediction.engine_breakdown.detail;
const engineItems = [
{
key: "team",
@@ -987,6 +1070,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
label: isBasketball ? "Takim Formu" : "Takim Gucu",
value: prediction.engine_breakdown.team,
color: "blue.400",
detail: engineDetail?.team,
},
{
key: "player",
@@ -994,6 +1078,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
label: isBasketball ? "Kadro Etkisi" : "Oyuncu Etkisi",
value: prediction.engine_breakdown.player,
color: "green.400",
detail: engineDetail?.player,
},
{
key: "odds",
@@ -1002,17 +1087,84 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
value: prediction.engine_breakdown.odds,
color: "orange.400",
},
{
key: "odds",
icon: LuTrendingUp,
label: "Oran Analizi",
value: prediction.engine_breakdown.odds,
color: "orange.400",
detail: engineDetail?.odds,
},
{
key: "referee",
icon: LuShieldAlert,
label: isBasketball ? "Yardimci Sinyaller" : "Hakem Etkisi",
value: prediction.engine_breakdown.referee,
color: "purple.400",
detail: engineDetail?.referee,
},
];
const liveScoreHome = prediction.match_info?.current_score_home;
const liveScoreAway = prediction.match_info?.current_score_away;
const isLive = Boolean(prediction.match_info?.is_live);
const isStale = Boolean(prediction.prediction_freshness?.is_stale_for_live);
const contradictions = prediction.match_commentary?.contradictions || [];
return (
<VStack align="stretch" gap={5}>
{isLive ? (
<Box
p={3}
bg={useColorModeValue("red.50", "red.950")}
borderWidth="1px"
borderColor={useColorModeValue("red.300", "red.800")}
borderRadius="xl"
>
<HStack justify="space-between" align="center">
<HStack gap={2}>
<Icon as={LuFlame} color="red.500" />
<Text fontWeight="bold" color="red.600">
🔴 CANLI
</Text>
{liveScoreHome != null && liveScoreAway != null ? (
<Text fontWeight="semibold">
{prediction.match_info.home_team} {liveScoreHome} - {liveScoreAway}{" "}
{prediction.match_info.away_team}
</Text>
) : null}
</HStack>
{isStale ? (
<Badge colorPalette="orange" variant="solid">
Maç öncesi tahmin
</Badge>
) : null}
</HStack>
</Box>
) : null}
{contradictions.length ? (
<Box
p={3}
bg={useColorModeValue("yellow.50", "yellow.950")}
borderWidth="1px"
borderColor={useColorModeValue("yellow.300", "yellow.800")}
borderRadius="xl"
>
<HStack align="start" gap={2}>
<Icon as={LuTriangleAlert} color="yellow.600" mt={0.5} />
<VStack align="start" gap={1}>
<Text fontWeight="semibold">Tahmin Çelişkileri</Text>
{contradictions.map((text, idx) => (
<Text key={idx} fontSize="sm" color="fg.muted">
{text}
</Text>
))}
</VStack>
</HStack>
</Box>
) : null}
<Card.Root bg={pageBg} borderColor={borderColor} borderRadius="2xl">
<Card.Body gap={5}>
<SectionTitle
@@ -1114,7 +1266,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
label={uiText("stake-label", "Onerilen Miktar (Stake)")}
value={formatUnits(
recommendedPick.stake_units ||
prediction.bet_advice.suggested_stake_units,
prediction.bet_advice.suggested_stake_units,
)}
helper={uiText(
"stake-info",
@@ -1171,7 +1323,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
</SimpleGrid>
{prediction.risk.is_surprise_risk ||
prediction.risk.warnings?.length ? (
prediction.risk.warnings?.length ? (
<Box
p={4}
bg={useColorModeValue("orange.50", "orange.950")}
@@ -1204,13 +1356,31 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{formatPercent(prediction.risk.surprise_score, 0)}
</Text>
) : null}
<ReasonList
items={[
...(prediction.risk.surprise_reasons || []),
...prediction.risk.warnings,
]}
resolveReason={resolveReason}
/>
{prediction.risk.surprise_breakdown?.length ? (
<VStack align="start" gap={1} mt={1}>
{prediction.risk.surprise_breakdown.map((entry) => (
<HStack key={entry.code} gap={2}>
<Badge
colorPalette={entry.points >= 15 ? "red" : entry.points >= 8 ? "orange" : "yellow"}
variant="subtle"
>
+{entry.points.toFixed(0)}
</Badge>
<Text fontSize="sm" color="fg.muted">
{entry.label}
</Text>
</HStack>
))}
</VStack>
) : (
<ReasonList
items={[
...(prediction.risk.surprise_reasons || []),
...prediction.risk.warnings,
]}
resolveReason={resolveReason}
/>
)}
</VStack>
</HStack>
</Box>
@@ -1245,15 +1415,31 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{item.label}
</Text>
</HStack>
<Text fontSize="sm" fontWeight="bold">
+{item.value.toFixed(1)}
</Text>
<HStack gap={2}>
{item.detail?.label ? (
<Badge
colorPalette={getEngineLabelPalette(item.detail.label)}
variant="subtle"
>
{getEngineLabelText(item.detail.label)}
</Badge>
) : null}
<Text fontSize="sm" fontWeight="bold">
+{item.value.toFixed(1)}
</Text>
</HStack>
</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")} />
{item.detail?.interpretation ? (
<Text fontSize="xs" color="fg.muted" mt={2}>
{item.detail.interpretation}
</Text>
) : null}
</Box>
))}
</SimpleGrid>
@@ -1325,6 +1511,36 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
"Butun secenekleri tek tabloda karsilastir.",
)}
/>
{prediction.match_commentary?.headline || prediction.match_commentary?.summary ? (
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="2xl">
<Card.Body gap={3}>
<SectionTitle
icon={LuBrain}
title="Maç Yorumu"
info="Modelin maç hakkındaki insan-okunabilir özeti"
/>
{prediction.match_commentary.headline ? (
<Text fontSize="md" fontWeight="bold">
{prediction.match_commentary.headline}
</Text>
) : null}
{prediction.match_commentary.summary ? (
<Text fontSize="sm" color="fg.muted">
{prediction.match_commentary.summary}
</Text>
) : null}
{prediction.match_commentary.notes?.length ? (
<VStack align="start" gap={1}>
{prediction.match_commentary.notes.map((note, idx) => (
<Text key={idx} fontSize="sm">
{note}
</Text>
))}
</VStack>
) : null}
</Card.Body>
</Card.Root>
) : null}
<ScoreCard prediction={prediction} sport={sport} />
<MarketBoardSection
marketBoard={prediction.market_board}