Commit aaff4469 by Arjun Jhukal

made anylytics dynamic

parent 7f758c97
import { IconButton } from '@mui/material' "use client";
import { ArrowLeft, More } from '@wandersonalwes/iconsax-react'
import { useGetAnalyticsQuery } from '@/services/dashboardApi';
import { AnalyticsProps } from '@/types/dashboard';
import { formatDateTime } from '@/utils/formatDateTime';
import { Box, ClickAwayListener, Fade, IconButton, List, ListItem, Paper, Popper } from '@mui/material';
import { ArrowLeft, More } from '@wandersonalwes/iconsax-react';
import { ApexOptions } from 'apexcharts'; import { ApexOptions } from 'apexcharts';
import React from 'react' import React, { useRef, useState } from 'react';
import Chart from "react-apexcharts"; import Chart from "react-apexcharts";
type AnalyticsType = "today" | "this_week" | "this_month" | "this_year" | "all_time";
const getColorByKey = (key: string, isProfit: boolean) => {
const palette: Record<string, string> = {
total_deposits: "#22C55E",
total_withdraw: "#E14A5E",
total_transactions: "#3B82F6",
total_users: "#F59E0B",
};
return isProfit ? palette[key] || "#6B7280" : "#EF4444";
};
// Skeleton loader
const AnalyticsSkeleton = () => (
<div className="col-span-1">
<div className="analytics__card p-4 rounded-[8px] border border-gray-100 shadow-sm animate-pulse">
<div className="flex justify-between items-center mb-3">
<div className="w-[80px] h-[10px] bg-gray-200 rounded"></div>
<div className="w-[20px] h-[20px] bg-gray-200 rounded-full"></div>
</div>
<div className="w-[120px] h-[10px] bg-gray-200 rounded mb-2"></div>
<div className="flex justify-between items-end">
<div>
<div className="w-[60px] h-[16px] bg-gray-200 rounded mb-2"></div>
<div className="w-[40px] h-[10px] bg-gray-200 rounded"></div>
</div>
<div className="w-[140px] h-[80px] bg-gray-200 rounded"></div>
</div>
</div>
</div>
);
export default function AdminAnalytics() { export default function AdminAnalytics() {
const charts = [ const [currentType, setCurrentType] = useState<AnalyticsType>("this_week");
{ const { data, isLoading } = useGetAnalyticsQuery({ type: currentType });
title: 'Total Revenue',
subtitle: 'Jul 23 - Jul 30 (2025)', const [openIndex, setOpenIndex] = useState<number | null>(null);
value: '$2.689M', const anchorRefs = useRef<(HTMLButtonElement | null)[]>([]);
change: 16,
isIncrease: true, const handleToggle = (index: number) => {
chartOptions: { setOpenIndex(prev => (prev === index ? null : index));
chart: { type: 'area', sparkline: { enabled: true } }, };
stroke: { curve: 'smooth', width: 2 },
fill: { const handleClose = (event: MouseEvent | TouchEvent) => {
type: 'gradient', if (anchorRefs.current.some(ref => ref?.contains(event.target as Node))) return;
gradient: { setOpenIndex(null);
shade: 'light', };
type: 'vertical',
shadeIntensity: 1, const handleSelect = (type: AnalyticsType) => {
gradientToColors: ['#E14A5E'], setCurrentType(type);
opacityFrom: 1, setOpenIndex(null);
opacityTo: 0, };
stops: [0, 100],
}, if (isLoading) {
}, return (
colors: ['#E14A5E'], <div className="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 analytics__wrapper mb-5">
tooltip: { enabled: true }, {Array.from({ length: 4 }).map((_, i) => (
} as ApexOptions, <AnalyticsSkeleton key={i} />
chartSeries: [{ name: 'Revenue', data: [400, 600, 700, 1200, 1400, 1800, 2500] }], ))}
}, </div>
{ );
title: 'New Users', }
subtitle: 'Jul 23 - Jul 30 (2025)',
value: '3.2K', if (!data?.data) return null;
change: 8,
isIncrease: true, const analyticsData = data.data;
chartOptions: { const chartKeys = Object.keys(analyticsData) as (keyof typeof analyticsData)[];
chart: { type: 'area', sparkline: { enabled: true } },
stroke: { curve: 'straight', width: 2 }, const charts = chartKeys.map((key) => {
fill: { const item: AnalyticsProps = analyticsData[key];
type: 'gradient', const color = getColorByKey(key, item.profit);
gradient: {
shade: 'dark', const chartOptions: ApexOptions = {
type: 'vertical', chart: { type: "area", sparkline: { enabled: true } },
shadeIntensity: 0.8, stroke: { curve: "smooth", width: 2 },
gradientToColors: ['#22C55E'], fill: {
opacityFrom: 0.8, type: "gradient",
opacityTo: 0, gradient: {
stops: [0, 100], shade: "light",
}, type: "vertical",
}, shadeIntensity: 1,
colors: ['#22C55E'], gradientToColors: [color],
tooltip: { enabled: true }, opacityFrom: 0.8,
} as ApexOptions, opacityTo: 0,
chartSeries: [{ name: 'Users', data: [200, 400, 600, 700, 900, 1200, 1500] }], stops: [0, 100],
},
{
title: 'Churn Rate',
subtitle: 'Jul 23 - Jul 30 (2025)',
value: '5.4%',
change: -4,
isIncrease: false,
chartOptions: {
chart: { type: 'area', sparkline: { enabled: true } },
stroke: { curve: 'smooth', width: 2 },
fill: {
type: 'gradient',
gradient: {
shade: 'light',
type: 'vertical',
shadeIntensity: 1,
gradientToColors: ['#F59E0B'],
opacityFrom: 1,
opacityTo: 0,
stops: [0, 100],
},
},
colors: ['#F59E0B'],
tooltip: { enabled: true },
} as ApexOptions,
chartSeries: [{ name: 'Churn', data: [8, 7, 6, 5, 5, 4, 3] }],
},
{
title: 'Support Tickets',
subtitle: 'Jul 23 - Jul 30 (2025)',
value: '1.2K',
change: -12,
isIncrease: false,
chartOptions: {
chart: { type: 'area', sparkline: { enabled: true } },
stroke: { curve: 'stepline', width: 2 },
fill: {
type: 'gradient',
gradient: {
shade: 'dark',
type: 'vertical',
shadeIntensity: 0.8,
gradientToColors: ['#3B82F6'],
opacityFrom: 0.7,
opacityTo: 0,
stops: [0, 100],
},
}, },
colors: ['#3B82F6'], },
tooltip: { enabled: true }, colors: [color],
} as ApexOptions, tooltip: { enabled: true },
chartSeries: [{ name: 'Tickets', data: [50, 45, 40, 38, 35, 30, 25] }], };
},
]; const chartSeries = [
{ name: item.name, data: item.breakdown ?? [0, 0, 0, 0, 0, 0, 0] },
];
// const chartSeries = [
// { return {
// name: "Revenue", key,
// data: [400, 600, 700, 1200, 1400, 1800, 2500], title: item.name,
// }, value: item.current,
// ]; change: item.percent,
isIncrease: item.profit,
message: item.message,
start_date: item.start_date,
end_date: item.end_date,
chartOptions,
chartSeries,
};
});
return ( return (
<div className="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 analytics__wrapper mb-5"> <div className="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 analytics__wrapper mb-5">
{charts.map((chart, index) => ( {charts.map((chart, index) => {
<div className="col-span-1" key={index} > const { date: startDate } = formatDateTime(chart.start_date);
<div className="analytics__card p-4 rounded-[8px]"> const { date } = formatDateTime(chart.end_date);
<div className="card__title flex justify-between items-center mb-2">
<div className="title"> const open = openIndex === index;
<strong className='text-[12px] leading-[120%] text-title font-[500] mb-1 block'>Total Revenue</strong> const id = open ? `analytics-action-${index}` : undefined;
<span className="text-[10px] text-para-light leading-[120%] font-[500]">Jul 23 - Jul 30 (2025)</span>
</div> return (
<div className="card__action"> <div className="col-span-1" key={index}>
<IconButton> <div className="analytics__card p-4 rounded-[8px]">
<More className='rotate-90' variant='TwoTone' /> <div className="card__title flex justify-between items-center mb-2">
</IconButton> <div className="title">
</div> <strong className='text-[12px] leading-[120%] text-title font-[500] mb-1 block'>{chart.title}</strong>
</div> <span className="text-[10px] text-para-light leading-[120%] font-[500]">{startDate} - {date}</span>
<div className="card__content">
<div className="flex gap-2 items-end justify-between mb-2">
<div className="content flex gap-2 items-center">
<strong className='text-[16px] leading-[120%] font-[600] text-title'>$2.689M</strong>
<p className={`flex gap-1 items-center ${chart.isIncrease ? "increase" : "decrease"}`}>
<span className="arrow w-[18px] h-[18px] rounded-full flex justify-center items-center "><ArrowLeft size={12} /></span>16%</p>
</div> </div>
<div className="relative aspect-[72/68] w-full max-w-[144px] h-[100px] text-right"> <Box>
<Chart <IconButton
options={chart.chartOptions} aria-describedby={id}
series={chart.chartSeries} ref={(el: HTMLButtonElement | null) => {
type="area" anchorRefs.current[index] = el;
width="100%" }}
height="100%" onClick={() => handleToggle(index)}
/> >
<More className="rotate-90" variant="TwoTone" />
</IconButton>
<Popper
open={open}
anchorEl={anchorRefs.current[index]}
placement="bottom-end"
transition
style={{ zIndex: 1300 }}
id={id}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={200}>
<Paper
elevation={3}
sx={{
width: 180,
borderRadius: 2,
mt: 1,
}}
>
<ClickAwayListener onClickAway={handleClose}>
<List>
{[
{ label: "Today", value: "today" },
{ label: "This Week", value: "this_week" },
{ label: "This Month", value: "this_month" },
{ label: "This Year", value: "this_year" },
{ label: "All Time", value: "all_time" },
].map((item) => (
<ListItem key={item.value}>
<button
onClick={() => handleSelect(item.value as AnalyticsType)}
className={`block w-full text-left py-2 px-4 hover:bg-[#FBF4FB] text-sm ${currentType === item.value ? "font-semibold text-primary" : ""}`}
>
{item.label}
</button>
</ListItem>
))}
</List>
</ClickAwayListener>
</Paper>
</Fade>
)}
</Popper>
</Box>
</div>
<div className="card__content">
<div className="flex gap-2 items-end justify-between mb-2">
<div className="content flex gap-2 items-center">
<strong className='text-[16px] leading-[120%] font-[600] text-title'>${chart.value}</strong>
<p className={`flex gap-1 items-center ${chart.isIncrease ? "increase" : "decrease"}`}>
<span className="arrow w-[18px] h-[18px] rounded-full flex justify-center items-center ">
<ArrowLeft size={12} />
</span>
{chart.change}%
</p>
</div>
<div className="relative aspect-[72/68] w-full max-w-[144px] h-[100px] text-right">
<Chart
options={chart.chartOptions}
series={chart.chartSeries}
type="area"
width="100%"
height="100%"
/>
</div>
</div> </div>
<p className='text-[12px] font-[500] text-para-light'>{chart.message}</p>
</div> </div>
<p className='text-[12px] font-[500] text-para-light'>compared to previous week</p>
</div> </div>
</div> </div>
</div> );
))} })}
</div> </div>
) );
} }
...@@ -19,13 +19,12 @@ export default function AdminHeader() { ...@@ -19,13 +19,12 @@ export default function AdminHeader() {
const [page, setPage] = React.useState(1); const [page, setPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(10); const [pageSize, setPageSize] = React.useState(10);
const { data } = useGetAllNotificationQuery({ page: page, per_page: pageSize }); const { data } = useGetAllNotificationQuery({ page: page, per_page: pageSize });
console.log(data);
return ( return (
<Box className='flex items-center gap-4 justify-between w-full'> <Box className='flex items-center gap-4 justify-between w-full'>
<AdminSearchBar /> <AdminSearchBar />
<div className="right flex items-center gap-4"> <div className="right flex items-center gap-4">
<CreatNewRecord /> <CreatNewRecord />
<NotificationPage notifications={data?.data?.data || []} pagination={data?.data?.pagination}/> <NotificationPage notifications={data?.data?.data || []} pagination={data?.data?.pagination} />
<Profile /> <Profile />
</div> </div>
</Box> </Box>
......
...@@ -51,7 +51,6 @@ export default function PlayerDetailPage({ id }: { id: number }) { ...@@ -51,7 +51,6 @@ export default function PlayerDetailPage({ id }: { id: number }) {
const { date } = formatDateTime(data?.data?.registered_date as string); const { date } = formatDateTime(data?.data?.registered_date as string);
console.log("user balance", userBalance);
return ( return (
<> <>
<section className="player__detail__intro mb-12 lg:mb-16"> <section className="player__detail__intro mb-12 lg:mb-16">
......
...@@ -35,7 +35,6 @@ export default function BannerSlider() { ...@@ -35,7 +35,6 @@ export default function BannerSlider() {
const { data, isLoading } = useGetAllBannerQuery(); const { data, isLoading } = useGetAllBannerQuery();
const [updateBanner, { isLoading: updating }] = useUpdateBannerMutation(); const [updateBanner, { isLoading: updating }] = useUpdateBannerMutation();
console.log("banner data", data);
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
banners: data?.data?.length banners: data?.data?.length
......
...@@ -34,7 +34,6 @@ export default function TransactionTable({ user_id, game_id, search }: { user_id ...@@ -34,7 +34,6 @@ export default function TransactionTable({ user_id, game_id, search }: { user_id
const { data, isLoading: loadingTransaction } = useGetAllTransactionQuery(queryArgs); const { data, isLoading: loadingTransaction } = useGetAllTransactionQuery(queryArgs);
console.log("all transaction", data);
const tableData = useMemo(() => data?.data?.data || [], [data]); const tableData = useMemo(() => data?.data?.data || [], [data]);
const columns = useMemo<ColumnDef<SingleDepositProps>[]>(() => [ const columns = useMemo<ColumnDef<SingleDepositProps>[]>(() => [
......
...@@ -17,7 +17,6 @@ export default function MenuPage() { ...@@ -17,7 +17,6 @@ export default function MenuPage() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [selectedMenus, setSelectedMenus] = React.useState<any[]>([]); const [selectedMenus, setSelectedMenus] = React.useState<any[]>([]);
console.log("menus", { menus, selectedMenus });
React.useEffect(() => { React.useEffect(() => {
......
...@@ -12,6 +12,7 @@ import { settingApi } from "@/services/settingApi"; ...@@ -12,6 +12,7 @@ import { settingApi } from "@/services/settingApi";
import { pageApi } from "@/services/pageApi"; import { pageApi } from "@/services/pageApi";
import { notificationApi } from "@/services/notificationApi"; import { notificationApi } from "@/services/notificationApi";
import { menuApi, userMenuApi } from "@/services/menuApi"; import { menuApi, userMenuApi } from "@/services/menuApi";
import { dashboardApi } from "@/services/dashboardApi";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {
...@@ -29,6 +30,7 @@ export const store = configureStore({ ...@@ -29,6 +30,7 @@ export const store = configureStore({
[notificationApi.reducerPath]: notificationApi.reducer, [notificationApi.reducerPath]: notificationApi.reducer,
[menuApi.reducerPath]: menuApi.reducer, [menuApi.reducerPath]: menuApi.reducer,
[userMenuApi.reducerPath]: userMenuApi.reducer, [userMenuApi.reducerPath]: userMenuApi.reducer,
[dashboardApi.reducerPath]: dashboardApi.reducer,
}, },
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware() getDefaultMiddleware()
...@@ -43,6 +45,7 @@ export const store = configureStore({ ...@@ -43,6 +45,7 @@ export const store = configureStore({
.concat(notificationApi.middleware) .concat(notificationApi.middleware)
.concat(menuApi.middleware) .concat(menuApi.middleware)
.concat(userMenuApi.middleware) .concat(userMenuApi.middleware)
.concat(dashboardApi.middleware)
}) })
export type RootState = ReturnType<typeof store.getState>; export type RootState = ReturnType<typeof store.getState>;
......
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQuery } from "./baseQuery";
import { AnalyticsResponse } from "@/types/dashboard";
export const dashboardApi = createApi({
reducerPath: "dashboardApi",
baseQuery: baseQuery,
tagTypes: ["Analytics", "Transactions"],
endpoints: (builder) => ({
getAnalytics: builder.query<AnalyticsResponse, { type: string }>({
query: ({ type }) => ({
url: `/api/admin/dashboard/overview?type=${type}`,
method: "GET",
}),
providesTags: ["Analytics"],
}),
getAdminTransactions: builder.query<any, void>({
query: () => ({
url: `/api/admin/dashboard/deposits`,
method: "GET",
}),
providesTags: ["Transactions"],
})
})
})
export const { useGetAnalyticsQuery, useGetAdminTransactionsQuery } = dashboardApi;
\ No newline at end of file
export type AnalyticsProps = {
name: string,
start_date: string,
end_date: string,
current: number,
previous: number,
percent: number,
profit: boolean,
message: string,
breakdown: number[]
}
export interface AnalyticsResponse {
data: {
total_deposits: AnalyticsProps,
total_transactions: AnalyticsProps,
total_users: AnalyticsProps,
total_withdraw: AnalyticsProps,
}
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment