This commit is contained in:
2026-04-23 22:23:35 +03:00
parent 4896323e04
commit 9e04ca5627
13 changed files with 1286 additions and 114 deletions
+231 -44
View File
@@ -12,15 +12,19 @@ import {
Spinner,
Button,
Card,
Table,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useParams, useRouter } from "next/navigation";
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 } from "react-icons/lu";
import { LuArrowLeft, LuCalendar, LuTrophy, LuChevronDown } from "react-icons/lu";
import type { MatchResponseDto } from "@/lib/api/matches/types";
import { useState, useMemo, useCallback } from "react";
// ─────────────────────────────────────────────────
// Utility Functions
// ─────────────────────────────────────────────────
function getMatchTimestamp(match: MatchResponseDto): number {
const raw = typeof match.mstUtc === "string" ? Number(match.mstUtc) : match.mstUtc;
@@ -46,53 +50,118 @@ function getTeamSideName(team: MatchResponseDto["homeTeam"] | MatchResponseDto["
}
function getTeamSideLogo(team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"], fallback?: unknown): string {
return String(team?.logo || team?.logoUrl || fallback || "");
return String(team?.logo || (team as Record<string, unknown> | undefined)?.logoUrl || fallback || "");
}
function getLeagueLabel(match: MatchResponseDto): string {
return String(match.leagueName || match.league?.name || "");
}
/**
* Football season logic: AugJun
* 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();
const month = date.getMonth() + 1; // 1-indexed
if (month >= 8) {
return `${year}-${year + 1}`;
}
return `${year - 1}-${year}`;
}
/**
* Group matches by season string, returning a Map ordered by newest season first.
*/
function groupMatchesBySeason(matches: MatchResponseDto[]): Map<string, MatchResponseDto[]> {
const groups = new Map<string, MatchResponseDto[]>();
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;
}
// ─────────────────────────────────────────────────
// Main Component
// ─────────────────────────────────────────────────
export default function TeamDetailContent() {
const t = useTranslations();
const params = useParams();
const router = useRouter();
const teamId = params.id as string;
const [currentPage, setCurrentPage] = useState(1);
const { data: teamData, isLoading: teamLoading } = useTeamById(teamId);
const { data: matchesData, isLoading: matchesLoading } = useTeamMatches(teamId, { limit: 30 });
const {
data: matchesResponse,
isLoading: matchesLoading,
isFetching: matchesFetching,
} = useTeamMatches(teamId, { page: currentPage, limit: 20 });
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
const seasonActiveBg = useColorModeValue("primary.500", "primary.400");
const seasonInactiveBg = useColorModeValue("gray.100", "gray.700");
const team = teamData?.data;
const matches: MatchResponseDto[] = matchesData?.data ?? [];
if (teamLoading) {
return (
<Flex justify="center" align="center" py={20}>
<Spinner size="lg" color="primary.500" />
</Flex>
);
}
if (!team) {
return (
<Flex justify="center" py={20} direction="column" align="center" gap={4}>
<Text color="fg.muted" fontSize="lg">Takım bulunamadı</Text>
<Button variant="outline" onClick={() => router.back()}>
<LuArrowLeft /> Geri
</Button>
</Flex>
);
}
const team = (teamData as Record<string, unknown> | undefined)?.data as Record<string, unknown> | undefined;
const paginationData = matchesResponse;
const matches: MatchResponseDto[] = paginationData?.data ?? [];
const totalPages = paginationData?.totalPages ?? 1;
const totalMatches = paginationData?.total ?? 0;
// Separate past and upcoming matches
const isFinished = (m: MatchResponseDto) => isMatchFinished(m);
const pastMatches = useMemo(
() => matches.filter((m) => isMatchFinished(m)),
[matches]
);
const upcomingMatches = useMemo(
() => matches.filter((m) => !isMatchFinished(m)),
[matches]
);
const pastMatches = matches.filter((m: MatchResponseDto) => isFinished(m));
const upcomingMatches = matches.filter((m: MatchResponseDto) => !isFinished(m));
// Group past matches by season
const seasonGroups = useMemo(
() => groupMatchesBySeason(pastMatches),
[pastMatches]
);
const seasonKeys = useMemo(() => [...seasonGroups.keys()], [seasonGroups]);
// Active season selection
const [activeSeason, setActiveSeason] = useState<string | null>(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]);
const getStatusBadge = (match: MatchResponseDto) => {
if (isMatchLive(match))
@@ -114,6 +183,25 @@ export default function TeamDetailContent() {
);
};
if (teamLoading) {
return (
<Flex justify="center" align="center" py={20}>
<Spinner size="lg" color="primary.500" />
</Flex>
);
}
if (!team) {
return (
<Flex justify="center" py={20} direction="column" align="center" gap={4}>
<Text color="fg.muted" fontSize="lg">Takım bulunamadı</Text>
<Button variant="outline" onClick={() => router.back()}>
<LuArrowLeft /> Geri
</Button>
</Flex>
);
}
return (
<SlideUp>
<Box>
@@ -127,10 +215,10 @@ export default function TeamDetailContent() {
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl" mb={6}>
<Card.Body>
<HStack gap={6} justify="center" align="center">
{team.logo ? (
{(team as Record<string, unknown>).logo ? (
<Image
src={team.logo}
alt={team.name}
src={String((team as Record<string, unknown>).logo)}
alt={String((team as Record<string, unknown>).name)}
boxSize="80px"
objectFit="contain"
/>
@@ -143,23 +231,23 @@ export default function TeamDetailContent() {
justify="center"
>
<Text fontSize="3xl" fontWeight="bold" color="primary.fg">
{team.name?.charAt(0) || "T"}
{String((team as Record<string, unknown>).name || "T").charAt(0)}
</Text>
</Flex>
)}
<VStack gap={1} align="start">
<Heading as="h1" size="xl">
{team.name}
{String((team as Record<string, unknown>).name)}
</Heading>
{team.country && (
{Boolean((team as Record<string, unknown>).country) && (
<Text fontSize="md" color="fg.muted">
🌍 {team.country}
🌍 {String((team as Record<string, unknown>).country)}
</Text>
)}
<HStack gap={4} mt={1}>
<Badge colorPalette="blue" variant="subtle">
<LuTrophy style={{ width: 12, height: 12 }} />
{matches.length} Maç
{totalMatches} Maç
</Badge>
<Badge colorPalette="green" variant="subtle">
<LuCalendar style={{ width: 12, height: 12 }} />
@@ -194,23 +282,65 @@ export default function TeamDetailContent() {
</FadeIn>
)}
{/* Past Matches */}
{/* Past Matches — Season Grouped */}
<FadeIn>
<Box>
<Heading as="h2" size="lg" mb={4}>
📊 Geçmiş Maçlar
</Heading>
{matchesLoading ? (
<Flex align="center" justify="space-between" mb={4} flexWrap="wrap" gap={2}>
<Heading as="h2" size="lg">
📊 Geçmiş Maçlar
</Heading>
{/* Pagination Info */}
<Text fontSize="xs" color="fg.muted">
Sayfa {currentPage}/{totalPages} Toplam {totalMatches} maç
</Text>
</Flex>
{/* Season Tabs */}
{seasonKeys.length > 0 && (
<HStack gap={2} mb={4} flexWrap="wrap">
{seasonKeys.map((season) => {
const isActive = season === displaySeason;
const count = seasonGroups.get(season)?.length ?? 0;
return (
<Button
key={season}
size="sm"
variant={isActive ? "solid" : "outline"}
bg={isActive ? seasonActiveBg : seasonInactiveBg}
color={isActive ? "white" : undefined}
borderRadius="full"
fontWeight={isActive ? "700" : "500"}
fontSize="xs"
px={4}
onClick={() => setActiveSeason(season)}
_hover={{
transform: "translateY(-1px)",
shadow: "sm",
}}
transition="all 0.2s"
>
🏆 {season} ({count})
</Button>
);
})}
</HStack>
)}
{matchesLoading || matchesFetching ? (
<Flex justify="center" py={8}>
<Spinner size="md" color="primary.500" />
</Flex>
) : pastMatches.length === 0 ? (
) : displayMatches.length === 0 && pastMatches.length === 0 ? (
<Text color="fg.muted" textAlign="center" py={8}>
Geçmiş maç bulunamadı
Bu sayfada geçmiş maç bulunamadı
</Text>
) : displayMatches.length === 0 ? (
<Text color="fg.muted" textAlign="center" py={8}>
Bu sezonda maç bulunamadı
</Text>
) : (
<VStack gap={2} align="stretch">
{pastMatches.map((match: MatchResponseDto) => (
{displayMatches.map((match: MatchResponseDto) => (
<MatchRow
key={match.id}
match={match}
@@ -222,6 +352,63 @@ export default function TeamDetailContent() {
))}
</VStack>
)}
{/* Pagination Controls */}
{totalPages > 1 && (
<Flex justify="center" gap={3} mt={6} align="center">
<Button
size="sm"
variant="outline"
onClick={handlePrevPage}
disabled={currentPage <= 1}
borderRadius="full"
>
Önceki
</Button>
<HStack gap={1}>
{Array.from({ length: Math.min(totalPages, 7) }, (_, i) => {
// Show pages around current page
let pageNum: number;
if (totalPages <= 7) {
pageNum = i + 1;
} else if (currentPage <= 4) {
pageNum = i + 1;
} else if (currentPage >= totalPages - 3) {
pageNum = totalPages - 6 + i;
} else {
pageNum = currentPage - 3 + i;
}
return (
<Button
key={pageNum}
size="sm"
variant={pageNum === currentPage ? "solid" : "ghost"}
bg={pageNum === currentPage ? seasonActiveBg : undefined}
color={pageNum === currentPage ? "white" : undefined}
borderRadius="full"
minW="36px"
onClick={() => {
setCurrentPage(pageNum);
setActiveSeason(null);
}}
>
{pageNum}
</Button>
);
})}
</HStack>
<Button
size="sm"
variant="outline"
onClick={handleNextPage}
disabled={currentPage >= totalPages}
borderRadius="full"
>
Sonraki
</Button>
</Flex>
)}
</Box>
</FadeIn>
</Box>