main
Deploy Iddaai Frontend / build-and-deploy (push) Failing after 41s

This commit is contained in:
2026-05-04 18:01:01 +03:00
parent ab5864df2f
commit 0d194f7409
39 changed files with 1260 additions and 438 deletions
+1 -1
View File
@@ -163,7 +163,7 @@ export function LoginModal({ open, onOpenChange, initialMode = "login" }: LoginM
<DialogContent>
<DialogHeader>
<DialogTitle>
<Heading size="lg" color="primary.500">
<Heading as="span" size="lg" color="primary.500">
{mode === "login" ? t("auth.sign-in") : t("auth.sign-up")}
</Heading>
</DialogTitle>
+20 -27
View File
@@ -42,12 +42,15 @@ import { LoginModal } from "@/components/auth/login-modal";
import { isAdminRole } from "@/lib/auth/roles";
import { LuLogIn, LuUser, LuShield, LuZap } from "react-icons/lu";
import GlobalSearch from "@/components/search/global-search";
import Image from "next/image";
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 [loginModalMode, setLoginModalMode] = useState<"login" | "register">(
"login",
);
const router = useRouter();
const { data: session, status } = useSession();
@@ -227,36 +230,22 @@ export default function Header() {
flexShrink={0}
mr={6}
>
<Flex
boxSize="32px"
bg="primary.500"
borderRadius="lg"
align="center"
justify="center"
shadow="sm"
>
<LuZap color="white" size={18} />
</Flex>
<img
src="/logo.png"
alt="iddaai logo"
width={36}
height={36}
style={{ objectFit: "contain" }}
/>
<Box>
<Text
fontSize="md"
fontWeight="800"
fontSize="xl"
fontWeight="900"
lineHeight="1"
color={{ base: "gray.900", _dark: "white" }}
letterSpacing="-0.02em"
letterSpacing="-0.04em"
>
Suggest
</Text>
<Text
fontSize="xs"
fontWeight="600"
lineHeight="1"
mt="1px"
color={{ base: "primary.600", _dark: "primary.300" }}
letterSpacing="0.08em"
textTransform="uppercase"
>
BET
iddaai
</Text>
</Box>
</ChakraLink>
@@ -325,7 +314,11 @@ export default function Header() {
</Box>
{/* Login Modal */}
<LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} initialMode={loginModalMode} />
<LoginModal
open={loginModalOpen}
onOpenChange={setLoginModalOpen}
initialMode={loginModalMode}
/>
</>
);
}
@@ -0,0 +1,94 @@
"use client";
import { Box, Flex, Heading, Text, VStack, HStack, Badge, Spinner } from "@chakra-ui/react";
import { Link as ChakraLink } from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp } from "@/components/motion";
import { useLeagueById } from "@/lib/api/leagues/use-hooks";
import { useQuery } from "@tanstack/react-query";
import { matchesService } from "@/lib/api/matches/service";
import MatchList from "@/components/matches/match-list";
import { LuTrophy, LuMapPin, LuArrowLeft } from "react-icons/lu";
import { Link } from "@/i18n/navigation";
export default function LeagueDetailContent({ leagueId }: { leagueId: string }) {
const t = useTranslations("leagues");
const leagueQuery = useLeagueById(leagueId);
const league = leagueQuery.data?.data;
const matchesQuery = useQuery({
queryKey: ["league-matches", leagueId, league?.sport],
queryFn: () => matchesService.queryMatches({
sport: league?.sport || "football",
leagueId: leagueId,
status: "Finished",
limit: 100,
}),
enabled: !!league,
});
const bgGradient = useColorModeValue(
"linear(to-r, primary.500, primary.700)",
"linear(to-r, primary.600, primary.900)"
);
const flatMatches = matchesQuery.data?.data?.[0]?.matches || [];
return (
<Box minH="calc(100vh - 80px)">
{/* Hero Section */}
<Box bgGradient={bgGradient} color="white" pt={16} pb={20} px={6} position="relative" overflow="hidden">
<Box position="absolute" top="-20%" right="-10%" opacity={0.1} transform="rotate(15deg)">
<LuTrophy size={400} />
</Box>
<Box maxW="7xl" mx="auto" position="relative" zIndex={1}>
<SlideUp>
<VStack align="flex-start" gap={4} maxW="3xl">
<ChakraLink as={Link} href="/leagues" color="whiteAlpha.900" _hover={{ color: "white" }} display="flex" alignItems="center" gap={2} mb={2} fontWeight="medium">
<LuArrowLeft /> Liglere Dön
</ChakraLink>
{leagueQuery.isLoading ? (
<Spinner color="white" borderWidth="3px" size="xl" />
) : league ? (
<>
<HStack gap={3}>
<Badge colorScheme={league.sport === "football" ? "green" : "orange"} variant="solid" bg="whiteAlpha.300" size="lg" px={4} py={1} rounded="full">
{league.sport}
</Badge>
{league.season && (
<Badge variant="outline" color="white" borderColor="whiteAlpha.400" size="lg" px={4} py={1} rounded="full">
SEZON: {league.season}
</Badge>
)}
</HStack>
<Heading as="h1" fontSize={{ base: "3xl", md: "5xl" }} fontWeight="800" letterSpacing="tight">
{league.name}
</Heading>
<HStack fontSize="lg" color="whiteAlpha.900">
<LuMapPin />
<Text>{league.country?.name || "Global"}</Text>
</HStack>
</>
) : (
<Heading>Lig Bulunamadı</Heading>
)}
</VStack>
</SlideUp>
</Box>
</Box>
{/* Main Content Area */}
<Box maxW="7xl" mx="auto" px={6} mt={-10} position="relative" zIndex={2} pb={20}>
<SlideUp transition={{ delay: 0.1, duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}>
<Box bg={useColorModeValue("white", "gray.900")} p={{ base: 4, md: 8 }} shadow="xl" borderRadius="2xl" borderWidth="1px" borderColor={useColorModeValue("gray.200", "gray.800")}>
<Heading size="md" mb={6}>Geçmiş Maçlar</Heading>
<MatchList flatMatches={flatMatches} isLoading={matchesQuery.isLoading || leagueQuery.isLoading} />
</Box>
</SlideUp>
</Box>
</Box>
);
}
+364 -260
View File
@@ -11,7 +11,9 @@ import {
Badge,
Spinner,
Input,
Tabs,
Grid,
GridItem,
Icon,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
@@ -22,8 +24,8 @@ import {
useSearchTeams,
} from "@/lib/api/leagues/use-hooks";
import type { CountryDto, LeagueDto, TeamDto } from "@/lib/api/leagues/types";
import { LuSearch, LuGlobe, LuTrophy, LuUsers } from "react-icons/lu";
import { useState } from "react";
import { LuSearch, LuGlobe, LuTrophy, LuUsers, LuArrowRight, LuMapPin } from "react-icons/lu";
import { useMemo, useState } from "react";
import { useDebounce } from "@/hooks/use-debounce";
import { Link } from "@/i18n/navigation";
import { InputGroup } from "@/components/ui/forms/input-group";
@@ -33,13 +35,24 @@ export default function LeaguesContent() {
const t = useTranslations("leagues");
const tMatches = useTranslations("matches");
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
const bgGradient = useColorModeValue(
"linear(to-r, primary.500, primary.700)",
"linear(to-r, primary.600, primary.900)"
);
const cardBg = useColorModeValue("white", "gray.900");
const borderColor = useColorModeValue("gray.200", "gray.800");
const hoverBg = useColorModeValue("gray.50", "whiteAlpha.50");
const [activeTab, setActiveTab] = useState<"leagues" | "teams">("leagues");
const [sportFilter, setSportFilter] = useState<string>("");
const [searchQuery, setSearchQuery] = useState("");
const debouncedQuery = useDebounce(searchQuery, 300);
const [selectedCountryId, setSelectedCountryId] = useState<string | null>(null);
const [teamSearchQuery, setTeamSearchQuery] = useState("");
const debouncedTeamQuery = useDebounce(teamSearchQuery, 300);
const [countrySearchQuery, setCountrySearchQuery] = useState("");
const debouncedCountryQuery = useDebounce(countrySearchQuery, 300);
const countries = useCountries();
const leagues = useLeagues(
@@ -48,288 +61,379 @@ export default function LeaguesContent() {
: undefined,
);
const searchTeams = useSearchTeams(
debouncedQuery.length >= 2 ? { q: debouncedQuery } : { q: "" },
debouncedTeamQuery.length >= 2 ? { q: debouncedTeamQuery } : { q: "" },
);
const filteredCountries = useMemo(() => {
if (!countries.data?.data) return [];
if (!debouncedCountryQuery) return countries.data.data;
return countries.data.data.filter((c) =>
c.name.toLowerCase().includes(debouncedCountryQuery.toLowerCase())
);
}, [countries.data?.data, debouncedCountryQuery]);
const displayedLeagues = useMemo(() => {
let sourceLeagues: LeagueDto[] = leagues.data?.data || [];
if (selectedCountryId) {
sourceLeagues = sourceLeagues.filter(l => l.countryId === selectedCountryId);
}
// Apply sport filter if selected
if (sportFilter) {
return sourceLeagues.filter(l => l.sport === sportFilter);
}
return sourceLeagues;
}, [selectedCountryId, leagues.data?.data, sportFilter]);
return (
<SlideUp>
<Box maxW="6xl" mx="auto">
<Heading as="h1" size="xl" fontWeight="bold" mb={6}>
{t("title")}
</Heading>
<Box minH="calc(100vh - 80px)">
{/* Hero Section */}
<Box bgGradient={bgGradient} color="white" pt={16} pb={20} px={6} position="relative" overflow="hidden">
<Box position="absolute" top="-20%" right="-10%" opacity={0.1} transform="rotate(15deg)">
<LuTrophy size={400} />
</Box>
<Box maxW="7xl" mx="auto" position="relative" zIndex={1}>
<SlideUp>
<VStack align="center" gap={4} textAlign="center" maxW="3xl" mx="auto">
<Badge colorScheme="whiteAlpha" variant="subtle" size="lg" px={4} py={1} rounded="full">
{t("title")}
</Badge>
<Heading as="h1" fontSize={{ base: "3xl", md: "5xl" }} fontWeight="800" letterSpacing="tight">
{activeTab === "leagues" ? t("countries-leagues") : tMatches("search-teams")}
</Heading>
<Text fontSize="lg" color="whiteAlpha.800" maxW="xl">
{activeTab === "leagues"
? "Explore top football and basketball leagues around the world. Filter by country and analyze historical matches."
: "Find your favorite teams across all leagues. Get deep insights and head-to-head statistics."}
</Text>
</VStack>
</SlideUp>
</Box>
</Box>
<Tabs.Root
value={activeTab}
onValueChange={(e) => setActiveTab(e.value as "leagues" | "teams")}
>
<Tabs.List>
<Tabs.Trigger value="leagues">
<LuGlobe />
{t("countries-leagues")}
</Tabs.Trigger>
<Tabs.Trigger value="teams">
<LuUsers />
{tMatches("search-teams")}
</Tabs.Trigger>
</Tabs.List>
{/* Countries & Leagues Tab */}
<Tabs.Content value="leagues">
<Flex gap={6} direction={{ base: "column", lg: "row" }}>
{/* Countries Sidebar */}
<Box w={{ base: "full", lg: "280px" }} flexShrink={0}>
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
{/* Main Content Area - Pulled up to overlap hero */}
<Box maxW="7xl" mx="auto" px={6} mt={-10} position="relative" zIndex={2} pb={20}>
<SlideUp transition={{ delay: 0.1, duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}>
<Card.Root bg={cardBg} shadow="xl" borderRadius="2xl" borderWidth="1px" borderColor={borderColor} overflow="hidden">
{/* Tab Navigation */}
<Flex borderBottomWidth="1px" borderColor={borderColor} bg={useColorModeValue("gray.50", "whiteAlpha.50")}>
<Flex flex={1}>
<Box
flex={1} py={4} textAlign="center" cursor="pointer"
borderBottomWidth="2px"
borderColor={activeTab === "leagues" ? "primary.500" : "transparent"}
color={activeTab === "leagues" ? "primary.500" : "fg.muted"}
fontWeight={activeTab === "leagues" ? "bold" : "medium"}
onClick={() => setActiveTab("leagues")}
transition="all 0.2s"
_hover={{ bg: hoverBg }}
>
<Card.Header>
<Heading as="h4" size="sm">
<HStack gap={2}>
<LuGlobe />
<Text>{t("countries")}</Text>
</HStack>
</Heading>
</Card.Header>
<Card.Body pt={0} maxH="600px" overflowY="auto">
{countries.isLoading ? (
<Flex justify="center" py={4}>
<Spinner size="sm" />
</Flex>
) : (
<VStack gap={1} align="stretch">
{countries.data?.data?.map((country: CountryDto) => (
<Flex
key={country.id}
px={3}
py={2}
borderRadius="md"
_hover={{
bg: "gray.50",
_dark: { bg: "gray.750" },
}}
cursor="pointer"
justify="space-between"
align="center"
<HStack justify="center" gap={2}>
<LuGlobe />
<Text>{t("countries-leagues")}</Text>
</HStack>
</Box>
<Box
flex={1} py={4} textAlign="center" cursor="pointer"
borderBottomWidth="2px"
borderColor={activeTab === "teams" ? "primary.500" : "transparent"}
color={activeTab === "teams" ? "primary.500" : "fg.muted"}
fontWeight={activeTab === "teams" ? "bold" : "medium"}
onClick={() => setActiveTab("teams")}
transition="all 0.2s"
_hover={{ bg: hoverBg }}
>
<HStack justify="center" gap={2}>
<LuUsers />
<Text>{tMatches("search-teams")}</Text>
</HStack>
</Box>
</Flex>
</Flex>
{/* LEAGUES TAB */}
{activeTab === "leagues" && (
<Flex direction={{ base: "column", lg: "row" }} minH="600px">
{/* Left Sidebar: Countries */}
<Box w={{ base: "full", lg: "320px" }} borderRightWidth={{ lg: "1px" }} borderColor={borderColor} bg={useColorModeValue("gray.50", "whiteAlpha.50")}>
<VStack align="stretch" h="full" gap={0}>
<Box p={4} borderBottomWidth="1px" borderColor={borderColor} bg={cardBg}>
<InputGroup startElement={<LuSearch color="gray.400" />} w="full">
<Input
placeholder={t("countries") + "..."}
variant="subtle"
borderRadius="full"
value={countrySearchQuery}
onChange={(e) => setCountrySearchQuery(e.target.value)}
/>
</InputGroup>
</Box>
<Box flex={1} overflowY="auto" maxH={{ base: "300px", lg: "600px" }} p={2}>
{countries.isLoading ? (
<Flex justify="center" py={10}><Spinner color="primary.500" /></Flex>
) : (
<VStack gap={1} align="stretch">
<Box
px={4} py={3} borderRadius="lg" cursor="pointer"
bg={selectedCountryId === null ? "primary.500" : "transparent"}
color={selectedCountryId === null ? "white" : "fg"}
_hover={{ bg: selectedCountryId === null ? "primary.600" : hoverBg }}
onClick={() => setSelectedCountryId(null)}
transition="all 0.2s"
>
<HStack gap={2}>
{country.flag ? (
<img
src={country.flag}
width="16"
height="16"
style={{ borderRadius: "2px" }}
alt={country.name}
/>
) : null}
<Text fontSize="sm">{country.name}</Text>
<HStack justify="space-between">
<HStack gap={3}>
<LuGlobe />
<Text fontWeight={selectedCountryId === null ? "bold" : "medium"}>{t("all")}</Text>
</HStack>
<Badge size="sm" bg={selectedCountryId === null ? "whiteAlpha.300" : "gray.100"} color={selectedCountryId === null ? "white" : "fg"}>
{leagues.data?.data?.length || 0}
</Badge>
</HStack>
<Badge size="xs" colorScheme="gray">
{country.leagues?.length || 0}
</Badge>
</Flex>
))}
</VStack>
)}
</Card.Body>
</Card.Root>
</Box>
</Box>
{filteredCountries.map((country: CountryDto) => {
const isSelected = selectedCountryId === country.id;
return (
<Box
key={country.id}
px={4} py={3} borderRadius="lg" cursor="pointer"
bg={isSelected ? "primary.500" : "transparent"}
color={isSelected ? "white" : "fg"}
_hover={{ bg: isSelected ? "primary.600" : hoverBg }}
onClick={() => setSelectedCountryId(country.id)}
transition="all 0.2s"
>
<HStack justify="space-between">
<HStack gap={3}>
{country.flag ? (
<img src={country.flag} width="20" height="20" style={{ borderRadius: "50%", objectFit: "cover" }} alt={country.name} />
) : <LuMapPin />}
<Text fontWeight={isSelected ? "bold" : "medium"}>{country.name}</Text>
</HStack>
<Badge size="sm" bg={isSelected ? "whiteAlpha.300" : "gray.100"} color={isSelected ? "white" : "fg"}>
{leagues.data?.data?.filter(l => l.countryId === country.id).length || 0}
</Badge>
</HStack>
</Box>
);
})}
</VStack>
)}
</Box>
</VStack>
</Box>
{/* Leagues List */}
<Box flex={1}>
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
>
<Card.Header>
<Flex justify="space-between" align="center">
<Heading as="h4" size="sm">
<HStack gap={2}>
<LuTrophy />
<Text>{t("leagues")}</Text>
</HStack>
</Heading>
<HStack gap={2}>
<Badge
cursor="pointer"
colorScheme={!sportFilter ? "primary" : "gray"}
onClick={() => setSportFilter("")}
>
{tMatches("all")}
</Badge>
<Badge
cursor="pointer"
colorScheme={
sportFilter === "football" ? "green" : "gray"
}
onClick={() =>
setSportFilter(
sportFilter === "football" ? "" : "football",
)
}
>
{tMatches("football")}
</Badge>
<Badge
cursor="pointer"
colorScheme={
sportFilter === "basketball" ? "orange" : "gray"
}
onClick={() =>
setSportFilter(
sportFilter === "basketball" ? "" : "basketball",
)
}
>
{tMatches("basketball")}
</Badge>
</HStack>
{/* Right Area: Leagues Grid */}
<Box flex={1} p={{ base: 4, md: 8 }} bg={cardBg}>
{/* Top Filters */}
<Flex justify="space-between" align="center" mb={6} direction={{ base: "column", sm: "row" }} gap={4}>
<Heading size="md" fontWeight="bold">
{selectedCountryId
? `${countries.data?.data?.find(c => c.id === selectedCountryId)?.name} ${t("leagues")}`
: t("leagues")}
<Text as="span" color="fg.muted" ml={2} fontWeight="normal" fontSize="sm">
({displayedLeagues.length})
</Text>
</Heading>
<HStack gap={2} bg={useColorModeValue("gray.100", "gray.800")} p={1} borderRadius="full">
<Box
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
bg={!sportFilter ? "white" : "transparent"}
color={!sportFilter ? "black" : "fg.muted"}
shadow={!sportFilter ? "sm" : "none"}
onClick={() => setSportFilter("")}
transition="all 0.2s"
_dark={{ bg: !sportFilter ? "gray.600" : "transparent", color: !sportFilter ? "white" : "gray.400" }}
>
{t("all")}
</Box>
<Box
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
bg={sportFilter === "football" ? "green.500" : "transparent"}
color={sportFilter === "football" ? "white" : "fg.muted"}
shadow={sportFilter === "football" ? "sm" : "none"}
onClick={() => setSportFilter(sportFilter === "football" ? "" : "football")}
transition="all 0.2s"
>
{tMatches("football")}
</Box>
<Box
px={4} py={1.5} borderRadius="full" cursor="pointer" fontSize="sm" fontWeight="medium"
bg={sportFilter === "basketball" ? "orange.500" : "transparent"}
color={sportFilter === "basketball" ? "white" : "fg.muted"}
shadow={sportFilter === "basketball" ? "sm" : "none"}
onClick={() => setSportFilter(sportFilter === "basketball" ? "" : "basketball")}
transition="all 0.2s"
>
{tMatches("basketball")}
</Box>
</HStack>
</Flex>
{/* Leagues Grid */}
{leagues.isLoading ? (
<Flex justify="center" py={20}><Spinner size="xl" color="primary.500" borderWidth="3px" /></Flex>
) : displayedLeagues.length === 0 ? (
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
<Box bg="gray.100" _dark={{ bg: "gray.800" }} p={6} borderRadius="full" mb={4}>
<LuTrophy size={40} color="gray" />
</Box>
<Heading size="md" mb={2}>Bulunamadı</Heading>
<Text color="fg.muted">Seçili kriterlere uygun lig bulunamadı.</Text>
</Flex>
</Card.Header>
<Card.Body pt={0}>
{leagues.isLoading ? (
<Flex justify="center" py={6}>
<Spinner size="sm" />
</Flex>
) : (
<VStack gap={2}>
{leagues.data?.data?.map((league: LeagueDto) => (
) : (
<Grid templateColumns={{ base: "1fr", md: "repeat(2, 1fr)", xl: "repeat(3, 1fr)" }} gap={4}>
{displayedLeagues.map((league: LeagueDto) => (
<GridItem key={league.id}>
<ChakraLink
key={league.id}
as={Link}
href="/matches"
p={3}
borderRadius="md"
href={`/leagues/${league.id}`}
display="block"
h="full"
p={5}
borderRadius="xl"
borderWidth="1px"
borderColor={borderColor}
bg={cardBg}
_hover={{
borderColor: "primary.300",
bg: "primary.50",
_dark: { bg: "gray.750" },
shadow: "md",
transform: "translateY(-2px)",
}}
display="flex"
justifyContent="space-between"
alignItems="center"
transition="all 0.2s"
textDecoration="none"
color="inherit"
data-group
>
<VStack align="start" gap={0}>
<Text fontWeight="semibold">{league.name}</Text>
<Text fontSize="xs" color="fg.muted">
{league.country?.name || ""}
</Text>
</VStack>
<HStack gap={2}>
{league.sport ? (
<Badge
size="xs"
colorScheme={
league.sport === "football"
? "green"
: "orange"
}
>
{league.sport}
</Badge>
) : null}
{league.season ? (
<Text fontSize="xs" color="fg.muted">
{league.season}
</Text>
) : null}
<Flex justify="space-between" align="flex-start" mb={4}>
<Box p={2} borderRadius="lg" bg={league.sport === "football" ? "green.50" : "orange.50"} _dark={{ bg: league.sport === "football" ? "green.900" : "orange.900" }}>
<LuTrophy size={20} color={league.sport === "football" ? "var(--chakra-colors-green-500)" : "var(--chakra-colors-orange-500)"} />
</Box>
<Badge size="sm" variant="subtle" colorScheme={league.sport === "football" ? "green" : "orange"}>
{league.sport}
</Badge>
</Flex>
<Heading size="sm" mb={1} lineClamp={1} _groupHover={{ color: "primary.500" }}>
{league.name}
</Heading>
<HStack color="fg.muted" fontSize="sm" gap={1}>
<LuMapPin size={14} />
<Text lineClamp={1}>{league.country?.name || "Global"}</Text>
</HStack>
{league.season && (
<Flex mt={4} pt={4} borderTopWidth="1px" borderColor={borderColor} justify="space-between" align="center">
<Text fontSize="xs" color="fg.muted" fontWeight="medium">SEZON: {league.season}</Text>
<Icon as={LuArrowRight} color="gray.400" _groupHover={{ color: "primary.500", transform: "translateX(4px)" }} transition="all 0.2s" />
</Flex>
)}
</ChakraLink>
))}
</VStack>
)}
</Card.Body>
</Card.Root>
</Box>
</Flex>
</Tabs.Content>
</GridItem>
))}
</Grid>
)}
</Box>
</Flex>
)}
{/* Teams Search Tab */}
<Tabs.Content value="teams">
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Body>
<InputGroup startElement={<LuSearch />} mb={4}>
<Input
placeholder={tMatches("search-teams")}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</InputGroup>
{debouncedQuery.length < 2 ? (
<Text color="fg.muted" textAlign="center" py={8}>
{t("search-at-least-2")}
</Text>
{/* TEAMS TAB */}
{activeTab === "teams" && (
<Box p={{ base: 4, md: 8 }}>
<Box maxW="2xl" mx="auto" mb={10}>
<InputGroup startElement={<LuSearch color="gray.400" size={20} />} w="full">
<Input
placeholder={tMatches("search-teams") + "..."}
value={teamSearchQuery}
onChange={(e) => setTeamSearchQuery(e.target.value)}
variant="outline"
borderRadius="xl"
fontSize="lg"
py={6}
boxShadow="sm"
_focus={{ boxShadow: "0 0 0 2px var(--chakra-colors-primary-500)" }}
/>
</InputGroup>
</Box>
{debouncedTeamQuery.length < 2 ? (
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
<Box bg="primary.50" _dark={{ bg: "primary.900" }} p={8} borderRadius="full" mb={6}>
<LuUsers size={64} color="var(--chakra-colors-primary-500)" />
</Box>
<Heading size="lg" mb={3}>{t("search-at-least-2")}</Heading>
<Text color="fg.muted" maxW="md">
Find detailed statistics, upcoming matches, and head-to-head analysis by searching for any team worldwide.
</Text>
</Flex>
) : searchTeams.isLoading ? (
<Flex justify="center" py={6}>
<Spinner size="md" />
<Flex justify="center" py={20}><Spinner size="xl" color="primary.500" borderWidth="3px" /></Flex>
) : searchTeams.data?.data?.length === 0 ? (
<Flex direction="column" align="center" justify="center" py={20} textAlign="center">
<Heading size="md" mb={2}>Takım Bulunamadı</Heading>
<Text color="fg.muted">"{debouncedTeamQuery}" aramasıyla eşleşen bir takım bulunamadı.</Text>
</Flex>
) : (
<VStack gap={2}>
<Grid templateColumns={{ base: "1fr", md: "repeat(2, 1fr)", xl: "repeat(3, 1fr)" }} gap={4}>
{searchTeams.data?.data?.map((team: TeamDto) => (
<ChakraLink
key={team.id}
as={Link}
href={`/teams/${team.id}`}
p={3}
borderRadius="md"
borderWidth="1px"
borderColor={borderColor}
_hover={{
borderColor: "primary.300",
bg: "primary.50",
_dark: { bg: "gray.750" },
}}
display="flex"
alignItems="center"
gap={3}
textDecoration="none"
color="inherit"
>
{team.logo ? (
<img
src={team.logo}
width="32"
height="32"
style={{ borderRadius: "50%" }}
alt={team.name}
/>
) : (
<Box
boxSize="32px"
borderRadius="full"
bg="gray.200"
_dark={{ bg: "gray.600" }}
/>
)}
<VStack align="start" gap={0}>
<Text fontWeight="semibold">{team.name}</Text>
<Text fontSize="xs" color="fg.muted">
{team.country || ""}
</Text>
</VStack>
<Badge
ml="auto"
size="xs"
colorScheme={
team.sport === "football" ? "green" : "orange"
}
<GridItem key={team.id}>
<ChakraLink
as={Link}
href={`/teams/${team.id}`}
display="flex"
alignItems="center"
p={4}
borderRadius="xl"
borderWidth="1px"
borderColor={borderColor}
bg={cardBg}
_hover={{
borderColor: "primary.300",
shadow: "md",
transform: "translateY(-2px)",
}}
transition="all 0.2s"
textDecoration="none"
color="inherit"
data-group
>
{team.sport}
</Badge>
</ChakraLink>
{team.logo ? (
<Box w={12} h={12} borderRadius="full" overflow="hidden" flexShrink={0} mr={4} bg="white" p={1} shadow="sm">
<img src={team.logo} width="100%" height="100%" style={{ objectFit: "contain" }} alt={team.name} />
</Box>
) : (
<Flex w={12} h={12} borderRadius="full" bg="gray.100" _dark={{ bg: "gray.700" }} align="center" justify="center" flexShrink={0} mr={4}>
<LuUsers size={20} color="gray" />
</Flex>
)}
<VStack align="start" gap={0} flex={1}>
<Heading size="sm" lineClamp={1} _groupHover={{ color: "primary.500" }}>{team.name}</Heading>
<HStack color="fg.muted" fontSize="xs" gap={1}>
<LuMapPin size={12} />
<Text lineClamp={1}>{team.country || "Global"}</Text>
</HStack>
</VStack>
<Badge ml={2} size="sm" colorScheme={team.sport === "football" ? "green" : "orange"} variant="subtle">
{team.sport}
</Badge>
</ChakraLink>
</GridItem>
))}
</VStack>
</Grid>
)}
</Card.Body>
</Card.Root>
</Tabs.Content>
</Tabs.Root>
</Box>
)}
</Card.Root>
</SlideUp>
</Box>
</SlideUp>
</Box>
);
}
+20 -2
View File
@@ -117,6 +117,10 @@ export default function MatchList({
);
}
const sortedFlatMatches = [...flatMatches].sort(
(a, b) => Number(a.mstUtc) - Number(b.mstUtc),
);
return (
<StaggerContainer>
<Grid
@@ -127,7 +131,7 @@ export default function MatchList({
}}
gap={4}
>
{flatMatches.map((match) => (
{sortedFlatMatches.map((match) => (
<StaggerItem key={match.id}>
<MatchCard match={match} />
</StaggerItem>
@@ -148,9 +152,23 @@ export default function MatchList({
);
}
// Sort leagues by their earliest match, and sort matches within each league
const sortedLeagues = [...leagues]
.map((league) => ({
...league,
matches: [...league.matches].sort(
(a, b) => Number(a.mstUtc) - Number(b.mstUtc),
),
}))
.sort((a, b) => {
const earliestA = Math.min(...a.matches.map((m) => Number(m.mstUtc)));
const earliestB = Math.min(...b.matches.map((m) => Number(m.mstUtc)));
return earliestA - earliestB;
});
return (
<StaggerContainer>
{leagues.map((league) => (
{sortedLeagues.map((league) => (
<StaggerItem key={league.id}>
<Box mb={6}>
{/* League Header */}
+93 -28
View File
@@ -8,7 +8,14 @@ import {
useInView,
type HTMLMotionProps,
} from "framer-motion";
import { forwardRef, type ReactNode, useEffect, useRef } from "react";
import {
forwardRef,
Key,
type ReactNode,
useEffect,
useRef,
useState,
} from "react";
// ========================
// Shared animation variants
@@ -381,34 +388,92 @@ interface SparkleProps {
color?: string;
}
export function Sparkles({ count = 6, color = "rgba(56, 178, 172, 0.6)" }: SparkleProps) {
interface SparkleConfig {
id: number;
size: number;
left: number;
bottom: number;
y: number;
duration: number;
delay: number;
}
export function Sparkles({
count = 6,
color = "rgba(56, 178, 172, 0.6)",
}: SparkleProps) {
const [sparkles, setSparkles] = useState<SparkleConfig[]>([]);
useEffect(() => {
const newSparkles = Array.from({ length: count }).map((_, i) => ({
id: i,
size: 4 + Math.random() * 4,
left: 10 + Math.random() * 80,
bottom: Math.random() * 30,
y: -(60 + Math.random() * 80),
duration: 2.5 + Math.random() * 2,
delay: Math.random() * 3,
}));
setSparkles(newSparkles);
}, [count]);
if (sparkles.length === 0) {
return (
<div
style={{
position: "absolute",
inset: 0,
overflow: "hidden",
pointerEvents: "none",
}}
/>
);
}
return (
<div style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }}>
{Array.from({ length: count }).map((_, i) => (
<motion.div
key={i}
style={{
position: "absolute",
width: 4 + Math.random() * 4,
height: 4 + Math.random() * 4,
borderRadius: "50%",
background: color,
left: `${10 + Math.random() * 80}%`,
bottom: `${Math.random() * 30}%`,
}}
animate={{
y: [0, -(60 + Math.random() * 80)],
opacity: [0, 1, 1, 0],
scale: [0.5, 1, 0.8, 0],
}}
transition={{
duration: 2.5 + Math.random() * 2,
repeat: Infinity,
delay: Math.random() * 3,
ease: "easeOut",
}}
/>
))}
<div
style={{
position: "absolute",
inset: 0,
overflow: "hidden",
pointerEvents: "none",
}}
>
{sparkles.map(
(sparkle: {
id: Key | null | undefined;
size: any;
left: any;
bottom: any;
y: string | number | null;
duration: any;
delay: any;
}) => (
<motion.div
key={sparkle.id}
style={{
position: "absolute",
width: sparkle.size,
height: sparkle.size,
borderRadius: "50%",
background: color,
left: `${sparkle.left}%`,
bottom: `${sparkle.bottom}%`,
}}
animate={{
y: [0, sparkle.y],
opacity: [0, 1, 1, 0],
scale: [0.5, 1, 0.8, 0],
}}
transition={{
duration: sparkle.duration,
repeat: Infinity,
delay: sparkle.delay,
ease: "easeOut",
}}
/>
),
)}
</div>
);
}
+24 -22
View File
@@ -10,7 +10,7 @@ import {
SelectValueText,
} from '@/components/ui/collections/select';
import { useParams } from 'next/navigation';
import { createListCollection } from '@chakra-ui/react';
import { createListCollection, ClientOnly } from '@chakra-ui/react';
import { usePathname, useRouter } from '@/i18n/navigation';
const LocaleSwitcher = () => {
@@ -40,27 +40,29 @@ const LocaleSwitcher = () => {
});
}
return (
<SelectRoot
disabled={isPending}
value={[locale]}
onValueChange={onSelectChange}
w={{ base: 'full', lg: '24' }}
size='sm'
variant='outline'
borderRadius='md'
collection={collections}
>
<SelectTrigger>
<SelectValueText placeholder='Select a language' />
</SelectTrigger>
<SelectContent zIndex='9999'>
{collections.items.map((collection) => (
<SelectItem key={collection.value} item={collection}>
{collection.label}
</SelectItem>
))}
</SelectContent>
</SelectRoot>
<ClientOnly fallback={<div style={{ height: '32px' }} />}>
<SelectRoot
disabled={isPending}
value={[locale]}
onValueChange={onSelectChange}
w={{ base: 'full', lg: '24' }}
size='sm'
variant='outline'
borderRadius='md'
collection={collections}
>
<SelectTrigger>
<SelectValueText placeholder='Select a language' />
</SelectTrigger>
<SelectContent zIndex='9999'>
{collections.items.map((collection) => (
<SelectItem key={collection.value} item={collection}>
{collection.label}
</SelectItem>
))}
</SelectContent>
</SelectRoot>
</ClientOnly>
);
};