Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions app/admin/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { ApplicationType } from '@/types/adminDashboard';

export const ADMIN_DASHBOARD_CONST = {
PAGE_TITLE: 'Admin Dashboard',
TABS: [
{
label: 'All',
value: 'all',
},
{
label: 'To Review',
value: 'toReview',
},
{
label: 'Payment Pending',
value: 'paymentPending',
},
{
label: 'Active',
value: 'active',
},
{
label: 'Expired',
value: 'expired',
},
{
label: 'Rejected',
value: 'rejected',
},
],
TABLE_COLUMNS: [
{ label: 'Applicant Name', value: 'applicantName' },
{ label: 'Application Type', value: 'type' },
{ label: 'Date Received', value: 'dateReceived' },
{ label: 'Status', value: 'status' },
{ label: 'Reviewer 1', value: 'reviewer1' },
{ label: 'Reviewer 2', value: 'reviewer2' },
],
};

export const ADMIN_DASHBOARD_MOCK = {
ADMIN_NAME: 'John Doe',
PROFILE_PICTURE_URL: undefined,
STAT_CARDS: [
{
label: 'Independent Members',
value: 30,
},
{
label: 'Organization Members',
value: 20,
},
{
label: 'New Applications',
value: 6,
},
],
APPLICATIONS_MOCK: [
{
id: 'APP01',
applicantName: 'Leighton Kramer',
type: 'Individual',
dateReceived: '2026-01-06',
status: 'toReview',
reviewer1: '',
reviewer2: '',
},
{
id: 'APP02',
applicantName: 'Marceline Avila',
type: 'Organization',
dateReceived: '2025-12-18',
status: 'toReview',
reviewer1: 'Jane Doe',
reviewer2: '',
},
{
id: 'APP03',
applicantName: 'Jensen Vang',
type: 'Organization',
dateReceived: '2025-11-11',
status: 'rejected',
reviewer1: 'John Doe',
reviewer2: 'Jane Doe',
},
{
id: 'APP04',
applicantName: 'Linda Wu',
type: 'Individual',
dateReceived: '2025-11-11',
status: 'paymentPending',
reviewer1: 'John Doe',
reviewer2: 'Jane Doe',
},
{
id: 'APP05',
applicantName: 'Carter Higgins',
type: 'Organization',
dateReceived: '2025-11-05',
status: 'active',
reviewer1: 'John Doe',
reviewer2: 'Jane Doe',
},
{
id: 'APP06',
applicantName: 'Sarah Johnson',
type: 'Individual',
dateReceived: '2025-11-10',
status: 'active',
reviewer1: 'Jane Doe',
reviewer2: 'John Doe',
},
{
id: 'APP07',
applicantName: 'Michael Chen',
type: 'Organization',
dateReceived: '2025-11-09',
status: 'expired',
reviewer1: 'John Doe',
reviewer2: '',
},
] as ApplicationType[],
};
72 changes: 72 additions & 0 deletions app/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
'use client';

import { useEffect, useState } from 'react';
import { inter } from '@/app/fonts';
import AdminNavbar from '@/components/AdminNavbar';
import StatCard from '@/components/StatCard';
import Tabs from '@/components/Tabs';
import AdminSearchBar from '@/components/AdminSearchBar';
import { ADMIN_DASHBOARD_MOCK, ADMIN_DASHBOARD_CONST } from './const';
import AdminDashboardTable from '@/components/AdminDashboardTable';
import AdminDashboardMobileTable from '@/components/AdminDashboardMobileTable';

export default function AdminDashboard() {
const [currentTab, setCurrentTab] = useState(ADMIN_DASHBOARD_CONST.TABS[0]);
const [searchQuery, setSearchQuery] = useState('');
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');

// Debounce the search query
// TODO: Use the debouncedSearchQuery to filter the applications in the backend
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchQuery(searchQuery);
}, 300); // 300ms delay

return () => clearTimeout(timer);
}, [searchQuery]);

return (
<>
<AdminNavbar />
<div className="md:px-7.25">
<Tabs
tabs={ADMIN_DASHBOARD_CONST.TABS}
currentTab={currentTab}
setCurrentTab={setCurrentTab}
additionalClasses={{
wrapper: 'md:mt-[43px] p-[10px] md:p-0',
}}
/>
</div>
<div className="px-4 md:px-8 mt-5.75 md:mt-10.5 flex justify-around gap-x-4 md:gap-x-6 w-full">
{ADMIN_DASHBOARD_MOCK.STAT_CARDS.map((card) => (
<StatCard key={card.label} label={card.label} value={card.value} />
))}
</div>
<div className="px-4 md:px-8 mb-20">
<AdminSearchBar value={searchQuery} onChange={setSearchQuery} />
<div className="block md:hidden">
<AdminDashboardMobileTable
applications={ADMIN_DASHBOARD_MOCK.APPLICATIONS_MOCK}
currentTab={currentTab}
pagination={{
itemCount: ADMIN_DASHBOARD_MOCK.APPLICATIONS_MOCK.length,
numberPerPage: 5,
}}
/>
</div>
<div className="hidden md:block">
<AdminDashboardTable
columns={ADMIN_DASHBOARD_CONST.TABLE_COLUMNS}
applications={ADMIN_DASHBOARD_MOCK.APPLICATIONS_MOCK}
currentTab={currentTab}
pagination={{
itemCount: ADMIN_DASHBOARD_MOCK.APPLICATIONS_MOCK.length,
numberPerPage: 5,
}}
/>
</div>
</div>
</>
);
}
11 changes: 11 additions & 0 deletions app/fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Roboto_Condensed, Inter } from 'next/font/google';

