v28
This commit is contained in:
@@ -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: 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();
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user