event and virtual pages

This commit is contained in:
brooktewabe 2026-03-02 19:10:22 +03:00
parent 7a6f8f4279
commit 0354a182f3
6 changed files with 356 additions and 102 deletions

View File

@ -1,10 +1,51 @@
import Link from "next/link" import Link from "next/link"
import { getEventById } from "@/lib/mock-data" import { getEventById } from "@/lib/mock-data"
import { MatchDetailView } from "@/components/betting/match-detail-view" 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 }> }) { export default async function EventPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params 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) { if (!event) {
return ( return (
@ -17,5 +58,5 @@ export default async function EventPage({ params }: { params: Promise<{ id: stri
) )
} }
return <MatchDetailView event={event} /> return <MatchDetailView event={event} apiSections={apiSections} />
} }

View File

@ -41,15 +41,15 @@ export default function LoginPage() {
/> />
))} ))}
</div> </div>
{/* FORTUNE box */} {/* HARIF box */}
<div className="bg-brand-accent px-3 py-1 -skew-x-12 flex items-center h-[38px]"> <div className="bg-brand-accent px-3 py-1 -skew-x-12 flex items-center h-[38px]">
<span className="text-2xl font-black text-white italic tracking-tighter skew-x-12 inline-block leading-none"> <span className="text-2xl font-black text-white italic tracking-tighter skew-x-12 inline-block leading-none">
FORTUNE HARIF
</span> </span>
</div> </div>
{/* SPORT text */} {/* SPORT text */}
<span className="text-2xl font-black text-brand-primary italic tracking-tighter ml-1 leading-none"> <span className="text-2xl font-black text-brand-primary italic tracking-tighter ml-1 leading-none">
BETS SPORT
</span> </span>
</div> </div>
</div> </div>

View File

