From e3cc6702ddeb862ded1db6e734d6a211a2e25f3a Mon Sep 17 00:00:00 2001 From: Fahri Can Date: Tue, 5 May 2026 01:04:50 +0300 Subject: [PATCH] main --- messages/en.json | 93 ++++- messages/tr.json | 21 +- src/components/matches/match-card.tsx | 377 ++++++++++-------- .../matches/match-detail-content.tsx | 247 ++++++++++-- src/components/teams/team-detail-content.tsx | 19 +- src/lib/api/matches/types.ts | 26 +- 6 files changed, 571 insertions(+), 212 deletions(-) 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} - ) : ( - - - {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} - ) : ( - - - {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} + ) : ( + + + {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} + ) : ( + + + {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.country.name} • `} {match.league.name} - )} + + )} + {/* Teams & Score */} {/* Home Team */} @@ -142,12 +191,12 @@ export default function MatchDetailContent() { {match.homeTeam.name} ) : ( {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} ) : ( {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}} + {teamName} + + + {t("no-sidelined")} + + + ); + } + + return ( + + + {teamLogo && {teamName}} + {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 {