diff --git a/messages/en.json b/messages/en.json
index bb17bd3..0c7f654 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -36,7 +36,9 @@
"logging-in": "Signing in...",
"registering": "Creating account...",
"login-success": "Login successful!",
- "register-success": "Registration successful!"
+ "register-success": "Registration successful!",
+ "login-required-title": "Login Required",
+ "login-required-message": "Please sign in or create an account to view match analysis."
},
"all-right-reserved": "All rights reserved.",
"privacy-policy": "Privacy Policy",
@@ -121,7 +123,22 @@
"recent-matches": "Recent Matches",
"home-team": "Home",
"away-team": "Away",
- "vs": "vs"
+ "vs": "vs",
+ "referee": "Referee",
+ "sidelined": "Injuries & Absences",
+ "injury": "Injury",
+ "suspended": "Suspended",
+ "other-reason": "Other",
+ "matches-missed": "Matches Missed",
+ "position": "Position",
+ "no-sidelined": "No injury information available",
+ "match-events": "Match Events",
+ "goal": "Goal",
+ "yellow-card": "Yellow Card",
+ "red-card": "Red Card",
+ "substitution": "Substitution",
+ "starters": "Starting XI",
+ "substitutes": "Substitutes"
},
"predictions": {
@@ -517,5 +534,77 @@
"items-per-page": "Items per page",
"showing": "Showing",
"results": "results"
+ },
+
+ "seo": {
+ "global": {
+ "title": "iddaai.com | AI-Powered Betting Predictions",
+ "description": "iddaai.com offers AI-powered betting predictions, detailed match analysis, and data-driven coupon building.",
+ "keywords": "betting, betting predictions, AI betting, match analysis, sure bets, football statistics"
+ },
+ "home": {
+ "title": "Home",
+ "description": "AI-powered betting predictions. Analyze matches, discover value bets, and build winning coupons."
+ },
+ "h2h": {
+ "title": "Head to Head Comparison",
+ "description": "Compare football teams head to head. In-depth statistics, past matches, and predictions."
+ },
+ "analysis": {
+ "title": "Multi-Match Analysis",
+ "description": "Analyze multiple matches at once. Detailed statistics and AI-driven strategies."
+ },
+ "leagues": {
+ "title": "Leagues & Teams",
+ "description": "Explore football and basketball leagues, countries, and team statistics worldwide."
+ },
+ "admin": {
+ "title": "Admin Panel",
+ "description": "Admin panel for managing users and system settings."
+ },
+ "matches": {
+ "title": "Matches & Fixtures",
+ "description": "View upcoming matches, live scores, and past fixtures with AI predictions."
+ },
+ "about": {
+ "title": "About Us",
+ "description": "Learn more about iddaai.com, our AI technology, and how we deliver betting insights."
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "description": "Your personalized dashboard for betting stats, predictions, and account overview."
+ },
+ "profile": {
+ "title": "My Profile",
+ "description": "Manage your user profile, subscription, and account settings."
+ },
+ "spor-toto": {
+ "title": "Spor Toto Predictions",
+ "description": "AI-powered Spor Toto predictions. Build coupons with conservative, balanced, or aggressive strategies."
+ },
+ "coupon-builder": {
+ "title": "AI Coupon Builder",
+ "description": "Automatically generate optimized betting coupons using advanced AI and statistical models."
+ },
+ "teams": {
+ "title": "Team Statistics",
+ "description": "Detailed statistics, form analysis, and predictive models for football teams."
+ },
+ "coupon-history": {
+ "title": "Coupon History",
+ "description": "Review your past betting coupons and track your performance."
+ },
+ "predictions": {
+ "title": "Betting Predictions",
+ "description": "Daily AI betting predictions, value odds, and high-confidence match tips."
+ },
+ "signup": {
+ "title": "Sign Up",
+ "description": "Create your iddaai.com account to access AI-powered betting predictions."
+ },
+ "signin": {
+ "title": "Sign In",
+ "description": "Sign in to your iddaai.com account to access AI predictions and tools."
+ }
}
}
diff --git a/messages/tr.json b/messages/tr.json
index 5beb69b..a05f4de 100644
--- a/messages/tr.json
+++ b/messages/tr.json
@@ -36,7 +36,9 @@
"logging-in": "Giriş yapılıyor...",
"registering": "Hesap oluşturuluyor...",
"login-success": "Giriş başarılı!",
- "register-success": "Kayıt başarılı!"
+ "register-success": "Kayıt başarılı!",
+ "login-required-title": "Giriş Yapmanız Gerekiyor",
+ "login-required-message": "Maç analizlerini görüntülemek için lütfen giriş yapın veya hesap oluşturun."
},
"all-right-reserved": "Tüm hakları saklıdır.",
"privacy-policy": "Gizlilik Politikası",
@@ -117,7 +119,22 @@
"recent-matches": "Son Maçlar",
"home-team": "Ev Sahibi",
"away-team": "Deplasman",
- "vs": "vs"
+ "vs": "vs",
+ "referee": "Hakem",
+ "sidelined": "Sakatlık & Eksikler",
+ "injury": "Sakatlık",
+ "suspended": "Cezalı",
+ "other-reason": "Diğer",
+ "matches-missed": "Kaçırılan Maç",
+ "position": "Pozisyon",
+ "no-sidelined": "Eksik oyuncu bilgisi yok",
+ "match-events": "Maç Olayları",
+ "goal": "Gol",
+ "yellow-card": "Sarı Kart",
+ "red-card": "Kırmızı Kart",
+ "substitution": "Oyuncu Değişikliği",
+ "starters": "İlk 11",
+ "substitutes": "Yedekler"
},
"predictions": {
"title": "Tahminler",
diff --git a/src/components/matches/match-card.tsx b/src/components/matches/match-card.tsx
index 83309d7..e38c92d 100644
--- a/src/components/matches/match-card.tsx
+++ b/src/components/matches/match-card.tsx
@@ -11,10 +11,13 @@ import {
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
+import { useSession } from "next-auth/react";
import { motion } from "framer-motion";
import { slideUpVariants } from "@/components/motion";
import type { MatchResponseDto } from "@/lib/api/matches/types";
import { useColorModeValue } from "@/components/ui/color-mode";
+import { useState } from "react";
+import { LoginModal } from "@/components/auth/login-modal";
interface MatchCardProps {
match: MatchResponseDto;
@@ -24,7 +27,10 @@ const MotionBox = motion.create(Box);
export default function MatchCard({ match }: MatchCardProps) {
const t = useTranslations("matches");
+ const tAuth = useTranslations("auth");
const router = useRouter();
+ const { data: session } = useSession();
+ const [loginModalOpen, setLoginModalOpen] = useState(false);
const cardBg = useColorModeValue("white", "gray.800");
const cardBorder = useColorModeValue("gray.100", "gray.700");
@@ -42,6 +48,10 @@ export default function MatchCard({ match }: MatchCardProps) {
: t("not-started");
const handleClick = () => {
+ if (!session) {
+ setLoginModalOpen(true);
+ return;
+ }
router.push(`/matches/${match.id}`);
};
@@ -49,180 +59,205 @@ export default function MatchCard({ match }: MatchCardProps) {
const matchDate = new Date(match.mstUtc);
return (
-
- {/* Status Badge */}
-
-
- {isLive && (
-
- )}
- {statusText}
-
-
-
- {matchDate.toLocaleDateString("tr-TR", {
- day: "2-digit",
- month: "short",
- hour: "2-digit",
- minute: "2-digit",
- })}
-
-
-
- {/* Teams */}
-
- {/* Home Team */}
-
- {match.homeTeamLogo ? (
-
- ) : (
-
-
- {match.homeTeamName?.charAt(0) || "H"}
-
-
- )}
-
+
+ {/* Status Badge */}
+
+
- {match.homeTeamName}
-
-
+ {isLive && (
+
+ )}
+ {statusText}
+
- {/* Score or VS */}
-
- {(isLive || isFinished) &&
- match.scoreHome !== undefined &&
- match.scoreAway !== undefined ? (
-
-
- {match.scoreHome}
-
-
- -
-
-
- {match.scoreAway}
-
-
- ) : (
-
- {t("vs")}
-
- )}
-
-
- {/* Away Team */}
-
- {match.awayTeamLogo ? (
-
- ) : (
-
-
- {match.awayTeamName?.charAt(0) || "A"}
-
-
- )}
-
- {match.awayTeamName}
-
-
-
-
- {/* League Info */}
- {(match.leagueName || match.countryName) && (
-
- {/* Flag handling if available in flat response, otherwise skip or pass from parent */}
-
- {match.countryName && `${match.countryName} • `}
- {match.leagueName}
+
+ {matchDate.toLocaleDateString("tr-TR", {
+ day: "2-digit",
+ month: "short",
+ hour: "2-digit",
+ minute: "2-digit",
+ })}
- )}
-
+
+ {/* Teams */}
+
+ {/* Home Team */}
+
+ {match.homeTeamLogo ? (
+
+ ) : (
+
+
+ {match.homeTeamName?.charAt(0) || "H"}
+
+
+ )}
+
+ {match.homeTeamName}
+
+
+
+ {/* Score or VS */}
+
+ {(isLive || isFinished) &&
+ match.scoreHome !== undefined &&
+ match.scoreAway !== undefined ? (
+
+
+ {match.scoreHome}
+
+
+ -
+
+
+ {match.scoreAway}
+
+
+ ) : (
+
+ {t("vs")}
+
+ )}
+
+
+ {/* Away Team */}
+
+ {match.awayTeamLogo ? (
+
+ ) : (
+
+
+ {match.awayTeamName?.charAt(0) || "A"}
+
+
+ )}
+
+ {match.awayTeamName}
+
+
+
+
+ {/* League Info */}
+ {(match.leagueName || match.countryName) && (
+
+ {/* Flag handling if available in flat response, otherwise skip or pass from parent */}
+
+ {match.countryName && `${match.countryName} • `}
+ {match.leagueName}
+
+
+ )}
+
+ {/* Auth hint for unauthenticated users */}
+ {!session && (
+
+
+ 🔒 {tAuth("login-required-title")}
+
+
+ )}
+
+
+ {/* Login Modal — shown when unauthenticated user clicks a match */}
+
+ >
);
}
diff --git a/src/components/matches/match-detail-content.tsx b/src/components/matches/match-detail-content.tsx
index d13e29f..49a979b 100644
--- a/src/components/matches/match-detail-content.tsx
+++ b/src/components/matches/match-detail-content.tsx
@@ -12,17 +12,50 @@ import {
Spinner,
Button,
Card,
+ Grid,
+ SimpleGrid,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
import { useColorModeValue } from "@/components/ui/color-mode";
-import { SlideUp } from "@/components/motion";
+import { SlideUp, FadeIn } from "@/components/motion";
import { useMatchDetails } from "@/lib/api/matches/use-hooks";
import { usePrediction } from "@/lib/api/predictions/use-hooks";
import PredictionCard from "@/components/matches/prediction-card";
import OddsCard from "@/components/matches/odds-card";
import LineupsCard from "@/components/matches/lineups-card";
-import { LuArrowLeft, LuRefreshCw } from "react-icons/lu";
+import { LuArrowLeft, LuRefreshCw, LuShield, LuFlag, LuUser } from "react-icons/lu";
+import type { MatchResponseDto } from "@/lib/api/matches/types";
+
+// ─────────────────────────────────────────────────
+// Type helpers for sidelined data
+// ─────────────────────────────────────────────────
+
+interface SidelinedPlayer {
+ playerName: string;
+ position?: string;
+ positionShort?: string;
+ description?: string;
+ type?: string;
+ matchesMissed?: number;
+ reasonIcon?: string;
+}
+
+interface SidelinedTeam {
+ teamId?: string;
+ teamName?: string;
+ totalSidelined?: number;
+ players?: SidelinedPlayer[];
+}
+
+interface SidelinedData {
+ homeTeam?: SidelinedTeam;
+ awayTeam?: SidelinedTeam;
+}
+
+// ─────────────────────────────────────────────────
+// Main Component
+// ─────────────────────────────────────────────────
export default function MatchDetailContent() {
const t = useTranslations("matches");
@@ -41,9 +74,13 @@ export default function MatchDetailContent() {
} = usePrediction(matchId);
const headerBg = useColorModeValue("white", "gray.800");
+ const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
+ const subtleBg = useColorModeValue("gray.50", "gray.900");
+ const injuryBg = useColorModeValue("red.50", "rgba(254, 178, 178, 0.08)");
+ const injuryBorder = useColorModeValue("red.100", "red.800");
- const match = matchData?.data;
+ const match = matchData?.data as MatchResponseDto | undefined;
const prediction = predictionData?.data;
if (matchLoading) {
@@ -70,6 +107,10 @@ export default function MatchDetailContent() {
const isLive = match.status === "LIVE";
const isFinished = match.status === "Finished";
+ const sidelined = (match.sidelined || {}) as SidelinedData;
+ const hasSidelined =
+ (sidelined.homeTeam?.players?.length || 0) > 0 ||
+ (sidelined.awayTeam?.players?.length || 0) > 0;
return (
@@ -85,26 +126,32 @@ export default function MatchDetailContent() {
{tCommon("back")}
- {/* Match Header */}
+
+ {/* ═══════════════════════════════════════════ */}
+ {/* Match Header Card */}
+ {/* ═══════════════════════════════════════════ */}
-
- {/* League Info */}
- {match.league && (
-
+ {/* League Banner */}
+ {match.league && (
+
+
{match.league.country?.flag && (
)}
-
+
+ {match.league.country?.name && `${match.league.country.name} • `}
{match.league.name}
- )}
+
+ )}
+
{/* Teams & Score */}
{/* Home Team */}
@@ -142,12 +191,12 @@ export default function MatchDetailContent() {
) : (
{match.homeTeam?.name}
-
+
{t("home-team")}
-
+
{/* Score */}
@@ -171,18 +220,20 @@ export default function MatchDetailContent() {
{match.score && (isLive || isFinished) ? (
{match.score.home}
-
- -
+
+ :
{match.score.away}
@@ -211,12 +262,12 @@ export default function MatchDetailContent() {
) : (
{match.awayTeam?.name}
-
+
{t("away-team")}
-
+
+
+ {/* Referee Info */}
+ {match.refereeName && (
+
+
+
+ {t("referee")}: {match.refereeName}
+
+
+ )}
- {/* Lineups Section */}
+ {/* ═══════════════════════════════════════════ */}
+ {/* Sidelined / Injuries Card */}
+ {/* ═══════════════════════════════════════════ */}
+ {hasSidelined && (
+
+
+
+
+ 🏥 {t("sidelined")}
+
+
+
+ {/* Home Team Sidelined */}
+
+
+ {/* Away Team Sidelined */}
+
+
+
+
+
+ )}
+
+ {/* ═══════════════════════════════════════════ */}
+ {/* Lineups Section */}
+ {/* ═══════════════════════════════════════════ */}
- {/* Prediction Section */}
+ {/* ═══════════════════════════════════════════ */}
+ {/* Prediction Section */}
+ {/* ═══════════════════════════════════════════ */}
@@ -275,7 +385,9 @@ export default function MatchDetailContent() {
)}
- {/* Odds Section */}
+ {/* ═══════════════════════════════════════════ */}
+ {/* Odds Section */}
+ {/* ═══════════════════════════════════════════ */}
{match.odds && Object.keys(match.odds).length > 0 && (
)}
@@ -283,3 +395,88 @@ export default function MatchDetailContent() {
);
}
+
+// ─────────────────────────────────────────────────
+// Sidelined Column Sub-Component
+// ─────────────────────────────────────────────────
+
+interface SidelinedColumnProps {
+ team?: SidelinedTeam;
+ teamName: string;
+ teamLogo?: string;
+ injuryBg: string;
+ injuryBorder: string;
+ t: ReturnType;
+}
+
+function SidelinedColumn({ team, teamName, teamLogo, injuryBg, injuryBorder, t }: SidelinedColumnProps) {
+ const players = team?.players || [];
+
+ if (players.length === 0) {
+ return (
+
+
+ {teamLogo && }
+ {teamName}
+
+
+ {t("no-sidelined")}
+
+
+ );
+ }
+
+ return (
+
+
+ {teamLogo && }
+ {teamName}
+
+ {players.length}
+
+
+
+ {players.map((player, idx) => (
+
+
+
+
+ {player.playerName}
+
+
+ {player.positionShort && (
+
+ {player.positionShort}
+
+ )}
+
+ {player.description || (
+ player.type === "injury"
+ ? t("injury")
+ : player.type === "suspended"
+ ? t("suspended")
+ : t("other-reason")
+ )}
+
+
+
+ {player.matchesMissed !== undefined && player.matchesMissed > 0 && (
+
+ {player.matchesMissed} {t("matches-missed")}
+
+ )}
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/teams/team-detail-content.tsx b/src/components/teams/team-detail-content.tsx
index 9969f75..1ba8bf4 100644
--- a/src/components/teams/team-detail-content.tsx
+++ b/src/components/teams/team-detail-content.tsx
@@ -15,12 +15,14 @@ import {
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
+import { useSession } from "next-auth/react";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp, FadeIn } from "@/components/motion";
import { useTeamById, useTeamMatches } from "@/lib/api/leagues/use-hooks";
import { LuArrowLeft, LuCalendar, LuTrophy, LuChevronDown } from "react-icons/lu";
import type { MatchResponseDto } from "@/lib/api/matches/types";
import { useState, useMemo, useCallback } from "react";
+import { LoginModal } from "@/components/auth/login-modal";
// ─────────────────────────────────────────────────
// Utility Functions
@@ -32,7 +34,7 @@ function getMatchTimestamp(match: MatchResponseDto): number {
}
function getMatchStatus(match: MatchResponseDto): string {
- return String(match.status || (match as Record).state || "").toUpperCase();
+ return String(match.status || match.state || "").toUpperCase();
}
function isMatchFinished(match: MatchResponseDto): boolean {
@@ -50,7 +52,7 @@ function getTeamSideName(team: MatchResponseDto["homeTeam"] | MatchResponseDto["
}
function getTeamSideLogo(team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"], fallback?: unknown): string {
- return String(team?.logo || (team as Record | undefined)?.logoUrl || fallback || "");
+ return String(team?.logo || fallback || "");
}
function getLeagueLabel(match: MatchResponseDto): string {
@@ -83,6 +85,8 @@ export default function TeamDetailContent() {
const t = useTranslations();
const params = useParams();
const router = useRouter();
+ const { data: session } = useSession();
+ const [loginModalOpen, setLoginModalOpen] = useState(false);
const teamId = params.id as string;
const [currentPage, setCurrentPage] = useState(1);
@@ -245,7 +249,7 @@ export default function TeamDetailContent() {
cardBg={cardBg}
borderColor={borderColor}
statusBadge={getStatusBadge(match)}
- onClick={() => router.push(`/tr/matches/${match.id}`)}
+ onClick={() => session ? router.push(`/matches/${match.id}`) : setLoginModalOpen(true)}
/>
))}
@@ -316,7 +320,7 @@ export default function TeamDetailContent() {
cardBg={cardBg}
borderColor={borderColor}
statusBadge={getStatusBadge(match)}
- onClick={() => router.push(`/tr/matches/${match.id}`)}
+ onClick={() => session ? router.push(`/matches/${match.id}`) : setLoginModalOpen(true)}
/>
))}
@@ -379,6 +383,13 @@ export default function TeamDetailContent() {
)}
+
+ {/* Login Modal — shown when unauthenticated user clicks a match */}
+
);
diff --git a/src/lib/api/matches/types.ts b/src/lib/api/matches/types.ts
index eba6ecf..d19de0a 100644
--- a/src/lib/api/matches/types.ts
+++ b/src/lib/api/matches/types.ts
@@ -83,31 +83,41 @@ export interface MatchResponseDto {
odds?: Record>;
// Nested Objects (from Backend include)
- homeTeam?: { name: string; logo?: string; [key: string]: unknown };
- awayTeam?: { name: string; logo?: string; [key: string]: unknown };
+ homeTeam?: { id?: string; name: string; logo?: string };
+ awayTeam?: { id?: string; name: string; logo?: string };
league?: {
name: string;
country?: { name: string; flag?: string };
- [key: string]: unknown;
};
lineups?: {
home: Array<{
- player?: { name: string; id: string; [key: string]: unknown };
+ player?: { name: string; id: string };
position?: string | null;
shirtNumber?: number | null;
isStarting?: boolean;
- [key: string]: unknown;
+ isProbable?: boolean;
+ lineupSource?: string;
+ projectionConfidence?: number;
}>;
away: Array<{
- player?: { name: string; id: string; [key: string]: unknown };
+ player?: { name: string; id: string };
position?: string | null;
shirtNumber?: number | null;
isStarting?: boolean;
- [key: string]: unknown;
+ isProbable?: boolean;
+ lineupSource?: string;
+ projectionConfidence?: number;
}>;
};
- [key: string]: unknown;
+ // Match detail enrichments
+ refereeName?: string;
+ sidelined?: Record;
+ events?: Array>;
+
+ // Additional fields from backend detail endpoint
+ lineupSource?: string;
+ stats?: Record;
}
export interface ActiveLeagueDto {