Files
iddaai-fe/src/components/dashboard/dashboard-content.tsx
T
fahricansecer fc7a1ba567
Deploy Iddaai Frontend / build-and-deploy (push) Successful in 4m0s
first
2026-04-16 13:36:34 +03:00

428 lines
13 KiB
TypeScript

"use client";
import {
Box,
Flex,
Heading,
Text,
SimpleGrid,
Card,
VStack,
HStack,
Badge,
Button,
} from "@chakra-ui/react";
import { useTranslations } from "next-intl";
import { useColorModeValue } from "@/components/ui/color-mode";
import { SlideUp, StaggerContainer, StaggerItem, ScrollSlideUp } from "@/components/motion";
import { Skeleton } from "@/components/ui/feedback/skeleton";
import { MatchCard } from "@/components/matches";
import { useQueryMatches } from "@/lib/api/matches/use-hooks";
import {
useUpcomingPredictions,
useValueBets,
} from "@/lib/api/predictions/use-hooks";
import { useUserBettingStats } from "@/lib/api/coupons/use-hooks";
import { useSession } from "next-auth/react";
import { LuTrendingUp, LuTarget, LuTicket, LuChartBar } from "react-icons/lu";
import { useRouter } from "next/navigation";
import type { LeagueWithMatchesDto, MatchResponseDto } from "@/lib/api/matches/types";
import type { MatchPredictionDto, ValueBetDto } from "@/lib/api/predictions/types";
// ========================
// Stats Card
// ========================
interface StatCardProps {
label: string;
value: string | number;
icon: React.ReactNode;
colorPalette?: string;
}
function StatCard({
label,
value,
icon,
colorPalette = "primary",
}: StatCardProps) {
const cardBg = useColorModeValue(
"rgba(255, 255, 255, 0.75)",
"rgba(26, 32, 44, 0.65)",
);
const borderColor = useColorModeValue(
"rgba(255, 255, 255, 0.8)",
"rgba(255, 255, 255, 0.06)",
);
return (
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
backdropFilter="blur(12px)"
_hover={{
transform: "translateY(-3px)",
shadow: "lg",
borderColor: `${colorPalette}.300`,
}}
transition="all 0.3s ease"
>
<Card.Body>
<HStack gap={4}>
<Flex
boxSize="48px"
bg={`${colorPalette}.subtle`}
borderRadius="xl"
align="center"
justify="center"
color={`${colorPalette}.fg`}
fontSize="xl"
shadow="sm"
>
{icon}
</Flex>
<VStack gap={0} align="flex-start">
<Text fontSize="2xl" fontWeight="900" lineHeight="1">
{value}
</Text>
<Text fontSize="xs" color="fg.muted" fontWeight="medium">
{label}
</Text>
</VStack>
</HStack>
</Card.Body>
</Card.Root>
);
}
// ========================
// Value Bet Mini Card
// ========================
interface ValueBetMiniCardProps {
matchName: string;
prediction: string;
odd: number;
expectedValue: number;
confidence: number;
}
function ValueBetMiniCard({
matchName,
prediction,
odd,
expectedValue,
confidence,
}: ValueBetMiniCardProps) {
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
return (
<Box
p={3}
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="lg"
>
<Text fontSize="xs" color="fg.muted" truncate mb={1}>
{matchName}
</Text>
<Flex justify="space-between" align="center">
<Text fontSize="sm" fontWeight="bold">
{prediction}
</Text>
<HStack gap={2}>
<Badge
colorPalette="green"
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
EV+ {(expectedValue * 100).toFixed(0)}%
</Badge>
<Badge
colorPalette="primary"
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{odd.toFixed(2)}
</Badge>
</HStack>
</Flex>
</Box>
);
}
// ========================
// Dashboard Content
// ========================
export default function DashboardContent() {
const t = useTranslations("dashboard");
const tCoupons = useTranslations("coupons");
const router = useRouter();
const { data: session } = useSession();
const cardBg = useColorModeValue("white", "gray.800");
const borderColor = useColorModeValue("gray.100", "gray.700");
// Data fetching
const queryMatches = useQueryMatches();
const { data: upcomingData, isLoading: upcomingLoading } =
useUpcomingPredictions();
const { data: valueBetsData, isLoading: valueBetsLoading } = useValueBets();
const { data: statsData, isLoading: statsLoading } = useUserBettingStats();
// Trigger match fetch for today
if (!queryMatches.data && !queryMatches.isPending) {
queryMatches.mutate({ sport: "football", limit: 20 });
}
const todayMatches: MatchResponseDto[] = queryMatches.data?.data?.flatMap((l: LeagueWithMatchesDto) => l.matches) ?? [];
const upcomingPredictions: MatchPredictionDto[] = upcomingData?.data?.matches ?? [];
const valueBets: ValueBetDto[] = valueBetsData?.data ?? [];
const userStats = statsData?.data;
const userName = session?.user?.name || "";
return (
<SlideUp>
<Box>
{/* Welcome Header */}
<Box mb={6}>
<Heading as="h1" size="xl" fontWeight="bold">
{t("title")}
</Heading>
{userName && (
<Text color="fg.muted" mt={1}>
{t("welcome")},{" "}
<Text as="span" fontWeight="semibold" color="fg">
{userName}
</Text>{" "}
👋
</Text>
)}
</Box>
{/* Stats Grid */}
<StaggerContainer>
<SimpleGrid columns={{ base: 2, md: 4 }} gap={4} mb={8}>
<StaggerItem>
<StatCard
label={tCoupons("total-coupons")}
value={userStats?.totalCoupons ?? "—"}
icon={<LuTicket />}
colorPalette="primary"
/>
</StaggerItem>
<StaggerItem>
<StatCard
label={tCoupons("win-rate")}
value={
userStats?.winRate ? `${Math.round(userStats.winRate)}%` : "—"
}
icon={<LuTrendingUp />}
colorPalette="green"
/>
</StaggerItem>
<StaggerItem>
<StatCard
label={tCoupons("won")}
value={userStats?.wonBets ?? "—"}
icon={<LuTarget />}
colorPalette="teal"
/>
</StaggerItem>
<StaggerItem>
<StatCard
label={tCoupons("pending")}
value={userStats?.pendingBets ?? "—"}
icon={<LuChartBar />}
colorPalette="yellow"
/>
</StaggerItem>
</SimpleGrid>
</StaggerContainer>
{/* Two Column Layout */}
<Flex
gap={6}
direction={{ base: "column", lg: "row" }}
align="flex-start"
>
{/* Left Column — Today's Matches */}
<Box flex={2} minW={0}>
<Flex justify="space-between" align="center" mb={4}>
<Heading as="h2" size="md">
{t("todays-matches")}
</Heading>
<Button
variant="ghost"
size="sm"
colorPalette="primary"
onClick={() => router.push("/matches")}
>
{t("view-all")}
</Button>
</Flex>
{queryMatches.isPending ? (
<SimpleGrid columns={{ base: 1, md: 2 }} gap={3} py={4}>
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} height="140px" borderRadius="xl" />
))}
</SimpleGrid>
) : todayMatches.length > 0 ? (
<StaggerContainer>
<SimpleGrid columns={{ base: 1, md: 2 }} gap={3}>
{todayMatches.slice(0, 6).map((match: MatchResponseDto) => (
<StaggerItem key={match.id}>
<MatchCard match={match} />
</StaggerItem>
))}
</SimpleGrid>
</StaggerContainer>
) : (
<Card.Root
bg={cardBg}
borderColor={borderColor}
borderRadius="xl"
>
<Card.Body>
<Flex justify="center" py={8}>
<Text color="fg.muted">{t("no-matches")}</Text>
</Flex>
</Card.Body>
</Card.Root>
)}
</Box>
{/* Right Column — Predictions & Value Bets */}
<VStack gap={6} flex={1} align="stretch" minW={0}>
{/* Upcoming Predictions */}
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Header pb={2}>
<Flex justify="space-between" align="center">
<Heading as="h3" size="sm">
{t("upcoming-predictions")}
</Heading>
<Button
variant="ghost"
size="xs"
colorPalette="primary"
onClick={() => router.push("/predictions")}
>
{t("view-all")}
</Button>
</Flex>
</Card.Header>
<Card.Body pt={0}>
{upcomingLoading ? (
<VStack gap={2} align="stretch">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} height="50px" borderRadius="lg" />
))}
</VStack>
) : upcomingPredictions.length > 0 ? (
<VStack gap={2} align="stretch">
{upcomingPredictions.slice(0, 4).map((pred: MatchPredictionDto, idx: number) => (
<Box
key={idx}
p={2.5}
borderWidth="1px"
borderColor={borderColor}
borderRadius="lg"
cursor="pointer"
_hover={{ bg: "bg.muted" }}
onClick={() =>
router.push(`/matches/${pred.match_info.match_id}`)
}
>
<Text fontSize="xs" color="fg.muted" truncate>
{pred.match_info.home_team} vs{" "}
{pred.match_info.away_team}
</Text>
{pred.main_pick && (
<Flex justify="space-between" align="center" mt={1}>
<Text fontSize="sm" fontWeight="bold">
{pred.main_pick.pick}
</Text>
<Badge
colorPalette="primary"
variant="subtle"
fontSize="2xs"
borderRadius="full"
>
{Math.round(
pred.main_pick.calibrated_confidence ??
pred.main_pick.confidence,
)}
%
</Badge>
</Flex>
)}
</Box>
))}
</VStack>
) : (
<Text
fontSize="sm"
color="fg.muted"
textAlign="center"
py={4}
>
{t("no-predictions")}
</Text>
)}
</Card.Body>
</Card.Root>
{/* Value Bets */}
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
<Card.Header pb={2}>
<Heading as="h3" size="sm">
{t("value-bets")}
</Heading>
</Card.Header>
<Card.Body pt={0}>
{valueBetsLoading ? (
<VStack gap={2} align="stretch">
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} height="44px" borderRadius="lg" />
))}
</VStack>
) : valueBets.length > 0 ? (
<VStack gap={2} align="stretch">
{valueBets.slice(0, 5).map((vb: ValueBetDto, idx: number) => (
<ValueBetMiniCard
key={idx}
matchName={vb.matchName}
prediction={vb.prediction}
odd={vb.odd}
expectedValue={vb.expectedValue}
confidence={vb.confidence}
/>
))}
</VStack>
) : (
<Text
fontSize="sm"
color="fg.muted"
textAlign="center"
py={4}
>
{t("no-predictions")}
</Text>
)}
</Card.Body>
</Card.Root>
</VStack>
</Flex>
</Box>
</SlideUp>
);
}