diff --git a/app/(admin)/admin/overview/page.tsx b/app/(admin)/admin/overview/page.tsx
new file mode 100644
index 0000000..9b50046
--- /dev/null
+++ b/app/(admin)/admin/overview/page.tsx
@@ -0,0 +1,108 @@
+'use client';
+
+import {
+ Users,
+ Package,
+ DollarSign,
+ Truck,
+ ShieldCheck,
+ Lock,
+ RefreshCw,
+ AlertCircle,
+} from 'lucide-react';
+import { useAdminDashboard } from '@/hooks/useAdminDashboard';
+
+interface MetricCardProps {
+ label: string;
+ value: string | number;
+ icon: React.ElementType;
+ isLoading: boolean;
+}
+
+function MetricCard({ label, value, icon: Icon, isLoading }: MetricCardProps) {
+ return (
+
+
+ {isLoading ? (
+
+ ) : (
+
{value}
+ )}
+
+ );
+}
+
+export default function AdminOverviewPage() {
+ const { stats, isLoading, isError, refetch } = useAdminDashboard();
+
+ return (
+
+
+
+
Admin Overview
+
+ Platform-wide metrics and activity summary
+
+
+
+
+
+ {isError && (
+
+
+ Failed to load stats. Please refresh or try again later.
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx
new file mode 100644
index 0000000..3d62782
--- /dev/null
+++ b/app/(admin)/layout.tsx
@@ -0,0 +1,14 @@
+import type { ReactNode } from 'react';
+import { AdminSidebar } from '@/components/admin/AdminSidebar';
+import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
+
+export default function AdminLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+
+ );
+}
diff --git a/app/(dashboard)/admin/page.tsx b/app/(dashboard)/admin/page.tsx
index e21339d..792c3e9 100644
--- a/app/(dashboard)/admin/page.tsx
+++ b/app/(dashboard)/admin/page.tsx
@@ -1,13 +1,5 @@
-import { DisconnectButton } from '@/components/wallet/DisconnectButton';
+import { redirect } from 'next/navigation';
export default function AdminDashboardPage() {
- return (
-
-
Admin Dashboard
- {/* Wallet session controls — see hooks/useWallet.ts for disconnect logic */}
-
-
-
-
- );
+ redirect('/admin/overview');
}
diff --git a/app/error.tsx b/app/error.tsx
new file mode 100644
index 0000000..7e31375
--- /dev/null
+++ b/app/error.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import { useEffect } from 'react';
+import { AlertTriangle } from 'lucide-react';
+
+interface ErrorProps {
+ error: Error & { digest?: string };
+ reset: () => void;
+}
+
+export default function GlobalError({ error, reset }: ErrorProps) {
+ useEffect(() => {
+ console.error('[GlobalError]', error);
+ }, [error]);
+
+ return (
+
+
+
+
+ Something went wrong
+
+
+ {error.message || 'An unexpected error occurred. Please try again.'}
+
+
+
+
+ );
+}
diff --git a/components/admin/AdminSidebar.tsx b/components/admin/AdminSidebar.tsx
new file mode 100644
index 0000000..715869d
--- /dev/null
+++ b/components/admin/AdminSidebar.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import Link from 'next/link';
+import { usePathname, useRouter } from 'next/navigation';
+import {
+ LayoutDashboard,
+ Users,
+ Package,
+ Shield,
+ Settings,
+ LogOut,
+} from 'lucide-react';
+import { useAuthStore } from '@/store/authStore';
+
+interface AdminNavItem {
+ label: string;
+ href: string;
+ icon: React.ElementType;
+}
+
+const NAV_ITEMS: AdminNavItem[] = [
+ { label: 'Overview', href: '/admin/overview', icon: LayoutDashboard },
+ { label: 'Users', href: '/admin/users', icon: Users },
+ { label: 'Deliveries', href: '/admin/deliveries', icon: Package },
+ { label: 'KYC Review', href: '/admin/kyc', icon: Shield },
+ { label: 'Settings', href: '/admin/settings', icon: Settings },
+];
+
+export function AdminSidebar() {
+ const pathname = usePathname();
+ const router = useRouter();
+ const clearUser = useAuthStore((s) => s.clearUser);
+
+ function handleLogout() {
+ localStorage.removeItem('authToken');
+ clearUser();
+ router.replace('/login');
+ }
+
+ return (
+
+ );
+}
diff --git a/components/auth/ProtectedRoute.tsx b/components/auth/ProtectedRoute.tsx
new file mode 100644
index 0000000..20d2156
--- /dev/null
+++ b/components/auth/ProtectedRoute.tsx
@@ -0,0 +1,48 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { Loader2 } from 'lucide-react';
+import { useAuth } from '@/hooks/useAuth';
+import type { UserRole } from '@/store/authStore';
+
+interface ProtectedRouteProps {
+ children: React.ReactNode;
+ requiredRole?: UserRole;
+}
+
+export function ProtectedRoute({ children, requiredRole }: ProtectedRouteProps) {
+ const router = useRouter();
+ const { isAuthenticated, isLoading, user } = useAuth();
+
+ useEffect(() => {
+ if (isLoading) return;
+
+ if (!isAuthenticated) {
+ router.replace('/login');
+ return;
+ }
+
+ if (requiredRole && user?.role !== requiredRole) {
+ router.replace('/');
+ }
+ }, [isAuthenticated, isLoading, requiredRole, user, router]);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return null;
+ }
+
+ if (requiredRole && user?.role !== requiredRole) {
+ return null;
+ }
+
+ return <>{children}>;
+}
diff --git a/components/ui/EmptyState.tsx b/components/ui/EmptyState.tsx
new file mode 100644
index 0000000..301bac8
--- /dev/null
+++ b/components/ui/EmptyState.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import type { LucideIcon } from 'lucide-react';
+
+interface EmptyStateProps {
+ icon: LucideIcon;
+ title: string;
+ description?: string;
+ action?: {
+ label: string;
+ onClick: () => void;
+ };
+}
+
+export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
+ return (
+
+
+
+
+
{title}
+ {description && (
+
{description}
+ )}
+ {action && (
+
+ )}
+
+ );
+}
diff --git a/hooks/useAdminDashboard.ts b/hooks/useAdminDashboard.ts
new file mode 100644
index 0000000..aacbd70
--- /dev/null
+++ b/hooks/useAdminDashboard.ts
@@ -0,0 +1,33 @@
+'use client';
+
+import { useQuery } from '@tanstack/react-query';
+import { adminService, type AdminStats } from '@/services/adminService';
+
+interface UseAdminDashboardReturn {
+ stats: AdminStats | undefined;
+ isLoading: boolean;
+ isError: boolean;
+ refetch: () => void;
+}
+
+export function useAdminDashboard(): UseAdminDashboardReturn {
+ const {
+ data: stats,
+ isLoading,
+ isError,
+ refetch,
+ } = useQuery({
+ queryKey: ['admin', 'stats'],
+ queryFn: async () => {
+ const response = await adminService.getStats();
+ if (!response.success || !response.data) {
+ throw new Error(response.message || 'Failed to fetch admin stats');
+ }
+ return response.data;
+ },
+ staleTime: 30_000,
+ retry: 1,
+ });
+
+ return { stats, isLoading, isError, refetch };
+}
diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts
new file mode 100644
index 0000000..36927d3
--- /dev/null
+++ b/hooks/useAuth.ts
@@ -0,0 +1,51 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import { useAuthStore, type AuthUser, type UserRole } from '@/store/authStore';
+import { authApiService } from '@/services/auth.service';
+
+interface UseAuthReturn {
+ isAuthenticated: boolean;
+ isLoading: boolean;
+ user: AuthUser | null;
+}
+
+const API_TO_STORE_ROLE: Record = {
+ customer: 'Customer',
+ driver: 'Driver',
+ admin: 'Admin',
+};
+
+export function useAuth(): UseAuthReturn {
+ const [isLoading, setIsLoading] = useState(true);
+ const { user, isAuthenticated, setUser, clearUser } = useAuthStore();
+
+ useEffect(() => {
+ const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null;
+
+ if (!token) {
+ clearUser();
+ setIsLoading(false);
+ return;
+ }
+
+ authApiService
+ .getCurrentUser()
+ .then((res) => {
+ if (res.data) {
+ const role: UserRole = API_TO_STORE_ROLE[res.data.role] ?? 'Customer';
+ setUser({ id: res.data.id, email: res.data.email, role });
+ } else {
+ clearUser();
+ }
+ })
+ .catch(() => {
+ localStorage.removeItem('authToken');
+ clearUser();
+ })
+ .finally(() => setIsLoading(false));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return { isAuthenticated, isLoading, user };
+}
diff --git a/lib/api.ts b/lib/api.ts
new file mode 100644
index 0000000..b9940af
--- /dev/null
+++ b/lib/api.ts
@@ -0,0 +1,41 @@
+import axios, {
+ type AxiosInstance,
+ type InternalAxiosRequestConfig,
+ type AxiosResponse,
+ type AxiosError,
+} from 'axios';
+
+const api: AxiosInstance = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Attach JWT bearer token to every outgoing request
+api.interceptors.request.use(
+ (config: InternalAxiosRequestConfig) => {
+ if (typeof window !== 'undefined') {
+ const token = localStorage.getItem('authToken');
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ }
+ return config;
+ },
+ (error: AxiosError) => Promise.reject(error),
+);
+
+// Handle 401 Unauthorized — clear stored token and redirect to login
+api.interceptors.response.use(
+ (response: AxiosResponse) => response,
+ (error: AxiosError) => {
+ if (error.response?.status === 401 && typeof window !== 'undefined') {
+ localStorage.removeItem('authToken');
+ window.location.href = '/login';
+ }
+ return Promise.reject(error);
+ },
+);
+
+export default api;
diff --git a/middleware.ts b/middleware.ts
index ae32628..227f743 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -2,7 +2,19 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
- // Placeholder middleware logic
+ const token = request.cookies.get('authToken')?.value;
+ const { pathname } = request.nextUrl;
+
+ const isProtected =
+ pathname.startsWith('/dashboard') ||
+ pathname.startsWith('/admin');
+
+ if (isProtected && !token) {
+ const loginUrl = request.nextUrl.clone();
+ loginUrl.pathname = '/login';
+ return NextResponse.redirect(loginUrl);
+ }
+
return NextResponse.next();
}
diff --git a/services/adminService.ts b/services/adminService.ts
new file mode 100644
index 0000000..8708caa
--- /dev/null
+++ b/services/adminService.ts
@@ -0,0 +1,23 @@
+import api from '@/lib/api';
+
+export interface AdminStats {
+ totalUsers: number;
+ activeDeliveries: number;
+ totalRevenue: number;
+ activeDrivers: number;
+ pendingKyc: number;
+ escrowLocked: number;
+}
+
+export interface AdminApiResponse {
+ success: boolean;
+ message: string;
+ data?: T;
+}
+
+export const adminService = {
+ async getStats(): Promise> {
+ const { data } = await api.get>('/admin/stats');
+ return data;
+ },
+};
diff --git a/services/auth.service.ts b/services/auth.service.ts
new file mode 100644
index 0000000..16145c1
--- /dev/null
+++ b/services/auth.service.ts
@@ -0,0 +1,22 @@
+import api from '@/lib/api';
+import type { ApiResponse, LoginResponse } from '@/services/authService';
+
+export const authApiService = {
+ async login(email: string, password: string): Promise> {
+ const { data } = await api.post>('/auth/login', {
+ email,
+ password,
+ });
+ return data;
+ },
+
+ async logout(): Promise> {
+ const { data } = await api.post>('/auth/logout');
+ return data;
+ },
+
+ async getCurrentUser(): Promise> {
+ const { data } = await api.get>('/auth/me');
+ return data;
+ },
+};