Commit d87de86c by Arjun Jhukal

udpated the new page activity log and wrapped the verify opt

parent 5f71d5ca
......@@ -54,7 +54,7 @@ function VerifyEmailContent() {
variant="contained"
color="primary"
className="!mb-6"
onClick={() => router.replace(PATH.DASHBOARD.ROOT)}
onClick={() => router.replace(PATH.AUTH.LOGIN.ROOT)}
>
Verify now
</Button>
......
// import VerifyOTPPage from '@/components/pages/auth/VerifyOtp'
import GlobalLoading from '@/components/organism/GlobalLoading'
import VerifyOTPPage from '@/components/pages/auth/VerifyOtp'
import React from 'react'
import React, { Suspense } from 'react'
export default function VerifyOTP() {
return (
<Suspense fallback={<GlobalLoading />}>
<VerifyOTPPage />
</Suspense>
)
}
import ActivityLogPage from '@/components/pages/dashboard/adminDashboard/activityLog'
import React from 'react'
export default function ActivityLog() {
return (
<ActivityLogPage />
)
}
// import { Button, IconButton, InputAdornment, OutlinedInput, useMediaQuery } from "@mui/material";
// import SelectField from "../atom/SelectField";
// import { DocumentDownload, SearchNormal } from "@wandersonalwes/iconsax-react";
// interface TableHeaderProps {
// search: string;
// setSearch?: (value: string) => void;
// filterMethod?: string;
// setFilterMethod?: (value: string) => void;
// onDownloadCSV: () => void;
// }
// export default function TableHeader({
// search,
// setSearch,
// filterMethod,
// setFilterMethod,
// onDownloadCSV,
// }: TableHeaderProps) {
// const downMD = useMediaQuery((theme) => theme.breakpoints.down('md'));
// return (
// <div className="table__header p-4 mb-4 flex justify-between">
// <div className="inpute__field relative">
// <OutlinedInput
// placeholder="Search keywords..."
// name="search"
// id="search"
// value={search}
// onChange={(e) => setSearch && setSearch(e.target.value)}
// startAdornment={
// <InputAdornment position="start">
// <IconButton edge="start">
// <SearchNormal size={20} />
// </IconButton>
// </InputAdornment>
// }
// />
// </div>
// {filterMethod ? <div className="header-right flex justify-end items-center gap-2">
// <SelectField
// name="search"
// value={filterMethod}
// onChange={(e) => setFilterMethod && setFilterMethod(e.target.value)}
// options={[
// { value: "all", name: "All Method" },
// { value: "crypto", name: "Crypto" },
// { value: "paypal", name: "USD/Paypal" },
// ]}
// />
// </div> : ""}
// <Button
// startIcon={
// !downMD && <DocumentDownload size={16} />
// }
// onClick={onDownloadCSV} sx={{
// borderRadius: "8px",
// border: "1px solid var(--Gray, #E0E0E3)",
// padding: "8px 16px",
// color: "#0E0E11",
// maxWidth: "fit-content",
// }}>{downMD ? <DocumentDownload size={16} /> : "Download CSV"}</Button>
// </div>
// );
// }
import { Button, IconButton, InputAdornment, OutlinedInput, useMediaQuery } from "@mui/material";
import SelectField from "../atom/SelectField";
import { DocumentDownload, SearchNormal } from "@wandersonalwes/iconsax-react";
interface FilterOption {
value: string;
label: string;
}
interface TableHeaderProps {
search: string;
setSearch?: (value: string) => void;
filterMethod?: string;
setFilterMethod?: (value: string) => void;
filterOptions?: FilterOption[];
onDownloadCSV: () => void;
}
......@@ -15,11 +86,13 @@ export default function TableHeader({
setSearch,
filterMethod,
setFilterMethod,
filterOptions,
onDownloadCSV,
}: TableHeaderProps) {
const downMD = useMediaQuery((theme) => theme.breakpoints.down('md'));
const downMD = useMediaQuery((theme: any) => theme.breakpoints.down('md'));
return (
<div className="table__header p-4 mb-4 flex justify-between">
<div className="table__header p-4 mb-4 flex justify-between items-center gap-4">
<div className="inpute__field relative">
<OutlinedInput
placeholder="Search keywords..."
......@@ -36,29 +109,34 @@ export default function TableHeader({
}
/>
</div>
{filterMethod ? <div className="header-right flex justify-end items-center gap-2">
<div className="header-right flex justify-end items-center gap-2">
{filterMethod && filterOptions && (
<SelectField
name="search"
name="filter"
value={filterMethod}
onChange={(e) => setFilterMethod && setFilterMethod(e.target.value)}
options={[
{ value: "all", name: "All Method" },
{ value: "crypto", name: "Crypto" },
{ value: "paypal", name: "USD/Paypal" },
]}
options={filterOptions.map(option => ({
value: option.value,
name: option.label
}))}
/>
</div> : ""}
)}
<Button
startIcon={
!downMD && <DocumentDownload size={16} />
}
onClick={onDownloadCSV} sx={{
startIcon={!downMD && <DocumentDownload size={16} />}
onClick={onDownloadCSV}
sx={{
borderRadius: "8px",
border: "1px solid var(--Gray, #E0E0E3)",
padding: "8px 16px",
color: "#0E0E11",
maxWidth: "fit-content",
}}>{downMD ? <DocumentDownload size={16} /> : "Download CSV"}</Button>
}}
>
{downMD ? <DocumentDownload size={16} /> : "Download CSV"}
</Button>
</div>
</div>
);
}
\ No newline at end of file
......@@ -9,6 +9,7 @@ import {
ListItemText,
} from "@mui/material";
import {
Activity,
ArrangeHorizontalCircle,
Game,
HambergerMenu,
......@@ -81,6 +82,12 @@ export default function AdminMenu({ open }: { open: boolean }) {
href: PATH.ADMIN.SETTINGS.ROOT,
active: pathname.startsWith(PATH.ADMIN.SETTINGS.ROOT),
},
{
label: "Activity",
icon: <Activity size={18} />,
href: PATH.ADMIN.ACTIVITY.ROOT,
active: pathname.startsWith(PATH.ADMIN.ACTIVITY.ROOT),
},
];
const [glassStyle, setGlassStyle] = React.useState({ top: 0, height: 0, opacity: 0 });
......
"use client";
import TableHeader from '@/components/molecules/TableHeader';
import CustomTable from '@/components/organism/Table';
import { User, Lock, ArrowUp, ArrowDown } from '@wandersonalwes/iconsax-react';
import React, { useMemo } from 'react';
import { useReactTable, getCoreRowModel, getPaginationRowModel, getSortedRowModel, ColumnDef } from '@tanstack/react-table';
import { Pagination, Box } from '@mui/material';
interface Activity {
id: number;
type: string;
user: string;
userId: string;
action: string;
details: string;
timestamp: string;
status: string;
icon: React.ReactNode;
}
export default function Activities() {
const activityTypes = [
{ value: 'all', label: 'All Activities' },
{ value: 'registration', label: 'Registration' },
{ value: 'login', label: 'Login/Logout' },
{ value: 'deposit', label: 'Deposits' },
{ value: 'withdrawal', label: 'Withdrawals' },
{ value: 'password_update', label: 'Security' },
{ value: 'game_play', label: 'Games' },
{ value: 'profile_update', label: 'Profile Updates' },
{ value: 'bonus', label: 'Bonuses' }
];
const activities: Activity[] = [
{
id: 1,
type: 'registration',
user: 'john_doe',
userId: 'USR001',
action: 'New user registered',
details: 'Account created successfully',
timestamp: '2025-10-16 14:30:25',
status: 'success',
icon: <User />
},
{
id: 2,
type: 'deposit',
user: 'sarah_smith',
userId: 'USR042',
action: 'Cash deposit',
details: 'Amount: $500.00 via Credit Card',
timestamp: '2025-10-16 14:25:10',
status: 'success',
icon: <User />
},
{
id: 3,
type: 'withdrawal',
user: 'mike_wilson',
userId: 'USR078',
action: 'Withdrawal request',
details: 'Amount: $250.00 to Bank Account',
timestamp: '2025-10-16 14:20:45',
status: 'pending',
icon: <User />
},
{
id: 4,
type: 'password_update',
user: 'emma_brown',
userId: 'USR023',
action: 'Password changed',
details: 'Security credentials updated',
timestamp: '2025-10-16 14:15:30',
status: 'success',
icon: <Lock />
},
{
id: 5,
type: 'login',
user: 'david_lee',
userId: 'USR056',
action: 'User login',
details: 'IP: 192.168.1.100, Location: New York, USA',
timestamp: '2025-10-16 14:10:15',
status: 'success',
icon: <User />
},
{
id: 6,
type: 'game_play',
user: 'lisa_garcia',
userId: 'USR089',
action: 'Game played',
details: 'Super Stakes Poker - Bet: $50, Won: $125',
timestamp: '2025-10-16 14:05:00',
status: 'success',
icon: <User />
},
{
id: 7,
type: 'withdrawal',
user: 'alex_martinez',
userId: 'USR034',
action: 'Withdrawal failed',
details: 'Amount: $1000.00 - Insufficient funds',
timestamp: '2025-10-16 14:00:45',
status: 'failed',
icon: <User />
},
{
id: 8,
type: 'profile_update',
user: 'rachel_white',
userId: 'USR067',
action: 'Profile updated',
details: 'Changed email and phone number',
timestamp: '2025-10-16 13:55:30',
status: 'success',
icon: <User />
},
{
id: 9,
type: 'bonus',
user: 'chris_taylor',
userId: 'USR045',
action: 'Bonus claimed',
details: 'Welcome Bonus: $100 credited',
timestamp: '2025-10-16 13:50:15',
status: 'success',
icon: <User />
},
{
id: 10,
type: 'logout',
user: 'olivia_anderson',
userId: 'USR091',
action: 'User logout',
details: 'Session ended normally',
timestamp: '2025-10-16 13:45:00',
status: 'success',
icon: <User />
},
{
id: 11,
type: 'deposit',
user: 'james_thomas',
userId: 'USR012',
action: 'Cash deposit',
details: 'Amount: $1,200.00 via Bank Transfer',
timestamp: '2025-10-16 13:40:45',
status: 'success',
icon: <User />
},
{
id: 12,
type: 'password_update',
user: 'sophia_jackson',
userId: 'USR073',
action: 'Password reset',
details: 'Reset via email verification',
timestamp: '2025-10-16 13:35:30',
status: 'success',
icon: <Lock />
}
];
const [search, setSearch] = React.useState("");
const [page, setPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(10);
const [activityType, setActivityType] = React.useState("all");
const [sorting, setSorting] = React.useState<any>([]);
// Filter data based on search and activity type
const filteredData = useMemo(() => {
return activities.filter(activity => {
const matchesSearch = search === "" ||
activity.user.toLowerCase().includes(search.toLowerCase()) ||
activity.userId.toLowerCase().includes(search.toLowerCase()) ||
activity.action.toLowerCase().includes(search.toLowerCase()) ||
activity.details.toLowerCase().includes(search.toLowerCase());
const matchesType = activityType === "all" || activity.type === activityType;
return matchesSearch && matchesType;
});
}, [search, activityType]);
const columns = useMemo<ColumnDef<Activity>[]>(() => [
{
accessorKey: "id",
header: ({ column }) => {
const sortState = column.getIsSorted();
const arrow =
sortState === "asc" ? (
<ArrowUp size={14} />
) : sortState === "desc" ? (
<ArrowDown size={14} />
) : <ArrowUp size={14} className="opacity-30" />;
return (
<p
onClick={() => column.toggleSorting()}
className="flex items-center gap-1 cursor-pointer"
>
#ID {arrow}
</p>
);
},
},
{
accessorKey: "user",
header: "User",
cell: ({ row }) => {
const { user, userId } = row.original;
const initials = user.split('_').map(n => n[0]).join('').toUpperCase();
return (
<Box className="flex justify-start items-center gap-2">
<small className="text-[10px] w-[24px] h-[24px] flex items-center justify-center uppercase rounded-[4px] bg-[#1EB41B]/10 font-[500] text-[#1EB41B]">
{initials}
</small>
<div className="name-detail">
<strong className="text-primary block text-[12px] leading-[120%] font-[500] capitalize">
{user.replace('_', ' ')}
</strong>
<small className="text-[10px] text-para-light font-[500]">
{userId}
</small>
</div>
</Box>
);
},
},
{
accessorKey: "type",
header: "Activity Type",
cell: ({ row }) => {
const type = row.original.type;
const display = type.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
return (
<span className="capitalize text-[12px] font-[500]">
{display}
</span>
);
},
},
{
accessorKey: "action",
header: "Action",
cell: ({ row }) => (
<span className="text-[12px] font-[500]">{row.original.action}</span>
),
},
{
accessorKey: "details",
header: "Details",
cell: ({ row }) => (
<span className="text-[11px] text-para-light max-w-[250px] block truncate">
{row.original.details}
</span>
),
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const status = row.original.status.toLowerCase();
const display = status.charAt(0).toUpperCase() + status.slice(1);
return (
<span
className={`px-2 py-1 max-w-[80px] block text-[10px] text-white status rounded-[8px] text-center ${status}`}
>
{display}
</span>
);
},
},
{
accessorKey: "timestamp",
header: "Timestamp",
cell: ({ row }) => {
const [date, time] = row.original.timestamp.split(' ');
return (
<Box>
<span className="text-[12px] font-[500] block">{date}</span>
<small className="text-[10px] text-para-light font-[500]">[{time}]</small>
</Box>
);
},
},
], []);
const table = useReactTable({
data: filteredData,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
const totalPages = Math.ceil(filteredData.length / pageSize);
return (
<div className="border-gray border-solid border-[1px] rounded-[8px] lg:rounded-[16px]">
<TableHeader
search={search}
setSearch={setSearch}
filterMethod={activityType}
setFilterMethod={(value) => {
setActivityType(value);
setPage(1);
}}
filterOptions={activityTypes}
onDownloadCSV={() => { }}
/>
<CustomTable
key={`${page}-${pageSize}-${search}-${activityType}`}
table={table}
loading={false}
/>
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mt-4 px-8 py-6 gap-4">
<div className="flex items-center gap-2">
<span className="text-[12px] font-[500]">Rows per page:</span>
<select
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
setPage(1);
}}
className="border border-gray-300 rounded p-1 text-[12px]"
>
{[5, 10, 15, 20].map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
{/* <span className="text-[12px] text-para-light ml-4">
Showing {filteredData.length > 0 ? ((page - 1) * pageSize) + 1 : 0} to {Math.min(page * pageSize, filteredData.length)} of {filteredData.length} activities
</span> */}
</div>
<Pagination
count={totalPages || 1}
page={page}
onChange={(_, value) => setPage(value)}
variant="outlined"
shape="rounded"
sx={{ gap: "8px" }}
/>
</div>
</div>
);
}
\ No newline at end of file
import { Dash } from '@wandersonalwes/iconsax-react'
import React from 'react'
export default function ActivityAnalyticCard() {
return (
<div className="bg-white rounded-lg shadow-sm p-4 border-l-4 border-blue-500">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Total Activities</p>
<p className="text-2xl font-bold text-gray-900">{12}</p>
</div>
<Dash className="w-8 h-8 text-blue-500" />
</div>
</div>
)
}
import React from 'react'
import ActivityAnalyticCard from './ActivityAnalyticCard'
import Activities from './Activities'
import PageHeader from '@/components/molecules/PageHeader'
export default function ActivityLogPage() {
return (
<section className="activity__log__root">
<PageHeader title='Activity Log' />
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-8">
{Array.from({ length: 4 }).map((_, index) => (
<ActivityAnalyticCard key={index.toString()} />
))}
</div>
<div className="section-title mb-4">
<h2 className="text-[20px] leading-[140%] font-[600]">
All Activities
</h2>
</div>
<Activities />
</section>
)
}
......@@ -47,6 +47,9 @@ export const PATH = {
SETTINGS: {
ROOT: "/settings"
},
ACTIVITY: {
ROOT: "/activity-log"
},
PAGES: {
ROOT: "/pages",
ADD_PAGE: {
......
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