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.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?.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 (