@ -43,15 +43,15 @@ export default function RegisterPage() {
/> />
))} ))}
</div> </div>
{/* FORTUNE box */} {/* HARIF box */}
<div className="bg-brand-accent px-3 py-1 -skew-x-12 flex items-center h-[38px]"> <div className="bg-brand-accent px-3 py-1 -skew-x-12 flex items-center h-[38px]">
<span className="text-2xl font-black text-white italic tracking-tighter skew-x-12 inline-block leading-none"> <span className="text-2xl font-black text-white italic tracking-tighter skew-x-12 inline-block leading-none">
FORTUNE HARIF
</span> </span>
</div> </div>
{/* SPORT text */} {/* SPORT text */}
<span className="text-2xl font-black text-brand-primary italic tracking-tighter ml-1 leading-none"> <span className="text-2xl font-black text-brand-primary italic tracking-tighter ml-1 leading-none">
BETS SPORT
</span> </span>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
const rules = [ const rules = [
{ {
title: "General Betting 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", title: "Live Betting",
@ -13,7 +13,7 @@ const rules = [
}, },
{ {
title: "Responsible Gambling", 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", title: "Account Rules",

View File

@ -1,141 +1,163 @@
"use client" "use client";
import { useState, useEffect } from "react" import { useState, useEffect } from "react";
import Image from "next/image" import Image from "next/image";
import { GamingSidebar } from "@/components/games/gaming-sidebar" import { GamingSidebar } from "@/components/games/gaming-sidebar";
import { GameRow } from "@/components/games/game-row" import { GameRow } from "@/components/games/game-row";
import { GameCard } from "@/components/games/game-card" import { GameCard } from "@/components/games/game-card";
import { Search, Heart, Clock, Star, Zap, Gamepad2, AlertCircle, LayoutGrid } from "lucide-react" import {
import { cn } from "@/lib/utils" Search,
import api from "@/lib/api" Heart,
Clock,
Star,
Zap,
Gamepad2,
AlertCircle,
LayoutGrid,
} from "lucide-react";
import { cn } from "@/lib/utils";
import api from "@/lib/api";
interface Provider { interface Provider {
provider_id: string provider_id: string;
provider_name: string provider_name: string;
logo_dark: string logo_dark: string;
logo_light: string logo_light: string;
enabled: boolean enabled: boolean;
} }
interface ApiGame { interface ApiGame {
gameId: string gameId: string;
providerId: string providerId: string;
provider: string provider: string;
name: string name: string;
category: string category: string;
deviceType: string deviceType: string;
hasDemo: boolean hasDemo: boolean;
hasFreeBets: boolean hasFreeBets: boolean;
demoUrl?: string demoUrl?: string;
image?: string // In case it gets added image?: string; // In case it gets added
thumbnail?: string thumbnail?: string;
provider_id?: string // Fallback 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() { export default function VirtualPage() {
const [activeCategory, setActiveCategory] = useState("all") const [activeCategory, setActiveCategory] = useState("all");
const [providers, setProviders] = useState<Provider[]>([]) const [providers, setProviders] = useState<Provider[]>([]);
const [games, setGames] = useState<ApiGame[]>([]) const [games, setGames] = useState<ApiGame[]>([]);
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
const fetchVirtualData = async () => { const fetchVirtualData = async () => {
try { try {
setIsLoading(true) setIsLoading(true);
const [providersRes, gamesRes] = await Promise.all([ const [providersRes, gamesRes] = await Promise.all([
api.get("/virtual-game/orchestrator/providers"), api.get("/virtual-game/orchestrator/providers"),
api.get("/virtual-game/orchestrator/games", { params: { limit: 2000 } }) api.get("/virtual-game/orchestrator/games", {
]) params: { limit: 2000 },
}),
const pData = providersRes.data ]);
const gData = gamesRes.data
const providersList = pData.providers || pData.data || pData || [] const pData = providersRes.data;
const gamesList = gData.data || gData.games || gData || [] const gData = gamesRes.data;
setProviders(Array.isArray(providersList) ? providersList : []) const providersList = pData.providers || pData.data || pData || [];
setGames(Array.isArray(gamesList) ? gamesList : []) const gamesList = gData.data || gData.games || gData || [];
setProviders(Array.isArray(providersList) ? providersList : []);
setGames(Array.isArray(gamesList) ? gamesList : []);
} catch (err: any) { } catch (err: any) {
console.error("Failed to fetch virtual games data:", err) console.error("Failed to fetch virtual games data:", err);
setError("Failed to load games data.") setError("Failed to load games data.");
} finally { } finally {
setIsLoading(false) setIsLoading(false);
} }
} };
fetchVirtualData() fetchVirtualData();
}, []) }, []);
// Create Sidebar Categories dynamically from providers // Create Sidebar Categories dynamically from providers
const sidebarCategories = [ const sidebarCategories = [
{ id: "all", name: "All Games", icon: LayoutGrid }, { id: "all", name: "All Games", icon: LayoutGrid },
...providers.map(p => ({ ...providers.map((p) => ({
id: p.provider_id, id: p.provider_id,
name: p.provider_name, 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 // Filter games based on active category
// If "all", group by provider // If "all", group by provider
let displayedGames: any[] = [] let displayedGames: any[] = [];
let groupedGames: { title: string, games: any[] }[] = [] let groupedGames: { title: string; games: any[] }[] = [];
const mapApiGameToCard = (game: ApiGame) => ({ const mapApiGameToCard = (game: ApiGame) => ({
id: game.gameId, id: game.gameId,
title: game.name, title: game.name,
image: game.thumbnail || game.image || DEFAULT_IMAGE, image: game.thumbnail || game.image || DEFAULT_IMAGE,
provider: game.provider provider: game.provider,
}) });
if (activeCategory === "all") { if (activeCategory === "all") {
// Group up to 12 games per provider for the rows // Group up to 12 games per provider for the rows
providers.forEach(p => { providers.forEach((p) => {
const providerIdStr = String(p.provider_id || "").trim().toLowerCase() const providerIdStr = String(p.provider_id || "")
.trim()
.toLowerCase();
const providerGames = games const providerGames = games
.filter(g => { .filter((g) => {
const gameProvId = String(g.providerId || g.provider_id || "").trim().toLowerCase() const gameProvId = String(g.providerId || g.provider_id || "")
return gameProvId === providerIdStr .trim()
.toLowerCase();
return gameProvId === providerIdStr;
}) })
.slice(0, 12) .slice(0, 12)
.map(mapApiGameToCard) .map(mapApiGameToCard);
if (providerGames.length > 0) { if (providerGames.length > 0) {
groupedGames.push({ groupedGames.push({
title: p.provider_name, title: p.provider_name,
games: providerGames games: providerGames,
}) });
} }
}) });
} else { } else {
displayedGames = games displayedGames = games
.filter(g => { .filter((g) => {
const gameProvId = String(g.providerId || g.provider_id || "").trim().toLowerCase() const gameProvId = String(g.providerId || g.provider_id || "")
const matches = gameProvId === String(activeCategory).trim().toLowerCase() .trim()
if (g.providerId?.toLowerCase().includes('pop') || g.provider_id?.toLowerCase().includes('pop')) { .toLowerCase();
} const matches =
return 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( 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 ( return (
<div className="flex h-[calc(100vh-140px)] overflow-hidden bg-brand-bg"> <div className="flex h-[calc(100vh-140px)] overflow-hidden bg-brand-bg">
{/* Sidebar */} {/* Sidebar */}
<GamingSidebar <GamingSidebar
title="Virtual" title="Virtual"
subtitle="Check out our games!" subtitle="Check out our games!"
activeCategory={activeCategory} activeCategory={activeCategory}
onCategoryChange={setActiveCategory} onCategoryChange={setActiveCategory}
categories={sidebarCategories} categories={sidebarCategories}
/> />
@ -155,22 +177,22 @@ export default function VirtualPage() {
<div className="mt-0"> <div className="mt-0">
{isLoading ? ( {isLoading ? (
<div className="p-8 text-center text-white/60 text-sm font-bold flex items-center justify-center gap-2"> <div className="p-8 text-center text-white/60 text-sm font-bold flex items-center justify-center gap-2">
<div className="size-4 rounded-full border-2 border-brand-primary border-t-transparent animate-spin" /> <div className="size-4 rounded-full border-2 border-brand-primary border-t-transparent animate-spin" />
Loading games... Loading games...
</div> </div>
) : error ? ( ) : error ? (
<div className="p-8 text-center text-red-400 text-sm font-bold flex items-center justify-center gap-2"> <div className="p-8 text-center text-red-400 text-sm font-bold flex items-center justify-center gap-2">
<AlertCircle className="size-4" /> <AlertCircle className="size-4" />
{error} {error}
</div> </div>
) : activeCategory === "all" ? ( ) : activeCategory === "all" ? (
// Show all categories // Show all categories
<div className="flex flex-col"> <div className="flex flex-col">
{groupedGames.map((category, index) => ( {groupedGames.map((category, index) => (
<GameRow <GameRow
key={category.title} key={category.title}
title={category.title} title={category.title}
games={category.games} games={category.games}
rows={1} rows={1}
/> />
))} ))}
@ -180,16 +202,16 @@ export default function VirtualPage() {
<div className="p-4"> <div className="p-4">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-white uppercase border-l-4 border-brand-primary pl-4"> <h2 className="text-lg font-bold text-white uppercase border-l-4 border-brand-primary pl-4">
{activeCategoryData?.provider_name || 'Games'} {activeCategoryData?.provider_name || "Games"}
</h2> </h2>
<button <button
onClick={() => setActiveCategory("all")} onClick={() => setActiveCategory("all")}
className="text-xs text-white/40 hover:text-white uppercase font-bold" className="text-xs text-white/40 hover:text-white uppercase font-bold"
> >
Back to Virtual Back to Virtual
</button> </button>
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{displayedGames.map((game, idx) => ( {displayedGames.map((game, idx) => (
<GameCard key={game.id || idx} {...game} /> <GameCard key={game.id || idx} {...game} />
@ -200,5 +222,5 @@ export default function VirtualPage() {
</div> </div>
</main> </main>
</div> </div>
) );
} }

191
lib/store/betting-types.ts Normal file
View File

@ -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
}