This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
SimpleGrid,
|
||||
Card,
|
||||
VStack,
|
||||
HStack,
|
||||
Badge,
|
||||
Spinner,
|
||||
Button,
|
||||
Separator,
|
||||
} from "@chakra-ui/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useColorModeValue } from "@/components/ui/color-mode";
|
||||
import {
|
||||
SlideUp,
|
||||
StaggerContainer,
|
||||
StaggerItem,
|
||||
AnimatedCounter,
|
||||
} from "@/components/motion";
|
||||
import { useAdminAnalytics, useAdminUsers } from "@/lib/api/admin/use-hooks";
|
||||
import type { AdminUserDto, AnalyticsOverviewDto } from "@/lib/api/admin/types";
|
||||
import { LuUsers, LuChartBar, LuActivity, LuShield } from "react-icons/lu";
|
||||
import { useState } from "react";
|
||||
|
||||
type AdminTab = "overview" | "users";
|
||||
|
||||
// ========================
|
||||
// Admin Stat Card
|
||||
// ========================
|
||||
|
||||
interface AdminStatProps {
|
||||
label: string;
|
||||
value: number;
|
||||
icon: React.ReactNode;
|
||||
colorPalette: string;
|
||||
}
|
||||
|
||||
function AdminStat({ label, value, icon, colorPalette }: AdminStatProps) {
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||
|
||||
return (
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||
<Card.Body>
|
||||
<HStack gap={4}>
|
||||
<Flex
|
||||
boxSize="48px"
|
||||
bg={`${colorPalette}.subtle`}
|
||||
borderRadius="xl"
|
||||
align="center"
|
||||
justify="center"
|
||||
color={`${colorPalette}.fg`}
|
||||
fontSize="xl"
|
||||
>
|
||||
{icon}
|
||||
</Flex>
|
||||
<VStack gap={0} align="flex-start">
|
||||
<Text fontSize="2xl" fontWeight="900" lineHeight="1">
|
||||
<AnimatedCounter value={value} />
|
||||
</Text>
|
||||
<Text fontSize="xs" color="fg.muted" fontWeight="medium">
|
||||
{label}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
);
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Admin Content
|
||||
// ========================
|
||||
|
||||
export default function AdminContent() {
|
||||
const t = useTranslations("admin");
|
||||
const tCommon = useTranslations("common");
|
||||
const [activeTab, setActiveTab] = useState<AdminTab>("overview");
|
||||
|
||||
const cardBg = useColorModeValue("white", "gray.800");
|
||||
const borderColor = useColorModeValue("gray.100", "gray.700");
|
||||
|
||||
const { data: analyticsData, isLoading: analyticsLoading } =
|
||||
useAdminAnalytics();
|
||||
const { data: usersData, isLoading: usersLoading } = useAdminUsers();
|
||||
|
||||
const analytics = analyticsData?.data as AnalyticsOverviewDto | undefined;
|
||||
const users = (usersData?.data as AdminUserDto[] | undefined) ?? [];
|
||||
|
||||
const tabs: { key: AdminTab; label: string }[] = [
|
||||
{ key: "overview", label: t("overview") },
|
||||
{ key: "users", label: t("user-management") },
|
||||
];
|
||||
|
||||
const getUserDisplayName = (user: AdminUserDto) => {
|
||||
if (user.firstName && user.lastName)
|
||||
return `${user.firstName} ${user.lastName}`;
|
||||
if (user.firstName) return user.firstName;
|
||||
return user.email.split("@")[0];
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideUp>
|
||||
<Box>
|
||||
<Flex justify="space-between" align="center" mb={6}>
|
||||
<VStack gap={1} align="flex-start">
|
||||
<Heading as="h1" size="xl" fontWeight="bold">
|
||||
{t("title")}
|
||||
</Heading>
|
||||
<Text color="fg.muted" fontSize="sm">
|
||||
{t("subtitle")}
|
||||
</Text>
|
||||
</VStack>
|
||||
<Badge
|
||||
colorPalette="red"
|
||||
variant="solid"
|
||||
px={3}
|
||||
py={1}
|
||||
borderRadius="full"
|
||||
>
|
||||
<LuShield />
|
||||
{t("admin-badge")}
|
||||
</Badge>
|
||||
</Flex>
|
||||
|
||||
{/* Tabs */}
|
||||
<HStack gap={2} mb={6}>
|
||||
{tabs.map((tab) => (
|
||||
<Button
|
||||
key={tab.key}
|
||||
variant={activeTab === tab.key ? "solid" : "outline"}
|
||||
colorPalette={activeTab === tab.key ? "primary" : "gray"}
|
||||
size="sm"
|
||||
borderRadius="full"
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
))}
|
||||
</HStack>
|
||||
|
||||
{/* Overview Tab */}
|
||||
{activeTab === "overview" &&
|
||||
(analyticsLoading ? (
|
||||
<Flex justify="center" py={16}>
|
||||
<Spinner size="lg" color="primary.500" />
|
||||
</Flex>
|
||||
) : (
|
||||
<StaggerContainer>
|
||||
<SimpleGrid columns={{ base: 2, md: 4 }} gap={4} mb={8}>
|
||||
<StaggerItem>
|
||||
<AdminStat
|
||||
label={t("total-users")}
|
||||
value={analytics?.totalUsers ?? 0}
|
||||
icon={<LuUsers />}
|
||||
colorPalette="primary"
|
||||
/>
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<AdminStat
|
||||
label={t("total-predictions")}
|
||||
value={analytics?.totalPredictions ?? 0}
|
||||
icon={<LuChartBar />}
|
||||
colorPalette="green"
|
||||
/>
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<AdminStat
|
||||
label={t("active-users")}
|
||||
value={analytics?.activeUsers ?? 0}
|
||||
icon={<LuActivity />}
|
||||
colorPalette="orange"
|
||||
/>
|
||||
</StaggerItem>
|
||||
<StaggerItem>
|
||||
<AdminStat
|
||||
label={t("total-coupons")}
|
||||
value={analytics?.totalCoupons ?? 0}
|
||||
icon={<LuShield />}
|
||||
colorPalette="purple"
|
||||
/>
|
||||
</StaggerItem>
|
||||
</SimpleGrid>
|
||||
</StaggerContainer>
|
||||
))}
|
||||
|
||||
{/* Users Tab */}
|
||||
{activeTab === "users" &&
|
||||
(usersLoading ? (
|
||||
<Flex justify="center" py={16}>
|
||||
<Spinner size="lg" color="primary.500" />
|
||||
</Flex>
|
||||
) : users.length > 0 ? (
|
||||
<Card.Root bg={cardBg} borderColor={borderColor} borderRadius="xl">
|
||||
<Card.Body>
|
||||
<VStack gap={0} align="stretch">
|
||||
{/* Table Header */}
|
||||
<Flex
|
||||
px={4}
|
||||
py={2}
|
||||
bg="bg.muted"
|
||||
borderRadius="lg"
|
||||
mb={2}
|
||||
fontWeight="semibold"
|
||||
fontSize="xs"
|
||||
color="fg.muted"
|
||||
>
|
||||
<Text flex={2}>{t("user-name")}</Text>
|
||||
<Text flex={2}>{t("user-email")}</Text>
|
||||
<Text flex={1} textAlign="center">
|
||||
{t("user-role")}
|
||||
</Text>
|
||||
<Text flex={1} textAlign="center">
|
||||
{t("user-status")}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
{/* User Rows */}
|
||||
{users.map((user: AdminUserDto, idx: number) => (
|
||||
<Box key={user.id ?? idx}>
|
||||
{idx > 0 && <Separator />}
|
||||
<Flex
|
||||
px={4}
|
||||
py={3}
|
||||
align="center"
|
||||
_hover={{ bg: "bg.muted" }}
|
||||
borderRadius="lg"
|
||||
>
|
||||
<Text
|
||||
flex={2}
|
||||
fontSize="sm"
|
||||
fontWeight="medium"
|
||||
truncate
|
||||
>
|
||||
{getUserDisplayName(user)}
|
||||
</Text>
|
||||
<Text flex={2} fontSize="sm" color="fg.muted" truncate>
|
||||
{user.email}
|
||||
</Text>
|
||||
<Flex flex={1} justify="center">
|
||||
<Badge
|
||||
colorPalette={
|
||||
user.role === "ADMIN" ? "red" : "gray"
|
||||
}
|
||||
variant="subtle"
|
||||
fontSize="2xs"
|
||||
borderRadius="full"
|
||||
>
|
||||
{user.role || "User"}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Flex flex={1} justify="center">
|
||||
<Badge
|
||||
colorPalette={user.isActive ? "green" : "gray"}
|
||||
variant="subtle"
|
||||
fontSize="2xs"
|
||||
borderRadius="full"
|
||||
>
|
||||
{user.isActive
|
||||
? tCommon("active")
|
||||
: tCommon("inactive")}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
) : (
|
||||
<Flex justify="center" py={16}>
|
||||
<Text color="fg.muted">{t("no-users")}</Text>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
</SlideUp>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as AdminContent } from "./admin-content";
|
||||
Reference in New Issue
Block a user