From 30592394efb5e15e4e3ab07ea5a7d8e86431d0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fahri=20Can=20Se=C3=A7er?= Date: Fri, 24 Apr 2026 00:27:14 +0300 Subject: [PATCH] gg --- src/components/auth/login-modal.tsx | 14 +- src/components/layout/header/header.tsx | 75 ++++++--- src/components/matches/lineups-card.tsx | 143 ++++++++++++++++++ .../matches/match-detail-content.tsx | 4 + src/components/teams/team-detail-content.tsx | 75 +++------ src/lib/api/leagues/types.ts | 1 + src/lib/api/matches/types.ts | 16 ++ src/lib/auth/auth-options.ts | 4 - src/{proxy.ts => middleware.ts} | 0 9 files changed, 246 insertions(+), 86 deletions(-) create mode 100644 src/components/matches/lineups-card.tsx rename src/{proxy.ts => middleware.ts} (100%) diff --git a/src/components/auth/login-modal.tsx b/src/components/auth/login-modal.tsx index 20c25e2..e1e7fe3 100644 --- a/src/components/auth/login-modal.tsx +++ b/src/components/auth/login-modal.tsx @@ -19,7 +19,7 @@ import { yupResolver } from "@hookform/resolvers/yup"; import * as yup from "yup"; import { signIn } from "next-auth/react"; import { toaster } from "@/components/ui/feedback/toaster"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { MdMail } from "react-icons/md"; import { BiUser } from "react-icons/bi"; import { authService } from "@/lib/api/auth/service"; @@ -45,15 +45,23 @@ type RegisterForm = yup.InferType; interface LoginModalProps { open: boolean; onOpenChange: (open: boolean) => void; + initialMode?: "login" | "register"; } /* ────────────────────────── Component ────────────────────────── */ -export function LoginModal({ open, onOpenChange }: LoginModalProps) { +export function LoginModal({ open, onOpenChange, initialMode = "login" }: LoginModalProps) { const t = useTranslations(); - const [mode, setMode] = useState<"login" | "register">("login"); + const [mode, setMode] = useState<"login" | "register">(initialMode); const [loading, setLoading] = useState(false); + // Update mode when modal opens + useEffect(() => { + if (open) { + setMode(initialMode); + } + }, [open, initialMode]); + /* ── Login form ── */ const loginForm = useForm({ resolver: yupResolver(loginSchema), diff --git a/src/components/layout/header/header.tsx b/src/components/layout/header/header.tsx index ba754f7..aaa5daa 100644 --- a/src/components/layout/header/header.tsx +++ b/src/components/layout/header/header.tsx @@ -47,6 +47,7 @@ export default function Header() { const t = useTranslations(); const [isSticky, setIsSticky] = useState(false); const [loginModalOpen, setLoginModalOpen] = useState(false); + const [loginModalMode, setLoginModalMode] = useState<"login" | "register">("login"); const router = useRouter(); const { data: session, status } = useSession(); @@ -63,10 +64,15 @@ export default function Header() { const handleLogout = async () => { await signOut({ redirect: false }); if (authConfig.isAuthRequired) { - router.replace("/signin"); + router.replace("/home"); } }; + const openAuthModal = (mode: "login" | "register") => { + setLoginModalMode(mode); + setLoginModalOpen(true); + }; + // Desktop auth section const renderAuthSection = () => { if (isLoading) return ; @@ -97,16 +103,27 @@ export default function Header() { } return ( - + + + + ); }; @@ -150,17 +167,29 @@ export default function Header() { } return ( - + + + + ); }; @@ -296,7 +325,7 @@ export default function Header() { {/* Login Modal */} - + ); } diff --git a/src/components/matches/lineups-card.tsx b/src/components/matches/lineups-card.tsx new file mode 100644 index 0000000..2d43093 --- /dev/null +++ b/src/components/matches/lineups-card.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { + Box, + Card, + Flex, + HStack, + Text, + VStack, + Badge, + SimpleGrid, + Icon, +} from "@chakra-ui/react"; +import { useColorModeValue } from "@/components/ui/color-mode"; +import { LuUsers, LuUser } from "react-icons/lu"; +import type { MatchResponseDto } from "@/lib/api/matches/types"; +import type { MatchPredictionDto } from "@/lib/api/predictions/types"; + +interface LineupsCardProps { + match: MatchResponseDto; + prediction?: MatchPredictionDto | null; +} + +export default function LineupsCard({ match, prediction }: LineupsCardProps) { + const cardBg = useColorModeValue("white", "gray.800"); + const borderColor = useColorModeValue("gray.100", "gray.700"); + const headerBg = useColorModeValue("gray.50", "whiteAlpha.50"); + + const homeLineups = match.lineups?.home?.filter((p) => p.isStarting) || []; + const awayLineups = match.lineups?.away?.filter((p) => p.isStarting) || []; + + if (homeLineups.length === 0 && awayLineups.length === 0) { + return null; + } + + // Determine if it's confirmed or probable + const source = prediction?.data_quality?.lineup_source; + const isConfirmed = source === "confirmed_live"; + const title = isConfirmed ? "İlk 11" : "Muhtemel Kadro"; + + return ( + + + + + + + {title} + + + + {isConfirmed ? "Onaylı" : "Muhtemel"} + + + + + {/* Home Team Lineup */} + + + {match.homeTeamName} + + + {homeLineups.map((p, idx) => ( + + + {p.shirtNumber && ( + + {p.shirtNumber} + + )} + + {p.player?.name || "Bilinmiyor"} + + {p.position && ( + + {p.position} + + )} + + ))} + + + + {/* Away Team Lineup */} + + + {match.awayTeamName} + + + {awayLineups.map((p, idx) => ( + + + {p.shirtNumber && ( + + {p.shirtNumber} + + )} + + {p.player?.name || "Bilinmiyor"} + + {p.position && ( + + {p.position} + + )} + + ))} + + + + + + ); +} diff --git a/src/components/matches/match-detail-content.tsx b/src/components/matches/match-detail-content.tsx index 4d92d2c..d13e29f 100644 --- a/src/components/matches/match-detail-content.tsx +++ b/src/components/matches/match-detail-content.tsx @@ -21,6 +21,7 @@ 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"; export default function MatchDetailContent() { @@ -237,6 +238,9 @@ export default function MatchDetailContent() { + {/* Lineups Section */} + + {/* Prediction Section */} diff --git a/src/components/teams/team-detail-content.tsx b/src/components/teams/team-detail-content.tsx index 685cc0e..9ec59fe 100644 --- a/src/components/teams/team-detail-content.tsx +++ b/src/components/teams/team-detail-content.tsx @@ -57,11 +57,6 @@ function getLeagueLabel(match: MatchResponseDto): string { return String(match.leagueName || match.league?.name || ""); } -/** - * Football season logic: Aug–Jun - * If month >= August (8) → season starts this year: "YYYY-(YYYY+1)" - * If month < August → season started last year: "(YYYY-1)-YYYY" - */ function getSeasonFromTimestamp(timestampMs: number): string { const date = new Date(timestampMs); const year = date.getFullYear(); @@ -73,28 +68,12 @@ function getSeasonFromTimestamp(timestampMs: number): string { return `${year - 1}-${year}`; } -/** - * Group matches by season string, returning a Map ordered by newest season first. - */ -function groupMatchesBySeason(matches: MatchResponseDto[]): Map { - const groups = new Map(); - - for (const match of matches) { - const ts = getMatchTimestamp(match); - const season = ts ? getSeasonFromTimestamp(ts) : "Bilinmiyor"; - if (!groups.has(season)) { - groups.set(season, []); - } - groups.get(season)!.push(match); - } - - // Sort by season key descending (newest first) - const sorted = new Map( - [...groups.entries()].sort((a, b) => b[0].localeCompare(a[0])) - ); - - return sorted; -} +const SEASONS = (() => { + const currentYear = new Date().getFullYear(); + const currentMonth = new Date().getMonth() + 1; + const startYear = currentMonth >= 8 ? currentYear : currentYear - 1; + return Array.from({ length: 5 }, (_, i) => `${startYear - i}-${startYear - i + 1}`); +})(); // ───────────────────────────────────────────────── // Main Component @@ -107,13 +86,14 @@ export default function TeamDetailContent() { const teamId = params.id as string; const [currentPage, setCurrentPage] = useState(1); + const [activeSeason, setActiveSeason] = useState(SEASONS[0]); const { data: teamData, isLoading: teamLoading } = useTeamById(teamId); const { data: matchesResponse, isLoading: matchesLoading, isFetching: matchesFetching, - } = useTeamMatches(teamId, { page: currentPage, limit: 20 }); + } = useTeamMatches(teamId, { page: currentPage, limit: 20, season: activeSeason }); const cardBg = useColorModeValue("white", "gray.800"); const borderColor = useColorModeValue("gray.100", "gray.700"); @@ -136,30 +116,16 @@ export default function TeamDetailContent() { [matches] ); - // Group past matches by season - const seasonGroups = useMemo( - () => groupMatchesBySeason(pastMatches), - [pastMatches] - ); - const seasonKeys = useMemo(() => [...seasonGroups.keys()], [seasonGroups]); - - // Active season selection - const [activeSeason, setActiveSeason] = useState(null); - const displaySeason = activeSeason ?? seasonKeys[0] ?? null; - const displayMatches = displaySeason ? seasonGroups.get(displaySeason) ?? [] : []; - // Pagination handlers const handleNextPage = useCallback(() => { if (currentPage < totalPages) { setCurrentPage((p) => p + 1); - setActiveSeason(null); // Reset season on page change } }, [currentPage, totalPages]); const handlePrevPage = useCallback(() => { if (currentPage > 1) { setCurrentPage((p) => p - 1); - setActiveSeason(null); } }, [currentPage]); @@ -296,11 +262,10 @@ export default function TeamDetailContent() { {/* Season Tabs */} - {seasonKeys.length > 0 && ( + {SEASONS.length > 0 && ( - {seasonKeys.map((season) => { - const isActive = season === displaySeason; - const count = seasonGroups.get(season)?.length ?? 0; + {SEASONS.map((season) => { + const isActive = season === activeSeason; return ( ); })} @@ -330,17 +298,13 @@ export default function TeamDetailContent() { - ) : displayMatches.length === 0 && pastMatches.length === 0 ? ( + ) : pastMatches.length === 0 ? ( - Bu sayfada geçmiş maç bulunamadı - - ) : displayMatches.length === 0 ? ( - - Bu sezonda maç bulunamadı + Bu sezonda geçmiş maç bulunamadı ) : ( - {displayMatches.map((match: MatchResponseDto) => ( + {pastMatches.map((match: MatchResponseDto) => ( { setCurrentPage(pageNum); - setActiveSeason(null); }} > {pageNum} diff --git a/src/lib/api/leagues/types.ts b/src/lib/api/leagues/types.ts index 53c57f7..253bf0f 100644 --- a/src/lib/api/leagues/types.ts +++ b/src/lib/api/leagues/types.ts @@ -23,6 +23,7 @@ export interface HeadToHeadParams { export interface TeamMatchesParams { page?: number; limit?: number; + season?: string; } export interface PaginatedMatchesResponse { diff --git a/src/lib/api/matches/types.ts b/src/lib/api/matches/types.ts index cdd0c5b..eba6ecf 100644 --- a/src/lib/api/matches/types.ts +++ b/src/lib/api/matches/types.ts @@ -90,6 +90,22 @@ export interface MatchResponseDto { country?: { name: string; flag?: string }; [key: string]: unknown; }; + lineups?: { + home: Array<{ + player?: { name: string; id: string; [key: string]: unknown }; + position?: string | null; + shirtNumber?: number | null; + isStarting?: boolean; + [key: string]: unknown; + }>; + away: Array<{ + player?: { name: string; id: string; [key: string]: unknown }; + position?: string | null; + shirtNumber?: number | null; + isStarting?: boolean; + [key: string]: unknown; + }>; + }; [key: string]: unknown; } diff --git a/src/lib/auth/auth-options.ts b/src/lib/auth/auth-options.ts index 5c512b6..3a42df0 100644 --- a/src/lib/auth/auth-options.ts +++ b/src/lib/auth/auth-options.ts @@ -111,10 +111,6 @@ export const authOptions: NextAuthOptions = { return session; }, }, - pages: { - signIn: "/signin", - error: "/signin", - }, session: { strategy: "jwt" }, secret: process.env.NEXTAUTH_SECRET, }; diff --git a/src/proxy.ts b/src/middleware.ts similarity index 100% rename from src/proxy.ts rename to src/middleware.ts