export const robotoCondensed = Roboto_Condensed({
variable: '--font-roboto-condensed',
subsets: ['latin'],
});

export const inter = Inter({
variable: '--font-inter',
subsets: ['latin'],
});
89 changes: 89 additions & 0 deletions components/AdminDashboardMobileTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Table from './Table';
import { formatDateWithOrdinal } from '@/lib/utils';
import StatusChip from './StatusChip';
import { AdminDashboardMobileTablePropTypes } from '@/types/adminDashboard';
import { useState, useMemo } from 'react';
import { Pagination } from './Pagination';

export default function AdminDashboardMobileTable({
applications,
currentTab,
pagination,
}: AdminDashboardMobileTablePropTypes) {
const [currentPage, setCurrentPage] = useState(1);

const handlePageChange = (page: number) => {
setCurrentPage(page);
};

// TODO: This should be handled in the backend
const filteredApplications = useMemo(() => {
return applications.filter((app) =>
currentTab.value !== 'all' ? app.status === currentTab.value : true,
);
}, [applications, currentTab]);

// TODO: This should be handled in the backend
const paginatedApplications = useMemo(() => {
if (!pagination) {
return filteredApplications;
}
const startIndex = (currentPage - 1) * pagination.numberPerPage;
const endIndex = startIndex + pagination.numberPerPage;
return filteredApplications.slice(startIndex, endIndex);
}, [filteredApplications, currentPage, pagination]);

return (
<>
<div className="flex flex-col mt-5.5 gap-y-4">
{paginatedApplications.map((app) => (
<div
key={app.id}
className="rounded-3xl border-2 border-[#BAB7B2] bg-[#F5F4F2] p-4.5"
>
<div className="flex justify-between">
<div className="font-bold text-[18px]">{app.applicantName}</div>
<div className="font-medium">
<StatusChip theme={app.status} />
</div>
</div>
<div className="flex text-[14px] leading-5 mt-3">
<div className="w-37.5 mr-3">
<p className="text-[#74654C] font-medium ">Type</p>
<p className="font-normal">{app.type}</p>
</div>
<div className="w-37.5">
<p className="text-[#74654C] font-medium">Date Received</p>
<p className="font-normal">
{formatDateWithOrdinal(app.dateReceived)}
</p>
</div>
</div>
<div className="border-t border-[#BAB7B2] mt-3 flex text-[14px] leading-5 ">
<div className="w-37.5 mr-3">
<p className="text-[#74654C] font-medium pt-3">Reviewer 1</p>
<p className="font-normal">
{app.reviewer1 === '' ? '-' : app.reviewer1}
</p>
</div>
<div className="w-37.5">
<p className="text-[#74654C] font-medium pt-3">Reviewer 2</p>
<p className="font-normal">
{app.reviewer2 === '' ? '-' : app.reviewer2}
</p>
</div>
</div>
</div>
))}
</div>
{pagination && (
<Pagination
activePage={currentPage}
itemCount={filteredApplications.length}
numberItemsPerPage={pagination.numberPerPage}
onPageChange={handlePageChange}
/>
)}
</>
);
}
78 changes: 78 additions & 0 deletions components/AdminDashboardTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Table from './Table';
import { formatDateWithOrdinal } from '@/lib/utils';
import StatusChip from './StatusChip';
import { AdminDashboardTablePropTypes } from '@/types/adminDashboard';
import { useState, useMemo } from 'react';

export default function AdminDashboardTable({
columns,
applications,
currentTab,
pagination,
}: AdminDashboardTablePropTypes) {
const [currentPage, setCurrentPage] = useState(1);

// TODO: This should be handled in the backend
const filteredApplications = useMemo(() => {
return applications.filter((app) =>
currentTab.value !== 'all' ? app.status === currentTab.value : true,
);
}, [applications, currentTab]);

// TODO: This should be handled in the backend
const paginatedApplications = useMemo(() => {
if (!pagination) {
return filteredApplications;
}
const startIndex = (currentPage - 1) * pagination.numberPerPage;
const endIndex = startIndex + pagination.numberPerPage;
return filteredApplications.slice(startIndex, endIndex);
}, [filteredApplications, currentPage, pagination]);

return (
<Table
columnNames={columns}
additionalClasses={{
wrapper: 'mt-4 md:mt-8',
}}
pagination={
pagination
? {
itemCount: filteredApplications.length,
numberPerPage: pagination.numberPerPage,
usePagination:
filteredApplications.length > pagination.numberPerPage,
}
: undefined
}
onPageChange={setCurrentPage}
>
{paginatedApplications.map((app) => (
<tr
key={app.id}
className="bg-[#F5F4F2] not-last:border-b not-last:border-[#BAB7B2] leading-6 tracking-[-0.31px] text-[16px]"
>
<td className="p-6 font-bold">{app.applicantName}</td>

<td className="p-6 font-normal">{app.type}</td>

<td className="p-6 font-normal">
{formatDateWithOrdinal(app.dateReceived)}
</td>

<td className="p-6 font-normal">
<StatusChip theme={app.status} />
</td>

<td className="p-6 font-normal">
{app.reviewer1 === '' ? '-' : app.reviewer1}
</td>

<td className="p-6 font-normal">
{app.reviewer2 === '' ? '-' : app.reviewer2}
</td>
</tr>
))}
</Table>
);
}
Loading
Loading