diff --git a/web/src/app/components/AdminDashboard/AdminDashboard.module.css b/web/src/app/components/AdminDashboard/AdminDashboard.module.css new file mode 100644 index 0000000..8500407 --- /dev/null +++ b/web/src/app/components/AdminDashboard/AdminDashboard.module.css @@ -0,0 +1,57 @@ +/* table */ +.tableContainer { + background-color: black; + width: 90%; + max-width: 1000px; +} + +.table { + border-collapse: separate; + border-spacing: 0 1rem; + background-color: black; +} + +.tableHeader { + border: none; + background-color: black; + border-bottom: 1px solid white; + padding: 1rem; +} + +.tableRow { + background-color: var(--mantine-color-customCharcoalGrey-1); + color: white; +} + +.leftRoundedCell { + padding: 1rem; + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; +} + +.rightRoundedCell { + padding: 1rem; + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; +} + +.pagination { + background: transparent; + color: white; + margin-top: 2rem; + button { + background: transparent; + color: white; + border-color: transparent; + width: 2.5rem; + height: 2.5rem; + font-size: 1.2rem; + &[data-active='true'] { + color: white; + background: var(--mantine-color-customDarkBlue-1); + } + } + button:hover { + background-color: var(--mantine-color-customAzureBlue-1); + } +} diff --git a/web/src/app/components/AdminDashboard/AdminDashboardTable.tsx b/web/src/app/components/AdminDashboard/AdminDashboardTable.tsx new file mode 100644 index 0000000..f8a6d6a --- /dev/null +++ b/web/src/app/components/AdminDashboard/AdminDashboardTable.tsx @@ -0,0 +1,67 @@ +import { Group, Pagination, Stack, Table, Text } from '@mantine/core'; +import styles from './AdminDashboard.module.css'; +import { AdminReview } from '@/app/models/adminReview'; +import { FC, useState } from 'react'; +import { date2string } from '@/app/features/date/dateConverter'; +import { ceil } from 'lodash'; + +interface Props { + data: AdminReview[]; +} +const AdminDashboardTable: FC = ({ data }) => { + const [page, onChange] = useState(1); + const entriesPerPage = 6; + + const totalPages = ceil(data.length / entriesPerPage); + + // Calculate the starting and ending index for the current page + const startIdx = (page - 1) * entriesPerPage; + const endIdx = startIdx + entriesPerPage; + const currentPageData = data.slice(startIdx, endIdx); + + return ( + +
+ + + + + Name + User Type + Date + Status + + + + {currentPageData.map((review) => ( + + {review.name} + {review.userType} + {date2string(review.date)} + {review.status} + + ))} + +
+
+ + + + + + + + + +
+
+ ); +}; + +export default AdminDashboardTable; diff --git a/web/src/app/components/Filter/AdminFilter.tsx b/web/src/app/components/Filter/AdminFilter.tsx new file mode 100644 index 0000000..f9e3057 --- /dev/null +++ b/web/src/app/components/Filter/AdminFilter.tsx @@ -0,0 +1,163 @@ +import { Checkbox, Title, Button, Stack, Modal, Flex } from '@mantine/core'; +import styles from './Filter.module.css'; +import { FC, useState } from 'react'; +import { IconArrowDown } from '@tabler/icons-react'; +import { Role, roleToString, stringToRole, stringsToRoles } from '@/app/type/role'; +import { Status, statusToString, stringsToStatuses } from '@/app/type/status'; + +interface Props { + filterUserTypes: Role[]; + setFilterUserTypes: (filterRoles: Role[]) => void; + filterStatus: Status[]; + setFilterStatus: (filterFields: Status[]) => void; +} + +const AdminFilter: FC = ({ + filterUserTypes, + setFilterUserTypes, + filterStatus, + setFilterStatus, +}) => { + const roles = [ + { label: 'Sponsor', value: Role.Sponsor }, + { label: 'Student', value: Role.Student }, + { label: 'Alumni', value: Role.Alumni }, + // Add other roles as needed + ]; + + const statusOptions = [ + { value: Status.Pending, label: 'Pending' }, + { value: Status.Approved, label: 'Approved' }, + { value: Status.Rejected, label: 'Rejected' }, + ]; + + const [isModalOpen, setIsModalOpen] = useState(false); + const [isPortrait, setIsPortrait] = useState(window.innerHeight > window.innerWidth); + + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + + return ( + <> + {!isPortrait && ( + + + Filters + + + + stringToRole(role))} // Convert enums to strings + onChange={(selectedValues: string[]) => { + setFilterUserTypes(stringsToRoles(selectedValues)); // Convert strings back to RoleType + }} + label="User Type" + labelProps={{ style: { color: 'customAzureBlue.1' } }} + classNames={{ label: styles.filterSubheading }} + > + {roles.map((role) => ( + + ))} + + + + + statusToString(status))} // Convert enums to strings + onChange={(selectedValues: string[]) => { + setFilterStatus(stringsToStatuses(selectedValues)); + }} + label="Status" + labelProps={{ style: { color: 'customAzureBlue.1' } }} + classNames={{ label: styles.filterSubheading }} + > + {statusOptions.map((status) => ( + + ))} + + + + )} + {isPortrait && ( + <> + + + + + + roleToString(role))} // Convert enums to strings + onChange={(selectedValues: string[]) => { + setFilterUserTypes(stringsToRoles(selectedValues)); // Convert strings back to RoleType + }} + label="User Type" + labelProps={{ style: { color: 'customAzureBlue.1' } }} + classNames={{ label: styles.filterSubheading }} + > + {roles.map((role) => ( + + ))} + + + + statusToString(status))} // Convert enums to strings + onChange={(selectedValues: string[]) => { + setFilterStatus(stringsToStatuses(selectedValues)); + }} + label="Status" + classNames={{ label: styles.filterSubheading }} + > + {statusOptions.map((status) => ( + + ))} + + + + + )} + + ); +}; + +export default AdminFilter; diff --git a/web/src/app/components/Navbar/Navbar.tsx b/web/src/app/components/Navbar/Navbar.tsx index c434af8..9dcf5fa 100644 --- a/web/src/app/components/Navbar/Navbar.tsx +++ b/web/src/app/components/Navbar/Navbar.tsx @@ -100,10 +100,10 @@ function Navbar() { diff --git a/web/src/app/components/SearchBar/SearchBar.tsx b/web/src/app/components/SearchBar/SearchBar.tsx index 1e029d8..4595a12 100644 --- a/web/src/app/components/SearchBar/SearchBar.tsx +++ b/web/src/app/components/SearchBar/SearchBar.tsx @@ -17,7 +17,7 @@ const SearchBar: FC = ({ search, setSearch, title, placeholder } const [isPortrait, setIsPortrait] = useState(window.innerHeight > window.innerWidth); return ( - + {!isPortrait ? ( <> diff --git a/web/src/app/features/date/dateConverter.ts b/web/src/app/features/date/dateConverter.ts new file mode 100644 index 0000000..f56d0b7 --- /dev/null +++ b/web/src/app/features/date/dateConverter.ts @@ -0,0 +1,5 @@ +export function date2string(date: Date): string { + const day = date.getDate().toString().padStart(2, '0'); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); // getMonth() returns 0-indexed month + return `${day}/${month}`; +} diff --git a/web/src/app/models/adminReview.ts b/web/src/app/models/adminReview.ts new file mode 100644 index 0000000..16a1498 --- /dev/null +++ b/web/src/app/models/adminReview.ts @@ -0,0 +1,10 @@ +import { UserType } from '../features/user/userSlice'; +import { Status } from '../type/status'; + +export interface AdminReview { + id: string; + name: string; + userType: UserType; + date: Date; + status: Status; +} diff --git a/web/src/app/pages/Admin/AdminDashboard.page.tsx b/web/src/app/pages/Admin/AdminDashboard.page.tsx index d44b9d2..08e8138 100644 --- a/web/src/app/pages/Admin/AdminDashboard.page.tsx +++ b/web/src/app/pages/Admin/AdminDashboard.page.tsx @@ -1,11 +1,140 @@ -import { Flex, Title } from '@mantine/core'; +import { Status } from '@/app/type/status'; +import { AdminReview } from '@/app/models/adminReview'; +import AdminDashboardTable from '@/app/components/AdminDashboard/AdminDashboardTable'; +import { useState, useEffect } from 'react'; +import { Role } from '@/app/type/role'; +import AdminFilter from '@/app/components/Filter/AdminFilter'; +import { Divider, Grid } from '@mantine/core'; +import SearchBar from '@/app/components/SearchBar/SearchBar'; + +// TODO: remove once integration is completed +const mockReview: AdminReview[] = [ + { + id: '1', + name: 'Google', + userType: Role.Alumni, + date: new Date(), + status: Status.Approved, + }, + { + id: '12', + name: 'Google', + userType: Role.Alumni, + date: new Date(), + status: Status.Approved, + }, + { + id: '123', + name: 'Google', + userType: Role.Alumni, + date: new Date(), + status: Status.Approved, + }, + { + id: '1234', + name: 'Google', + userType: Role.Student, + date: new Date(), + status: Status.Approved, + }, + { + id: '123456', + name: 'Google', + userType: Role.Student, + date: new Date(), + status: Status.Approved, + }, + { + id: '1234567', + name: 'Google', + userType: Role.Sponsor, + date: new Date(), + status: Status.Approved, + }, + { + id: '12345678', + name: 'Google', + userType: Role.Sponsor, + date: new Date(), + status: Status.Approved, + }, +]; export function AdminDashboard() { + const [filterUserTypes, setFilterUserTypes] = useState([]); + const [filterStatus, setFilterStatus] = useState([]); + const [isPortrait, setIsPortrait] = useState(window.innerHeight > window.innerWidth); + const [filteredReview, setFilteredReview] = useState(mockReview); + const [search, setSearch] = useState(''); + useEffect(() => { + const handleResize = () => { + setIsPortrait(window.innerHeight > window.innerWidth); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + useEffect(() => { + const filtered = mockReview.filter((review) => { + const matchesUserType = + filterUserTypes.length === 0 || filterUserTypes.includes(review.userType as Role); + const matchesStatus = filterStatus.length === 0 || filterStatus.includes(review.status); + + return matchesUserType && matchesStatus; + }); + console.log('Review:', filtered, filterUserTypes); + + setFilteredReview([...filtered]); + }, [filterStatus, filterUserTypes]); + return ( <> - - Admin Dashboard - + + {!isPortrait ? ( + <> + + + + + + + + + + + + ) : ( + + + + + + )} + ); } diff --git a/web/src/app/pages/Admin/AdminPage.module.css b/web/src/app/pages/Admin/AdminPage.module.css new file mode 100644 index 0000000..ef07868 --- /dev/null +++ b/web/src/app/pages/Admin/AdminPage.module.css @@ -0,0 +1,37 @@ +/* table */ +.tableContainer { + background-color: black; + width: 80%; + max-width: 800px; +} + +.table { + border-collapse: separate; + border-spacing: 0 1rem; + background-color: black; + height: 80%; +} + +.tableHeader { + border: none; + background-color: black; + border-bottom: 1px solid white; + padding: 1rem; +} + +.tableRow { + background-color: var(--mantine-color-customCharcoalGrey-1); + color: white; +} + +.leftRoundedCell { + padding: 1rem; + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; +} + +.rightRoundedCell { + padding: 1rem; + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; +} diff --git a/web/src/app/pages/Alumni/AlumniBoard.page.tsx b/web/src/app/pages/Alumni/AlumniBoard.page.tsx index 8bd8cb7..087b361 100644 --- a/web/src/app/pages/Alumni/AlumniBoard.page.tsx +++ b/web/src/app/pages/Alumni/AlumniBoard.page.tsx @@ -51,7 +51,7 @@ export function AlumniBoard() { diff --git a/web/src/app/pages/Sponsor/SponsorsBoard.page.tsx b/web/src/app/pages/Sponsor/SponsorsBoard.page.tsx index 5dc646b..6269c3f 100644 --- a/web/src/app/pages/Sponsor/SponsorsBoard.page.tsx +++ b/web/src/app/pages/Sponsor/SponsorsBoard.page.tsx @@ -51,7 +51,7 @@ export function SponsorsBoard() { diff --git a/web/src/app/pages/Student/StudentsBoard.page.tsx b/web/src/app/pages/Student/StudentsBoard.page.tsx index 4f753ea..06b73af 100644 --- a/web/src/app/pages/Student/StudentsBoard.page.tsx +++ b/web/src/app/pages/Student/StudentsBoard.page.tsx @@ -50,7 +50,7 @@ export function StudentsBoard() { diff --git a/web/src/app/type/role.ts b/web/src/app/type/role.ts index 89d7169..f1c1e91 100644 --- a/web/src/app/type/role.ts +++ b/web/src/app/type/role.ts @@ -4,10 +4,22 @@ export enum Role { Alumni = 'alumni', Admin = 'admin', Member = 'member', - Unknown = 'unknown' // Fallback in case error + Unknown = 'unknown', // Fallback in case error } export function stringToRole(roleString: any): Role { - const roleKey = Object.keys(Role).find(key => Role[key as keyof typeof Role] === roleString) as keyof typeof Role | undefined; - return roleKey ? Role[roleKey] : Role.Unknown; + const roleKey = Object.keys(Role).find((key) => Role[key as keyof typeof Role] === roleString) as + | keyof typeof Role + | undefined; + return roleKey ? Role[roleKey] : Role.Unknown; // Fallback in case error +} + +export function roleToString(role: Role): string { + return role; +} + +export function stringsToRoles(values: string[]): Role[] { + return values + .map((value) => stringToRole(value)) + .filter((role): role is Role => role !== undefined); } diff --git a/web/src/app/type/status.ts b/web/src/app/type/status.ts new file mode 100644 index 0000000..66ec922 --- /dev/null +++ b/web/src/app/type/status.ts @@ -0,0 +1,18 @@ +export enum Status { + Pending = 'pending', + Approved = 'approved', + Rejected = 'rejected', +} + +export function statusToString(status: Status): string { + return status; +} + +export function stringToStatus(status: string): Status | undefined { + return Object.values(Status).find((value) => value === status); +} + +// Convert Array of Strings to Array of Status Enums +export function stringsToStatuses(statuses: string[]): Status[] { + return statuses.map((value) => stringToStatus(value) ?? Status.Pending); // Default to Status.Pending if not found +}