diff --git a/messages/en.json b/messages/en.json
index 47b0328..c9541e7 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -235,14 +235,21 @@
"HTFT": "Half Time / Full Time",
"HT/FT": "Half Time / Full Time",
"OE": "Odd / Even",
- "HT_OU05": "First Half 0.5 Goals"
+ "HT_OU05": "First Half 0.5 Goals",
+ "HT_OU15": "First Half 1.5 Goals",
+ "CARDS": "Cards 4.5",
+ "HCAP": "Handicap Result"
},
"ui": {
"summary-title": "Prediction Summary",
"summary-info": "Shows model signals and uncertainty in a conservative summary.",
+ "model-signal-disclaimer": "This is a model signal; it is not a guaranteed result, guarantee, or hit-rate promise. Signal score can be wrong because of in-match variance, lineups, and data quality.",
"main-recommendation": "Highlighted Signal",
"best-market-copy": "is the strongest option in this market.",
"confidence-label": "Confidence",
+ "confidence-interval": "Confidence Interval",
+ "confidence-interval-warning": "The confidence interval is wide. Even with a signal, it is not recommended as a standalone pick.",
+ "confidence-band": "Band",
"odds-label": "Odds",
"edge-label": "Theoretical Edge",
"edge-info": "The theoretical gap between model probability and market probability; it is not a guarantee or a certain profit expectation.",
@@ -253,8 +260,22 @@
"playability-label": "Model signal",
"quick-read": "Quick read",
"lineup-source": "Lineup Source",
+ "lineup-confirmed-live": "Confirmed starting XI",
+ "lineup-probable-xi": "Probable starting XI",
+ "unknown": "Unknown",
"model-label": "Model",
"engine-info": "Shows which components influence the prediction the most.",
+ "engine-team-football": "Team Strength",
+ "engine-team-basketball": "Team Form",
+ "engine-player-football": "Player Impact",
+ "engine-player-basketball": "Lineup Impact",
+ "engine-odds": "Odds Analysis",
+ "engine-referee-football": "Referee Impact",
+ "engine-referee-basketball": "Supporting Signals",
+ "engine-label-high": "High",
+ "engine-label-medium": "Medium",
+ "engine-label-low": "Low",
+ "engine-label-very-low": "Very Low",
"best-single-pick": "Strongest Signal",
"alternative-markets": "Alternative Markets",
"alternative-markets-info": "Options outside the main recommendation.",
@@ -264,7 +285,48 @@
"all-markets-info": "Compares every option in a single table.",
"market-board-info": "The probability distribution the model sees for each market.",
"bet-advice-info": "The model's final action recommendation.",
- "recommended-stake-inline": "Suggested size"
+ "recommended-stake-inline": "Suggested size",
+ "model-probability-short": "Model",
+ "market-probability-short": "Market",
+ "theoretical-edge-inline": "Theoretical edge",
+ "playable": "Playable",
+ "risky": "Risky",
+ "hit-probability": "Hit Probability",
+ "calibrated-confidence": "Calibrated Confidence",
+ "score-scenario-football": "Score Scenario",
+ "score-scenario-basketball": "Points Scenario",
+ "score-scenario-info-football": "Expected score and the most likely scenarios.",
+ "score-scenario-info-basketball": "Expected points distribution and the most likely match scenarios.",
+ "full-time-football": "Full Time",
+ "full-time-basketball": "Full-Time Points",
+ "half-time-football": "Half Time",
+ "half-time-basketball": "Half-Time Points",
+ "expected-total-football": "Total xG",
+ "expected-total-basketball": "Expected Total Points",
+ "live": "LIVE",
+ "pre-match-prediction": "Pre-match prediction",
+ "prediction-contradictions": "Prediction Contradictions",
+ "data-quality": "Data Quality",
+ "data-quality-info": "How reliable the lineup, odds, and match data are.",
+ "risk-info": "Upset probability and uncertainty level.",
+ "risk-commentary": "Risk Commentary",
+ "risk-default-comment": "The model asks for extra caution on this match.",
+ "surprise-score": "Upset score",
+ "match-commentary-title": "Match Commentary",
+ "match-commentary-info": "The model's human-readable summary of the match.",
+ "reasoning-info": "High-level summary of why the model reads this match this way.",
+ "bet-advice-play": "PLAY",
+ "bet-advice-pass": "PASS",
+ "signal-tier-core": "Core",
+ "signal-tier-value": "Value",
+ "signal-tier-lean": "Lean",
+ "signal-tier-longshot": "Longshot",
+ "signal-tier-pass": "Pass",
+ "confidence-high": "High",
+ "confidence-medium": "Medium",
+ "confidence-low": "Low",
+ "confidence-unknown": "Unknown",
+ "info": "Info"
}
},
"coupons": {
@@ -324,6 +386,9 @@
"candidate-pool-help": "Only football matches that have not started yet are listed here. Finished and live matches are excluded.",
"candidate-pool-subtitle": "Source: live_matches table • sport: football • status: not started",
"match-count-suffix": "matches",
+ "match-count-label": "Coupon Match Count",
+ "match-count-help": "How many matches should the AI coupon include? You can choose between 2 and 15. If you do not select any matches, the full bulletin is scanned.",
+ "match-count-auto": "Full bulletin ({count} matches)",
"upcoming-badge": "Upcoming",
"upcoming-reference": "Upcoming pool",
"finished-badge": "Finished",
@@ -419,7 +484,8 @@
"countries": "Countries",
"leagues": "Leagues",
"countries-leagues": "Countries & Leagues",
- "search-at-least-2": "Type at least 2 characters to search teams."
+ "search-at-least-2": "Type at least 2 characters to search teams.",
+ "all": "All"
},
"h2h": {
"title": "Head to Head",
@@ -475,7 +541,9 @@
"analytics": "Analytics Overview",
"user-management": "User Management",
"users": "Users",
+ "premium-users": "Premium Users",
"settings": "Settings",
+ "subscription": "Subscription",
"usage-limits": "Usage Limits",
"total-users": "Total Users",
"active-users": "Active Users",
@@ -495,7 +563,25 @@
"user-email": "Email",
"user-role": "Role",
"user-status": "Status",
- "no-users": "No users found."
+ "no-users": "No users found.",
+ "restricted": "Restricted",
+ "admin-access-required": "Admin access required",
+ "admin-access-description": "This area is only available to superadmin accounts.",
+ "search-users-placeholder": "Search by email or name...",
+ "all-roles": "View All Roles",
+ "standard-user": "Standard User",
+ "superadmin": "System Administrator (Admin)",
+ "all-plans": "View All Plans",
+ "plan-free": "Free",
+ "plan-plus": "Plus Plan",
+ "plan-premium": "Premium Plan",
+ "plan-past-due": "Past Due",
+ "plan-cancelled": "Cancelled",
+ "edit-user-title": "Edit User: {email}",
+ "user-role-field": "User Role",
+ "subscription-plan-field": "Subscription Plan",
+ "subscription-end-date": "Subscription End Date (Optional)",
+ "account-active-question": "Is the account active?"
},
"common": {
"limits": {
@@ -689,4 +775,4 @@
"remaining": "remaining"
}
}
-}
\ No newline at end of file
+}
diff --git a/messages/tr.json b/messages/tr.json
index 4db8b94..7f90dfb 100644
--- a/messages/tr.json
+++ b/messages/tr.json
@@ -235,14 +235,21 @@
"HTFT": "İlk Yarı / Maç Sonu",
"HT/FT": "İlk Yarı / Maç Sonu",
"OE": "Tek / Çift",
- "HT_OU05": "İlk Yarı 0.5 Gol"
+ "HT_OU05": "İlk Yarı 0.5 Gol",
+ "HT_OU15": "İlk Yarı 1.5 Gol",
+ "CARDS": "Kartlar 4.5",
+ "HCAP": "Handikap Sonucu"
},
"ui": {
"summary-title": "Tahmin Özeti",
"summary-info": "Model sinyallerini ve belirsizlikleri sade şekilde gösterir.",
+ "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.",
"main-recommendation": "Öne Çıkan Sinyal",
"best-market-copy": "marketinde en güçlü seçim.",
"confidence-label": "Güven",
+ "confidence-interval": "Güven Aralığı",
+ "confidence-interval-warning": "Güven aralığı geniş. Sinyal olsa bile tek başına oynanması önerilmez.",
+ "confidence-band": "Band",
"odds-label": "Oran",
"edge-label": "Teorik Avantaj",
"edge-info": "Model olasılığı ile piyasa olasılığı arasındaki teorik farktır; tutma garantisi veya kesin kazanç beklentisi değildir.",
@@ -253,8 +260,22 @@
"playability-label": "Model sinyali",
"quick-read": "Hızlı yorum",
"lineup-source": "Kadronun Kaynağı",
+ "lineup-confirmed-live": "Onaylı ilk 11",
+ "lineup-probable-xi": "Muhtemel ilk 11",
+ "unknown": "Bilinmiyor",
"model-label": "Model",
"engine-info": "Tahmini en çok hangi bileşenlerin etkilediğini gösterir.",
+ "engine-team-football": "Takım Gücü",
+ "engine-team-basketball": "Takım Formu",
+ "engine-player-football": "Oyuncu Etkisi",
+ "engine-player-basketball": "Kadro Etkisi",
+ "engine-odds": "Oran Analizi",
+ "engine-referee-football": "Hakem Etkisi",
+ "engine-referee-basketball": "Yardımcı Sinyaller",
+ "engine-label-high": "Yüksek",
+ "engine-label-medium": "Orta",
+ "engine-label-low": "Düşük",
+ "engine-label-very-low": "Çok Düşük",
"best-single-pick": "En Güçlü Sinyal",
"alternative-markets": "Alternatif Marketler",
"alternative-markets-info": "Ana tahmin dışındaki seçenekler.",
@@ -264,7 +285,48 @@
"all-markets-info": "Bütün seçenekleri tek tabloda karşılaştırır.",
"market-board-info": "Modelin her markette gördüğü olasılık dağılımı.",
"bet-advice-info": "Modelin nihai aksiyon önerisi.",
- "recommended-stake-inline": "Önerilen miktar"
+ "recommended-stake-inline": "Önerilen miktar",
+ "model-probability-short": "Model",
+ "market-probability-short": "Piyasa",
+ "theoretical-edge-inline": "Teorik avantaj",
+ "playable": "Oynanabilir",
+ "risky": "Riskli",
+ "hit-probability": "Tutma Olasılığı",
+ "calibrated-confidence": "Kalibre Güven",
+ "score-scenario-football": "Skor Senaryosu",
+ "score-scenario-basketball": "Sayı Senaryosu",
+ "score-scenario-info-football": "Beklenen skor ve en olası senaryolar.",
+ "score-scenario-info-basketball": "Beklenen sayı dağılımı ve en olası maç senaryoları.",
+ "full-time-football": "Maç Sonu",
+ "full-time-basketball": "Maç Sonu Sayı",
+ "half-time-football": "İlk Yarı",
+ "half-time-basketball": "İlk Yarı Sayı",
+ "expected-total-football": "Toplam xG",
+ "expected-total-basketball": "Beklenen Toplam Sayı",
+ "live": "CANLI",
+ "pre-match-prediction": "Maç öncesi tahmin",
+ "prediction-contradictions": "Tahmin Çelişkileri",
+ "data-quality": "Veri Kalitesi",
+ "data-quality-info": "Kadro, oran ve maç verisinin ne kadar güvenilir olduğu.",
+ "risk-info": "Sürpriz ihtimali ve belirsizlik seviyesi.",
+ "risk-commentary": "Risk Yorumu",
+ "risk-default-comment": "Model bu maçta ekstra dikkat istiyor.",
+ "surprise-score": "Sürpriz skoru",
+ "match-commentary-title": "Maç Yorumu",
+ "match-commentary-info": "Modelin maç hakkındaki insan okunabilir özeti.",
+ "reasoning-info": "Modelin bu maçı neden bu şekilde okuduğunun üst seviye özeti.",
+ "bet-advice-play": "OYNA",
+ "bet-advice-pass": "OYNAMA",
+ "signal-tier-core": "Çekirdek",
+ "signal-tier-value": "Değer",
+ "signal-tier-lean": "Yorum",
+ "signal-tier-longshot": "Sürpriz",
+ "signal-tier-pass": "Pas",
+ "confidence-high": "Yüksek",
+ "confidence-medium": "Orta",
+ "confidence-low": "Düşük",
+ "confidence-unknown": "Belirsiz",
+ "info": "Bilgi"
}
},
"coupons": {
@@ -312,6 +374,8 @@
"coupon": "Kupon",
"candidate-match-count": "Aday Maç",
"candidate-match-count-help": "Kupon oluşturmak için şu anda uygun olan yaklaşan futbol maçı sayısı.",
+ "finished-match-count": "Biten Maç",
+ "finished-match-count-help": "Biten futbol maçları için isteğe bağlı referans listesi. Bunlar kupon tahmininde asla kullanılmaz.",
"selected-match-count": "Seçilen Maç",
"selected-match-count-help": "Maçları siz seçerseniz AI kuponu sadece bu havuzdan üretir.",
"suggested-bet-count": "Önerilen Bahis",
@@ -323,12 +387,24 @@
"candidate-pool-subtitle": "Kaynak: live_matches tablosu - spor: futbol - durum: başlamamış",
"match-count-suffix": "maç",
"upcoming-badge": "Yaklaşan",
+ "upcoming-reference": "Yaklaşan havuz",
+ "finished-badge": "Bitti",
+ "prediction-locked": "Tahmine Kapalı",
+ "read-only-short": "Salt okunur",
"selected-short": "Seçildi",
"select-match": "Seç",
+ "match-state": "Maç Durumu",
"selection-mode": "AI Havuzu",
"manual-pool": "Manuel havuz",
"auto-pool": "Otomatik havuz",
+ "finished-reference-only": "Sadece referans",
"no-upcoming-matches": "Şu anda kupon oluşturmaya uygun yaklaşan futbol maçı bulunmuyor.",
+ "finished-matches-title": "Biten Maçlar",
+ "finished-matches-help": "Bu maçlar sadece referans için gösterilir. Seçilemezler ve kupon tahmini oluşturulmadan önce backend tarafından filtrelenirler.",
+ "finished-matches-subtitle": "İsteğe bağlı arşiv görünümü. Skorlar ve maç sonu istatistikleri kupon tahmin akışına gönderilmez.",
+ "show-finished-matches": "Biten maçları göster",
+ "hide-finished-matches": "Biten maçları gizle",
+ "no-finished-matches": "Geçerli görünüm için biten futbol maçı bulunamadı.",
"manual-selection-active": "AI yalnızca aşağıda seçtiğiniz maçları kullanacak.",
"automatic-selection-active": "Henüz manuel seçim yok. AI tüm yaklaşan maç havuzundan seçecek.",
"selected-matches-panel-title": "Seçili Maç Havuzu",
@@ -465,7 +541,9 @@
"analytics": "Analitik Genel Bakış",
"user-management": "Kullanıcı Yönetimi",
"users": "Kullanıcılar",
+ "premium-users": "Premium Kullanıcı",
"settings": "Ayarlar",
+ "subscription": "Abonelik",
"usage-limits": "Kullanım Limitleri",
"total-users": "Toplam Kullanıcı",
"active-users": "Aktif Kullanıcı",
@@ -485,7 +563,25 @@
"user-email": "E-Posta",
"user-role": "Rol",
"user-status": "Durum",
- "no-users": "Kullanıcı bulunamadı."
+ "no-users": "Kullanıcı bulunamadı.",
+ "restricted": "Kısıtlı",
+ "admin-access-required": "Admin erişimi gerekli",
+ "admin-access-description": "Bu alan yalnızca superadmin hesapları tarafından kullanılabilir.",
+ "search-users-placeholder": "E-posta veya isim ara...",
+ "all-roles": "Tüm Rolleri Gör",
+ "standard-user": "Standart Kullanıcı",
+ "superadmin": "Sistem Yöneticisi (Admin)",
+ "all-plans": "Tüm Paketleri Gör",
+ "plan-free": "Ücretsiz (Free)",
+ "plan-plus": "Plus Paketi",
+ "plan-premium": "Premium Paketi",
+ "plan-past-due": "Ödeme Gecikti (Past Due)",
+ "plan-cancelled": "İptal Edildi (Cancelled)",
+ "edit-user-title": "Kullanıcı Düzenle: {email}",
+ "user-role-field": "Kullanıcı Rolü",
+ "subscription-plan-field": "Abonelik Paketi",
+ "subscription-end-date": "Abonelik Bitiş Tarihi (Opsiyonel)",
+ "account-active-question": "Hesap Aktif mi?"
},
"common": {
"limits": {
@@ -679,4 +775,4 @@
"remaining": "kalan"
}
}
-}
\ No newline at end of file
+}
diff --git a/next-env.d.ts b/next-env.d.ts
index 9edff1c..c4b7818 100644
--- a/next-env.d.ts
+++ b/next-env.d.ts
@@ -1,6 +1,6 @@
///
///
-import "./.next/types/routes.d.ts";
+import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/src/components/admin/admin-content.tsx b/src/components/admin/admin-content.tsx
index 6d615d2..a18fffc 100644
--- a/src/components/admin/admin-content.tsx
+++ b/src/components/admin/admin-content.tsx
@@ -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("overview");
const [editingUser, setEditingUser] = useState(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() {
- Restricted
+ {t("restricted")}
- Admin access required
+ {t("admin-access-required")}
- This area is only available to superadmin accounts.
+ {t("admin-access-description")}
@@ -236,7 +251,7 @@ export default function AdminContent() {
}
colorPalette="purple"
@@ -272,32 +287,49 @@ export default function AdminContent() {
setSearchParams({ ...searchParams, search: e.target.value })}
+ onChange={(e) =>
+ setSearchParams({
+ ...searchParams,
+ search: e.target.value,
+ })
+ }
/>
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" },
]}
/>
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" },
]}
/>
@@ -310,7 +342,11 @@ export default function AdminContent() {
) : users.length > 0 ? (
-
+
{/* Table Header */}
@@ -330,7 +366,7 @@ export default function AdminContent() {
{t("user-role")}
- {t("subscription", { fallback: "Subscription" })}
+ {t("subscription")}
{t("user-status")}
@@ -357,7 +393,12 @@ export default function AdminContent() {
>
{getUserDisplayName(user)}
-
+
{user.email}
@@ -372,9 +413,20 @@ export default function AdminContent() {
{formatRoleLabel(user.role)}
-
+
{user.subscriptionExpiresAt ? (
- {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",
+ },
+ )}
) : (
@@ -436,25 +495,45 @@ export default function AdminContent() {
{/* Pagination */}
{meta && meta.totalPages > 1 && (
-
+
- Sayfa {meta.page} / {meta.totalPages}
+
+ {tCommon("page")} {meta.page} / {meta.totalPages}
+
)}
diff --git a/src/components/admin/edit-user-modal.tsx b/src/components/admin/edit-user-modal.tsx
index d31c88c..95fe36c 100644
--- a/src/components/admin/edit-user-modal.tsx
+++ b/src/components/admin/edit-user-modal.tsx
@@ -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 (
+
+ );
+}
+
+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("");
- const [isActive, setIsActive] = useState(true);
+ const [role, setRole] = useState(user.role || "user");
+ const [plan, setPlan] = useState(user.subscriptionStatus || "free");
+ const [expiresAt, setExpiresAt] = useState(
+ 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 (
!e.open && onClose()}>
- Kullanıcı Düzenle: {user.email}
+
+ {t("edit-user-title", { email: user.email })}
+
-
+
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" },
]}
/>
-
+
{
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" },
]}
/>
{plan !== "free" && (
-
- setExpiresAt(e.target.value)}
+
+ setExpiresAt(e.target.value)}
/>
)}
-
- setIsActive(e.checked)}>
- {isActive ? "Aktif" : "Pasif"}
+
+ setIsActive(e.checked)}
+ >
+ {isActive ? tCommon("active") : tCommon("inactive")}
-
diff --git a/src/components/h2h/h2h-content.tsx b/src/components/h2h/h2h-content.tsx
index f530122..14ad691 100644
--- a/src/components/h2h/h2h-content.tsx
+++ b/src/components/h2h/h2h-content.tsx
@@ -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",
},
diff --git a/src/components/matches/prediction-card.tsx b/src/components/matches/prediction-card.tsx
index 6463508..550f115 100644
--- a/src/components/matches/prediction-card.tsx
+++ b/src/components/matches/prediction-card.tsx
@@ -46,6 +46,16 @@ interface PredictionCardProps {
prediction: MatchPredictionDto;
}
+type PredictionUiMessages = Record;
+
+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 (
- Model {formatProbability(modelProb, 0)}
+ {labels.model} {formatProbability(modelProb, 0)}
- Piyasa {formatProbability(impliedProb, 0)}
+ {labels.market} {formatProbability(impliedProb, 0)}
@@ -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;
+ 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)}
- {getConfidenceBandLabel(pick.confidence_interval?.band)}
+ {getConfidenceBandLabel(pick.confidence_interval?.band, ui)}
- Teorik avantaj {formatEdgeSignal(pick.ev_edge)}
+ {labels.theoreticalEdgeInline} {formatEdgeSignal(pick.ev_edge)}
@@ -619,12 +655,12 @@ function PickCard({
value={formatSignalScore(pick.play_score)}
/>
@@ -632,6 +668,10 @@ function PickCard({
@@ -661,8 +701,7 @@ function PickCard({
borderColor={intervalWarningBorder}
>
- Guven araligi genis. Sinyal olsa bile tek basina oynanmasi
- onerilmez.
+ {labels.confidenceIntervalWarning}
) : null}
@@ -676,11 +715,13 @@ function SummaryTable({
marketLabels,
title,
info,
+ ui,
}: {
items: MatchBetSummaryItemDto[];
marketLabels?: Record;
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)}
{getMarketLabel(item.market, marketLabels)}
@@ -753,7 +794,7 @@ function SummaryTable({
)}
variant="subtle"
>
- {getConfidenceBandLabel(item.confidence_interval?.band)}
+ {getConfidenceBandLabel(item.confidence_interval?.band, ui)}
{formatUnits(item.stake_units)}
@@ -812,7 +853,6 @@ function SummaryTable({
{formatUnits(item.stake_units)}
*/}
-
@@ -825,12 +865,14 @@ function MarketBoardSection({
marketLabels,
title,
info,
+ ui,
}: {
marketBoard?: Record;
betSummary?: MatchBetSummaryItemDto[];
marketLabels?: Record;
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")}
) : null}
{summary?.signal_tier ? (
@@ -891,7 +935,7 @@ function MarketBoardSection({
)}
variant="subtle"
>
- {getSignalTierLabel(summary.signal_tier)}
+ {getSignalTierLabel(summary.signal_tier, ui)}
) : null}
{summary?.bet_grade ? (
@@ -913,12 +957,16 @@ function MarketBoardSection({
{interval ? (
- Guven araligi: {formatInterval(interval)}
+ {getUiText(ui, "confidence-interval", "Güven Aralığı")}:{" "}
+ {formatInterval(interval)}
) : null}
@@ -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({
@@ -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 (
{isLive ? (
- 🔴 CANLI
+ 🔴 {uiText("live", "CANLI")}
{liveScoreHome != null && liveScoreAway != null ? (
- {prediction.match_info.home_team} {liveScoreHome} - {liveScoreAway}{" "}
- {prediction.match_info.away_team}
+ {prediction.match_info.home_team} {liveScoreHome} -{" "}
+ {liveScoreAway} {prediction.match_info.away_team}
) : null}
{isStale ? (
- Maç öncesi tahmin
+ {uiText("pre-match-prediction", "Maç öncesi tahmin")}
) : null}
@@ -1146,15 +1250,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{contradictions.length ? (
- Tahmin Çelişkileri
+
+ {uiText("prediction-contradictions", "Tahmin Çelişkileri")}
+
{contradictions.map((text, idx) => (
• {text}
@@ -1169,17 +1275,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
@@ -1190,9 +1296,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
mt={0.5}
/>
- 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.",
+ )}
@@ -1201,9 +1308,9 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
@@ -1220,12 +1327,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{getMarketLabel(recommendedPick.market, marketLabels)}{" "}
- {uiText("best-market-copy", "marketinde en guclu secim.")}
+ {uiText("best-market-copy", "marketinde en güçlü seçim.")}
{getConfidenceBandLabel(
prediction.bet_advice.confidence_band,
+ ui,
)}
{recommendedPick.confidence_interval ? (
@@ -1239,7 +1347,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
@@ -1284,7 +1392,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
borderRadius="2xl"
>
- {uiText("quick-read", "Hizli yorum")}
+ {uiText("quick-read", "Hızlı yorum")}
{prediction.risk.is_surprise_risk ||
- prediction.risk.warnings?.length ? (
+ prediction.risk.warnings?.length ? (
@@ -1339,12 +1454,17 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
mt={0.5}
/>
- Risk Yorumu
+
+ {uiText("risk-commentary", "Risk Yorumu")}
+
{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.",
+ ))}
{prediction.risk.surprise_score !== undefined ? (
- Sürpriz skoru:{" "}
+ {uiText("surprise-score", "Sürpriz skoru")}:{" "}
{formatPercent(prediction.risk.surprise_score, 0)}
) : null}
@@ -1361,7 +1481,13 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
{prediction.risk.surprise_breakdown.map((entry) => (
= 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.",
)}
/>
@@ -1403,7 +1529,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
- {getEngineLabelText(item.detail.label)}
+ {getEngineLabelText(item.detail.label, ui)}
) : null}
@@ -1432,9 +1558,8 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
-
{item.detail?.interpretation ? (
{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.",
)}
/>
@@ -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}
/>
))}
@@ -1505,19 +1620,24 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
- {prediction.match_commentary?.headline || prediction.match_commentary?.summary ? (
+ {prediction.match_commentary?.headline ||
+ prediction.match_commentary?.summary ? (
{prediction.match_commentary.headline ? (
@@ -1541,7 +1661,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
) : null}
-
+
{prediction.v27_engine ? (
@@ -1562,7 +1683,7 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {
- {prediction.bet_advice.playable ? "OYNA" : "OYNAMA"}
+ {prediction.bet_advice.playable
+ ? uiText("bet-advice-play", "OYNA")
+ : uiText("bet-advice-pass", "OYNAMA")}
- {getConfidenceBandLabel(prediction.bet_advice.confidence_band)}
+ {getConfidenceBandLabel(
+ prediction.bet_advice.confidence_band,
+ ui,
+ )}
- {getSignalTierLabel(prediction.bet_advice.signal_tier)}
+ {getSignalTierLabel(prediction.bet_advice.signal_tier, ui)}
{resolveReason(prediction.bet_advice.reason)}
- {uiText("recommended-stake-inline", "Onerilen miktar")}:{" "}
+ {uiText("recommended-stake-inline", "Önerilen miktar")}:{" "}
{formatUnits(prediction.bet_advice.suggested_stake_units)}
@@ -1616,7 +1742,10 @@ export default function PredictionCard({ prediction }: PredictionCardProps) {