main
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 3m36s

This commit is contained in:
2026-05-05 14:06:01 +03:00
parent e3cc6702dd
commit 4df27e3e6d
10 changed files with 367 additions and 53 deletions
+1
View File
@@ -2,6 +2,7 @@ export { default as MatchCard } from "./match-card";
export { default as MatchList } from "./match-list";
export { default as SportFilter } from "./sport-filter";
export { default as LeagueSidebar } from "./league-sidebar";
export { default as LeagueFilterBar } from "./league-filter-bar";
export { default as PredictionCard } from "./prediction-card";
export { default as MatchDetailContent } from "./match-detail-content";
export { default as MatchesContent } from "./matches-content";
@@ -0,0 +1,175 @@
"use client";
import {
Box,
Flex,
Text,
Badge,
Image,
ScrollArea,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import type { ActiveLeagueDto } from "@/lib/api/matches/types";
interface LeagueFilterBarProps {
leagues: ActiveLeagueDto[];
selectedLeagueId: string | null;
onSelect: (leagueId: string | null) => void;
isLoading?: boolean;
}
/**
* LeagueFilterBar — Horizontal scrollable league filter chips for mobile.
* Shows country flag, league name, country name, and live/match count badges.
*/
export default function LeagueFilterBar({
leagues,
selectedLeagueId,
onSelect,
isLoading,
}: LeagueFilterBarProps) {
const t = useTranslations("matches");
const chipBg = useColorModeValue("white", "gray.800");
const chipBorder = useColorModeValue("gray.200", "gray.600");
const activeBg = useColorModeValue("primary.50", "primary.900");
const activeBorder = useColorModeValue("primary.400", "primary.500");
const countryText = useColorModeValue("gray.500", "gray.400");
if (isLoading) {
return (
<Flex gap={2} overflow="hidden" pb={2}>
{Array.from({ length: 5 }).map((_, i) => (
<Box
key={i}
h="42px"
w="120px"
bg="bg.muted"
borderRadius="full"
flexShrink={0}
animation="pulse 1.5s ease-in-out infinite"
/>
))}
</Flex>
);
}
return (
<ScrollArea.Root width="full" size="xs">
<ScrollArea.Viewport>
<ScrollArea.Content py="1">
<Flex gap={2} flexWrap="nowrap" pb={1}>
{/* "All Leagues" chip */}
<Box
as="button"
onClick={() => onSelect(null)}
px={3.5}
py={2}
borderRadius="full"
borderWidth="1.5px"
borderColor={selectedLeagueId === null ? activeBorder : chipBorder}
bg={selectedLeagueId === null ? activeBg : chipBg}
cursor="pointer"
flexShrink={0}
transition="all 0.15s"
_hover={{ borderColor: activeBorder }}
whiteSpace="nowrap"
>
<Text
fontSize="xs"
fontWeight={selectedLeagueId === null ? "bold" : "medium"}
color={selectedLeagueId === null ? "primary.fg" : "fg"}
>
{t("all-leagues")}
</Text>
</Box>
{/* League chips */}
{leagues.map((league) => {
const isActive = selectedLeagueId === league.id;
return (
<Box
as="button"
key={league.id}
onClick={() => onSelect(league.id)}
px={3}
py={1.5}
borderRadius="full"
borderWidth="1.5px"
borderColor={isActive ? activeBorder : chipBorder}
bg={isActive ? activeBg : chipBg}
cursor="pointer"
flexShrink={0}
transition="all 0.15s"
_hover={{ borderColor: activeBorder }}
whiteSpace="nowrap"
>
<Flex align="center" gap={1.5}>
{/* Flag or fallback */}
{league.countryFlag ? (
<Image
src={league.countryFlag}
alt={league.countryName || ""}
boxSize="14px"
objectFit="contain"
borderRadius="xs"
flexShrink={0}
/>
) : league.countryName ? (
<Flex
boxSize="14px"
bg="gray.200"
borderRadius="xs"
align="center"
justify="center"
flexShrink={0}
fontSize="6px"
fontWeight="bold"
color="gray.600"
>
{league.countryName.slice(0, 2).toUpperCase()}
</Flex>
) : null}
{/* League name + country */}
<Flex direction="column" align="flex-start" gap={0} lineHeight="1">
<Text
fontSize="xs"
fontWeight={isActive ? "bold" : "medium"}
color={isActive ? "primary.fg" : "fg"}
>
{league.name}
</Text>
{league.countryName && (
<Text fontSize="2xs" color={countryText} lineHeight="1">
{league.countryName}
</Text>
)}
</Flex>
{/* Live badge */}
{league.liveCount > 0 && (
<Badge
colorPalette="red"
variant="solid"
borderRadius="full"
fontSize="2xs"
px={1}
ml={0.5}
>
{league.liveCount}
</Badge>
)}
</Flex>
</Box>
);
})}
</Flex>
</ScrollArea.Content>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar orientation="horizontal" />
<ScrollArea.Corner />
</ScrollArea.Root>
);
}
+44 -11
View File
@@ -24,6 +24,7 @@ export default function LeagueSidebar({
const borderColor = useColorModeValue("gray.100", "gray.700");
const activeBg = useColorModeValue("primary.50", "primary.900");
const hoverBg = useColorModeValue("gray.50", "gray.750");
const countryTextColor = useColorModeValue("gray.500", "gray.400");
if (isLoading) {
return (
@@ -111,26 +112,58 @@ export default function LeagueSidebar({
>
<Flex justify="space-between" align="center">
<Flex align="center" gap={2} minW={0} flex={1}>
{league.countryFlag && (
{/* Country Flag or Fallback */}
{league.countryFlag ? (
<Image
src={league.countryFlag}
alt={league.countryName || ""}
boxSize="16px"
boxSize="18px"
objectFit="contain"
flexShrink={0}
borderRadius="sm"
/>
) : (
<Flex
boxSize="18px"
bg="gray.200"
borderRadius="sm"
align="center"
justify="center"
flexShrink={0}
fontSize="8px"
fontWeight="bold"
color="gray.600"
>
{league.countryName?.slice(0, 2)?.toUpperCase() || "??"}
</Flex>
)}
<Text
fontSize="sm"
fontWeight={isActive ? "bold" : "medium"}
color={isActive ? "primary.fg" : "fg"}
truncate
>
{league.name}
</Text>
{/* League Name + Country */}
<Box minW={0} flex={1}>
<Text
fontSize="sm"
fontWeight={isActive ? "bold" : "medium"}
color={isActive ? "primary.fg" : "fg"}
truncate
lineHeight="1.3"
>
{league.name}
</Text>
{league.countryName && (
<Text
fontSize="2xs"
color={countryTextColor}
truncate
lineHeight="1.2"
>
{league.countryName}
</Text>
)}
</Box>
</Flex>
<Flex gap={1.5} flexShrink={0}>
{/* Badges */}
<Flex gap={1.5} flexShrink={0} ml={2}>
{league.liveCount > 0 && (
<Badge
colorPalette="red"
+115 -22
View File
@@ -1,13 +1,15 @@
"use client";
import { Box, Flex, Heading } from "@chakra-ui/react";
import { useEffect } from "react";
import { Box, Flex, Heading, Group, Button } from "@chakra-ui/react";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { SlideUp } from "@/components/motion";
import { SportFilter, LeagueSidebar, MatchList } from "@/components/matches";
import { SportFilter, LeagueSidebar, LeagueFilterBar, MatchList } from "@/components/matches";
import { useQueryMatches, useActiveLeagues } from "@/lib/api/matches/use-hooks";
import { useMatchStore } from "@/lib/stores/match-store";
type QuickFilter = "all" | "today" | "live" | "next_1_hour";
export default function MatchesContent() {
const t = useTranslations("matches");
@@ -15,6 +17,9 @@ export default function MatchesContent() {
const leagueFilter = useMatchStore((s) => s.leagueFilter);
const setSport = useMatchStore((s) => s.setSport);
const setLeague = useMatchStore((s) => s.setLeague);
const [quickFilter, setQuickFilter] = useState<QuickFilter>("all");
const [dateFilter, setDateFilter] = useState<string>("");
// Fetch active leagues for sidebar
const { data: leaguesData, isLoading: leaguesLoading } =
@@ -26,42 +31,58 @@ export default function MatchesContent() {
// Trigger query on sport/league change
const { data: matchesData, isPending: matchesLoading } = (() => {
// We use the queryMatches mutation for initial data
// but for the UI we want a reactive approach.
// Let's use the standard list with league filter
return {
data: queryMatches.data,
isPending: queryMatches.isPending,
};
})();
const triggerQuery = (currentSport: typeof sport, currentLeague: string | null, currentFilter: QuickFilter, currentDate?: string) => {
const payload: any = {
sport: currentSport,
leagueId: currentLeague || undefined,
limit: 100,
};
if (currentDate) {
payload.date = currentDate;
} else if (currentFilter === "today") {
// YYYY-MM-DD for today
payload.date = new Date().toISOString().split("T")[0];
} else if (currentFilter === "live") {
payload.status = "LIVE";
} else if (currentFilter === "next_1_hour") {
payload.dateRange = {
from: new Date().toISOString(),
to: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
};
}
queryMatches.mutate(payload);
};
// Auto-trigger query when sport or league changes
const handleSportChange = (newSport: typeof sport) => {
setSport(newSport);
queryMatches.mutate({
sport: newSport,
leagueId: undefined,
limit: 100,
});
setLeague(null);
triggerQuery(newSport, null, quickFilter, dateFilter);
};
const handleLeagueChange = (leagueId: string | null) => {
setLeague(leagueId);
queryMatches.mutate({
sport,
leagueId: leagueId || undefined,
limit: 100,
});
triggerQuery(sport, leagueId, quickFilter, dateFilter);
};
const handleQuickFilterChange = (filter: QuickFilter) => {
setDateFilter(""); // Clear specific date
setQuickFilter(filter);
triggerQuery(sport, leagueFilter, filter, undefined);
};
// Initial load
useEffect(() => {
if (!queryMatches.data && !queryMatches.isPending) {
queryMatches.mutate({
sport,
leagueId: leagueFilter || undefined,
limit: 100,
});
triggerQuery(sport, leagueFilter, quickFilter, dateFilter);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -75,7 +96,7 @@ export default function MatchesContent() {
<Flex
justify="space-between"
align="center"
mb={6}
mb={4}
flexWrap="wrap"
gap={3}
>
@@ -85,6 +106,78 @@ export default function MatchesContent() {
<SportFilter value={sport} onChange={handleSportChange} />
</Flex>
{/* Quick Filters */}
<Flex mb={6} overflowX="auto" pb={2} css={{ "&::-webkit-scrollbar": { display: "none" } }} gap={4} align="center">
<Group attached>
<Button
size="sm"
onClick={() => handleQuickFilterChange("all")}
colorPalette={quickFilter === "all" ? "primary" : "gray"}
variant={quickFilter === "all" ? "solid" : "outline"}
>
{t("all-matches")}
</Button>
<Button
size="sm"
onClick={() => handleQuickFilterChange("today")}
colorPalette={quickFilter === "today" ? "primary" : "gray"}
variant={quickFilter === "today" ? "solid" : "outline"}
>
{t("today-matches")}
</Button>
<Button
size="sm"
onClick={() => handleQuickFilterChange("live")}
colorPalette={quickFilter === "live" ? "primary" : "gray"}
variant={quickFilter === "live" ? "solid" : "outline"}
>
{t("live")}
</Button>
<Button
size="sm"
onClick={() => handleQuickFilterChange("next_1_hour")}
colorPalette={quickFilter === "next_1_hour" ? "primary" : "gray"}
variant={quickFilter === "next_1_hour" ? "solid" : "outline"}
>
{t("next-1-hour")}
</Button>
</Group>
<input
type="date"
value={dateFilter}
onChange={(e) => {
const dateVal = e.target.value;
setDateFilter(dateVal);
if (dateVal) {
setQuickFilter("all"); // Reset quick filter highlight
triggerQuery(sport, leagueFilter, "all", dateVal);
} else {
handleQuickFilterChange("all");
}
}}
style={{
padding: "0.25rem 0.5rem",
borderRadius: "0.375rem",
border: "1px solid var(--chakra-colors-gray-200)",
fontSize: "0.875rem",
background: "transparent",
color: "inherit",
outline: "none"
}}
/>
</Flex>
{/* Mobile League Filter Bar (visible on small screens only) */}
<Box display={{ base: "block", lg: "none" }} mb={4}>
<LeagueFilterBar
leagues={leagues}
selectedLeagueId={leagueFilter}
onSelect={handleLeagueChange}
isLoading={leaguesLoading}
/>
</Box>
{/* Main Content */}
<Flex
gap={6}