first
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 4m0s

This commit is contained in:
2026-04-16 13:36:34 +03:00
parent de5e145c4e
commit fc7a1ba567
218 changed files with 32370 additions and 0 deletions
@@ -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>
);
}
+147
View File
@@ -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>
);
}