This commit is contained in:
+4
-15
@@ -47,7 +47,6 @@
|
|||||||
"low": "Low",
|
"low": "Low",
|
||||||
"medium": "Medium",
|
"medium": "Medium",
|
||||||
"high": "High",
|
"high": "High",
|
||||||
|
|
||||||
"nav": {
|
"nav": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
@@ -68,7 +67,6 @@
|
|||||||
"coupons": "Coupons",
|
"coupons": "Coupons",
|
||||||
"tools": "Tools"
|
"tools": "Tools"
|
||||||
},
|
},
|
||||||
|
|
||||||
"landing": {
|
"landing": {
|
||||||
"hero-title": "AI-Powered Betting Predictions",
|
"hero-title": "AI-Powered Betting Predictions",
|
||||||
"hero-subtitle": "Make smarter bets with our advanced AI prediction engine. Analyze matches, discover value bets, and build winning coupons.",
|
"hero-subtitle": "Make smarter bets with our advanced AI prediction engine. Analyze matches, discover value bets, and build winning coupons.",
|
||||||
@@ -88,7 +86,6 @@
|
|||||||
"stats-users": "Active Users",
|
"stats-users": "Active Users",
|
||||||
"stats-matches": "Matches Analyzed"
|
"stats-matches": "Matches Analyzed"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"welcome": "Welcome back",
|
"welcome": "Welcome back",
|
||||||
@@ -101,7 +98,6 @@
|
|||||||
"no-matches": "No matches available today.",
|
"no-matches": "No matches available today.",
|
||||||
"no-predictions": "No predictions available."
|
"no-predictions": "No predictions available."
|
||||||
},
|
},
|
||||||
|
|
||||||
"matches": {
|
"matches": {
|
||||||
"title": "Matches",
|
"title": "Matches",
|
||||||
"filter-sport": "Sport",
|
"filter-sport": "Sport",
|
||||||
@@ -138,9 +134,11 @@
|
|||||||
"red-card": "Red Card",
|
"red-card": "Red Card",
|
||||||
"substitution": "Substitution",
|
"substitution": "Substitution",
|
||||||
"starters": "Starting XI",
|
"starters": "Starting XI",
|
||||||
"substitutes": "Substitutes"
|
"substitutes": "Substitutes",
|
||||||
|
"all-matches": "All Matches",
|
||||||
|
"today-matches": "Today's Matches",
|
||||||
|
"next-1-hour": "Next 1 Hour"
|
||||||
},
|
},
|
||||||
|
|
||||||
"predictions": {
|
"predictions": {
|
||||||
"title": "Predictions",
|
"title": "Predictions",
|
||||||
"upcoming": "Upcoming",
|
"upcoming": "Upcoming",
|
||||||
@@ -268,7 +266,6 @@
|
|||||||
"recommended-stake-inline": "Suggested size"
|
"recommended-stake-inline": "Suggested size"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"coupons": {
|
"coupons": {
|
||||||
"title": "Coupon Builder",
|
"title": "Coupon Builder",
|
||||||
"builder-title": "Coupon Builder",
|
"builder-title": "Coupon Builder",
|
||||||
@@ -392,7 +389,6 @@
|
|||||||
"engine-mode-label": "Engine Mode",
|
"engine-mode-label": "Engine Mode",
|
||||||
"engine-mode-help": "AI: Gemini-based AI prediction. Frequency: Database-driven statistical analysis."
|
"engine-mode-help": "AI: Gemini-based AI prediction. Frequency: Database-driven statistical analysis."
|
||||||
},
|
},
|
||||||
|
|
||||||
"profile": {
|
"profile": {
|
||||||
"title": "Profile",
|
"title": "Profile",
|
||||||
"account-settings": "Account Settings",
|
"account-settings": "Account Settings",
|
||||||
@@ -417,7 +413,6 @@
|
|||||||
"win-rate": "Win Rate",
|
"win-rate": "Win Rate",
|
||||||
"total-profit": "Total Profit"
|
"total-profit": "Total Profit"
|
||||||
},
|
},
|
||||||
|
|
||||||
"leagues": {
|
"leagues": {
|
||||||
"title": "Leagues & Teams",
|
"title": "Leagues & Teams",
|
||||||
"countries": "Countries",
|
"countries": "Countries",
|
||||||
@@ -425,7 +420,6 @@
|
|||||||
"countries-leagues": "Countries & Leagues",
|
"countries-leagues": "Countries & Leagues",
|
||||||
"search-at-least-2": "Type at least 2 characters to search teams."
|
"search-at-least-2": "Type at least 2 characters to search teams."
|
||||||
},
|
},
|
||||||
|
|
||||||
"h2h": {
|
"h2h": {
|
||||||
"title": "Head to Head",
|
"title": "Head to Head",
|
||||||
"team-1": "Team 1",
|
"team-1": "Team 1",
|
||||||
@@ -435,7 +429,6 @@
|
|||||||
"draws": "Draws",
|
"draws": "Draws",
|
||||||
"no-matches-found": "No head-to-head matches found between these teams."
|
"no-matches-found": "No head-to-head matches found between these teams."
|
||||||
},
|
},
|
||||||
|
|
||||||
"analysis": {
|
"analysis": {
|
||||||
"title": "Multi-Match Analysis",
|
"title": "Multi-Match Analysis",
|
||||||
"select-matches": "Select Matches",
|
"select-matches": "Select Matches",
|
||||||
@@ -447,7 +440,6 @@
|
|||||||
"matches-analyzed": "matches analyzed",
|
"matches-analyzed": "matches analyzed",
|
||||||
"no-history": "No analysis history yet."
|
"no-history": "No analysis history yet."
|
||||||
},
|
},
|
||||||
|
|
||||||
"spor-toto": {
|
"spor-toto": {
|
||||||
"title": "Spor Toto",
|
"title": "Spor Toto",
|
||||||
"sync-bulletins": "Sync Bulletins",
|
"sync-bulletins": "Sync Bulletins",
|
||||||
@@ -475,7 +467,6 @@
|
|||||||
"rollover-stats": "Rollover Stats",
|
"rollover-stats": "Rollover Stats",
|
||||||
"prediction-generated": "Prediction generated successfully!"
|
"prediction-generated": "Prediction generated successfully!"
|
||||||
},
|
},
|
||||||
|
|
||||||
"admin": {
|
"admin": {
|
||||||
"title": "Admin Panel",
|
"title": "Admin Panel",
|
||||||
"subtitle": "Manage users, monitor predictions, and system overview.",
|
"subtitle": "Manage users, monitor predictions, and system overview.",
|
||||||
@@ -505,7 +496,6 @@
|
|||||||
"user-status": "Status",
|
"user-status": "Status",
|
||||||
"no-users": "No users found."
|
"no-users": "No users found."
|
||||||
},
|
},
|
||||||
|
|
||||||
"common": {
|
"common": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -535,7 +525,6 @@
|
|||||||
"showing": "Showing",
|
"showing": "Showing",
|
||||||
"results": "results"
|
"results": "results"
|
||||||
},
|
},
|
||||||
|
|
||||||
"seo": {
|
"seo": {
|
||||||
"global": {
|
"global": {
|
||||||
"title": "iddaai.com | AI-Powered Betting Predictions",
|
"title": "iddaai.com | AI-Powered Betting Predictions",
|
||||||
|
|||||||
+4
-1
@@ -134,7 +134,10 @@
|
|||||||
"red-card": "Kırmızı Kart",
|
"red-card": "Kırmızı Kart",
|
||||||
"substitution": "Oyuncu Değişikliği",
|
"substitution": "Oyuncu Değişikliği",
|
||||||
"starters": "İlk 11",
|
"starters": "İlk 11",
|
||||||
"substitutes": "Yedekler"
|
"substitutes": "Yedekler",
|
||||||
|
"all-matches": "Tüm Maçlar",
|
||||||
|
"today-matches": "Bugünün Maçları",
|
||||||
|
"next-1-hour": "1 Saat İçinde"
|
||||||
},
|
},
|
||||||
"predictions": {
|
"predictions": {
|
||||||
"title": "Tahminler",
|
"title": "Tahminler",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/types/routes.d.ts";
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
+13
-2
@@ -2,16 +2,27 @@ import type { NextConfig } from "next";
|
|||||||
import createNextIntlPlugin from "next-intl/plugin";
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: 'standalone',
|
output: "standalone",
|
||||||
experimental: {
|
experimental: {
|
||||||
optimizePackageImports: ["@chakra-ui/react"],
|
optimizePackageImports: ["@chakra-ui/react"],
|
||||||
},
|
},
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
|
||||||
|
if (!apiUrl) {
|
||||||
|
throw new Error("url is not defined");
|
||||||
|
}
|
||||||
|
// Remove the trailing /api to map uploads from the base backend url
|
||||||
|
const backendUrl = apiUrl.replace(/\/api\/?$/, "");
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/api/backend/:path*",
|
source: "/api/backend/:path*",
|
||||||
destination: `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3005/api'}/:path*`,
|
destination: `${apiUrl}/:path*`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/uploads/:path*",
|
||||||
|
destination: `${backendUrl}/uploads/:path*`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { default as MatchCard } from "./match-card";
|
|||||||
export { default as MatchList } from "./match-list";
|
export { default as MatchList } from "./match-list";
|
||||||
export { default as SportFilter } from "./sport-filter";
|
export { default as SportFilter } from "./sport-filter";
|
||||||
export { default as LeagueSidebar } from "./league-sidebar";
|
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 PredictionCard } from "./prediction-card";
|
||||||
export { default as MatchDetailContent } from "./match-detail-content";
|
export { default as MatchDetailContent } from "./match-detail-content";
|
||||||
export { default as MatchesContent } from "./matches-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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ export default function LeagueSidebar({
|
|||||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||||
const activeBg = useColorModeValue("primary.50", "primary.900");
|
const activeBg = useColorModeValue("primary.50", "primary.900");
|
||||||
const hoverBg = useColorModeValue("gray.50", "gray.750");
|
const hoverBg = useColorModeValue("gray.50", "gray.750");
|
||||||
|
const countryTextColor = useColorModeValue("gray.500", "gray.400");
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -111,26 +112,58 @@ export default function LeagueSidebar({
|
|||||||
>
|
>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Flex align="center" gap={2} minW={0} flex={1}>
|
<Flex align="center" gap={2} minW={0} flex={1}>
|
||||||
{league.countryFlag && (
|
{/* Country Flag or Fallback */}
|
||||||
|
{league.countryFlag ? (
|
||||||
<Image
|
<Image
|
||||||
src={league.countryFlag}
|
src={league.countryFlag}
|
||||||
alt={league.countryName || ""}
|
alt={league.countryName || ""}
|
||||||
boxSize="16px"
|
boxSize="18px"
|
||||||
objectFit="contain"
|
objectFit="contain"
|
||||||
flexShrink={0}
|
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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* League Name + Country */}
|
||||||
|
<Box minW={0} flex={1}>
|
||||||
<Text
|
<Text
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
fontWeight={isActive ? "bold" : "medium"}
|
fontWeight={isActive ? "bold" : "medium"}
|
||||||
color={isActive ? "primary.fg" : "fg"}
|
color={isActive ? "primary.fg" : "fg"}
|
||||||
truncate
|
truncate
|
||||||
|
lineHeight="1.3"
|
||||||
>
|
>
|
||||||
{league.name}
|
{league.name}
|
||||||
</Text>
|
</Text>
|
||||||
|
{league.countryName && (
|
||||||
|
<Text
|
||||||
|
fontSize="2xs"
|
||||||
|
color={countryTextColor}
|
||||||
|
truncate
|
||||||
|
lineHeight="1.2"
|
||||||
|
>
|
||||||
|
{league.countryName}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Flex gap={1.5} flexShrink={0}>
|
{/* Badges */}
|
||||||
|
<Flex gap={1.5} flexShrink={0} ml={2}>
|
||||||
{league.liveCount > 0 && (
|
{league.liveCount > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
colorPalette="red"
|
colorPalette="red"
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Box, Flex, Heading } from "@chakra-ui/react";
|
import { Box, Flex, Heading, Group, Button } from "@chakra-ui/react";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { SlideUp } from "@/components/motion";
|
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 { useQueryMatches, useActiveLeagues } from "@/lib/api/matches/use-hooks";
|
||||||
import { useMatchStore } from "@/lib/stores/match-store";
|
import { useMatchStore } from "@/lib/stores/match-store";
|
||||||
|
|
||||||
|
type QuickFilter = "all" | "today" | "live" | "next_1_hour";
|
||||||
|
|
||||||
export default function MatchesContent() {
|
export default function MatchesContent() {
|
||||||
const t = useTranslations("matches");
|
const t = useTranslations("matches");
|
||||||
|
|
||||||
@@ -16,6 +18,9 @@ export default function MatchesContent() {
|
|||||||
const setSport = useMatchStore((s) => s.setSport);
|
const setSport = useMatchStore((s) => s.setSport);
|
||||||
const setLeague = useMatchStore((s) => s.setLeague);
|
const setLeague = useMatchStore((s) => s.setLeague);
|
||||||
|
|
||||||
|
const [quickFilter, setQuickFilter] = useState<QuickFilter>("all");
|
||||||
|
const [dateFilter, setDateFilter] = useState<string>("");
|
||||||
|
|
||||||
// Fetch active leagues for sidebar
|
// Fetch active leagues for sidebar
|
||||||
const { data: leaguesData, isLoading: leaguesLoading } =
|
const { data: leaguesData, isLoading: leaguesLoading } =
|
||||||
useActiveLeagues(sport);
|
useActiveLeagues(sport);
|
||||||
@@ -26,42 +31,58 @@ export default function MatchesContent() {
|
|||||||
|
|
||||||
// Trigger query on sport/league change
|
// Trigger query on sport/league change
|
||||||
const { data: matchesData, isPending: matchesLoading } = (() => {
|
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 {
|
return {
|
||||||
data: queryMatches.data,
|
data: queryMatches.data,
|
||||||
isPending: queryMatches.isPending,
|
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
|
// Auto-trigger query when sport or league changes
|
||||||
const handleSportChange = (newSport: typeof sport) => {
|
const handleSportChange = (newSport: typeof sport) => {
|
||||||
setSport(newSport);
|
setSport(newSport);
|
||||||
queryMatches.mutate({
|
setLeague(null);
|
||||||
sport: newSport,
|
triggerQuery(newSport, null, quickFilter, dateFilter);
|
||||||
leagueId: undefined,
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLeagueChange = (leagueId: string | null) => {
|
const handleLeagueChange = (leagueId: string | null) => {
|
||||||
setLeague(leagueId);
|
setLeague(leagueId);
|
||||||
queryMatches.mutate({
|
triggerQuery(sport, leagueId, quickFilter, dateFilter);
|
||||||
sport,
|
};
|
||||||
leagueId: leagueId || undefined,
|
|
||||||
limit: 100,
|
const handleQuickFilterChange = (filter: QuickFilter) => {
|
||||||
});
|
setDateFilter(""); // Clear specific date
|
||||||
|
setQuickFilter(filter);
|
||||||
|
triggerQuery(sport, leagueFilter, filter, undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!queryMatches.data && !queryMatches.isPending) {
|
if (!queryMatches.data && !queryMatches.isPending) {
|
||||||
queryMatches.mutate({
|
triggerQuery(sport, leagueFilter, quickFilter, dateFilter);
|
||||||
sport,
|
|
||||||
leagueId: leagueFilter || undefined,
|
|
||||||
limit: 100,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
@@ -75,7 +96,7 @@ export default function MatchesContent() {
|
|||||||
<Flex
|
<Flex
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
align="center"
|
align="center"
|
||||||
mb={6}
|
mb={4}
|
||||||
flexWrap="wrap"
|
flexWrap="wrap"
|
||||||
gap={3}
|
gap={3}
|
||||||
>
|
>
|
||||||
@@ -85,6 +106,78 @@ export default function MatchesContent() {
|
|||||||
<SportFilter value={sport} onChange={handleSportChange} />
|
<SportFilter value={sport} onChange={handleSportChange} />
|
||||||
</Flex>
|
</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 */}
|
{/* Main Content */}
|
||||||
<Flex
|
<Flex
|
||||||
gap={6}
|
gap={6}
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import { useSearchTeams } from "@/lib/api/leagues/use-hooks";
|
|||||||
import { useRouter } from "@/i18n/navigation";
|
import { useRouter } from "@/i18n/navigation";
|
||||||
import { LuSearch, LuX } from "react-icons/lu";
|
import { LuSearch, LuX } from "react-icons/lu";
|
||||||
import type { TeamDto } from "@/lib/api/leagues/types";
|
import type { TeamDto } from "@/lib/api/leagues/types";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
export default function GlobalSearch() {
|
export default function GlobalSearch() {
|
||||||
|
const { data: session } = useSession();
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [debouncedQuery, setDebouncedQuery] = useState("");
|
const [debouncedQuery, setDebouncedQuery] = useState("");
|
||||||
@@ -35,6 +37,7 @@ export default function GlobalSearch() {
|
|||||||
const borderColor = useColorModeValue("gray.200", "gray.700");
|
const borderColor = useColorModeValue("gray.200", "gray.700");
|
||||||
const hoverBg = useColorModeValue("gray.50", "gray.800");
|
const hoverBg = useColorModeValue("gray.50", "gray.800");
|
||||||
const inputBg = useColorModeValue("gray.50", "gray.800");
|
const inputBg = useColorModeValue("gray.50", "gray.800");
|
||||||
|
const shortcutBg = useColorModeValue("gray.100", "gray.700");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => setDebouncedQuery(query), 300);
|
const timer = setTimeout(() => setDebouncedQuery(query), 300);
|
||||||
@@ -85,6 +88,12 @@ export default function GlobalSearch() {
|
|||||||
[router],
|
[router],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If user is not logged in, don't show the team search,
|
||||||
|
// as it requires auth to view team detail pages.
|
||||||
|
if (!session) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
@@ -142,7 +151,7 @@ export default function GlobalSearch() {
|
|||||||
fontSize="xs"
|
fontSize="xs"
|
||||||
color="fg.muted"
|
color="fg.muted"
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
bg={useColorModeValue("gray.100", "gray.700")}
|
bg={shortcutBg}
|
||||||
px={1.5}
|
px={1.5}
|
||||||
py={0.5}
|
py={0.5}
|
||||||
borderRadius="md"
|
borderRadius="md"
|
||||||
|
|||||||
Reference in New Issue
Block a user