From d8b3cf69ff155b3dc9c060fc65a263337f0bcc1c Mon Sep 17 00:00:00 2001
From: Pranav Kale Jain
Date: Fri, 8 May 2026 16:30:54 +0530
Subject: [PATCH] feat: complete admin projects management dashboard
---
src/App.tsx | 12 +
.../components/AnalyticsCards.tsx | 96 +++++++
.../components/AssignJudgeModal.tsx | 150 +++++++++++
.../components/BulkActionBar.tsx | 83 ++++++
.../AdminProjects/components/EmptyState.tsx | 53 ++++
.../components/FilterSidebar.tsx | 60 +++++
.../components/LoadingSkeleton.tsx | 44 ++++
.../components/ProjectStatusBadge.tsx | 31 +++
.../AdminProjects/components/ProjectsGrid.tsx | 109 ++++++++
.../components/ProjectsTable.tsx | 180 +++++++++++++
.../AdminProjects/components/SearchBar.tsx | 90 +++++++
.../constants/projectStatus.constants.ts | 51 ++++
.../mock/mockAdminProjectsApi.ts | 98 ++++++++
.../AdminProjects/pages/AdminProjectsPage.tsx | 236 ++++++++++++++++++
.../services/adminProjects.service.ts | 28 +++
.../types/adminProjects.types.ts | 68 +++++
src/features/Dashboard/mock/dashboardData.ts | 2 +-
src/features/SideBar/v1/Section/SideBar.tsx | 2 +-
18 files changed, 1391 insertions(+), 2 deletions(-)
create mode 100644 src/features/AdminProjects/components/AnalyticsCards.tsx
create mode 100644 src/features/AdminProjects/components/AssignJudgeModal.tsx
create mode 100644 src/features/AdminProjects/components/BulkActionBar.tsx
create mode 100644 src/features/AdminProjects/components/EmptyState.tsx
create mode 100644 src/features/AdminProjects/components/FilterSidebar.tsx
create mode 100644 src/features/AdminProjects/components/LoadingSkeleton.tsx
create mode 100644 src/features/AdminProjects/components/ProjectStatusBadge.tsx
create mode 100644 src/features/AdminProjects/components/ProjectsGrid.tsx
create mode 100644 src/features/AdminProjects/components/ProjectsTable.tsx
create mode 100644 src/features/AdminProjects/components/SearchBar.tsx
create mode 100644 src/features/AdminProjects/constants/projectStatus.constants.ts
create mode 100644 src/features/AdminProjects/mock/mockAdminProjectsApi.ts
create mode 100644 src/features/AdminProjects/pages/AdminProjectsPage.tsx
create mode 100644 src/features/AdminProjects/services/adminProjects.service.ts
create mode 100644 src/features/AdminProjects/types/adminProjects.types.ts
diff --git a/src/App.tsx b/src/App.tsx
index b542d78..2f4f7f7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,6 +12,8 @@ import Contact from "./features/Contact_And_Support/v1/Pages/Contact";
import ViewEvent from "./features/Events/v1/Pages/ViewEvent";
import LoginPage from "./features/Auth/v1/Pages/LoginPage";
import SignUpPage from "./features/Auth/v1/Pages/SignUpPage";
+import ProjectDetailPage from "./features/Projects/pages/ProjectDetailPage";
+import AdminProjectsPage from "./features/AdminProjects/pages/AdminProjectsPage";
import { startAutoUpdater } from "./system/updater/autoUpdater";
import ProtectedRoute from "./routes/ProtectedRoute";
@@ -62,6 +64,16 @@ function App() {
} />
} />
+ {/* Admin Specific Routes */}
+
+
+
+ }
+ />
+
404 Not Found} />
diff --git a/src/features/AdminProjects/components/AnalyticsCards.tsx b/src/features/AdminProjects/components/AnalyticsCards.tsx
new file mode 100644
index 0000000..90c4ad2
--- /dev/null
+++ b/src/features/AdminProjects/components/AnalyticsCards.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { ProjectAnalytics } from '../types/adminProjects.types';
+import { AreaChart, Area, ResponsiveContainer } from 'recharts';
+import { TrendingUp, TrendingDown, Users, CheckCircle2, XCircle, Clock, BarChart3, AlertTriangle } from 'lucide-react';
+import { cn } from '../../../lib/utils';
+
+interface AnalyticsCardsProps {
+ data: ProjectAnalytics | null;
+}
+
+const AnalyticsCards: React.FC = ({ data }) => {
+ if (!data) return null;
+
+ const stats = [
+ { label: 'Total Projects', value: data.totalProjects, trend: '+12%', trendUp: true, icon: BarChart3, color: 'blue', data: data.trends.total },
+ { label: 'Submitted', value: data.submittedProjects, trend: '+5%', trendUp: true, icon: Users, color: 'indigo', data: data.trends.submitted },
+ { label: 'Approved', value: data.approvedProjects, trend: '+8%', trendUp: true, icon: CheckCircle2, color: 'emerald', data: data.trends.approved },
+ { label: 'Pending', value: data.pendingReviews, trend: '+24%', trendUp: false, icon: Clock, color: 'amber', data: data.trends.pending },
+ { label: 'Rejected', value: data.rejectedProjects, trend: '-2%', trendUp: false, icon: XCircle, color: 'rose', data: data.trends.rejected },
+ { label: 'Avg Score', value: `${data.averageScore}/10`, trend: '+0.4', trendUp: true, icon: TrendingUp, color: 'violet', data: data.trends.total.map(v => v * 0.1) },
+ { label: 'Flagged', value: data.flaggedProjects, trend: '+2', trendUp: false, icon: AlertTriangle, color: 'orange', data: data.trends.rejected },
+ ];
+
+ const colorMap: Record = {
+ blue: 'from-blue-500/20 to-transparent text-blue-500',
+ indigo: 'from-indigo-500/20 to-transparent text-indigo-500',
+ emerald: 'from-emerald-500/20 to-transparent text-emerald-500',
+ amber: 'from-amber-500/20 to-transparent text-amber-500',
+ rose: 'from-rose-500/20 to-transparent text-rose-500',
+ violet: 'from-violet-500/20 to-transparent text-violet-500',
+ orange: 'from-orange-500/20 to-transparent text-orange-500',
+ };
+
+ const chartColorMap: Record = {
+ blue: '#3b82f6',
+ indigo: '#6366f1',
+ emerald: '#10b881',
+ amber: '#f59e0b',
+ rose: '#f43f5e',
+ violet: '#8b5cf6',
+ orange: '#f97316',
+ };
+
+ return (
+
+ {stats.map((stat, i) => (
+
+
+
+
+
+
+ {stat.trendUp ? : }
+ {stat.trend}
+
+
+
+
+
{stat.label}
+
{stat.value}
+
+
+ {/* Mini Chart */}
+
+
+ ({ v }))}>
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+};
+
+export default AnalyticsCards;
diff --git a/src/features/AdminProjects/components/AssignJudgeModal.tsx b/src/features/AdminProjects/components/AssignJudgeModal.tsx
new file mode 100644
index 0000000..63adab5
--- /dev/null
+++ b/src/features/AdminProjects/components/AssignJudgeModal.tsx
@@ -0,0 +1,150 @@
+import React, { useState } from 'react';
+import { X, Search, Check, Info } from 'lucide-react';
+import { ProjectJudge } from '../types/adminProjects.types';
+import { cn } from '../../../lib/utils';
+
+interface AssignJudgeModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ judges: ProjectJudge[];
+ onAssign: (judgeIds: string[]) => void;
+ projectName: string;
+}
+
+const AssignJudgeModal: React.FC = ({ isOpen, onClose, judges, onAssign, projectName }) => {
+ const [selectedIds, setSelectedIds] = useState([]);
+ const [search, setSearch] = useState('');
+ const [activeTrack, setActiveTrack] = useState('All Tracks');
+
+ if (!isOpen) return null;
+
+ const toggleJudge = (id: string) => {
+ setSelectedIds(prev =>
+ prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
+ );
+ };
+
+ const filteredJudges = judges.filter(j => {
+ const matchesSearch = j.name.toLowerCase().includes(search.toLowerCase()) ||
+ j.expertise.some(e => e.toLowerCase().includes(search.toLowerCase()));
+
+ const matchesTrack = activeTrack === 'All Tracks' ||
+ j.expertise.some(e => e.toLowerCase() === activeTrack.toLowerCase()) ||
+ (activeTrack === 'AI/ML' && j.expertise.some(e => ['ai', 'ml', 'neural nets'].includes(e.toLowerCase())));
+
+ return matchesSearch && matchesTrack;
+ });
+
+ return (
+
+
+
+
+
+
+
Assign Judges
+
Project: {projectName}
+
+
+
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+ {['All Tracks', 'AI/ML', 'Cybersecurity'].map((tag) => (
+ setActiveTrack(tag)}
+ className={cn(
+ "px-3 py-1.5 rounded-full text-xs font-medium border transition-all",
+ tag === activeTrack ? "bg-blue-500/10 border-blue-500/30 text-blue-400" : "bg-slate-800 border-slate-700 text-slate-400 hover:text-slate-200"
+ )}
+ >
+ {tag}
+
+ ))}
+
+
+
+
Recommended Judges ({filteredJudges.length})
+
+ {filteredJudges.map((judge) => (
+
toggleJudge(judge.id)}
+ className={cn(
+ "group relative flex items-center gap-4 p-4 rounded-2xl border transition-all cursor-pointer",
+ selectedIds.includes(judge.id)
+ ? "bg-blue-500/5 border-blue-500/30 ring-1 ring-blue-500/30"
+ : "bg-slate-800/30 border-slate-800 hover:border-slate-700"
+ )}
+ >
+
+
+
+
+
+
+
+
{judge.name}
+
+ {judge.matchPercentage}% Match
+
+
+
{judge.role}
+
+ {judge.expertise.map(e => (
+
+ {e}
+
+ ))}
+
+
+
+
+ {selectedIds.includes(judge.id) && }
+
+
+ ))}
+
+
+
+
+
+
+ {selectedIds.length} judge{selectedIds.length !== 1 ? 's' : ''} selected
+
+
+
+ Cancel
+
+ onAssign(selectedIds)}
+ disabled={selectedIds.length === 0}
+ className="px-6 py-2.5 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl font-bold transition-all shadow-lg shadow-blue-500/20 active:scale-95"
+ >
+ Assign Selected
+
+
+
+
+
+ );
+};
+
+export default AssignJudgeModal;
diff --git a/src/features/AdminProjects/components/BulkActionBar.tsx b/src/features/AdminProjects/components/BulkActionBar.tsx
new file mode 100644
index 0000000..22799d6
--- /dev/null
+++ b/src/features/AdminProjects/components/BulkActionBar.tsx
@@ -0,0 +1,83 @@
+import React from 'react';
+import { CheckCircle, XCircle, UserPlus, Trash2, Download, X } from 'lucide-react';
+
+interface BulkActionBarProps {
+ selectedCount: number;
+ onClear: () => void;
+ onApprove: () => void;
+ onReject: () => void;
+ onAssignJudges: () => void;
+ onDelete: () => void;
+}
+
+const BulkActionBar: React.FC = ({
+ selectedCount,
+ onClear,
+ onApprove,
+ onReject,
+ onAssignJudges,
+ onDelete
+}) => {
+ if (selectedCount === 0) return null;
+
+ return (
+
+
+
+
+ {selectedCount}
+
+
Projects Selected
+
+
+
+
+
+
+
+
+ Assign Judge
+
+
+
+
+ Bulk Approve
+
+
+
+
+ Bulk Reject
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default BulkActionBar;
diff --git a/src/features/AdminProjects/components/EmptyState.tsx b/src/features/AdminProjects/components/EmptyState.tsx
new file mode 100644
index 0000000..bd63de0
--- /dev/null
+++ b/src/features/AdminProjects/components/EmptyState.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { Search, FolderX, Filter } from 'lucide-react';
+
+interface EmptyStateProps {
+ type?: 'no-results' | 'no-data';
+ onClearFilters?: () => void;
+}
+
+const EmptyState: React.FC = ({ type = 'no-results', onClearFilters }) => {
+ return (
+
+
+
+
+ {type === 'no-results' ? (
+
+ ) : (
+
+ )}
+
+ !
+
+
+
+
+
+ {type === 'no-results' ? 'No projects found' : 'No projects yet'}
+
+
+ {type === 'no-results'
+ ? "We couldn't find any projects matching your current filters. Try refining your search or clearing all active parameters to start fresh."
+ : "There are no projects in this organization yet. New submissions will appear here once they are received."}
+
+
+ {type === 'no-results' && onClearFilters && (
+
+
+
+ Clear all filters
+
+
+ View Archive
+
+
+ )}
+
+ );
+};
+
+export default EmptyState;
diff --git a/src/features/AdminProjects/components/FilterSidebar.tsx b/src/features/AdminProjects/components/FilterSidebar.tsx
new file mode 100644
index 0000000..d4ed1bd
--- /dev/null
+++ b/src/features/AdminProjects/components/FilterSidebar.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { X } from 'lucide-react';
+import { ProjectFilters } from '../types/adminProjects.types';
+
+interface FilterSidebarProps {
+ filters: ProjectFilters;
+ setFilters: (filters: ProjectFilters) => void;
+}
+
+const FilterSidebar: React.FC = ({ filters, setFilters }) => {
+ const activeFilters = [
+ { key: 'status', value: filters.status, label: `Status: ${filters.status}` },
+ { key: 'event', value: filters.event, label: `Event: ${filters.event}` },
+ { key: 'track', value: filters.track, label: `Track: ${filters.track}` },
+ { key: 'judgeAssignment', value: filters.judgeAssignment, label: `Judges: ${filters.judgeAssignment}` },
+ ].filter(f => f.value !== 'All');
+
+ if (activeFilters.length === 0) return null;
+
+ const removeFilter = (key: string) => {
+ setFilters({ ...filters, [key]: 'All' });
+ };
+
+ const clearAll = () => {
+ setFilters({
+ ...filters,
+ status: 'All',
+ event: 'All',
+ track: 'All',
+ judgeAssignment: 'All'
+ });
+ };
+
+ return (
+
+ {activeFilters.map((filter) => (
+
+ {filter.label}
+ removeFilter(filter.key)}
+ className="hover:text-white transition-colors"
+ >
+
+
+
+ ))}
+
+ Clear all
+
+
+ );
+};
+
+export default FilterSidebar;
diff --git a/src/features/AdminProjects/components/LoadingSkeleton.tsx b/src/features/AdminProjects/components/LoadingSkeleton.tsx
new file mode 100644
index 0000000..c53117d
--- /dev/null
+++ b/src/features/AdminProjects/components/LoadingSkeleton.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+
+const LoadingSkeleton: React.FC<{ view?: 'table' | 'grid' }> = ({ view = 'table' }) => {
+ if (view === 'grid') {
+ return (
+
+ {Array.from({ length: 8 }).map((_, i) => (
+
+ ))}
+
+ );
+ }
+
+ return (
+
+
+ {Array.from({ length: 10 }).map((_, i) => (
+
+ ))}
+
+ );
+};
+
+export default LoadingSkeleton;
diff --git a/src/features/AdminProjects/components/ProjectStatusBadge.tsx b/src/features/AdminProjects/components/ProjectStatusBadge.tsx
new file mode 100644
index 0000000..98c7c33
--- /dev/null
+++ b/src/features/AdminProjects/components/ProjectStatusBadge.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { ProjectStatus } from '../types/adminProjects.types';
+import { PROJECT_STATUS_COLORS } from '../constants/projectStatus.constants';
+import { cn } from '../../../lib/utils';
+
+interface ProjectStatusBadgeProps {
+ status: ProjectStatus;
+ className?: string;
+}
+
+const ProjectStatusBadge: React.FC = ({ status, className }) => {
+ const styles = PROJECT_STATUS_COLORS[status] || PROJECT_STATUS_COLORS.Pending;
+
+ return (
+
+
+ {status}
+
+ );
+};
+
+export default ProjectStatusBadge;
diff --git a/src/features/AdminProjects/components/ProjectsGrid.tsx b/src/features/AdminProjects/components/ProjectsGrid.tsx
new file mode 100644
index 0000000..be37df4
--- /dev/null
+++ b/src/features/AdminProjects/components/ProjectsGrid.tsx
@@ -0,0 +1,109 @@
+import React from 'react';
+import { MoreVertical, Star, Users, CheckCircle, Clock } from 'lucide-react';
+import { Project } from '../types/adminProjects.types';
+import ProjectStatusBadge from './ProjectStatusBadge';
+import { cn } from '../../../lib/utils';
+
+interface ProjectsGridProps {
+ projects: Project[];
+ selectedIds: string[];
+ onSelect: (id: string) => void;
+ onViewProject: (project: Project) => void;
+ onAction: (id: string, action: string) => void;
+}
+
+const ProjectsGrid: React.FC = ({
+ projects,
+ selectedIds,
+ onSelect,
+ onViewProject,
+ onAction,
+}) => {
+ return (
+
+ {projects.map((project) => (
+
onViewProject(project)}
+ >
+ {/* Checkbox Overlay */}
+
{ e.stopPropagation(); onSelect(project.id); }}
+ >
+
+ {selectedIds.includes(project.id) && }
+
+
+
+ {/* Thumbnail */}
+
+
+
+
+
+
+
+
+ {project.avgScore}
+
+
+
+
+ {/* Content */}
+
+
+
+ {project.name}
+
+ { e.stopPropagation(); onAction(project.id, 'menu'); }}
+ className="p-1 text-slate-500 hover:text-white transition-colors"
+ >
+
+
+
+
+
{project.teamName}
+
+
+
+ {project.assignedJudges.map((judge, i) => (
+
+ ))}
+ {project.assignedJudges.length === 0 && (
+
+
+
+ )}
+
+
+
+
+ Updated 2h ago
+
+
+
+
+ ))}
+
+ );
+};
+
+export default ProjectsGrid;
diff --git a/src/features/AdminProjects/components/ProjectsTable.tsx b/src/features/AdminProjects/components/ProjectsTable.tsx
new file mode 100644
index 0000000..5e6d0c5
--- /dev/null
+++ b/src/features/AdminProjects/components/ProjectsTable.tsx
@@ -0,0 +1,180 @@
+import React from 'react';
+import { MoreVertical, ExternalLink, Shield, Trash2, CheckCircle, XCircle, Users } from 'lucide-react';
+import { Project } from '../types/adminProjects.types';
+import ProjectStatusBadge from './ProjectStatusBadge';
+import { format } from 'date-fns';
+import { cn } from '../../../lib/utils';
+
+interface ProjectsTableProps {
+ projects: Project[];
+ selectedIds: string[];
+ onSelect: (id: string) => void;
+ onSelectAll: () => void;
+ onViewProject: (project: Project) => void;
+ onAction: (id: string, action: string) => void;
+ pagination: { page: number; pageSize: number };
+ setPagination: (pagination: { page: number; pageSize: number }) => void;
+}
+
+const ProjectsTable: React.FC = ({
+ projects,
+ selectedIds,
+ onSelect,
+ onSelectAll,
+ onViewProject,
+ onAction,
+ pagination,
+ setPagination,
+}) => {
+ const isAllSelected = projects.length > 0 && selectedIds.length === projects.length;
+
+ return (
+
+
+
+
+
+
+
+
+ Project Name
+ Team
+ Event
+ Track
+ Status
+ Avg Score
+ Judges
+
+
+
+
+ {projects.map((project) => (
+ onViewProject(project)}
+ >
+ { e.stopPropagation(); onSelect(project.id); }}>
+
+ {selectedIds.includes(project.id) && }
+
+
+
+
+
+
+
+
+
{project.name}
+
{format(new Date(project.submissionDate), 'MMM d, h:mm a')}
+
+
+
+
+ {project.teamName}
+
+
+ {project.eventName}
+
+
+
+ {project.track}
+
+
+
+
+
+
+
+ {project.avgScore}
+ /10
+
+
+
+
+ {project.assignedJudges.map((judge, i) => (
+
+ ))}
+ {project.assignedJudges.length === 0 &&
Unassigned }
+
+
+ e.stopPropagation()}>
+
+
+
+
+
+
onAction(project.id, 'approve')} className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-emerald-500 flex items-center gap-2">
+ Approve
+
+
onAction(project.id, 'reject')} className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-800 hover:text-rose-500 flex items-center gap-2">
+ Reject
+
+
onAction(project.id, 'assign')} className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-800 flex items-center gap-2">
+ Assign Judge
+
+
+
onAction(project.id, 'suspend')} className="w-full text-left px-4 py-2 text-sm text-slate-300 hover:bg-slate-800 flex items-center gap-2">
+ Suspend
+
+
onAction(project.id, 'delete')} className="w-full text-left px-4 py-2 text-sm text-rose-500 hover:bg-rose-500/10 flex items-center gap-2">
+ Delete
+
+
+
+
+
+ ))}
+
+
+
+
+ {/* Pagination Placeholder */}
+
+
+ Page {pagination.page}
+
+
+ setPagination({ ...pagination, page: Math.max(1, pagination.page - 1) })}
+ disabled={pagination.page === 1}
+ className="px-3 py-1.5 bg-slate-900 border border-slate-800 text-slate-300 rounded-lg text-xs font-bold hover:bg-slate-800 transition-all disabled:opacity-30 disabled:cursor-not-allowed"
+ >
+ Previous
+
+ setPagination({ ...pagination, page: pagination.page + 1 })}
+ disabled={projects.length < pagination.pageSize}
+ className="px-3 py-1.5 bg-slate-800 border border-slate-700 text-slate-300 rounded-lg text-xs font-bold hover:bg-slate-700 transition-all disabled:opacity-30 disabled:cursor-not-allowed"
+ >
+ Next
+
+
+
+
+ );
+};
+
+export default ProjectsTable;
diff --git a/src/features/AdminProjects/components/SearchBar.tsx b/src/features/AdminProjects/components/SearchBar.tsx
new file mode 100644
index 0000000..7780dc0
--- /dev/null
+++ b/src/features/AdminProjects/components/SearchBar.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { Search, Filter, RotateCcw, LayoutGrid, List } from 'lucide-react';
+import { ProjectFilters } from '../types/adminProjects.types';
+import { TRACKS, EVENTS } from '../constants/projectStatus.constants';
+
+interface SearchBarProps {
+ filters: ProjectFilters;
+ setFilters: (filters: ProjectFilters) => void;
+ view: 'table' | 'grid';
+ setView: (view: 'table' | 'grid') => void;
+ onRefresh: () => void;
+}
+
+const SearchBar: React.FC = ({ filters, setFilters, view, setView, onRefresh }) => {
+ return (
+
+
+
+
+
+
setFilters({ ...filters, search: e.target.value })}
+ />
+
+
+
+
setFilters({ ...filters, status: e.target.value as any })}
+ >
+ All Status
+ Pending
+ Approved
+ Rejected
+ Suspended
+
+
+
setFilters({ ...filters, event: e.target.value })}
+ >
+ All Events
+ {EVENTS.map(e => {e} )}
+
+
+
setFilters({ ...filters, track: e.target.value })}
+ >
+ All Tracks
+ {TRACKS.map(t => {t} )}
+
+
+
+
+
+
+
+
+
+ setView('table')}
+ className={`p-2 rounded-xl transition-all ${view === 'table' ? 'bg-blue-600 text-white' : 'text-slate-500 hover:text-slate-300'}`}
+ >
+
+
+ setView('grid')}
+ className={`p-2 rounded-xl transition-all ${view === 'grid' ? 'bg-blue-600 text-white' : 'text-slate-500 hover:text-slate-300'}`}
+ >
+
+
+
+
+
+ );
+};
+
+export default SearchBar;
diff --git a/src/features/AdminProjects/constants/projectStatus.constants.ts b/src/features/AdminProjects/constants/projectStatus.constants.ts
new file mode 100644
index 0000000..1bc27e0
--- /dev/null
+++ b/src/features/AdminProjects/constants/projectStatus.constants.ts
@@ -0,0 +1,51 @@
+import { ProjectStatus } from "../types/adminProjects.types";
+
+export const PROJECT_STATUS_COLORS: Record = {
+ Pending: {
+ bg: 'bg-amber-500/10',
+ text: 'text-amber-500',
+ border: 'border-amber-500/20',
+ glow: 'shadow-[0_0_15px_rgba(245,158,11,0.1)]',
+ },
+ Approved: {
+ bg: 'bg-emerald-500/10',
+ text: 'text-emerald-500',
+ border: 'border-emerald-500/20',
+ glow: 'shadow-[0_0_15px_rgba(16,185,129,0.1)]',
+ },
+ Rejected: {
+ bg: 'bg-rose-500/10',
+ text: 'text-rose-500',
+ border: 'border-rose-500/20',
+ glow: 'shadow-[0_0_15px_rgba(244,63,94,0.1)]',
+ },
+ Suspended: {
+ bg: 'bg-slate-500/10',
+ text: 'text-slate-500',
+ border: 'border-slate-500/20',
+ glow: 'shadow-[0_0_15px_rgba(100,116,139,0.1)]',
+ },
+ Draft: {
+ bg: 'bg-blue-500/10',
+ text: 'text-blue-500',
+ border: 'border-blue-500/20',
+ glow: 'shadow-[0_0_15px_rgba(59,130,246,0.1)]',
+ },
+};
+
+export const TRACKS = [
+ 'AI & ML',
+ 'Web3',
+ 'Sustainability',
+ 'FinTech',
+ 'HealthTech',
+ 'Cybersecurity',
+ 'Open Innovation',
+];
+
+export const EVENTS = [
+ 'Global Hackathon 2026',
+ 'Nexus AI Summit',
+ 'Code for Change',
+ 'DeFi Builders 2025',
+];
diff --git a/src/features/AdminProjects/mock/mockAdminProjectsApi.ts b/src/features/AdminProjects/mock/mockAdminProjectsApi.ts
new file mode 100644
index 0000000..9e91fa0
--- /dev/null
+++ b/src/features/AdminProjects/mock/mockAdminProjectsApi.ts
@@ -0,0 +1,98 @@
+import { Project, ProjectAnalytics, ProjectJudge } from "../types/adminProjects.types";
+
+const MOCK_JUDGES: ProjectJudge[] = [
+ { id: 'j1', name: 'Elena Soros', role: 'Senior AI Researcher @ NeuralLabs', expertise: ['AI/ML', 'Neural Nets'], matchPercentage: 98 },
+ { id: 'j2', name: 'Marcus Kovic', role: 'Lead Data Architect @ Prism', expertise: ['AI/ML', 'Data Scale'], matchPercentage: 92 },
+ { id: 'j3', name: 'Jane Chen', role: 'Principal Engineer @ FutureLogic', expertise: ['AI/ML', 'Logistics'], matchPercentage: 85 },
+ { id: 'j4', name: 'Alex Rivera', role: 'System Architect @ CommDesk', expertise: ['Web3', 'Security'], matchPercentage: 75 },
+];
+
+const generateProjects = (count: number): Project[] => {
+ const names = ['NeuroSense AI', 'DecentralCart', 'GreenGrid Ops', 'MindFlow App', 'CipherVault', 'EcoTrack', 'QuantumPay', 'BioSync'];
+ const teams = ['Synapse Labs', 'EtherDevs', 'EcoCoders', 'Neural Knights', 'Security Squad', 'Earth Keepers', 'Qubit Team', 'Health Pioneers'];
+ const statuses: Project['status'][] = ['Approved', 'Pending', 'Rejected', 'Suspended'];
+ const tracks = ['AI & ML', 'Web3', 'Sustainability', 'FinTech', 'HealthTech'];
+
+ return Array.from({ length: count }, (_, i) => ({
+ id: `proj-${i + 1}`,
+ name: names[i % names.length] + (i > 7 ? ` ${Math.floor(i / 8)}` : ''),
+ description: 'A revolutionary project built during the Global Hackathon 2026 focusing on solving real-world problems using cutting-edge technology.',
+ teamName: teams[i % teams.length],
+ eventName: 'Global Hackathon 2026',
+ track: tracks[i % tracks.length],
+ status: statuses[i % statuses.length],
+ avgScore: parseFloat((Math.random() * 4 + 6).toFixed(1)), // 6.0 to 10.0
+ assignedJudges: [MOCK_JUDGES[i % MOCK_JUDGES.length]],
+ submissionDate: new Date(Date.now() - Math.random() * 1000000000).toISOString(),
+ lastUpdated: new Date().toISOString(),
+ isFlagged: Math.random() > 0.9,
+ techStack: ['React', 'TypeScript', 'Node.js', 'PostgreSQL'],
+ thumbnail: `https://picsum.photos/seed/${i}/400/225`,
+ }));
+};
+
+const MOCK_PROJECTS = generateProjects(50);
+
+export const mockAdminProjectsApi = {
+ getProjects: async (filters: any, pagination: { page: number, pageSize: number }) => {
+ await new Promise(r => setTimeout(r, 800)); // Simulate network lag
+
+ let filtered = [...MOCK_PROJECTS];
+
+ if (filters.search) {
+ const s = filters.search.toLowerCase();
+ filtered = filtered.filter(p =>
+ p.name.toLowerCase().includes(s) ||
+ p.teamName.toLowerCase().includes(s) ||
+ p.track.toLowerCase().includes(s)
+ );
+ }
+
+ if (filters.status && filters.status !== 'All') {
+ filtered = filtered.filter(p => p.status === filters.status);
+ }
+
+ if (filters.track && filters.track !== 'All') {
+ filtered = filtered.filter(p => p.track === filters.track);
+ }
+
+ const start = (pagination.page - 1) * pagination.pageSize;
+ return {
+ projects: filtered.slice(start, start + pagination.pageSize),
+ totalCount: filtered.length,
+ };
+ },
+
+ getAnalytics: async (): Promise => {
+ return {
+ totalProjects: 1248,
+ submittedProjects: 842,
+ approvedProjects: 312,
+ rejectedProjects: 42,
+ pendingReviews: 156,
+ averageScore: 8.4,
+ flaggedProjects: 12,
+ trends: {
+ total: [40, 45, 52, 58, 65, 72, 80],
+ submitted: [30, 32, 38, 42, 48, 52, 60],
+ approved: [10, 12, 15, 18, 22, 25, 30],
+ pending: [20, 20, 23, 24, 26, 27, 30],
+ rejected: [2, 3, 2, 4, 3, 5, 4],
+ }
+ };
+ },
+
+ getJudges: async (): Promise => {
+ return MOCK_JUDGES;
+ },
+
+ bulkUpdateStatus: async (projectIds: string[], status: Project['status']) => {
+ await new Promise(r => setTimeout(r, 500));
+ return { success: true };
+ },
+
+ assignJudges: async (projectIds: string[], judgeIds: string[]) => {
+ await new Promise(r => setTimeout(r, 500));
+ return { success: true };
+ }
+};
diff --git a/src/features/AdminProjects/pages/AdminProjectsPage.tsx b/src/features/AdminProjects/pages/AdminProjectsPage.tsx
new file mode 100644
index 0000000..817a231
--- /dev/null
+++ b/src/features/AdminProjects/pages/AdminProjectsPage.tsx
@@ -0,0 +1,236 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { Plus, Download, Filter } from 'lucide-react';
+import { adminProjectsService } from '../services/adminProjects.service';
+import { Project, ProjectAnalytics, ProjectFilters, ProjectJudge } from '../types/adminProjects.types';
+
+import AnalyticsCards from '../components/AnalyticsCards';
+import SearchBar from '../components/SearchBar';
+import FilterSidebar from '../components/FilterSidebar';
+import ProjectsTable from '../components/ProjectsTable';
+import ProjectsGrid from '../components/ProjectsGrid';
+import BulkActionBar from '../components/BulkActionBar';
+import AssignJudgeModal from '../components/AssignJudgeModal';
+import EmptyState from '../components/EmptyState';
+import LoadingSkeleton from '../components/LoadingSkeleton';
+
+const AdminProjectsPage: React.FC = () => {
+ // State
+ const [projects, setProjects] = useState([]);
+ const [analytics, setAnalytics] = useState(null);
+ const [judges, setJudges] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [view, setView] = useState<'table' | 'grid'>('table');
+ const [selectedIds, setSelectedIds] = useState([]);
+ const [isJudgeModalOpen, setIsJudgeModalOpen] = useState(false);
+ const [activeProjectForJudge, setActiveProjectForJudge] = useState(null);
+
+ const [filters, setFilters] = useState({
+ search: '',
+ status: 'All',
+ event: 'All',
+ track: 'All',
+ scoreRange: [0, 10],
+ judgeAssignment: 'All'
+ });
+
+ const [pagination, setPagination] = useState({
+ page: 1,
+ pageSize: 10
+ });
+
+ const [debouncedSearch, setDebouncedSearch] = useState('');
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ setDebouncedSearch(filters.search);
+ }, 500);
+ return () => clearTimeout(timer);
+ }, [filters.search]);
+
+ // Fetch Data
+ const fetchData = useCallback(async () => {
+ setLoading(true);
+ try {
+ const [projectsData, analyticsData, judgesData] = await Promise.all([
+ adminProjectsService.fetchProjects({ ...filters, search: debouncedSearch }, pagination),
+ adminProjectsService.fetchAnalytics(),
+ adminProjectsService.fetchJudges()
+ ]);
+ setProjects(projectsData.projects);
+ setAnalytics(analyticsData);
+ setJudges(judgesData);
+ } catch (error) {
+ console.error('Failed to fetch projects:', error);
+ } finally {
+ setLoading(false);
+ }
+ }, [debouncedSearch, filters.status, filters.event, filters.track, filters.scoreRange, filters.judgeAssignment, pagination]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ // Handlers
+ const handleSelect = (id: string) => {
+ setSelectedIds(prev =>
+ prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
+ );
+ };
+
+ const handleSelectAll = () => {
+ if (selectedIds.length === projects.length) {
+ setSelectedIds([]);
+ } else {
+ setSelectedIds(projects.map(p => p.id));
+ }
+ };
+
+ const handleAction = (id: string, action: string) => {
+ const project = projects.find(p => p.id === id);
+ if (!project) return;
+
+ switch (action) {
+ case 'approve':
+ updateProjectStatus([id], 'Approved');
+ break;
+ case 'reject':
+ updateProjectStatus([id], 'Rejected');
+ break;
+ case 'assign':
+ setActiveProjectForJudge(project);
+ setIsJudgeModalOpen(true);
+ break;
+ case 'delete':
+ setProjects(prev => prev.filter(p => p.id !== id));
+ break;
+ default:
+ console.log(`Action ${action} for ${id}`);
+ }
+ };
+
+ const updateProjectStatus = async (ids: string[], status: Project['status']) => {
+ try {
+ await adminProjectsService.bulkUpdateStatus(ids, status);
+ setProjects(prev => prev.map(p =>
+ ids.includes(p.id) ? { ...p, status } : p
+ ));
+ setSelectedIds([]);
+ // Show success toast (not implemented here but good to have)
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ const handleAssignJudges = async (judgeIds: string[]) => {
+ const targetIds = activeProjectForJudge ? [activeProjectForJudge.id] : selectedIds;
+ try {
+ await adminProjectsService.assignJudges(targetIds, judgeIds);
+ setIsJudgeModalOpen(false);
+ setActiveProjectForJudge(null);
+ setSelectedIds([]);
+ fetchData();
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ return (
+
+ {/* Header Section */}
+
+
+
Projects Management
+
Monitor and moderate global hackathon submissions across all tracks.
+
+
+
+
+ Export CSV
+
+
+
+ New Project
+
+
+
+
+ {/* Analytics Section */}
+
+
+ {/* Main Content Area */}
+
+
+
+
+
+ {loading ? (
+
+ ) : projects.length > 0 ? (
+ view === 'table' ? (
+
console.log('View', p)}
+ onAction={handleAction}
+ pagination={pagination}
+ setPagination={setPagination}
+ />
+ ) : (
+ console.log('View', p)}
+ onAction={handleAction}
+ />
+ )
+ ) : (
+ setFilters({ ...filters, search: '', status: 'All', event: 'All', track: 'All' })}
+ />
+ )}
+
+
+ {/* Modals & Floating UI */}
+
setSelectedIds([])}
+ onApprove={() => updateProjectStatus(selectedIds, 'Approved')}
+ onReject={() => updateProjectStatus(selectedIds, 'Rejected')}
+ onAssignJudges={() => {
+ setActiveProjectForJudge(null);
+ setIsJudgeModalOpen(true);
+ }}
+ onDelete={() => {
+ setProjects(prev => prev.filter(p => !selectedIds.includes(p.id)));
+ setSelectedIds([]);
+ }}
+ />
+
+ {
+ setIsJudgeModalOpen(false);
+ setActiveProjectForJudge(null);
+ }}
+ judges={judges}
+ projectName={activeProjectForJudge?.name || `${selectedIds.length} Selected Projects`}
+ onAssign={handleAssignJudges}
+ />
+
+ );
+};
+
+export default AdminProjectsPage;
diff --git a/src/features/AdminProjects/services/adminProjects.service.ts b/src/features/AdminProjects/services/adminProjects.service.ts
new file mode 100644
index 0000000..239b7ed
--- /dev/null
+++ b/src/features/AdminProjects/services/adminProjects.service.ts
@@ -0,0 +1,28 @@
+import { mockAdminProjectsApi } from "../mock/mockAdminProjectsApi";
+import { PaginationParams, ProjectFilters, ProjectStatus } from "../types/adminProjects.types";
+
+export const adminProjectsService = {
+ fetchProjects: async (filters: ProjectFilters, pagination: PaginationParams) => {
+ return await mockAdminProjectsApi.getProjects(filters, pagination);
+ },
+
+ fetchAnalytics: async () => {
+ return await mockAdminProjectsApi.getAnalytics();
+ },
+
+ fetchJudges: async () => {
+ return await mockAdminProjectsApi.getJudges();
+ },
+
+ updateProjectStatus: async (projectId: string, status: ProjectStatus) => {
+ return await mockAdminProjectsApi.bulkUpdateStatus([projectId], status);
+ },
+
+ bulkUpdateStatus: async (projectIds: string[], status: ProjectStatus) => {
+ return await mockAdminProjectsApi.bulkUpdateStatus(projectIds, status);
+ },
+
+ assignJudges: async (projectIds: string[], judgeIds: string[]) => {
+ return await mockAdminProjectsApi.assignJudges(projectIds, judgeIds);
+ }
+};
diff --git a/src/features/AdminProjects/types/adminProjects.types.ts b/src/features/AdminProjects/types/adminProjects.types.ts
new file mode 100644
index 0000000..afd4f46
--- /dev/null
+++ b/src/features/AdminProjects/types/adminProjects.types.ts
@@ -0,0 +1,68 @@
+export type ProjectStatus = 'Pending' | 'Approved' | 'Rejected' | 'Suspended' | 'Draft';
+
+export interface ProjectJudge {
+ id: string;
+ name: string;
+ avatar?: string;
+ expertise: string[];
+ matchPercentage?: number;
+ role: string;
+}
+
+export interface ProjectAnalytics {
+ totalProjects: number;
+ submittedProjects: number;
+ approvedProjects: number;
+ rejectedProjects: number;
+ pendingReviews: number;
+ averageScore: number;
+ flaggedProjects: number;
+ trends: {
+ total: number[];
+ submitted: number[];
+ approved: number[];
+ pending: number[];
+ rejected: number[];
+ };
+}
+
+export interface Project {
+ id: string;
+ name: string;
+ description: string;
+ thumbnail?: string;
+ teamName: string;
+ eventName: string;
+ track: string;
+ status: ProjectStatus;
+ avgScore: number;
+ assignedJudges: ProjectJudge[];
+ submissionDate: string;
+ lastUpdated: string;
+ isFlagged: boolean;
+ techStack: string[];
+ githubUrl?: string;
+ liveDemoUrl?: string;
+}
+
+export interface ProjectFilters {
+ search: string;
+ status: ProjectStatus | 'All';
+ event: string | 'All';
+ track: string | 'All';
+ scoreRange: [number, number];
+ judgeAssignment: 'All' | 'Assigned' | 'Unassigned';
+}
+
+export interface PaginationParams {
+ page: number;
+ pageSize: number;
+}
+
+export interface AdminProjectsState {
+ projects: Project[];
+ loading: boolean;
+ error: string | null;
+ totalCount: number;
+ analytics: ProjectAnalytics | null;
+}
diff --git a/src/features/Dashboard/mock/dashboardData.ts b/src/features/Dashboard/mock/dashboardData.ts
index 8cafb70..e5bceab 100644
--- a/src/features/Dashboard/mock/dashboardData.ts
+++ b/src/features/Dashboard/mock/dashboardData.ts
@@ -3,7 +3,7 @@ import { DashboardData } from "../types/dashboard";
export const dashboardData: DashboardData = {
user: {
name: "Arjun Mehta",
- role: "Member",
+ role: "Admin",
},
summary: {
diff --git a/src/features/SideBar/v1/Section/SideBar.tsx b/src/features/SideBar/v1/Section/SideBar.tsx
index bda5dee..d64ae68 100644
--- a/src/features/SideBar/v1/Section/SideBar.tsx
+++ b/src/features/SideBar/v1/Section/SideBar.tsx
@@ -44,7 +44,7 @@ const SideBar = () => {
} text="Dashboard" link="/org/dashboard" />
- } text="Projects" link="/org" />
+ } text="Projects" link="/org/admin/projects" />
} text="Teams" link="/org/member" />
} text="Events" link="/org/events" />
} text="Contact Submissions" link="/org/contact" />