main
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 2m23s

This commit is contained in:
2026-05-12 17:41:16 +03:00
parent b2ccc98226
commit 66877b88ca
7 changed files with 666 additions and 242 deletions
+111 -32
View File
@@ -15,7 +15,10 @@ import {
Separator,
Input,
} from "@chakra-ui/react";
import { NativeSelectRoot, NativeSelectField } from "@/components/ui/forms/native-select";
import {
NativeSelectRoot,
NativeSelectField,
} from "@/components/ui/forms/native-select";
import { useTranslations, useFormatter } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import {
@@ -27,7 +30,13 @@ import {
import { useAdminAnalytics, useAdminUsers } from "@/lib/api/admin/use-hooks";
import type { AdminUserDto, AnalyticsOverviewDto } from "@/lib/api/admin/types";
import { formatRoleLabel, isAdminRole } from "@/lib/auth/roles";
import { LuUsers, LuChartBar, LuActivity, LuShield, LuPencil } from "react-icons/lu";
import {
LuUsers,
LuChartBar,
LuActivity,
LuShield,
LuPencil,
} from "react-icons/lu";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { EditUserModal } from "./edit-user-modal";
@@ -88,13 +97,19 @@ export default function AdminContent() {
const format = useFormatter();
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
const [editingUser, setEditingUser] = useState<AdminUserDto | null>(null);
const [searchParams, setSearchParams] = useState({ search: "", role: "", subscriptionStatus: "", page: 1, limit: 10 });
const [searchParams, setSearchParams] = useState({
search: "",
role: "",
subscriptionStatus: "",
page: 1,
limit: 10,
});
const [debouncedSearch, setDebouncedSearch] = useState("");
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedSearch(searchParams.search);
setSearchParams(prev => ({ ...prev, page: 1 }));
setSearchParams((prev) => ({ ...prev, page: 1 }));
}, 500);
return () => clearTimeout(handler);
}, [searchParams.search]);
@@ -113,7 +128,7 @@ export default function AdminContent() {
role: searchParams.role,
subscriptionStatus: searchParams.subscriptionStatus,
page: searchParams.page,
limit: searchParams.limit
limit: searchParams.limit,
},
canAccessAdmin,
);
@@ -150,13 +165,13 @@ export default function AdminContent() {
<VStack gap={3}>
<Badge colorPalette="red" variant="subtle" borderRadius="full">
<LuShield />
Restricted
{t("restricted")}
</Badge>
<Heading as="h2" size="md">
Admin access required
{t("admin-access-required")}
</Heading>
<Text color="fg.muted" textAlign="center" maxW="md">
This area is only available to superadmin accounts.
{t("admin-access-description")}
</Text>
</VStack>
</Card.Body>
@@ -236,7 +251,7 @@ export default function AdminContent() {
</StaggerItem>
<StaggerItem>
<AdminStat
label={t("premium-users", { fallback: "Premium Users" })}
label={t("premium-users")}
value={analytics?.users?.premium ?? 0}
icon={<LuShield />}
colorPalette="purple"
@@ -272,32 +287,49 @@ export default function AdminContent() {
<Card.Body py={4}>
<SimpleGrid columns={{ base: 1, md: 3 }} gap={4}>
<Input
placeholder="E-posta veya isim ara..."
placeholder={t("search-users-placeholder")}
value={searchParams.search}
onChange={(e) => setSearchParams({ ...searchParams, search: e.target.value })}
onChange={(e) =>
setSearchParams({
...searchParams,
search: e.target.value,
})
}
/>
<NativeSelectRoot>
<NativeSelectField
placeholder="Tüm Rolleri Gör"
placeholder={t("all-roles")}
value={searchParams.role}
onChange={(e) => setSearchParams({ ...searchParams, role: e.target.value, page: 1 })}
onChange={(e) =>
setSearchParams({
...searchParams,
role: e.target.value,
page: 1,
})
}
items={[
{ label: "Standart Kullanıcı", value: "user" },
{ label: "Admin", value: "superadmin" }
{ label: t("standard-user"), value: "user" },
{ label: t("superadmin"), value: "superadmin" },
]}
/>
</NativeSelectRoot>
<NativeSelectRoot>
<NativeSelectField
placeholder="Tüm Paketleri Gör"
placeholder={t("all-plans")}
value={searchParams.subscriptionStatus}
onChange={(e) => setSearchParams({ ...searchParams, subscriptionStatus: e.target.value, page: 1 })}
onChange={(e) =>
setSearchParams({
...searchParams,
subscriptionStatus: e.target.value,
page: 1,
})
}
items={[
{ label: "Ücretsiz (Free)", value: "free" },
{ label: t("plan-free"), value: "free" },
{ label: "Plus", value: "plus" },
{ label: "Premium", value: "premium" },
{ label: "Gecikmiş", value: "past_due" },
{ label: "İptal", value: "cancelled" }
{ label: t("plan-past-due"), value: "past_due" },
{ label: t("plan-cancelled"), value: "cancelled" },
]}
/>
</NativeSelectRoot>
@@ -310,7 +342,11 @@ export default function AdminContent() {
<Spinner size="lg" color="primary.500" />
</Flex>
) : users.length > 0 ? (
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
>
<Card.Body>
<VStack gap={0} align="stretch">
{/* Table Header */}
@@ -330,7 +366,7 @@ export default function AdminContent() {
{t("user-role")}
</Text>
<Text flex={1} textAlign="center">
{t("subscription", { fallback: "Subscription" })}
{t("subscription")}
</Text>
<Text flex={1} textAlign="center">
{t("user-status")}
@@ -357,7 +393,12 @@ export default function AdminContent() {
>
{getUserDisplayName(user)}
</Text>
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
<Text
flex={2}
fontSize="sm"
color="fg.muted"
truncate
>
{user.email}
</Text>
<Flex flex={1} justify="center">
@@ -372,9 +413,20 @@ export default function AdminContent() {
{formatRoleLabel(user.role)}
</Badge>
</Flex>
<Flex flex={1} justify="center" direction="column" align="center" gap={1}>
<Flex
flex={1}
justify="center"
direction="column"
align="center"
gap={1}
>
<Badge
colorPalette={user.subscriptionStatus === "premium" || user.subscriptionStatus === "plus" ? "purple" : "gray"}
colorPalette={
user.subscriptionStatus === "premium" ||
user.subscriptionStatus === "plus"
? "purple"
: "gray"
}
variant="subtle"
fontSize="2xs"
borderRadius="full"
@@ -384,7 +436,14 @@ export default function AdminContent() {
</Badge>
{user.subscriptionExpiresAt ? (
<Text fontSize="2xs" color="fg.muted">
{format.dateTime(new Date(user.subscriptionExpiresAt), { year: 'numeric', month: '2-digit', day: '2-digit' })}
{format.dateTime(
new Date(user.subscriptionExpiresAt),
{
year: "numeric",
month: "2-digit",
day: "2-digit",
},
)}
</Text>
) : (
<Text fontSize="2xs" color="fg.muted">
@@ -436,25 +495,45 @@ export default function AdminContent() {
{/* Pagination */}
{meta && meta.totalPages > 1 && (
<Flex justify="center" pt={4} pb={2} gap={2} borderTopWidth="1px" borderColor={borderColor} mt={2}>
<Flex
justify="center"
pt={4}
pb={2}
gap={2}
borderTopWidth="1px"
borderColor={borderColor}
mt={2}
>
<Button
size="sm"
variant="outline"
disabled={!meta.hasPreviousPage}
onClick={() => setSearchParams({ ...searchParams, page: meta.page - 1 })}
onClick={() =>
setSearchParams({
...searchParams,
page: meta.page - 1,
})
}
>
Önceki
{tCommon("previous")}
</Button>
<Flex align="center" gap={2} fontSize="sm">
<Text>Sayfa {meta.page} / {meta.totalPages}</Text>
<Text>
{tCommon("page")} {meta.page} / {meta.totalPages}
</Text>
</Flex>
<Button
size="sm"
variant="outline"
disabled={!meta.hasNextPage}
onClick={() => setSearchParams({ ...searchParams, page: meta.page + 1 })}
onClick={() =>
setSearchParams({
...searchParams,
page: meta.page + 1,
})
}
>
Sonraki
{tCommon("next")}
</Button>
</Flex>
)}
+101 -67
View File
@@ -1,10 +1,6 @@
"use client";
import {
Button,
VStack,
Input,
} from "@chakra-ui/react";
import { Button, VStack, Input } from "@chakra-ui/react";
import {
DialogRoot,
DialogContent,
@@ -15,11 +11,14 @@ import {
DialogCloseTrigger,
} from "@/components/ui/overlays/dialog";
import { Field } from "@/components/ui/forms/field";
import { NativeSelectRoot, NativeSelectField } from "@/components/ui/forms/native-select";
import {
NativeSelectRoot,
NativeSelectField,
} from "@/components/ui/forms/native-select";
import { Switch } from "@/components/ui/forms/switch";
import { useTranslations } from "next-intl";
import { AdminUserDto } from "@/lib/api/admin/types";
import { useState, useEffect } from "react";
import { useState } from "react";
import {
useUpdateUserRole,
useUpdateUserSubscription,
@@ -33,52 +32,73 @@ interface EditUserModalProps {
}
export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) {
if (!user) return null;
return (
<EditUserModalContent
key={user.id}
user={user}
isOpen={isOpen}
onClose={onClose}
/>
);
}
function formatDateInputValue(value?: string | null): string {
if (!value) return "";
try {
return new Date(value).toISOString().split("T")[0];
} catch {
return "";
}
}
function EditUserModalContent({
user,
isOpen,
onClose,
}: {
user: AdminUserDto;
isOpen: boolean;
onClose: () => void;
}) {
const t = useTranslations("admin");
const tCommon = useTranslations("common");
const [role, setRole] = useState("user");
const [plan, setPlan] = useState("free");
const [expiresAt, setExpiresAt] = useState<string>("");
const [isActive, setIsActive] = useState(true);
const [role, setRole] = useState(user.role || "user");
const [plan, setPlan] = useState(user.subscriptionStatus || "free");
const [expiresAt, setExpiresAt] = useState<string>(
formatDateInputValue(user.subscriptionExpiresAt),
);
const [isActive, setIsActive] = useState(user.isActive);
const { mutateAsync: updateRole, isPending: rolePending } = useUpdateUserRole();
const { mutateAsync: updateSub, isPending: subPending } = useUpdateUserSubscription();
const { mutateAsync: toggleActive, isPending: togglePending } = useToggleUserActive();
useEffect(() => {
if (user) {
setRole(user.role || "user");
setPlan(user.subscriptionStatus || "free");
setIsActive(user.isActive);
if (user.subscriptionExpiresAt) {
try {
const date = new Date(user.subscriptionExpiresAt);
setExpiresAt(date.toISOString().split('T')[0]);
} catch(e) { setExpiresAt(""); }
} else {
setExpiresAt("");
}
}
}, [user]);
const { mutateAsync: updateRole, isPending: rolePending } =
useUpdateUserRole();
const { mutateAsync: updateSub, isPending: subPending } =
useUpdateUserSubscription();
const { mutateAsync: toggleActive, isPending: togglePending } =
useToggleUserActive();
const handleSave = async () => {
if (!user) return;
try {
if (role !== user.role) {
await updateRole({ id: user.id, dto: { role } });
}
const currentExpiresAtStr = user.subscriptionExpiresAt
? new Date(user.subscriptionExpiresAt).toISOString().split('T')[0]
const currentExpiresAtStr = user.subscriptionExpiresAt
? new Date(user.subscriptionExpiresAt).toISOString().split("T")[0]
: "";
if (plan !== user.subscriptionStatus || expiresAt !== currentExpiresAtStr) {
await updateSub({
id: user.id,
dto: {
if (
plan !== user.subscriptionStatus ||
expiresAt !== currentExpiresAtStr
) {
await updateSub({
id: user.id,
dto: {
plan,
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null
}
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : null,
},
});
}
if (isActive !== user.isActive) {
@@ -92,78 +112,92 @@ export function EditUserModal({ user, isOpen, onClose }: EditUserModalProps) {
const isPending = rolePending || subPending || togglePending;
if (!user) return null;
return (
<DialogRoot open={isOpen} onOpenChange={(e) => !e.open && onClose()}>
<DialogContent>
<DialogHeader>
<DialogTitle>Kullanıcı Düzenle: {user.email}</DialogTitle>
<DialogTitle>
{t("edit-user-title", { email: user.email })}
</DialogTitle>
</DialogHeader>
<DialogBody>
<VStack gap={4} align="stretch">
<Field label="Kullanıcı Rolü">
<Field label={t("user-role-field")}>
<NativeSelectRoot>
<NativeSelectField
value={role}
onChange={(e) => setRole(e.target.value)}
items={[
{ label: "Standart Kullanıcı", value: "user" },
{ label: "Sistem Yöneticisi (Admin)", value: "superadmin" },
{ label: t("standard-user"), value: "user" },
{ label: t("superadmin"), value: "superadmin" },
]}
/>
</NativeSelectRoot>
</Field>
<Field label="Abonelik Paketi">
<Field label={t("subscription-plan-field")}>
<NativeSelectRoot>
<NativeSelectField
value={plan}
onChange={(e) => {
const newPlan = e.target.value;
setPlan(newPlan);
if ((newPlan === "premium" || newPlan === "plus") && !expiresAt) {
if (
(newPlan === "premium" || newPlan === "plus") &&
!expiresAt
) {
const d = new Date();
d.setDate(d.getDate() + 30);
setExpiresAt(d.toISOString().split('T')[0]);
} else if (newPlan === "free" || newPlan === "cancelled" || newPlan === "past_due") {
setExpiresAt(d.toISOString().split("T")[0]);
} else if (
newPlan === "free" ||
newPlan === "cancelled" ||
newPlan === "past_due"
) {
setExpiresAt("");
}
}}
items={[
{ label: "Ücretsiz (Free)", value: "free" },
{ label: "Plus Paketi", value: "plus" },
{ label: "Premium Paketi", value: "premium" },
{ label: "Ödeme Gecikti (Past Due)", value: "past_due" },
{ label: "İptal Edildi (Cancelled)", value: "cancelled" },
{ label: t("plan-free"), value: "free" },
{ label: t("plan-plus"), value: "plus" },
{ label: t("plan-premium"), value: "premium" },
{ label: t("plan-past-due"), value: "past_due" },
{ label: t("plan-cancelled"), value: "cancelled" },
]}
/>
</NativeSelectRoot>
</Field>
{plan !== "free" && (
<Field label="Abonelik Bitiş Tarihi (Opsiyonel)">
<Input
type="date"
value={expiresAt}
onChange={(e) => setExpiresAt(e.target.value)}
<Field label={t("subscription-end-date")}>
<Input
type="date"
value={expiresAt}
onChange={(e) => setExpiresAt(e.target.value)}
/>
</Field>
)}
<Field label="Hesap Aktif mi?">
<Switch checked={isActive} onCheckedChange={(e) => setIsActive(e.checked)}>
{isActive ? "Aktif" : "Pasif"}
<Field label={t("account-active-question")}>
<Switch
checked={isActive}
onCheckedChange={(e) => setIsActive(e.checked)}
>
{isActive ? tCommon("active") : tCommon("inactive")}
</Switch>
</Field>
</VStack>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isPending}>
İptal
{tCommon("cancel")}
</Button>
<Button colorPalette="primary" onClick={handleSave} loading={isPending}>
Kaydet
<Button
colorPalette="primary"
onClick={handleSave}
loading={isPending}
>
{tCommon("save")}
</Button>
</DialogFooter>
<DialogCloseTrigger />
+4 -4
View File
@@ -18,10 +18,10 @@ import { useTranslations } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp } from "@/components/motion";
import { useSearchTeams, useHeadToHead } from "@/lib/api/leagues/use-hooks";
import type { TeamDto, HeadToHeadDto } from "@/lib/api/leagues/types";
import type { TeamDto } from "@/lib/api/leagues/types";
import type { MatchResponseDto } from "@/lib/api/matches/types";
import { LuSearch, LuArrowLeftRight } from "react-icons/lu";
import { useState, useEffect } from "react";
import { useState } from "react";
import { useDebounce } from "@/hooks/use-debounce";
function TeamSearchInput({
@@ -134,7 +134,7 @@ export default function H2HContent() {
?.data
? [
{
label: team1?.name || t("team1"),
label: team1?.name || t("team-1"),
value: h2h.data.data.team1Wins,
color: "green",
},
@@ -144,7 +144,7 @@ export default function H2HContent() {
color: "gray",
},
{
label: team2?.name || t("team2"),
label: team2?.name || t("team-2"),
value: h2h.data.data.team2Wins,
color: "blue",
},
+258 -129
View File
@@ -46,6 +46,16 @@ interface PredictionCardProps {
prediction: MatchPredictionDto;
}
type PredictionUiMessages = Record<string, string>;
function getUiText(
ui: PredictionUiMessages | undefined,
key: string,
fallback: string,
): string {
return ui?.[key] || fallback;
}
function formatReasonFallback(reason: string): string {
if (reason.startsWith("risk:")) return formatReasonFallback(reason.slice(5));
const evMatch = reason.match(/^ev_edge_([+\-][\d.]+%)_grade_(\w)$/);
@@ -158,16 +168,16 @@ function getEngineLabelPalette(label?: string): string {
}
}
function getEngineLabelText(label?: string): string {
function getEngineLabelText(label?: string, ui?: PredictionUiMessages): string {
switch ((label || "").toUpperCase()) {
case "YUKSEK":
return "Yüksek";
return getUiText(ui, "engine-label-high", "Yüksek");
case "ORTA":
return "Orta";
return getUiText(ui, "engine-label-medium", "Orta");
case "DUSUK":
return "Düşük";
return getUiText(ui, "engine-label-low", "Düşük");
case "COK_DUSUK":
return "Çok Düşük";
return getUiText(ui, "engine-label-very-low", "Çok Düşük");
default:
return label || "";
}
@@ -214,23 +224,30 @@ function getConfidenceBandPalette(band?: string) {
}
}
function getConfidenceBandLabel(band?: string) {
function getConfidenceBandLabel(band?: string, ui?: PredictionUiMessages) {
switch ((band || "").toUpperCase()) {
case "HIGH":
return "Yüksek";
return getUiText(ui, "confidence-high", "Yüksek");
case "MEDIUM":
return "Orta";
return getUiText(ui, "confidence-medium", "Orta");
case "LOW":
return "Düşük";
return getUiText(ui, "confidence-low", "Düşük");
default:
return "Belirsiz";
return getUiText(ui, "confidence-unknown", "Belirsiz");
}
}
function getLineupSourceLabel(source?: string): string {
if (source === "confirmed_live") return "Onayli ilk 11";
if (source === "probable_xi") return "Muhtemel ilk 11";
return source ? formatReasonFallback(source) : "Bilinmiyor";
function getLineupSourceLabel(
source?: string,
ui?: PredictionUiMessages,
): string {
if (source === "confirmed_live")
return getUiText(ui, "lineup-confirmed-live", "Onaylı ilk 11");
if (source === "probable_xi")
return getUiText(ui, "lineup-probable-xi", "Muhtemel ilk 11");
return source
? formatReasonFallback(source)
: getUiText(ui, "unknown", "Bilinmiyor");
}
function formatInterval(
@@ -359,22 +376,28 @@ function getSignalTierPalette(tier?: SignalTier) {
}
}
function getSignalTierLabel(tier?: SignalTier) {
function getSignalTierLabel(tier?: SignalTier, ui?: PredictionUiMessages) {
switch (tier) {
case "CORE":
return "Çekirdek";
return getUiText(ui, "signal-tier-core", "Çekirdek");
case "VALUE":
return "Değer";
return getUiText(ui, "signal-tier-value", "Değer");
case "LEAN":
return "Yorum";
return getUiText(ui, "signal-tier-lean", "Yorum");
case "LONGSHOT":
return "Sürpriz";
return getUiText(ui, "signal-tier-longshot", "Sürpriz");
default:
return "Pas";
return getUiText(ui, "signal-tier-pass", "Pas");
}
}
function TooltipIcon({ content }: { content: string }) {
function TooltipIcon({
content,
ariaLabel = "Bilgi",
}: {
content: string;
ariaLabel?: string;
}) {
return (
<Tooltip
content={content}
@@ -383,7 +406,7 @@ function TooltipIcon({ content }: { content: string }) {
contentProps={{ maxW: "260px", fontSize: "xs", px: 3, py: 2 }}
>
<IconButton
aria-label="Bilgi"
aria-label={ariaLabel}
variant="ghost"
size="2xs"
colorPalette="gray"
@@ -498,9 +521,14 @@ function ReasonList({
function ProbabilitySplit({
modelProb,
impliedProb,
labels,
}: {
modelProb: number;
impliedProb: number;
labels: {
model: string;
market: string;
};
}) {
const trackBg = useColorModeValue("gray.100", "gray.700");
if (!impliedProb || impliedProb <= 0) return null;
@@ -508,10 +536,10 @@ function ProbabilitySplit({
<VStack align="stretch" gap={2}>
<Flex justify="space-between">
<Text fontSize="xs" color="blue.600" fontWeight="semibold">
Model {formatProbability(modelProb, 0)}
{labels.model} {formatProbability(modelProb, 0)}
</Text>
<Text fontSize="xs" color="orange.500" fontWeight="semibold">
Piyasa {formatProbability(impliedProb, 0)}
{labels.market} {formatProbability(impliedProb, 0)}
</Text>
</Flex>
<Box position="relative">
@@ -538,6 +566,7 @@ function PickCard({
palette,
marketLabels,
labels,
ui,
}: {
pick: MatchPickDto;
stakeFallback?: number;
@@ -545,12 +574,19 @@ function PickCard({
resolveReason: (reason: string) => string;
palette: string;
marketLabels?: Record<string, string>;
ui?: PredictionUiMessages;
labels: {
confidence: string;
odds: string;
recommendedStake: string;
playScore: string;
playability: string;
confidenceInterval: string;
confidenceBand: string;
confidenceIntervalWarning: string;
theoreticalEdgeInline: string;
modelProbability: string;
marketProbability: string;
};
}) {
const bg = useColorModeValue(`${palette}.50`, `${palette}.950`);
@@ -591,16 +627,16 @@ function PickCard({
colorPalette={getSignalTierPalette(pick.signal_tier)}
variant="subtle"
>
{getSignalTierLabel(pick.signal_tier)}
{getSignalTierLabel(pick.signal_tier, ui)}
</Badge>
<Badge colorPalette={confidenceBandPalette} variant="subtle">
{getConfidenceBandLabel(pick.confidence_interval?.band)}
{getConfidenceBandLabel(pick.confidence_interval?.band, ui)}
</Badge>
<Badge
colorPalette={getEdgePalette(pick.ev_edge)}
variant="subtle"
>
Teorik avantaj {formatEdgeSignal(pick.ev_edge)}
{labels.theoreticalEdgeInline} {formatEdgeSignal(pick.ev_edge)}
</Badge>
</HStack>
</VStack>
@@ -619,12 +655,12 @@ function PickCard({
value={formatSignalScore(pick.play_score)}
/>
<MetricTile
label="Guven Araligi"
label={labels.confidenceInterval}
value={formatInterval(pick.confidence_interval)}
/>
<MetricTile
label="Band"
value={getConfidenceBandLabel(pick.confidence_interval?.band)}
label={labels.confidenceBand}
value={getConfidenceBandLabel(pick.confidence_interval?.band, ui)}
accent={`${confidenceBandPalette}.500`}
/>
</SimpleGrid>
@@ -632,6 +668,10 @@ function PickCard({
<ProbabilitySplit
modelProb={pick.probability}
impliedProb={pick.implied_prob}
labels={{
model: labels.modelProbability,
market: labels.marketProbability,
}}
/>
<Box>
<HStack justify="space-between" mb={1.5}>
@@ -661,8 +701,7 @@ function PickCard({
borderColor={intervalWarningBorder}
>
<Text fontSize="sm" color="fg.muted">
Guven araligi genis. Sinyal olsa bile tek basina oynanmasi
onerilmez.
{labels.confidenceIntervalWarning}
</Text>
</Box>
) : null}
@@ -676,11 +715,13 @@ function SummaryTable({
marketLabels,
title,
info,
ui,
}: {
items: MatchBetSummaryItemDto[];
marketLabels?: Record<string, string>;
title: string;
info: string;
ui?: PredictionUiMessages;
}) {
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
@@ -728,7 +769,7 @@ function SummaryTable({
colorPalette={getSignalTierPalette(item.signal_tier)}
variant="subtle"
>
{getSignalTierLabel(item.signal_tier)}
{getSignalTierLabel(item.signal_tier, ui)}
</Badge>
<Text fontWeight="semibold">
{getMarketLabel(item.market, marketLabels)}
@@ -753,7 +794,7 @@ function SummaryTable({
)}
variant="subtle"
>
{getConfidenceBandLabel(item.confidence_interval?.band)}
{getConfidenceBandLabel(item.confidence_interval?.band, ui)}
</Badge>
<Badge variant="surface">
{formatUnits(item.stake_units)}
@@ -812,7 +853,6 @@ function SummaryTable({
<Badge variant="surface">{formatUnits(item.stake_units)}</Badge>
</HStack>
</Flex> */}
</VStack>
</Card.Body>
</Card.Root>
@@ -825,12 +865,14 @@ function MarketBoardSection({
marketLabels,
title,
info,
ui,
}: {
marketBoard?: Record<string, MarketBoardEntryDto>;
betSummary?: MatchBetSummaryItemDto[];
marketLabels?: Record<string, string>;
title: string;
info: string;
ui?: PredictionUiMessages;
}) {
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
@@ -881,7 +923,9 @@ function MarketBoardSection({
colorPalette={summary.playable ? "green" : "gray"}
variant="subtle"
>
{summary.playable ? "Oynanabilir" : "Riskli"}
{summary.playable
? getUiText(ui, "playable", "Oynanabilir")
: getUiText(ui, "risky", "Riskli")}
</Badge>
) : null}
{summary?.signal_tier ? (
@@ -891,7 +935,7 @@ function MarketBoardSection({
)}
variant="subtle"
>
{getSignalTierLabel(summary.signal_tier)}
{getSignalTierLabel(summary.signal_tier, ui)}
</Badge>
) : null}
{summary?.bet_grade ? (
@@ -913,12 +957,16 @@ function MarketBoardSection({
</Flex>
<SimpleGrid columns={3} gap={2} mb={3}>
<MetricTile
label="Tutma Olasiligi"
label={getUiText(ui, "hit-probability", "Tutma Olasılığı")}
value={formatPercent(entry.confidence, 0)}
accent="green.500"
/>
<MetricTile
label="Kalibre Guven"
label={getUiText(
ui,
"calibrated-confidence",
"Kalibre Güven",
)}
value={
summary
? formatPercent(summary.calibrated_confidence, 0)
@@ -933,7 +981,8 @@ function MarketBoardSection({
</SimpleGrid>
{interval ? (
<Text fontSize="xs" color="fg.muted" mb={3}>
Guven araligi: {formatInterval(interval)}
{getUiText(ui, "confidence-interval", "Güven Aralığı")}:{" "}
{formatInterval(interval)}
</Text>
) : null}
<VStack align="stretch" gap={2.5}>
@@ -951,7 +1000,7 @@ function MarketBoardSection({
value={probability * 100}
color={
entry.pick === outcome ||
entry.pick?.toUpperCase() === outcome.toUpperCase()
entry.pick?.toUpperCase() === outcome.toUpperCase()
? "green.400"
: "blue.400"
}
@@ -973,9 +1022,11 @@ function MarketBoardSection({
function ScoreCard({
prediction,
sport,
ui,
}: {
prediction: MatchPredictionDto;
sport: SportType;
ui?: PredictionUiMessages;
}) {
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
@@ -987,24 +1038,52 @@ function ScoreCard({
<Card.Body gap={4}>
<SectionTitle
icon={LuTarget}
title={isBasketball ? "Sayi Senaryosu" : "Skor Senaryosu"}
title={
isBasketball
? getUiText(ui, "score-scenario-basketball", "Sayı Senaryosu")
: getUiText(ui, "score-scenario-football", "Skor Senaryosu")
}
info={
isBasketball
? "Beklenen sayi dagilimi ve en olasi mac senaryolari."
: "Beklenen skor ve en olasi senaryolar."
? getUiText(
ui,
"score-scenario-info-basketball",
"Beklenen sayı dağılımı ve en olası maç senaryoları.",
)
: getUiText(
ui,
"score-scenario-info-football",
"Beklenen skor ve en olası senaryolar.",
)
}
/>
<SimpleGrid columns={{ base: 1, md: 3 }} gap={3}>
<MetricTile
label={isBasketball ? "Mac Sonu Sayi" : "Mac Sonu"}
label={
isBasketball
? getUiText(ui, "full-time-basketball", "Maç Sonu Sayı")
: getUiText(ui, "full-time-football", "Maç Sonu")
}
value={prediction.score_prediction.ft}
/>
<MetricTile
label={isBasketball ? "Ilk Yari Sayi" : "Ilk Yari"}
label={
isBasketball
? getUiText(ui, "half-time-basketball", "İlk Yarı Sayı")
: getUiText(ui, "half-time-football", "İlk Yarı")
}
value={prediction.score_prediction.ht}
/>
<MetricTile
label={isBasketball ? "Beklenen Toplam Sayi" : "Toplam xG"}
label={
isBasketball
? getUiText(
ui,
"expected-total-basketball",
"Beklenen Toplam Sayı",
)
: getUiText(ui, "expected-total-football", "Toplam xG")
}
value={prediction.score_prediction.xg_total.toFixed(2)}
/>
</SimpleGrid>
@@ -1053,6 +1132,16 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
const pageBg = useColorModeValue("gray.50", "gray.900");
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.200", "gray.700");
const liveBg = useColorModeValue("red.50", "red.950");
const liveBorderColor = useColorModeValue("red.300", "red.800");
const warningBg = useColorModeValue("yellow.50", "yellow.950");
const warningBorderColor = useColorModeValue("yellow.300", "yellow.800");
const orangeBg = useColorModeValue("orange.50", "orange.950");
const orangeBorderColor = useColorModeValue("orange.200", "orange.800");
const greenBg = useColorModeValue("green.50", "green.950");
const greenBorderColor = useColorModeValue("green.200", "green.800");
const statCardBg = useColorModeValue("gray.50", "whiteAlpha.50");
const trackBgColor = useColorModeValue("gray.100", "gray.700");
const riskPalette = getRiskPalette(prediction.risk.level);
const qualityPalette = getQualityPalette(prediction.data_quality.label);
const recommendedPick = prediction.main_pick;
@@ -1067,7 +1156,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{
key: "team",
icon: LuGauge,
label: isBasketball ? "Takim Formu" : "Takim Gucu",
label: isBasketball
? uiText("engine-team-basketball", "Takım Formu")
: uiText("engine-team-football", "Takım Gücü"),
value: prediction.engine_breakdown.team,
color: "blue.400",
detail: engineDetail?.team,
@@ -1075,7 +1166,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{
key: "player",
icon: LuSparkles,
label: isBasketball ? "Kadro Etkisi" : "Oyuncu Etkisi",
label: isBasketball
? uiText("engine-player-basketball", "Kadro Etkisi")
: uiText("engine-player-football", "Oyuncu Etkisi"),
value: prediction.engine_breakdown.player,
color: "green.400",
detail: engineDetail?.player,
@@ -1083,14 +1176,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{
key: "odds",
icon: LuTrendingUp,
label: "Oran Analizi",
value: prediction.engine_breakdown.odds,
color: "orange.400",
},
{
key: "odds",
icon: LuTrendingUp,
label: "Oran Analizi",
label: uiText("engine-odds", "Oran Analizi"),
value: prediction.engine_breakdown.odds,
color: "orange.400",
detail: engineDetail?.odds,
@@ -1098,7 +1184,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{
key: "referee",
icon: LuShieldAlert,
label: isBasketball ? "Yardimci Sinyaller" : "Hakem Etkisi",
label: isBasketball
? uiText("engine-referee-basketball", "Yardımcı Sinyaller")
: uiText("engine-referee-football", "Hakem Etkisi"),
value: prediction.engine_breakdown.referee,
color: "purple.400",
detail: engineDetail?.referee,
@@ -1110,33 +1198,49 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
const isLive = Boolean(prediction.match_info?.is_live);
const isStale = Boolean(prediction.prediction_freshness?.is_stale_for_live);
const contradictions = prediction.match_commentary?.contradictions || [];
const pickCardLabels = {
confidence: uiText("confidence-label", "Güven"),
odds: uiText("odds-label", "Oran"),
recommendedStake: uiText("stake-label-short", "Stake"),
playScore: uiText("play-score-label", "Model Sinyali"),
playability: uiText("playability-label", "Model sinyali"),
confidenceInterval: uiText("confidence-interval", "Güven Aralığı"),
confidenceBand: uiText("confidence-band", "Band"),
confidenceIntervalWarning: uiText(
"confidence-interval-warning",
"Güven aralığı geniş. Sinyal olsa bile tek başına oynanması önerilmez.",
),
theoreticalEdgeInline: uiText("theoretical-edge-inline", "Teorik avantaj"),
modelProbability: uiText("model-probability-short", "Model"),
marketProbability: uiText("market-probability-short", "Piyasa"),
};
return (
<VStack align="stretch" gap={5}>
{isLive ? (
<Box
p={3}
bg={useColorModeValue("red.50", "red.950")}
bg={liveBg}
borderWidth="1px"
borderColor={useColorModeValue("red.300", "red.800")}
borderColor={liveBorderColor}
borderRadius="xl"
>
<HStack justify="space-between" align="center">
<HStack gap={2}>
<Icon as={LuFlame} color="red.500" />
<Text fontWeight="bold" color="red.600">
🔴 CANLI
🔴 {uiText("live", "CANLI")}
</Text>
{liveScoreHome != null && liveScoreAway != null ? (
<Text fontWeight="semibold">
{prediction.match_info.home_team} {liveScoreHome} - {liveScoreAway}{" "}
{prediction.match_info.away_team}
{prediction.match_info.home_team} {liveScoreHome} -{" "}
{liveScoreAway} {prediction.match_info.away_team}
</Text>
) : null}
</HStack>
{isStale ? (
<Badge colorPalette="orange" variant="solid">
Maç öncesi tahmin
{uiText("pre-match-prediction", "Maç öncesi tahmin")}
</Badge>
) : null}
</HStack>
@@ -1146,15 +1250,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{contradictions.length ? (
<Box
p={3}
bg={useColorModeValue("yellow.50", "yellow.950")}
bg={warningBg}
borderWidth="1px"
borderColor={useColorModeValue("yellow.300", "yellow.800")}
borderColor={warningBorderColor}
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>
<Text fontWeight="semibold">
{uiText("prediction-contradictions", "Tahmin Çelişkileri")}
</Text>
{contradictions.map((text, idx) => (
<Text key={idx} fontSize="sm" color="fg.muted">
{text}
@@ -1169,17 +1275,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<Card.Body gap={5}>
<SectionTitle
icon={LuBrain}
title={uiText("summary-title", "Tahmin Ozeti")}
title={uiText("summary-title", "Tahmin Özeti")}
info={uiText(
"summary-info",
"Model sinyallerini ve belirsizlikleri sade sekilde gosterir.",
"Model sinyallerini ve belirsizlikleri sade şekilde gösterir.",
)}
/>
<Box
p={3}
bg={useColorModeValue("orange.50", "orange.950")}
bg={orangeBg}
borderWidth="1px"
borderColor={useColorModeValue("orange.200", "orange.800")}
borderColor={orangeBorderColor}
borderRadius="xl"
>
<HStack align="start" gap={2}>
@@ -1190,9 +1296,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
mt={0.5}
/>
<Text fontSize="sm" color="fg.muted" lineHeight="tall">
Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi
değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi
nedeniyle yanılabilir.
{uiText(
"model-signal-disclaimer",
"Bu bir model sinyalidir; kesin sonuç, garanti veya tutma yüzdesi değildir. Sinyal puanı maç içi varyans, kadro ve veri kalitesi nedeniyle yanılabilir.",
)}
</Text>
</HStack>
</Box>
@@ -1201,9 +1308,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<Grid templateColumns={{ base: "1fr", xl: "1.4fr 1fr" }} gap={4}>
<Box
p={4}
bg={useColorModeValue("green.50", "green.950")}
bg={greenBg}
borderWidth="1px"
borderColor={useColorModeValue("green.200", "green.800")}
borderColor={greenBorderColor}
borderRadius="2xl"
>
<HStack justify="space-between" align="start" mb={4}>
@@ -1220,12 +1327,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
</Text>
<Text fontSize="sm" color="fg.muted">
{getMarketLabel(recommendedPick.market, marketLabels)}{" "}
{uiText("best-market-copy", "marketinde en guclu secim.")}
{uiText("best-market-copy", "marketinde en güçlü seçim.")}
</Text>
<HStack gap={2} flexWrap="wrap">
<Badge colorPalette={mainBandPalette} variant="subtle">
{getConfidenceBandLabel(
prediction.bet_advice.confidence_band,
ui,
)}
</Badge>
{recommendedPick.confidence_interval ? (
@@ -1239,7 +1347,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
</HStack>
<SimpleGrid columns={{ base: 2, md: 4 }} gap={3}>
<MetricTile
label={uiText("confidence-label", "Guven")}
label={uiText("confidence-label", "Güven")}
value={formatPercent(
recommendedPick.calibrated_confidence,
0,
@@ -1250,7 +1358,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
value={formatOdds(recommendedPick.odds)}
/>
<MetricTile
label="Guven Araligi"
label={uiText("confidence-interval", "Güven Aralığı")}
value={formatInterval(recommendedPick.confidence_interval)}
/>
<MetricTile
@@ -1258,19 +1366,19 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
value={formatEdgeSignal(recommendedPick.ev_edge)}
helper={uiText(
"edge-info",
"Model olasiligi ile piyasa olasiligi arasindaki teorik farktir; tutma garantisi veya kesin kazanc beklentisi degildir.",
"Model olasılığı ile piyasa olasılığı arasındaki teorik farktır; tutma garantisi veya kesin kazanç beklentisi değildir.",
)}
accent={`${getEdgePalette(recommendedPick.ev_edge)}.500`}
/>
<MetricTile
label={uiText("stake-label", "Onerilen Miktar (Stake)")}
label={uiText("stake-label", "Önerilen Miktar (Stake)")}
value={formatUnits(
recommendedPick.stake_units ||
prediction.bet_advice.suggested_stake_units,
prediction.bet_advice.suggested_stake_units,
)}
helper={uiText(
"stake-info",
"Stake, bu bahis icin onerilen bahis birimidir. 2.0u demek, kendi bankroll planinizdaki 2 birimlik bahis anlamina gelir.",
"Stake, bu bahis için önerilen bahis birimidir. 2.0u, kendi bankroll planınızdaki 2 birimlik bahis anlamına gelir.",
)}
/>
</SimpleGrid>
@@ -1284,7 +1392,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
borderRadius="2xl"
>
<Text fontSize="sm" fontWeight="semibold" mb={3}>
{uiText("quick-read", "Hizli yorum")}
{uiText("quick-read", "Hızlı yorum")}
</Text>
<ReasonList
items={[
@@ -1299,21 +1407,28 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<SimpleGrid columns={{ base: 1, md: 2, xl: 4 }} gap={3}>
<MetricTile
label="Veri Kalitesi"
label={uiText("data-quality", "Veri Kalitesi")}
value={formatPercent(prediction.data_quality.score * 100, 0)}
helper="Kadro, oran ve mac verisinin ne kadar guvenilir oldugu."
helper={uiText(
"data-quality-info",
"Kadro, oran ve maç verisinin ne kadar güvenilir olduğu.",
)}
accent={`${qualityPalette}.500`}
/>
<MetricTile
label={t("risk-level")}
value={`${prediction.risk.level} (${prediction.risk.score}/100)`}
helper="Surpriz ihtimali ve belirsizlik seviyesi."
helper={uiText(
"risk-info",
"Sürpriz ihtimali ve belirsizlik seviyesi.",
)}
accent={`${riskPalette}.500`}
/>
<MetricTile
label={uiText("lineup-source", "Lineup Kaynagi")}
label={uiText("lineup-source", "Kadronun Kaynağı")}
value={getLineupSourceLabel(
prediction.data_quality.lineup_source,
ui,
)}
/>
<MetricTile
@@ -1323,12 +1438,12 @@ 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")}
bg={orangeBg}
borderWidth="1px"
borderColor={useColorModeValue("orange.200", "orange.800")}
borderColor={orangeBorderColor}
borderRadius="2xl"
>
<HStack align="start" gap={3}>
@@ -1339,12 +1454,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
mt={0.5}
/>
<VStack align="start" gap={1.5}>
<Text fontWeight="semibold">Risk Yorumu</Text>
<Text fontWeight="semibold">
{uiText("risk-commentary", "Risk Yorumu")}
</Text>
<Text fontSize="sm" color="fg.muted">
{prediction.risk.surprise_comment ||
(prediction.risk.surprise_type
? `${resolveReason(prediction.risk.surprise_type)}`
: "Model bu maçta ekstra dikkat istiyor.")}
: uiText(
"risk-default-comment",
"Model bu maçta ekstra dikkat istiyor.",
))}
</Text>
{prediction.risk.surprise_score !== undefined ? (
<Text
@@ -1352,7 +1472,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
fontWeight="semibold"
color="orange.600"
>
Sürpriz skoru:{" "}
{uiText("surprise-score", "Sürpriz skoru")}:{" "}
{formatPercent(prediction.risk.surprise_score, 0)}
</Text>
) : null}
@@ -1361,7 +1481,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{prediction.risk.surprise_breakdown.map((entry) => (
<HStack key={entry.code} gap={2}>
<Badge
colorPalette={entry.points >= 15 ? "red" : entry.points >= 8 ? "orange" : "yellow"}
colorPalette={
entry.points >= 15
? "red"
: entry.points >= 8
? "orange"
: "yellow"
}
variant="subtle"
>
+{entry.points.toFixed(0)}
@@ -1395,7 +1521,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
title={t("engine-breakdown-title")}
info={uiText(
"engine-info",
"Tahmini en cok hangi bilesenlerin etkiledigini gosterir.",
"Tahmini en çok hangi bileşenlerin etkilediğini gösterir.",
)}
/>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={4}>
@@ -1403,7 +1529,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<Box
key={item.key}
p={4}
bg={useColorModeValue("gray.50", "whiteAlpha.50")}
bg={statCardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="xl"
@@ -1421,7 +1547,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
colorPalette={getEngineLabelPalette(item.detail.label)}
variant="subtle"
>
{getEngineLabelText(item.detail.label)}
{getEngineLabelText(item.detail.label, ui)}
</Badge>
) : null}
<Text fontSize="sm" fontWeight="bold">
@@ -1432,9 +1558,8 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<Bar
value={Math.min(item.value, 100)}
color={item.color}
trackBg={useColorModeValue("gray.100", "gray.700")}
trackBg={trackBgColor}
/>
<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}
@@ -1454,13 +1579,8 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
palette="green"
stakeFallback={prediction.bet_advice.suggested_stake_units}
marketLabels={marketLabels}
labels={{
confidence: uiText("confidence-label", "Guven"),
odds: uiText("odds-label", "Oran"),
recommendedStake: uiText("stake-label-short", "Stake"),
playScore: uiText("play-score-label", "Model Sinyali"),
playability: uiText("playability-label", "Model sinyali"),
}}
labels={pickCardLabels}
ui={ui}
/>
) : null}
@@ -1472,7 +1592,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
title={uiText("alternative-markets", "Alternatif Marketler")}
info={uiText(
"alternative-markets-info",
"Ana tahmin disindaki secenekler.",
"Ana tahmin dışındaki seçenekler.",
)}
/>
<SimpleGrid columns={{ base: 1, xl: 2 }} gap={4}>
@@ -1483,18 +1603,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
title={
pick.playable
? uiText("alternative", "Alternatif")
: uiText("pass-market", "PASS market")
: uiText("pass-market", "Elenen Market")
}
resolveReason={resolveReason}
palette={pick.ev_edge > 0 ? "blue" : "orange"}
marketLabels={marketLabels}
labels={{
confidence: uiText("confidence-label", "Guven"),
odds: uiText("odds-label", "Oran"),
recommendedStake: uiText("stake-label-short", "Stake"),
playScore: uiText("play-score-label", "Model Sinyali"),
playability: uiText("playability-label", "Model sinyali"),
}}
labels={pickCardLabels}
ui={ui}
/>
))}
</SimpleGrid>
@@ -1505,19 +1620,24 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<SummaryTable
items={prediction.bet_summary || []}
marketLabels={marketLabels}
title={uiText("all-markets-title", "Tum Marketler")}
title={uiText("all-markets-title", "Tüm Marketler")}
info={uiText(
"all-markets-info",
"Butun secenekleri tek tabloda karsilastir.",
"Bütün seçenekleri tek tabloda karşılaştırır.",
)}
ui={ui}
/>
{prediction.match_commentary?.headline || prediction.match_commentary?.summary ? (
{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"
title={uiText("match-commentary-title", "Maç Yorumu")}
info={uiText(
"match-commentary-info",
"Modelin maç hakkındaki insan okunabilir özeti.",
)}
/>
{prediction.match_commentary.headline ? (
<Text fontSize="md" fontWeight="bold">
@@ -1541,7 +1661,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
</Card.Body>
</Card.Root>
) : null}
<ScoreCard prediction={prediction} sport={sport} />
<ScoreCard prediction={prediction} sport={sport} ui={ui} />
<MarketBoardSection
marketBoard={prediction.market_board}
betSummary={prediction.bet_summary || []}
@@ -1549,8 +1669,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
title={t("market-board")}
info={uiText(
"market-board-info",
"Modelin her markette gordugu olasilik dagilimi.",
"Modelin her markette gördüğü olasılık dağılımı.",
)}
ui={ui}
/>
{prediction.v27_engine ? (
@@ -1562,7 +1683,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<SectionTitle
icon={LuSparkles}
title={t("bet-advice")}
info={uiText("bet-advice-info", "Modelin nihai aksiyon onerisi.")}
info={uiText("bet-advice-info", "Modelin nihai aksiyon önerisi.")}
/>
<HStack
justify="space-between"
@@ -1579,7 +1700,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
px={3}
py={1}
>
{prediction.bet_advice.playable ? "OYNA" : "OYNAMA"}
{prediction.bet_advice.playable
? uiText("bet-advice-play", "OYNA")
: uiText("bet-advice-pass", "OYNAMA")}
</Badge>
<Badge
colorPalette={mainBandPalette}
@@ -1589,7 +1712,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
px={3}
py={1}
>
{getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
{getConfidenceBandLabel(
prediction.bet_advice.confidence_band,
ui,
)}
</Badge>
<Badge
colorPalette={getSignalTierPalette(
@@ -1601,14 +1727,14 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
px={3}
py={1}
>
{getSignalTierLabel(prediction.bet_advice.signal_tier)}
{getSignalTierLabel(prediction.bet_advice.signal_tier, ui)}
</Badge>
<Text color="fg.muted">
{resolveReason(prediction.bet_advice.reason)}
</Text>
</HStack>
<Badge variant="surface" fontSize="sm" px={3} py={1}>
{uiText("recommended-stake-inline", "Onerilen miktar")}:{" "}
{uiText("recommended-stake-inline", "Önerilen miktar")}:{" "}
{formatUnits(prediction.bet_advice.suggested_stake_units)}
</Badge>
</HStack>
@@ -1616,7 +1742,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
<SectionTitle
icon={LuBrain}
title={t("reasoning")}
info="Modelin bu maci neden bu sekilde okudugunun ust seviye ozeti."
info={uiText(
"reasoning-info",
"Modelin bu maçı neden bu şekilde okuduğunun üst seviye özeti.",
)}
/>
<ReasonList
items={prediction.reasoning_factors}