diff --git a/app/admin/const.ts b/app/admin/const.ts
new file mode 100644
index 0000000..9450fcd
--- /dev/null
+++ b/app/admin/const.ts
@@ -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[],
+};
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index e69de29..e44f275 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -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 (
+ <>
+
+
+
+
+
+ {ADMIN_DASHBOARD_MOCK.STAT_CARDS.map((card) => (
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/app/fonts.ts b/app/fonts.ts
new file mode 100644
index 0000000..9e9f732
--- /dev/null
+++ b/app/fonts.ts
@@ -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'],
+});
diff --git a/components/AdminDashboardMobileTable.tsx b/components/AdminDashboardMobileTable.tsx
new file mode 100644
index 0000000..5821ff2
--- /dev/null
+++ b/components/AdminDashboardMobileTable.tsx
@@ -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 (
+ <>
+
+ {paginatedApplications.map((app) => (
+
+
+
{app.applicantName}
+
+
+
+
+
+
+
+
Date Received
+
+ {formatDateWithOrdinal(app.dateReceived)}
+
+
+
+
+
+
Reviewer 1
+
+ {app.reviewer1 === '' ? '-' : app.reviewer1}
+
+
+
+
Reviewer 2
+
+ {app.reviewer2 === '' ? '-' : app.reviewer2}
+
+
+
+
+ ))}
+
+ {pagination && (
+
+ )}
+ >
+ );
+}
diff --git a/components/AdminDashboardTable.tsx b/components/AdminDashboardTable.tsx
new file mode 100644
index 0000000..3f923c4
--- /dev/null
+++ b/components/AdminDashboardTable.tsx
@@ -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 (
+ pagination.numberPerPage,
+ }
+ : undefined
+ }
+ onPageChange={setCurrentPage}
+ >
+ {paginatedApplications.map((app) => (
+
+ | {app.applicantName} |
+
+ {app.type} |
+
+
+ {formatDateWithOrdinal(app.dateReceived)}
+ |
+
+
+
+ |
+
+
+ {app.reviewer1 === '' ? '-' : app.reviewer1}
+ |
+
+
+ {app.reviewer2 === '' ? '-' : app.reviewer2}
+ |
+
+ ))}
+
+ );
+}
diff --git a/components/AdminNavbar.tsx b/components/AdminNavbar.tsx
new file mode 100644
index 0000000..b41f2c9
--- /dev/null
+++ b/components/AdminNavbar.tsx
@@ -0,0 +1,22 @@
+import { robotoCondensed } from '@/app/fonts';
+
+export default function AdminNavbar() {
+ return (
+
+
+ RPRC Membership Management
+
+
+ );
+}
diff --git a/components/AdminSearchBar.tsx b/components/AdminSearchBar.tsx
new file mode 100644
index 0000000..8223bbf
--- /dev/null
+++ b/components/AdminSearchBar.tsx
@@ -0,0 +1,34 @@
+import { inter } from '@/app/fonts';
+import { Search } from 'lucide-react';
+
+type AdminSearchBarProps = {
+ value: string;
+ onChange: (value: string) => void;
+ placeholder?: string;
+};
+
+export default function AdminSearchBar({
+ value,
+ onChange,
+ placeholder = 'Search by name...',
+}: AdminSearchBarProps) {
+ return (
+
+
+ onChange(e.target.value)}
+ className={`
+ ${inter.className}
+ w-full placeholder:text-[#A9A295] placeholder:text-[16px] focus:outline-none
+ `}
+ />
+
+ );
+}
diff --git a/components/Button.tsx b/components/Button.tsx
new file mode 100644
index 0000000..ab1287f
--- /dev/null
+++ b/components/Button.tsx
@@ -0,0 +1,35 @@
+type IconButtonAdditionalClassesPropTypes = {
+ button: string[];
+};
+
+type ButtonComponentPropTypes = {
+ children: React.ReactNode;
+ name?: string;
+ value?: string;
+ handleClick: React.MouseEventHandler;
+ additionalClasses?: IconButtonAdditionalClassesPropTypes;
+ isActive?: boolean;
+ disabled?: boolean;
+};
+
+export const Button = ({
+ children,
+ name,
+ value,
+ handleClick,
+ disabled = false,
+ additionalClasses,
+ isActive = false,
+}: Readonly) => {
+ return (
+
+ );
+};
diff --git a/components/Pagination.tsx b/components/Pagination.tsx
new file mode 100644
index 0000000..46134e9
--- /dev/null
+++ b/components/Pagination.tsx
@@ -0,0 +1,111 @@
+import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
+import { Button } from './Button';
+import { useState, useEffect } from 'react';
+
+type PaginationPropTypes = {
+ activePage: number;
+ itemCount: number;
+ numberItemsPerPage?: number;
+ onPageChange: (page: number) => void;
+};
+
+type usePaginationPropTypes = {
+ numberItemsPerPage?: number;
+ activePage: number;
+ itemCount: number;
+};
+
+const usePagination = ({
+ numberItemsPerPage,
+ itemCount,
+ activePage,
+}: usePaginationPropTypes) => {
+ const [pageCount, setPageCount] = useState(1);
+ const [numberPerPage, setNumberPerPage] = useState(10);
+
+ const windowSize = 5;
+ const maxPage = pageCount;
+ const shouldShift = activePage > windowSize;
+
+ const windowEnd = shouldShift ? Math.min(activePage, maxPage) : windowSize;
+ const windowStart = Math.max(windowEnd - (windowSize - 1), 1);
+
+ const setupPagination = (itemCount: number) => {
+ const numberOfPages = Math.ceil(itemCount / numberPerPage);
+ setPageCount(numberOfPages);
+ };
+
+ useEffect(() => {
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ if (numberItemsPerPage) setNumberPerPage(numberItemsPerPage);
+ }, [numberItemsPerPage]);
+
+ useEffect(() => {
+ if (!itemCount) return;
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ setupPagination(itemCount);
+ }, [itemCount, numberPerPage]);
+
+ return {
+ pageCount,
+ windowStart,
+ };
+};
+
+export const Pagination = ({
+ activePage,
+ itemCount,
+ numberItemsPerPage,
+ onPageChange,
+}: PaginationPropTypes) => {
+ const hook = usePagination({ numberItemsPerPage, itemCount, activePage });
+
+ if (hook.pageCount <= 1) return null;
+
+ return (
+
+
+
+
+
+ {Array.from({ length: Math.min(5, hook.pageCount) }).map(
+ (_, index) => {
+ const pageNumber = hook.windowStart + index;
+ const isActive = activePage === pageNumber;
+
+ return (
+
+ );
+ },
+ )}
+
+
+
+
+
+ );
+};
diff --git a/components/StatCard.tsx b/components/StatCard.tsx
new file mode 100644
index 0000000..99c0e74
--- /dev/null
+++ b/components/StatCard.tsx
@@ -0,0 +1,24 @@
+import { inter, robotoCondensed } from '@/app/fonts';
+
+type StatCardProps = {
+ label: string;
+ value: number;
+};
+
+export default function StatCard({ label, value }: StatCardProps) {
+ return (
+
+
+
+ {value}
+
+ {value}
+
+
+ {label}
+
+
+ );
+}
diff --git a/components/StatusChip.tsx b/components/StatusChip.tsx
new file mode 100644
index 0000000..c0face2
--- /dev/null
+++ b/components/StatusChip.tsx
@@ -0,0 +1,36 @@
+import { inter } from '@/app/fonts';
+
+type StatusChipType = {
+ color: string;
+ label: string;
+ theme: string;
+};
+
+const STATUS_CHIPS: StatusChipType[] = [
+ { color: '#00519F', label: 'To Review', theme: 'toReview' },
+ { color: '#393533', label: 'Rejected', theme: 'rejected' },
+ { color: '#C67D38', label: 'Payment Pending', theme: 'paymentPending' },
+ { color: '#5EB42D', label: 'Active', theme: 'active' },
+ { color: '#BE282B', label: 'Expired', theme: 'expired' },
+];
+
+type StatusChipProps = {
+ theme: 'toReview' | 'rejected' | 'paymentPending' | 'active' | 'expired';
+};
+
+export default function StatusChip({ theme }: StatusChipProps) {
+ const status = STATUS_CHIPS.find((s) => s.theme === theme);
+
+ if (!status) return null;
+
+ return (
+
+ );
+}
diff --git a/components/Table.tsx b/components/Table.tsx
new file mode 100644
index 0000000..bc1786f
--- /dev/null
+++ b/components/Table.tsx
@@ -0,0 +1,77 @@
+import { inter } from '@/app/fonts';
+import { ReactNode, useState } from 'react';
+import { ColumnType } from '@/types/adminDashboard';
+import { Pagination } from './Pagination';
+
+type TableProps = {
+ columnNames: ColumnType[];
+ sort?: {
+ sortedBy?: string;
+ sortingOrder: 'asc' | 'desc';
+ };
+ pagination?: {
+ itemCount: number;
+ numberPerPage: number;
+ usePagination?: boolean;
+ };
+ children: ReactNode;
+ additionalClasses?: {
+ wrapper?: string;
+ table?: string;
+ };
+ onPageChange?: (page: number) => void;
+};
+
+export default function Table({
+ columnNames,
+ children,
+ additionalClasses,
+ pagination,
+ onPageChange,
+}: TableProps) {
+ const [activePage, setActivePage] = useState(1);
+
+ const handlePageChange = (page: number) => {
+ setActivePage(page);
+ onPageChange?.(page);
+ };
+
+ return (
+
+
+
+
+
+ {columnNames.map((column, idx) => (
+ |
+ {column.label}
+ |
+ ))}
+
+
+
+ {children}
+
+
+
+ {pagination?.usePagination && (
+
+ )}
+
+ );
+}
diff --git a/components/Tabs.tsx b/components/Tabs.tsx
new file mode 100644
index 0000000..19450cd
--- /dev/null
+++ b/components/Tabs.tsx
@@ -0,0 +1,47 @@
+import { inter } from '@/app/fonts';
+
+type Tab = {
+ label: string;
+ value: string;
+};
+
+type TabsProps = {
+ tabs: Tab[];
+ currentTab: Tab;
+ setCurrentTab: (tab: Tab) => void;
+ additionalClasses?: {
+ wrapper: string;
+ };
+};
+
+export default function Tabs({
+ tabs,
+ currentTab,
+ setCurrentTab,
+ additionalClasses,
+}: TabsProps) {
+ return (
+
+ {tabs.map((tab) => {
+ const isActive = tab.value === currentTab.value;
+
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index bd0c391..c7ca753 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -1,6 +1,30 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
+}
+
+function getOrdinal(day: number) {
+ if (day > 3 && day < 21) return 'th';
+ switch (day % 10) {
+ case 1:
+ return 'st';
+ case 2:
+ return 'nd';
+ case 3:
+ return 'rd';
+ default:
+ return 'th';
+ }
+}
+
+export function formatDateWithOrdinal(date: string | Date) {
+ const d = new Date(date);
+
+ const day = d.getDate();
+ const month = d.toLocaleString('en-US', { month: 'short' });
+ const year = d.getFullYear();
+
+ return `${day}${getOrdinal(day)} ${month} ${year}`;
}
diff --git a/types/adminDashboard.ts b/types/adminDashboard.ts
new file mode 100644
index 0000000..824fac7
--- /dev/null
+++ b/types/adminDashboard.ts
@@ -0,0 +1,40 @@
+export type ColumnType = {
+ label: string;
+ value: string;
+ width?: string;
+};
+
+export type ApplicationType = {
+ id: string;
+ applicantName: string;
+ type: 'Individual' | 'Organization';
+ dateReceived: string | Date;
+ status: 'toReview' | 'rejected' | 'paymentPending' | 'active' | 'expired';
+ reviewer1: string;
+ reviewer2: string;
+};
+
+export type AdminDashboardTablePropTypes = {
+ columns: ColumnType[];
+ applications: ApplicationType[];
+ currentTab: {
+ label: string;
+ value: string;
+ };
+ pagination?: {
+ itemCount: number;
+ numberPerPage: number;
+ };
+};
+
+export type AdminDashboardMobileTablePropTypes = {
+ applications: ApplicationType[];
+ currentTab: {
+ label: string;
+ value: string;
+ };
+ pagination?: {
+ itemCount: number;
+ numberPerPage: number;
+ };
+};