This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Text,
|
||||
Heading,
|
||||
Badge,
|
||||
VStack,
|
||||
HStack,
|
||||
Image,
|
||||
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 type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||
|
||||
function getMatchTimestamp(match: MatchResponseDto): number {
|
||||
const raw = typeof match.mstUtc === "string" ? Number(match.mstUtc) : match.mstUtc;
|
||||
return Number.isFinite(raw) ? raw : 0;
|
||||
}
|
||||
|
||||
function getMatchStatus(match: MatchResponseDto): string {
|
||||
return String(match.status || (match as Record<string, unknown>).state || "").toUpperCase();
|
||||
}
|
||||
|
||||
function isMatchFinished(match: MatchResponseDto): boolean {
|
||||
const status = getMatchStatus(match);
|
||||
return status === "FT" || status === "FINISHED" || status === "POSTGAME" || status === "POST_GAME";
|
||||
}
|
||||
|
||||
function isMatchLive(match: MatchResponseDto): boolean {
|
||||
const status = getMatchStatus(match);
|
||||
return status === "LIVE" || status === "INPROGRESS" || status === "IN_PROGRESS";
|
||||
}
|
||||
|
||||
function getTeamSideName(team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"], fallback?: unknown): string {
|
||||
return String(team?.name || fallback || "");
|
||||
}
|
||||
|
||||
function getTeamSideLogo(team: MatchResponseDto["homeTeam"] | MatchResponseDto["awayTeam"], fallback?: unknown): string {
|
||||
return String(team?.logo || team?.logoUrl || fallback || "");
|
||||
}
|
||||
|
||||
function getLeagueLabel(match: MatchResponseDto): string {
|
||||
return String(match.leagueName || match.league?.name || "");
|
||||
}
|
||||
|
||||
export default function TeamDetailContent() {
|
||||
const t = useTranslations();
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const teamId = params.id as string;
|
||||
|
||||
const { data: teamData, isLoading: teamLoading } = useTeamById(teamId);
|
||||
const { data: matchesData, isLoading: matchesLoading } = useTeamMatches(teamId, { limit: 30 });
|
||||
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = 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>
|
||||
);
|
||||
}
|
||||
|
||||
// Separate past and upcoming matches
|
||||
const isFinished = (m: MatchResponseDto) => isMatchFinished(m);
|
||||
|
||||
const pastMatches = matches.filter((m: MatchResponseDto) => isFinished(m));
|
||||
const upcomingMatches = matches.filter((m: MatchResponseDto) => !isFinished(m));
|
||||
|
||||
const getStatusBadge = (match: MatchResponseDto) => {
|
||||
if (isMatchLive(match))
|
||||
return (
|
||||
<Badge colorPalette="red" variant="subtle" fontSize="xs">
|
||||
Canlı
|
||||
</Badge>
|
||||
);
|
||||
if (isMatchFinished(match))
|
||||
return (
|
||||
<Badge colorPalette="gray" variant="subtle" fontSize="xs">
|
||||
Bitti
|
||||
</Badge>
|
||||
);
|
||||
return (
|
||||
<Badge colorPalette="green" variant="subtle" fontSize="xs">
|
||||
Yaklaşan
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideUp>
|
||||
<Box>
|
||||
{/* Back Button */}
|
||||
<Button variant="ghost" size="sm" mb={4} onClick={() => router.back()} gap={1.5}>
|
||||
<LuArrowLeft />
|
||||
Geri
|
||||
</Button>
|
||||
|
||||
{/* Team Header */}
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl" mb={6}>
|
||||
<Card.Body>
|
||||
<HStack gap={6} justify="center" align="center">
|
||||
{team.logo ? (
|
||||
<Image
|
||||
src={team.logo}
|
||||
alt={team.name}
|
||||
boxSize="80px"
|
||||
objectFit="contain"
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
boxSize="80px"
|
||||
bg="primary.subtle"
|
||||
borderRadius="xl"
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<Text fontSize="3xl" fontWeight="bold" color="primary.fg">
|
||||
{team.name?.charAt(0) || "T"}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
<VStack gap={1} align="start">
|
||||
<Heading as="h1" size="xl">
|
||||
{team.name}
|
||||
</Heading>
|
||||
{team.country && (
|
||||
<Text fontSize="md" color="fg.muted">
|
||||
🌍 {team.country}
|
||||
</Text>
|
||||
)}
|
||||
<HStack gap={4} mt={1}>
|
||||
<Badge colorPalette="blue" variant="subtle">
|
||||
<LuTrophy style={{ width: 12, height: 12 }} />
|
||||
{matches.length} Maç
|
||||
</Badge>
|
||||
<Badge colorPalette="green" variant="subtle">
|
||||
<LuCalendar style={{ width: 12, height: 12 }} />
|
||||
{upcomingMatches.length} Yaklaşan
|
||||
</Badge>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
|
||||
{/* Upcoming Matches */}
|
||||
{upcomingMatches.length > 0 && (
|
||||
<FadeIn>
|
||||
<Box mb={6}>
|
||||
<Heading as="h2" size="lg" mb={4}>
|
||||
📅 Yaklaşan Maçlar
|
||||
</Heading>
|
||||
<VStack gap={2} align="stretch">
|
||||
{upcomingMatches.map((match: MatchResponseDto) => (
|
||||
<MatchRow
|
||||
key={match.id}
|
||||
match={match}
|
||||
cardBg={cardBg}
|
||||
borderColor={borderColor}
|
||||
statusBadge={getStatusBadge(match)}
|
||||
onClick={() => router.push(`/tr/matches/${match.id}`)}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
</FadeIn>
|
||||
)}
|
||||
|
||||
{/* Past Matches */}
|
||||
<FadeIn>
|
||||
<Box>
|
||||
<Heading as="h2" size="lg" mb={4}>
|
||||
📊 Geçmiş Maçlar
|
||||
</Heading>
|
||||
{matchesLoading ? (
|
||||
<Flex justify="center" py={8}>
|
||||
<Spinner size="md" color="primary.500" />
|
||||
</Flex>
|
||||
) : pastMatches.length === 0 ? (
|
||||
<Text color="fg.muted" textAlign="center" py={8}>
|
||||
Geçmiş maç bulunamadı
|
||||
</Text>
|
||||
) : (
|
||||
<VStack gap={2} align="stretch">
|
||||
{pastMatches.map((match: MatchResponseDto) => (
|
||||
<MatchRow
|
||||
key={match.id}
|
||||
match={match}
|
||||
cardBg={cardBg}
|
||||
borderColor={borderColor}
|
||||
statusBadge={getStatusBadge(match)}
|
||||
onClick={() => router.push(`/tr/matches/${match.id}`)}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</Box>
|
||||
</FadeIn>
|
||||
</Box>
|
||||
</SlideUp>
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────
|
||||
// Match Row Component
|
||||
// ─────────────────────────────────────────────────
|
||||
|
||||
interface MatchRowProps {
|
||||
match: MatchResponseDto;
|
||||
cardBg: string;
|
||||
borderColor: string;
|
||||
statusBadge: React.ReactNode;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function MatchRow({ match, cardBg, borderColor, statusBadge, onClick }: MatchRowProps) {
|
||||
const hoverBg = useColorModeValue("gray.50", "gray.700");
|
||||
const matchTimestamp = getMatchTimestamp(match);
|
||||
const homeTeamName = getTeamSideName(match.homeTeam, match.homeTeamName);
|
||||
const awayTeamName = getTeamSideName(match.awayTeam, match.awayTeamName);
|
||||
const homeTeamLogo = getTeamSideLogo(match.homeTeam, match.homeTeamLogo);
|
||||
const awayTeamLogo = getTeamSideLogo(match.awayTeam, match.awayTeamLogo);
|
||||
const leagueLabel = getLeagueLabel(match);
|
||||
const hasScore = isMatchFinished(match) || isMatchLive(match);
|
||||
|
||||
return (
|
||||
<Card.Root
|
||||
bg={cardBg}
|
||||
borderColor={borderColor}
|
||||
borderRadius="lg"
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
_hover={{ bg: hoverBg, transform: "translateY(-1px)", shadow: "sm" }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Card.Body py={3} px={4}>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack gap={3} flex={1}>
|
||||
{/* Home Team */}
|
||||
<HStack gap={2} flex={1} justify="flex-end">
|
||||
<Text fontSize="sm" fontWeight="600" textAlign="right" truncate>
|
||||
{homeTeamName}
|
||||
</Text>
|
||||
{homeTeamLogo ? (
|
||||
<Image src={homeTeamLogo} alt="" boxSize="24px" objectFit="contain" flexShrink={0} />
|
||||
) : (
|
||||
<Flex boxSize="24px" bg="primary.subtle" borderRadius="full" align="center" justify="center" flexShrink={0}>
|
||||
<Text fontSize="xs" fontWeight="bold">{homeTeamName?.charAt(0)}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
{/* Score / VS */}
|
||||
<VStack gap={0} flexShrink={0} minW="60px">
|
||||
{hasScore && match.scoreHome !== undefined && match.scoreHome !== null ? (
|
||||
<Text fontSize="md" fontWeight="900">
|
||||
{match.scoreHome} - {match.scoreAway}
|
||||
</Text>
|
||||
) : (
|
||||
<Text fontSize="sm" color="fg.muted" fontWeight="600">
|
||||
vs
|
||||
</Text>
|
||||
)}
|
||||
<Text fontSize="2xs" color="fg.muted">
|
||||
{matchTimestamp
|
||||
? new Date(matchTimestamp).toLocaleDateString("tr-TR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
})
|
||||
: "-"}
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
{/* Away Team */}
|
||||
<HStack gap={2} flex={1}>
|
||||
{awayTeamLogo ? (
|
||||
<Image src={awayTeamLogo} alt="" boxSize="24px" objectFit="contain" flexShrink={0} />
|
||||
) : (
|
||||
<Flex boxSize="24px" bg="primary.subtle" borderRadius="full" align="center" justify="center" flexShrink={0}>
|
||||
<Text fontSize="xs" fontWeight="bold">{awayTeamName?.charAt(0)}</Text>
|
||||
</Flex>
|
||||
)}
|
||||
<Text fontSize="sm" fontWeight="600" truncate>
|
||||
{awayTeamName}
|
||||
</Text>
|
||||
</HStack>
|
||||
</HStack>
|
||||
|
||||
{/* Status + League */}
|
||||
<HStack gap={2} flexShrink={0} ml={3}>
|
||||
{leagueLabel && (
|
||||
<Text fontSize="2xs" color="fg.muted" display={{ base: "none", md: "block" }}>
|
||||
{leagueLabel}
|
||||
</Text>
|
||||
)}
|
||||
{statusBadge}
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Input,
|
||||
Text,
|
||||
VStack,
|
||||
HStack,
|
||||
Heading,
|
||||
Image,
|
||||
Spinner,
|
||||
Card,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import { SlideUp } from "@/components/motion";
|
||||
import { useSearchTeams } from "@/lib/api/leagues/use-hooks";
|
||||
import { useRouter } from "@/i18n/navigation";
|
||||
import { LuSearch } from "react-icons/lu";
|
||||
import type { TeamDto } from "@/lib/api/leagues/types";
|
||||
|
||||
export default function TeamsContent() {
|
||||
const t = useTranslations();
|
||||
const [query, setQuery] = useState("");
|
||||
const router = useRouter();
|
||||
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||
const hoverBg = useColorModeValue("gray.50", "gray.700");
|
||||
|
||||
const { data: searchData, isLoading } = useSearchTeams({ q: query });
|
||||
const teams: TeamDto[] = searchData?.data ?? [];
|
||||
|
||||
return (
|
||||
<SlideUp>
|
||||
<Box>
|
||||
<Heading as="h1" size="xl" mb={6}>
|
||||
🔍 {t("nav.teams")}
|
||||
</Heading>
|
||||
|
||||
{/* Search Bar */}
|
||||
<Flex
|
||||
align="center"
|
||||
bg={cardBg}
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor={borderColor}
|
||||
px={4}
|
||||
py={2}
|
||||
mb={6}
|
||||
gap={3}
|
||||
>
|
||||
<LuSearch style={{ opacity: 0.5, flexShrink: 0 }} />
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="Takım adı yazın... (min 2 karakter)"
|
||||
variant="flushed"
|
||||
size="lg"
|
||||
fontSize="md"
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
{/* Results */}
|
||||
{isLoading ? (
|
||||
<Flex justify="center" py={10}>
|
||||
<Spinner size="lg" color="primary.500" />
|
||||
</Flex>
|
||||
) : query.length < 2 ? (
|
||||
<Flex justify="center" py={16} direction="column" align="center" gap={3}>
|
||||
<Text fontSize="5xl">⚽</Text>
|
||||
<Text color="fg.muted" fontSize="lg">
|
||||
Aramak istediğiniz takımın adını yazın
|
||||
</Text>
|
||||
<Text color="fg.muted" fontSize="sm">
|
||||
Örnek: Galatasaray, Barcelona, Manchester City
|
||||
</Text>
|
||||
</Flex>
|
||||
) : teams.length === 0 ? (
|
||||
<Flex justify="center" py={10}>
|
||||
<Text color="fg.muted">Sonuç bulunamadı</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<VStack gap={3} align="stretch">
|
||||
{teams.map((team: TeamDto) => (
|
||||
<Card.Root
|
||||
key={team.id}
|
||||
bg={cardBg}
|
||||
borderColor={borderColor}
|
||||
borderRadius="xl"
|
||||
cursor="pointer"
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
bg: hoverBg,
|
||||
transform: "translateY(-2px)",
|
||||
shadow: "md",
|
||||
}}
|
||||
onClick={() => router.push(`/teams/${team.id}`)}
|
||||
>
|
||||
<Card.Body>
|
||||
<HStack gap={4}>
|
||||
{team.logo ? (
|
||||
<Image
|
||||
src={team.logo}
|
||||
alt={team.name}
|
||||
boxSize="48px"
|
||||
objectFit="contain"
|
||||
borderRadius="lg"
|
||||
/>
|
||||
) : (
|
||||
<Flex
|
||||
boxSize="48px"
|
||||
bg="primary.subtle"
|
||||
borderRadius="lg"
|
||||
align="center"
|
||||
justify="center"
|
||||
>
|
||||
<Text fontSize="xl" fontWeight="bold" color="primary.fg">
|
||||
{team.name?.charAt(0) || "T"}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
<Box flex={1}>
|
||||
<Text fontSize="md" fontWeight="700">
|
||||
{team.name}
|
||||
</Text>
|
||||
{team.country && (
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
{team.country}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Text fontSize="sm" color="fg.muted">
|
||||
→
|
||||
</Text>
|
||||
</HStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</Box>
|
||||
</SlideUp>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user