diff --git a/messages/en.json b/messages/en.json index 0c7f654..c6efb24 100644 --- a/messages/en.json +++ b/messages/en.json @@ -47,7 +47,6 @@ "low": "Low", "medium": "Medium", "high": "High", - "nav": { "home": "Home", "dashboard": "Dashboard", @@ -68,7 +67,6 @@ "coupons": "Coupons", "tools": "Tools" }, - "landing": { "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.", @@ -88,7 +86,6 @@ "stats-users": "Active Users", "stats-matches": "Matches Analyzed" }, - "dashboard": { "title": "Dashboard", "welcome": "Welcome back", @@ -101,7 +98,6 @@ "no-matches": "No matches available today.", "no-predictions": "No predictions available." }, - "matches": { "title": "Matches", "filter-sport": "Sport", @@ -138,9 +134,11 @@ "red-card": "Red Card", "substitution": "Substitution", "starters": "Starting XI", - "substitutes": "Substitutes" + "substitutes": "Substitutes", + "all-matches": "All Matches", + "today-matches": "Today's Matches", + "next-1-hour": "Next 1 Hour" }, - "predictions": { "title": "Predictions", "upcoming": "Upcoming", @@ -268,7 +266,6 @@ "recommended-stake-inline": "Suggested size" } }, - "coupons": { "title": "Coupon Builder", "builder-title": "Coupon Builder", @@ -392,7 +389,6 @@ "engine-mode-label": "Engine Mode", "engine-mode-help": "AI: Gemini-based AI prediction. Frequency: Database-driven statistical analysis." }, - "profile": { "title": "Profile", "account-settings": "Account Settings", @@ -417,7 +413,6 @@ "win-rate": "Win Rate", "total-profit": "Total Profit" }, - "leagues": { "title": "Leagues & Teams", "countries": "Countries", @@ -425,7 +420,6 @@ "countries-leagues": "Countries & Leagues", "search-at-least-2": "Type at least 2 characters to search teams." }, - "h2h": { "title": "Head to Head", "team-1": "Team 1", @@ -435,7 +429,6 @@ "draws": "Draws", "no-matches-found": "No head-to-head matches found between these teams." }, - "analysis": { "title": "Multi-Match Analysis", "select-matches": "Select Matches", @@ -447,7 +440,6 @@ "matches-analyzed": "matches analyzed", "no-history": "No analysis history yet." }, - "spor-toto": { "title": "Spor Toto", "sync-bulletins": "Sync Bulletins", @@ -475,7 +467,6 @@ "rollover-stats": "Rollover Stats", "prediction-generated": "Prediction generated successfully!" }, - "admin": { "title": "Admin Panel", "subtitle": "Manage users, monitor predictions, and system overview.", @@ -505,7 +496,6 @@ "user-status": "Status", "no-users": "No users found." }, - "common": { "loading": "Loading...", "save": "Save", @@ -535,7 +525,6 @@ "showing": "Showing", "results": "results" }, - "seo": { "global": { "title": "iddaai.com | AI-Powered Betting Predictions", diff --git a/messages/tr.json b/messages/tr.json index a05f4de..3af689d 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -134,7 +134,10 @@ "red-card": "Kırmızı Kart", "substitution": "Oyuncu Değişikliği", "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": { "title": "Tahminler", diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts index 2f2f505..1f73211 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,16 +2,27 @@ import type { NextConfig } from "next"; import createNextIntlPlugin from "next-intl/plugin"; const nextConfig: NextConfig = { - output: 'standalone', + output: "standalone", experimental: { optimizePackageImports: ["@chakra-ui/react"], }, reactCompiler: true, 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 [ { 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*`, }, ]; }, diff --git a/src/components/matches/index.ts b/src/components/matches/index.ts index d7acc10..ece95f1 100644 --- a/src/components/matches/index.ts +++ b/src/components/matches/index.ts @@ -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"; diff --git a/src/components/matches/league-filter-bar.tsx b/src/components/matches/league-filter-bar.tsx new file mode 100644 index 0000000..5756e3b --- /dev/null +++ b/src/components/matches/league-filter-bar.tsx @@ -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 ( + + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + ); + } + + return ( + + + + + {/* "All Leagues" chip */} + 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" + > + + {t("all-leagues")} + + + + {/* League chips */} + {leagues.map((league) => { + const isActive = selectedLeagueId === league.id; + return ( + 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" + > + + {/* Flag or fallback */} + {league.countryFlag ? ( + {league.countryName + ) : league.countryName ? ( + + {league.countryName.slice(0, 2).toUpperCase()} + + ) : null} + + {/* League name + country */} + + + {league.name} + + {league.countryName && ( + + {league.countryName} + + )} + + + {/* Live badge */} + {league.liveCount > 0 && ( + + {league.liveCount} + + )} + + + ); + })} + + + + + + + ); +} diff --git a/src/components/matches/league-sidebar.tsx b/src/components/matches/league-sidebar.tsx index 2e01b8f..aade4cb 100644 --- a/src/components/matches/league-sidebar.tsx +++ b/src/components/matches/league-sidebar.tsx @@ -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({ > - {league.countryFlag && ( + {/* Country Flag or Fallback */} + {league.countryFlag ? ( {league.countryName + ) : ( + + {league.countryName?.slice(0, 2)?.toUpperCase() || "??"} + )} - - {league.name} - + + {/* League Name + Country */} + + + {league.name} + + {league.countryName && ( + + {league.countryName} + + )} + - + {/* Badges */} + {league.liveCount > 0 && ( s.leagueFilter); const setSport = useMatchStore((s) => s.setSport); const setLeague = useMatchStore((s) => s.setLeague); + + const [quickFilter, setQuickFilter] = useState("all"); + const [dateFilter, setDateFilter] = useState(""); // 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() { @@ -85,6 +106,78 @@ export default function MatchesContent() { + {/* Quick Filters */} + + + + + + + + + { + 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" + }} + /> + + + {/* Mobile League Filter Bar (visible on small screens only) */} + + + + {/* Main Content */} { const timer = setTimeout(() => setDebouncedQuery(query), 300); @@ -85,6 +88,12 @@ export default function GlobalSearch() { [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 (