From 0354a182f3714505b912e5f3e09c4a5620b7a0dd Mon Sep 17 00:00:00 2001 From: brooktewabe Date: Mon, 2 Mar 2026 19:10:22 +0300 Subject: [PATCH] event and virtual pages --- app/event/[id]/page.tsx | 45 +++++++- app/login/page.tsx | 6 +- app/register/page.tsx | 6 +- app/rules/page.tsx | 4 +- app/virtual/page.tsx | 206 ++++++++++++++++++++----------------- lib/store/betting-types.ts | 191 ++++++++++++++++++++++++++++++++++ 6 files changed, 356 insertions(+), 102 deletions(-) create mode 100644 lib/store/betting-types.ts diff --git a/app/event/[id]/page.tsx b/app/event/[id]/page.tsx index bf92f67..915d7cd 100644 --- a/app/event/[id]/page.tsx +++ b/app/event/[id]/page.tsx @@ -1,10 +1,51 @@ import Link from "next/link" import { getEventById } from "@/lib/mock-data" import { MatchDetailView } from "@/components/betting/match-detail-view" +import { + fetchEvents, + fetchOddsForEvent, + apiEventToAppEvent, + get1X2ForEvent, + apiOddsToSections, +} from "@/lib/betting-api" +import type { Event } from "@/lib/mock-data" export default async function EventPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params - const event = getEventById(id) + let event: Event | undefined = getEventById(id) + let apiSections: { id: string; title: string; outcomes: { label: string; odds: number }[] }[] | undefined + + const numericId = id.trim() !== "" && !Number.isNaN(Number(id)) ? Number(id) : null + if (numericId !== null) { + try { + const [eventsRes, oddsRes] = await Promise.all([ + fetchEvents({ page_size: 500, page: 1 }), + fetchOddsForEvent(numericId), + ]) + const apiEvent = (eventsRes.data ?? []).find((e) => e.id === numericId) + if (apiEvent) { + event = apiEventToAppEvent(apiEvent, get1X2ForEvent(oddsRes.data ?? [], apiEvent.id)) + } else { + event = { + id: String(numericId), + sport: "Football", + sportIcon: "⚽", + league: "", + country: "", + homeTeam: "Home", + awayTeam: "Away", + time: "", + date: "", + isLive: false, + markets: [], + totalMarkets: 0, + } + } + apiSections = apiOddsToSections(oddsRes.data ?? []) + } catch { + if (!event) event = undefined + } + } if (!event) { return ( @@ -17,5 +58,5 @@ export default async function EventPage({ params }: { params: Promise<{ id: stri ) } - return + return } diff --git a/app/login/page.tsx b/app/login/page.tsx index 4727b17..c9dc277 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -41,15 +41,15 @@ export default function LoginPage() { /> ))} - {/* FORTUNE box */} + {/* HARIF box */}
- FORTUNE + HARIF
{/* SPORT text */} - BETS + SPORT diff --git a/app/register/page.tsx b/app/register/page.tsx index 7a6752f..b2fc675 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -43,15 +43,15 @@ export default function RegisterPage() { /> ))} - {/* FORTUNE box */} + {/* HARIF box */}
- FORTUNE + HARIF
{/* SPORT text */} - BETS + SPORT diff --git a/app/rules/page.tsx b/app/rules/page.tsx index 8c66f94..d1e91bd 100644 --- a/app/rules/page.tsx +++ b/app/rules/page.tsx @@ -1,7 +1,7 @@ const rules = [ { title: "General Betting Rules", - content: "All bets are subject to FortuneBets terms and conditions. By placing a bet, you agree to abide by these rules. The minimum bet amount is 5 ETB and the maximum payout is 500,000 ETB per bet.", + content: "All bets are subject to Harifsport terms and conditions. By placing a bet, you agree to abide by these rules. The minimum bet amount is 5 ETB and the maximum payout is 500,000 ETB per bet.", }, { title: "Live Betting", @@ -13,7 +13,7 @@ const rules = [ }, { title: "Responsible Gambling", - content: "FortuneBets is committed to responsible gambling. Users may set deposit limits, loss limits, or self-exclude at any time. Gambling should be entertaining, not a source of income.", + content: "Harifsport is committed to responsible gambling. Users may set deposit limits, loss limits, or self-exclude at any time. Gambling should be entertaining, not a source of income.", }, { title: "Account Rules", diff --git a/app/virtual/page.tsx b/app/virtual/page.tsx index 843169d..ace6c36 100644 --- a/app/virtual/page.tsx +++ b/app/virtual/page.tsx @@ -1,141 +1,163 @@ -"use client" +"use client"; -import { useState, useEffect } from "react" -import Image from "next/image" -import { GamingSidebar } from "@/components/games/gaming-sidebar" -import { GameRow } from "@/components/games/game-row" -import { GameCard } from "@/components/games/game-card" -import { Search, Heart, Clock, Star, Zap, Gamepad2, AlertCircle, LayoutGrid } from "lucide-react" -import { cn } from "@/lib/utils" -import api from "@/lib/api" +import { useState, useEffect } from "react"; +import Image from "next/image"; +import { GamingSidebar } from "@/components/games/gaming-sidebar"; +import { GameRow } from "@/components/games/game-row"; +import { GameCard } from "@/components/games/game-card"; +import { + Search, + Heart, + Clock, + Star, + Zap, + Gamepad2, + AlertCircle, + LayoutGrid, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import api from "@/lib/api"; interface Provider { - provider_id: string - provider_name: string - logo_dark: string - logo_light: string - enabled: boolean + provider_id: string; + provider_name: string; + logo_dark: string; + logo_light: string; + enabled: boolean; } interface ApiGame { - gameId: string - providerId: string - provider: string - name: string - category: string - deviceType: string - hasDemo: boolean - hasFreeBets: boolean - demoUrl?: string - image?: string // In case it gets added - thumbnail?: string - provider_id?: string // Fallback + gameId: string; + providerId: string; + provider: string; + name: string; + category: string; + deviceType: string; + hasDemo: boolean; + hasFreeBets: boolean; + demoUrl?: string; + image?: string; // In case it gets added + thumbnail?: string; + provider_id?: string; // Fallback } -const DEFAULT_IMAGE = "https://st.pokgaming.com/gameThumbnails/246.jpg" +const DEFAULT_IMAGE = "https://st.pokgaming.com/gameThumbnails/246.jpg"; export default function VirtualPage() { - const [activeCategory, setActiveCategory] = useState("all") - const [providers, setProviders] = useState([]) - const [games, setGames] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [error, setError] = useState(null) + const [activeCategory, setActiveCategory] = useState("all"); + const [providers, setProviders] = useState([]); + const [games, setGames] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { const fetchVirtualData = async () => { try { - setIsLoading(true) + setIsLoading(true); const [providersRes, gamesRes] = await Promise.all([ api.get("/virtual-game/orchestrator/providers"), - api.get("/virtual-game/orchestrator/games", { params: { limit: 2000 } }) - ]) - - const pData = providersRes.data - const gData = gamesRes.data + api.get("/virtual-game/orchestrator/games", { + params: { limit: 2000 }, + }), + ]); - const providersList = pData.providers || pData.data || pData || [] - const gamesList = gData.data || gData.games || gData || [] + const pData = providersRes.data; + const gData = gamesRes.data; - setProviders(Array.isArray(providersList) ? providersList : []) - setGames(Array.isArray(gamesList) ? gamesList : []) + const providersList = pData.providers || pData.data || pData || []; + const gamesList = gData.data || gData.games || gData || []; + + setProviders(Array.isArray(providersList) ? providersList : []); + setGames(Array.isArray(gamesList) ? gamesList : []); } catch (err: any) { - console.error("Failed to fetch virtual games data:", err) - setError("Failed to load games data.") + console.error("Failed to fetch virtual games data:", err); + setError("Failed to load games data."); } finally { - setIsLoading(false) + setIsLoading(false); } - } + }; - fetchVirtualData() - }, []) + fetchVirtualData(); + }, []); // Create Sidebar Categories dynamically from providers const sidebarCategories = [ { id: "all", name: "All Games", icon: LayoutGrid }, - ...providers.map(p => ({ + ...providers.map((p) => ({ id: p.provider_id, name: p.provider_name, - icon: p.logo_dark || p.logo_light || Gamepad2 - })) - ] + icon: p.logo_dark || p.logo_light || Gamepad2, + })), + ]; // Filter games based on active category // If "all", group by provider - let displayedGames: any[] = [] - let groupedGames: { title: string, games: any[] }[] = [] + let displayedGames: any[] = []; + let groupedGames: { title: string; games: any[] }[] = []; const mapApiGameToCard = (game: ApiGame) => ({ id: game.gameId, title: game.name, image: game.thumbnail || game.image || DEFAULT_IMAGE, - provider: game.provider - }) + provider: game.provider, + }); if (activeCategory === "all") { // Group up to 12 games per provider for the rows - providers.forEach(p => { - const providerIdStr = String(p.provider_id || "").trim().toLowerCase() + providers.forEach((p) => { + const providerIdStr = String(p.provider_id || "") + .trim() + .toLowerCase(); const providerGames = games - .filter(g => { - const gameProvId = String(g.providerId || g.provider_id || "").trim().toLowerCase() - return gameProvId === providerIdStr + .filter((g) => { + const gameProvId = String(g.providerId || g.provider_id || "") + .trim() + .toLowerCase(); + return gameProvId === providerIdStr; }) .slice(0, 12) - .map(mapApiGameToCard) - + .map(mapApiGameToCard); + if (providerGames.length > 0) { groupedGames.push({ title: p.provider_name, - games: providerGames - }) + games: providerGames, + }); } - }) + }); } else { - displayedGames = games - .filter(g => { - const gameProvId = String(g.providerId || g.provider_id || "").trim().toLowerCase() - const matches = gameProvId === String(activeCategory).trim().toLowerCase() - if (g.providerId?.toLowerCase().includes('pop') || g.provider_id?.toLowerCase().includes('pop')) { - } - return matches + .filter((g) => { + const gameProvId = String(g.providerId || g.provider_id || "") + .trim() + .toLowerCase(); + const matches = + gameProvId === String(activeCategory).trim().toLowerCase(); + if ( + g.providerId?.toLowerCase().includes("pop") || + g.provider_id?.toLowerCase().includes("pop") + ) { + } + return matches; }) - .map(mapApiGameToCard) - + .map(mapApiGameToCard); } const activeCategoryData = providers.find( - p => String(p.provider_id || "").trim().toLowerCase() === String(activeCategory).trim().toLowerCase() - ) + (p) => + String(p.provider_id || "") + .trim() + .toLowerCase() === String(activeCategory).trim().toLowerCase(), + ); return (
{/* Sidebar */} - @@ -155,22 +177,22 @@ export default function VirtualPage() {
{isLoading ? (
-
- Loading games... +
+ Loading games...
) : error ? (
- - {error} + + {error}
) : activeCategory === "all" ? ( // Show all categories
{groupedGames.map((category, index) => ( - ))} @@ -180,16 +202,16 @@ export default function VirtualPage() {

- {activeCategoryData?.provider_name || 'Games'} + {activeCategoryData?.provider_name || "Games"}

-
- +
{displayedGames.map((game, idx) => ( @@ -200,5 +222,5 @@ export default function VirtualPage() {
- ) + ); } diff --git a/lib/store/betting-types.ts b/lib/store/betting-types.ts new file mode 100644 index 0000000..2a63172 --- /dev/null +++ b/lib/store/betting-types.ts @@ -0,0 +1,191 @@ +/** + * Betting API and app types. Used by betting-store and betting-api. + */ + +/** Sport IDs for API (sport_id). Use for live and prematch. */ +export const SportEnum = { + SOCCER: 1, + BASKETBALL: 18, + TENNIS: 13, + VOLLEYBALL: 91, + HANDBALL: 78, + BASEBALL: 16, + HORSE_RACING: 2, + GREYHOUNDS: 4, + ICE_HOCKEY: 17, + SNOOKER: 14, + AMERICAN_FOOTBALL: 12, + CRICKET: 3, + FUTSAL: 83, + DARTS: 15, + TABLE_TENNIS: 92, + BADMINTON: 94, + RUGBY_UNION: 8, + RUGBY_LEAGUE: 19, + AUSTRALIAN_RULES: 36, + BOWLS: 66, + BOXING: 9, + GAELIC_SPORTS: 75, + FLOORBALL: 90, + BEACH_VOLLEYBALL: 95, + WATER_POLO: 110, + SQUASH: 107, + E_SPORTS: 151, + MMA: 162, + SURFING: 148, +} as const + +export type SportId = (typeof SportEnum)[keyof typeof SportEnum] + +export type ApiEvent = { + id: number + source_event_id: string + sport_id: number + match_name: string + home_team: string + away_team: string + home_team_id: number + away_team_id: number + home_team_image: string + away_team_image: string + league_id: number + league_name: string + league_cc: string + start_time: string + source: string + status: string + is_live: boolean + is_featured?: boolean + is_active?: boolean + total_odd_outcomes?: number + number_of_bets?: number + total_amount?: number + average_bet_amount?: number + total_potential_winnings?: number + score?: string + match_minute?: number + timer_status?: string + added_time?: number + match_period?: number + fetched_at?: string + updated_at?: string +} + +export type ApiLeague = { + id: number + name: string + cc: string + bet365_id?: number + sport_id: number + default_is_active: boolean + default_is_featured: boolean +} + +export type ApiOddsOutcome = { + id: string + name?: string + odds: string + header?: string + handicap?: string +} + +export type ApiOdds = { + id: number + event_id: number + market_type: string + market_name: string + market_category: string + market_id: number + number_of_outcomes: number + raw_odds: ApiOddsOutcome[] + fetched_at: string + expires_at: string + is_active: boolean +} + +export type ApiTopLeaguesResponse = { + leagues: Array<{ + league_id: number + league_name: string + league_cc: string + league_sport_id: number + events: ApiEvent[] + }> +} + +export type EventsParams = { + page?: number + page_size?: number + sport_id?: number + league_id?: number + /** RFC3339 datetime; filter events with start_time >= this */ + first_start_time?: string + /** RFC3339 datetime; filter events with start_time <= this (e.g. for 3h/6h/12h windows) */ + last_start_time?: string + /** When true, return only in-play/live events */ + is_live?: boolean +} + +/** Quick filter key for time-based event filtering */ +export type QuickFilterKey = "all" | "today" | "3h" | "6h" | "9h" | "12h" + +export type EventsResponse = { + data: ApiEvent[] + total?: number + page?: number + total_pages?: number + message?: string + status?: string +} + +export type LeaguesResponse = { + data: ApiLeague[] + message?: string + status?: string +} + +export type OddsResponse = { + data: ApiOdds[] + message?: string + status?: string +} + +export type DetailMarketSectionFromApi = { + id: string + title: string + outcomes: { label: string; odds: number }[] +} + +export type MarketTabKey = + | "main" + | "goals" + | "handicap" + | "half_time" + | "correct_score" + | "1st_half" + | "2nd_half" + | "combo" + | "chance_mix" + | "home" + +export type TabColumnCell = { id: string; label: string; odds: number } + +export type AppEvent = { + id: string + sport: string + sportIcon: string + league: string + country: string + homeTeam: string + awayTeam: string + time: string + date: string + isLive: boolean + markets: { id: string; label: string; odds: number }[] + totalMarkets: number + rawOdds?: ApiOdds[] + /** Live: e.g. "2 - 1" */ + score?: string + /** Live: match minute */ + matchMinute?: number +}