diff --git a/app/(admin)/_components/AdminCard.tsx b/app/(admin)/_components/AdminCard.tsx new file mode 100644 index 0000000..c0daa42 --- /dev/null +++ b/app/(admin)/_components/AdminCard.tsx @@ -0,0 +1,139 @@ +import React from "react"; +import { cn } from "./utils"; + +type CardProps = { + title?: string; + subtitle?: string; + className?: string; + children: React.ReactNode; +}; + +export function Card({ title, subtitle, className, children }: CardProps) { + return ( +
+ {title ? ( +

+ {title} +

+ ) : null} + {subtitle ? ( +

+ {subtitle} +

+ ) : null} +
{children}
+
+ ); +} + +type StatCardProps = { + label: string; + value: string; + trend?: string; +}; + +export function StatCard({ label, value, trend }: StatCardProps) { + return ( + +

+ {label} +

+

+ {value} +

+ {trend ? ( +

+ {trend} +

+ ) : null} +
+ ); +} + +export function PageTitle({ + title, + description, +}: { + title: string; + description?: string; +}) { + return ( +
+

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ ); +} + +const statusStyles: Record< + "success" | "warning" | "danger" | "neutral", + string +> = { + success: + "border-emerald-200 bg-emerald-50 text-emerald-700 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-300", + warning: + "border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-300", + danger: + "border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-300", + neutral: + "border-slate-200 bg-slate-100 text-slate-700 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-300", +}; + +export function StatusBadge({ + label, + tone, +}: { + label: string; + tone: "success" | "warning" | "danger" | "neutral"; +}) { + return ( + + {label} + + ); +} + +export function ActionButton({ + children, + variant = "secondary", +}: { + children: React.ReactNode; + variant?: "primary" | "secondary" | "danger"; +}) { + const variants: Record = { + primary: + "border-slate-900 bg-slate-900 text-white hover:bg-slate-800 dark:border-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600", + secondary: + "border-slate-200 bg-white text-slate-700 hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700", + danger: + "border-rose-200 bg-rose-50 text-rose-700 hover:bg-rose-100 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-300 dark:hover:bg-rose-900", + }; + + return ( + + ); +} diff --git a/app/(admin)/_components/AdminNavbar.tsx b/app/(admin)/_components/AdminNavbar.tsx new file mode 100644 index 0000000..b76e8b8 --- /dev/null +++ b/app/(admin)/_components/AdminNavbar.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { + Bell, + Menu, + Moon, + PanelLeftClose, + PanelLeftOpen, + Search, + Sun, +} from "lucide-react"; +import { usePathname } from "next/navigation"; + +const titleMap: Record = { + "/admin": "Dashboard", + "/admin/incident-reports": "Incident Reports", + "/admin/agencies": "Agencies", + "/admin/users": "Users", + "/admin/notifications": "Notifications", + "/admin/analytics": "Analytics", + "/admin/data-export": "Data Export", + "/admin/subscriptions": "Subscriptions", + "/admin/settings": "Settings", +}; + +export default function AdminNavbar({ + onMenuClick, + isDesktopCollapsed, + onToggleDesktopCollapse, + theme, + onToggleTheme, +}: { + onMenuClick: () => void; + isDesktopCollapsed: boolean; + onToggleDesktopCollapse: () => void; + theme: "light" | "dark"; + onToggleTheme: () => void; +}) { + const pathname = usePathname(); + const title = titleMap[pathname] || "Admin"; + + return ( +
+
+ + + + +
+

+ {title} +

+

+ Disaster response management +

+
+ + + + + + + + +
+
+ ); +} diff --git a/app/(admin)/_components/AdminSidebar.tsx b/app/(admin)/_components/AdminSidebar.tsx new file mode 100644 index 0000000..c88b859 --- /dev/null +++ b/app/(admin)/_components/AdminSidebar.tsx @@ -0,0 +1,104 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { X } from "lucide-react"; +import { navItems } from "./adminData"; +import { cn } from "./utils"; + +type AdminSidebarProps = { + isOpen: boolean; + onClose: () => void; + isDesktopCollapsed: boolean; +}; + +export default function AdminSidebar({ + isOpen, + onClose, + isDesktopCollapsed, +}: AdminSidebarProps) { + const pathname = usePathname(); + + return ( + <> + {/* Mobile overlay */} +
+ + {/* Sidebar */} + + + ); +} diff --git a/app/(admin)/_components/AdminTable.tsx b/app/(admin)/_components/AdminTable.tsx new file mode 100644 index 0000000..d431c8f --- /dev/null +++ b/app/(admin)/_components/AdminTable.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +type Column = { + key: string; + title: string; + className?: string; + render: (row: T) => React.ReactNode; +}; + +export function AdminTable({ + columns, + rows, +}: { + columns: Column[]; + rows: T[]; +}) { + return ( +
+ + + + {columns.map((column) => ( + + ))} + + + + {rows.map((row, index) => ( + + {columns.map((column) => ( + + ))} + + ))} + +
+ {column.title} +
+ {column.render(row)} +
+
+ ); +} diff --git a/app/(admin)/_components/adminData.ts b/app/(admin)/_components/adminData.ts new file mode 100644 index 0000000..267b158 --- /dev/null +++ b/app/(admin)/_components/adminData.ts @@ -0,0 +1,27 @@ +import { + Bell, + ChartSpline, + Database, + FileUp, + LayoutDashboard, + Settings, + ShieldAlert, + Users, + UserCog, +} from "lucide-react"; + +export const navItems = [ + { label: "Dashboard", href: "/admin", icon: LayoutDashboard }, + { + label: "Incident Reports", + href: "/admin/incident-reports", + icon: ShieldAlert, + }, + { label: "Agencies", href: "/admin/agencies", icon: UserCog }, + { label: "Users", href: "/admin/users", icon: Users }, + { label: "Notifications", href: "/admin/notifications", icon: Bell }, + { label: "Analytics", href: "/admin/analytics", icon: ChartSpline }, + { label: "Data Export", href: "/admin/data-export", icon: FileUp }, + { label: "Subscriptions", href: "/admin/subscriptions", icon: Database }, + { label: "Settings", href: "/admin/settings", icon: Settings }, +]; diff --git a/app/(admin)/_components/utils.ts b/app/(admin)/_components/utils.ts new file mode 100644 index 0000000..737ea7e --- /dev/null +++ b/app/(admin)/_components/utils.ts @@ -0,0 +1,3 @@ +export function cn(...classes: Array) { + return classes.filter(Boolean).join(" "); +} diff --git a/app/(admin)/admin/agencies/page.tsx b/app/(admin)/admin/agencies/page.tsx new file mode 100644 index 0000000..bc82513 --- /dev/null +++ b/app/(admin)/admin/agencies/page.tsx @@ -0,0 +1,99 @@ +import { + ActionButton, + Card, + PageTitle, + StatusBadge, +} from "../../_components/AdminCard"; +import { AdminTable } from "../../_components/AdminTable"; + +const agencies = [ + { + name: "Rapid Medical Unit", + category: "Medical", + contact: "rmu@agency.org", + status: "Active", + activeIncidents: 13, + }, + { + name: "Urban Fire Service", + category: "Fire", + contact: "ufs@agency.org", + status: "Trial", + activeIncidents: 21, + }, + { + name: "Flood Response Team", + category: "Flood", + contact: "frt@agency.org", + status: "Inactive", + activeIncidents: 4, + }, +]; + +export default function AgenciesPage() { + return ( +
+ + + + ( + {row.name} + ), + }, + { + key: "category", + title: "Category", + render: (row) => row.category, + }, + { + key: "contact", + title: "Contact Info", + render: (row) => row.contact, + }, + { + key: "status", + title: "Subscription Status", + render: (row) => ( + + ), + }, + { + key: "activeIncidents", + title: "Active Incident Count", + render: (row) => row.activeIncidents, + }, + { + key: "actions", + title: "Actions", + render: () => ( +
+ View + Edit + Deactivate +
+ ), + }, + ]} + /> +
+
+ ); +} diff --git a/app/(admin)/admin/analytics/page.tsx b/app/(admin)/admin/analytics/page.tsx new file mode 100644 index 0000000..bd62873 --- /dev/null +++ b/app/(admin)/admin/analytics/page.tsx @@ -0,0 +1,60 @@ +import { Card, PageTitle, StatCard } from "../../_components/AdminCard"; + +export default function AnalyticsPage() { + return ( +
+ + +
+ +
+ {[42, 65, 28, 54, 36, 71].map((height, index) => ( +
+ ))} +
+ + + +
+
+ + + +
+
+
+
+ + +
+ {Array.from({ length: 48 }).map((_, i) => ( +
+ ))} +
+ + +
+ + + + +
+
+ ); +} diff --git a/app/(admin)/admin/data-export/page.tsx b/app/(admin)/admin/data-export/page.tsx new file mode 100644 index 0000000..b78ef83 --- /dev/null +++ b/app/(admin)/admin/data-export/page.tsx @@ -0,0 +1,42 @@ +import { Card, PageTitle } from "../../_components/AdminCard"; + +export default function DataExportPage() { + return ( +
+ + + +
+ + + +
+
+ + +
+ + +
+
+
+ ); +} diff --git a/app/(admin)/admin/incident-reports/page.tsx b/app/(admin)/admin/incident-reports/page.tsx new file mode 100644 index 0000000..55fc058 --- /dev/null +++ b/app/(admin)/admin/incident-reports/page.tsx @@ -0,0 +1,129 @@ +import { + ActionButton, + Card, + PageTitle, + StatusBadge, +} from "../../_components/AdminCard"; +import { AdminTable } from "../../_components/AdminTable"; + +const rows = [ + { + id: "IR-3021", + title: "Bridge Fire Alert", + category: "Fire", + location: "Lake Bridge", + status: "Active", + timestamp: "2026-03-17 10:22", + }, + { + id: "IR-3022", + title: "Flash Flood Warning", + category: "Flood", + location: "South Basin", + status: "Pending", + timestamp: "2026-03-17 09:54", + }, + { + id: "IR-3023", + title: "Power Grid Failure", + category: "Infrastructure", + location: "Central City", + status: "Resolved", + timestamp: "2026-03-16 21:04", + }, +]; + +export default function IncidentReportsPage() { + return ( +
+ + + +
+ + + + + +
+
+ + + ( + {row.id} + ), + }, + { key: "title", title: "Title", render: (row) => row.title }, + { + key: "category", + title: "Category", + render: (row) => row.category, + }, + { + key: "location", + title: "Location", + render: (row) => row.location, + }, + { + key: "status", + title: "Status", + render: (row) => ( + + ), + }, + { + key: "timestamp", + title: "Timestamp", + render: (row) => row.timestamp, + }, + { + key: "actions", + title: "Actions", + render: () => ( +
+ View + Edit + Delete + Approve + Reject +
+ ), + }, + ]} + /> +
+
+ ); +} diff --git a/app/(admin)/admin/notifications/page.tsx b/app/(admin)/admin/notifications/page.tsx new file mode 100644 index 0000000..61e3128 --- /dev/null +++ b/app/(admin)/admin/notifications/page.tsx @@ -0,0 +1,82 @@ +import { Card, PageTitle, StatCard } from "../../_components/AdminCard"; + +export default function NotificationsPage() { + return ( +
+ + + +
+
+
+ + +
+
+ +