From 89946fcef9638dd6d363481addd047092b2bdc47 Mon Sep 17 00:00:00 2001
From: spliceboti <44727389-spliceboti@users.noreply.replit.com>
Date: Wed, 9 Jul 2025 23:54:32 +0000
Subject: [PATCH] Set up the basic structure and functionality for the DJ
management system
Initializes project structure, adds core components, and configures essential dependencies.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 3a22ac80-cd1d-4441-9e36-f24fc2f4c3de
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3478f7c3-db8c-4fca-9165-3adbdf1b5829/e8da43e7-d99c-4328-9fdc-485bdeecffc1.jpg
---
.gitignore | 6 +
.replit | 36 +
client/index.html | 13 +
client/src/App.tsx | 110 +
.../src/components/admin/AdminDashboard.tsx | 90 +
.../src/components/admin/AssignmentTool.tsx | 170 +
client/src/components/admin/DJManagement.tsx | 263 +
.../components/admin/EventTypeManagement.tsx | 251 +
.../src/components/admin/PendingRequests.tsx | 123 +
.../components/admin/ScheduleTemplates.tsx | 352 +
.../availability/AvailabilityCalendar.tsx | 268 +
.../components/dashboard/CalendarWidget.tsx | 107 +
.../components/dashboard/DashboardStats.tsx | 70 +
.../src/components/dashboard/QuickActions.tsx | 58 +
.../components/dashboard/UpcomingEvents.tsx | 116 +
client/src/components/events/EventList.tsx | 198 +
client/src/components/events/EventModal.tsx | 256 +
client/src/components/layout/AppLayout.tsx | 58 +
client/src/components/layout/Sidebar.tsx | 117 +
.../src/components/profile/ProfileModal.tsx | 326 +
client/src/components/ui/accordion.tsx | 56 +
client/src/components/ui/alert-dialog.tsx | 139 +
client/src/components/ui/alert.tsx | 59 +
client/src/components/ui/aspect-ratio.tsx | 5 +
client/src/components/ui/avatar.tsx | 50 +
client/src/components/ui/badge.tsx | 36 +
client/src/components/ui/breadcrumb.tsx | 115 +
client/src/components/ui/button.tsx | 56 +
client/src/components/ui/calendar.tsx | 68 +
client/src/components/ui/card.tsx | 79 +
client/src/components/ui/carousel.tsx | 260 +
client/src/components/ui/chart.tsx | 365 +
client/src/components/ui/checkbox.tsx | 28 +
client/src/components/ui/collapsible.tsx | 11 +
client/src/components/ui/command.tsx | 151 +
client/src/components/ui/context-menu.tsx | 198 +
client/src/components/ui/data-table.tsx | 160 +
client/src/components/ui/dialog.tsx | 122 +
client/src/components/ui/drawer.tsx | 118 +
client/src/components/ui/dropdown-menu.tsx | 198 +
client/src/components/ui/form.tsx | 178 +
client/src/components/ui/hover-card.tsx | 29 +
client/src/components/ui/image-cropper.tsx | 164 +
client/src/components/ui/input-otp.tsx | 69 +
client/src/components/ui/input.tsx | 22 +
client/src/components/ui/label.tsx | 24 +
client/src/components/ui/menubar.tsx | 256 +
client/src/components/ui/navigation-menu.tsx | 128 +
client/src/components/ui/pagination.tsx | 117 +
client/src/components/ui/popover.tsx | 29 +
client/src/components/ui/progress.tsx | 28 +
client/src/components/ui/radio-group.tsx | 42 +
client/src/components/ui/resizable.tsx | 45 +
client/src/components/ui/scroll-area.tsx | 46 +
client/src/components/ui/select.tsx | 160 +
client/src/components/ui/separator.tsx | 29 +
client/src/components/ui/sheet.tsx | 140 +
client/src/components/ui/sidebar.tsx | 75 +
client/src/components/ui/skeleton.tsx | 15 +
client/src/components/ui/slider.tsx | 26 +
client/src/components/ui/switch.tsx | 27 +
client/src/components/ui/table.tsx | 117 +
client/src/components/ui/tabs.tsx | 53 +
client/src/components/ui/textarea.tsx | 22 +
client/src/components/ui/toast.tsx | 127 +
client/src/components/ui/toaster.tsx | 33 +
client/src/components/ui/toggle-group.tsx | 61 +
client/src/components/ui/toggle.tsx | 43 +
client/src/components/ui/tooltip.tsx | 30 +
client/src/hooks/use-mobile.tsx | 19 +
client/src/hooks/use-toast.ts | 191 +
client/src/hooks/useAuth.ts | 14 +
client/src/index.css | 221 +
client/src/lib/authUtils.ts | 3 +
client/src/lib/queryClient.ts | 57 +
client/src/lib/utils.ts | 6 +
client/src/main.tsx | 5 +
client/src/pages/Availability.tsx | 10 +
client/src/pages/Events.tsx | 34 +
client/src/pages/Home.tsx | 22 +
client/src/pages/Landing.tsx | 174 +
client/src/pages/Profile.tsx | 123 +
client/src/pages/PublicDJProfile.tsx | 156 +
client/src/pages/Schedule.tsx | 139 +
client/src/pages/admin/AdminDashboard.tsx | 10 +
client/src/pages/admin/AssignmentTool.tsx | 17 +
client/src/pages/admin/EventTypes.tsx | 10 +
client/src/pages/admin/ManageDJs.tsx | 10 +
client/src/pages/admin/Templates.tsx | 10 +
client/src/pages/not-found.tsx | 21 +
components.json | 20 +
drizzle.config.ts | 14 +
package-lock.json | 9334 +++++++++++++++++
package.json | 109 +
postcss.config.js | 6 +
replit.md | 136 +
server/db.ts | 15 +
server/index.ts | 70 +
server/replitAuth.ts | 157 +
server/routes.ts | 566 +
server/storage.ts | 558 +
server/vite.ts | 85 +
shared/schema.ts | 338 +
tailwind.config.ts | 90 +
tsconfig.json | 23 +
vite.config.ts | 37 +
106 files changed, 20207 insertions(+)
create mode 100644 .gitignore
create mode 100644 client/index.html
create mode 100644 client/src/App.tsx
create mode 100644 client/src/components/admin/AdminDashboard.tsx
create mode 100644 client/src/components/admin/AssignmentTool.tsx
create mode 100644 client/src/components/admin/DJManagement.tsx
create mode 100644 client/src/components/admin/EventTypeManagement.tsx
create mode 100644 client/src/components/admin/PendingRequests.tsx
create mode 100644 client/src/components/admin/ScheduleTemplates.tsx
create mode 100644 client/src/components/availability/AvailabilityCalendar.tsx
create mode 100644 client/src/components/dashboard/CalendarWidget.tsx
create mode 100644 client/src/components/dashboard/DashboardStats.tsx
create mode 100644 client/src/components/dashboard/QuickActions.tsx
create mode 100644 client/src/components/dashboard/UpcomingEvents.tsx
create mode 100644 client/src/components/events/EventList.tsx
create mode 100644 client/src/components/events/EventModal.tsx
create mode 100644 client/src/components/layout/AppLayout.tsx
create mode 100644 client/src/components/layout/Sidebar.tsx
create mode 100644 client/src/components/profile/ProfileModal.tsx
create mode 100644 client/src/components/ui/accordion.tsx
create mode 100644 client/src/components/ui/alert-dialog.tsx
create mode 100644 client/src/components/ui/alert.tsx
create mode 100644 client/src/components/ui/aspect-ratio.tsx
create mode 100644 client/src/components/ui/avatar.tsx
create mode 100644 client/src/components/ui/badge.tsx
create mode 100644 client/src/components/ui/breadcrumb.tsx
create mode 100644 client/src/components/ui/button.tsx
create mode 100644 client/src/components/ui/calendar.tsx
create mode 100644 client/src/components/ui/card.tsx
create mode 100644 client/src/components/ui/carousel.tsx
create mode 100644 client/src/components/ui/chart.tsx
create mode 100644 client/src/components/ui/checkbox.tsx
create mode 100644 client/src/components/ui/collapsible.tsx
create mode 100644 client/src/components/ui/command.tsx
create mode 100644 client/src/components/ui/context-menu.tsx
create mode 100644 client/src/components/ui/data-table.tsx
create mode 100644 client/src/components/ui/dialog.tsx
create mode 100644 client/src/components/ui/drawer.tsx
create mode 100644 client/src/components/ui/dropdown-menu.tsx
create mode 100644 client/src/components/ui/form.tsx
create mode 100644 client/src/components/ui/hover-card.tsx
create mode 100644 client/src/components/ui/image-cropper.tsx
create mode 100644 client/src/components/ui/input-otp.tsx
create mode 100644 client/src/components/ui/input.tsx
create mode 100644 client/src/components/ui/label.tsx
create mode 100644 client/src/components/ui/menubar.tsx
create mode 100644 client/src/components/ui/navigation-menu.tsx
create mode 100644 client/src/components/ui/pagination.tsx
create mode 100644 client/src/components/ui/popover.tsx
create mode 100644 client/src/components/ui/progress.tsx
create mode 100644 client/src/components/ui/radio-group.tsx
create mode 100644 client/src/components/ui/resizable.tsx
create mode 100644 client/src/components/ui/scroll-area.tsx
create mode 100644 client/src/components/ui/select.tsx
create mode 100644 client/src/components/ui/separator.tsx
create mode 100644 client/src/components/ui/sheet.tsx
create mode 100644 client/src/components/ui/sidebar.tsx
create mode 100644 client/src/components/ui/skeleton.tsx
create mode 100644 client/src/components/ui/slider.tsx
create mode 100644 client/src/components/ui/switch.tsx
create mode 100644 client/src/components/ui/table.tsx
create mode 100644 client/src/components/ui/tabs.tsx
create mode 100644 client/src/components/ui/textarea.tsx
create mode 100644 client/src/components/ui/toast.tsx
create mode 100644 client/src/components/ui/toaster.tsx
create mode 100644 client/src/components/ui/toggle-group.tsx
create mode 100644 client/src/components/ui/toggle.tsx
create mode 100644 client/src/components/ui/tooltip.tsx
create mode 100644 client/src/hooks/use-mobile.tsx
create mode 100644 client/src/hooks/use-toast.ts
create mode 100644 client/src/hooks/useAuth.ts
create mode 100644 client/src/index.css
create mode 100644 client/src/lib/authUtils.ts
create mode 100644 client/src/lib/queryClient.ts
create mode 100644 client/src/lib/utils.ts
create mode 100644 client/src/main.tsx
create mode 100644 client/src/pages/Availability.tsx
create mode 100644 client/src/pages/Events.tsx
create mode 100644 client/src/pages/Home.tsx
create mode 100644 client/src/pages/Landing.tsx
create mode 100644 client/src/pages/Profile.tsx
create mode 100644 client/src/pages/PublicDJProfile.tsx
create mode 100644 client/src/pages/Schedule.tsx
create mode 100644 client/src/pages/admin/AdminDashboard.tsx
create mode 100644 client/src/pages/admin/AssignmentTool.tsx
create mode 100644 client/src/pages/admin/EventTypes.tsx
create mode 100644 client/src/pages/admin/ManageDJs.tsx
create mode 100644 client/src/pages/admin/Templates.tsx
create mode 100644 client/src/pages/not-found.tsx
create mode 100644 components.json
create mode 100644 drizzle.config.ts
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 postcss.config.js
create mode 100644 replit.md
create mode 100644 server/db.ts
create mode 100644 server/index.ts
create mode 100644 server/replitAuth.ts
create mode 100644 server/routes.ts
create mode 100644 server/storage.ts
create mode 100644 server/vite.ts
create mode 100644 shared/schema.ts
create mode 100644 tailwind.config.ts
create mode 100644 tsconfig.json
create mode 100644 vite.config.ts
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f9ba7f8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules
+dist
+.DS_Store
+server/public
+vite.config.ts.*
+*.tar.gz
\ No newline at end of file
diff --git a/.replit b/.replit
index e69de29..51f344f 100644
--- a/.replit
+++ b/.replit
@@ -0,0 +1,36 @@
+modules = ["nodejs-20", "web", "postgresql-16"]
+run = "npm run dev"
+hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"]
+
+[nix]
+channel = "stable-24_05"
+
+[deployment]
+deploymentTarget = "autoscale"
+build = ["npm", "run", "build"]
+run = ["npm", "run", "start"]
+
+[[ports]]
+localPort = 5000
+externalPort = 80
+
+[workflows]
+runButton = "Project"
+
+[[workflows.workflow]]
+name = "Project"
+mode = "parallel"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "workflow.run"
+args = "Start application"
+
+[[workflows.workflow]]
+name = "Start application"
+author = "agent"
+
+[[workflows.workflow.tasks]]
+task = "shell.exec"
+args = "npm run dev"
+waitForPort = 5000
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000..4b4d09e
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/App.tsx b/client/src/App.tsx
new file mode 100644
index 0000000..61e53b1
--- /dev/null
+++ b/client/src/App.tsx
@@ -0,0 +1,110 @@
+import { Switch, Route } from "wouter";
+import { queryClient } from "./lib/queryClient";
+import { QueryClientProvider } from "@tanstack/react-query";
+import { Toaster } from "@/components/ui/toaster";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { useAuth } from "@/hooks/useAuth";
+import { useEffect } from "react";
+import { useToast } from "@/hooks/use-toast";
+import { isUnauthorizedError } from "@/lib/authUtils";
+
+// Pages
+import NotFound from "@/pages/not-found";
+import Landing from "@/pages/Landing";
+import Home from "@/pages/Home";
+import Profile from "@/pages/Profile";
+import Events from "@/pages/Events";
+import Schedule from "@/pages/Schedule";
+import Availability from "@/pages/Availability";
+import AdminDashboard from "@/pages/admin/AdminDashboard";
+import ManageDJs from "@/pages/admin/ManageDJs";
+import EventTypes from "@/pages/admin/EventTypes";
+import Templates from "@/pages/admin/Templates";
+import AssignmentTool from "@/pages/admin/AssignmentTool";
+import PublicDJProfile from "@/pages/PublicDJProfile";
+
+function ProtectedRoute({ children }: { children: React.ReactNode }) {
+ const { toast } = useToast();
+ const { isAuthenticated, isLoading } = useAuth();
+
+ useEffect(() => {
+ if (!isLoading && !isAuthenticated) {
+ toast({
+ title: "Unauthorized",
+ description: "You are logged out. Logging in again...",
+ variant: "destructive",
+ });
+ setTimeout(() => {
+ window.location.href = "/api/login";
+ }, 500);
+ return;
+ }
+ }, [isAuthenticated, isLoading, toast]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!isAuthenticated) {
+ return null;
+ }
+
+ return <>{children}>;
+}
+
+function Router() {
+ const { isAuthenticated, isLoading } = useAuth();
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Public routes */}
+
+
+ {/* Protected routes */}
+ {isAuthenticated ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+ )}
+
+ {/* Fallback */}
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/client/src/components/admin/AdminDashboard.tsx b/client/src/components/admin/AdminDashboard.tsx
new file mode 100644
index 0000000..220722e
--- /dev/null
+++ b/client/src/components/admin/AdminDashboard.tsx
@@ -0,0 +1,90 @@
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+import { PendingRequests } from "./PendingRequests";
+import { AssignmentTool } from "./AssignmentTool";
+
+export function AdminDashboard() {
+ const { data: stats, isLoading } = useQuery({
+ queryKey: ["/api/stats/admin"],
+ });
+
+ if (isLoading) {
+ return (
+
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+ }
+
+ const statCards = [
+ {
+ title: "Total DJs",
+ value: stats?.totalDJs || 0,
+ icon: "fas fa-users",
+ color: "blue",
+ },
+ {
+ title: "Active Events",
+ value: stats?.activeEvents || 0,
+ icon: "fas fa-calendar-check",
+ color: "green",
+ },
+ {
+ title: "Pending Requests",
+ value: stats?.pendingRequests || 0,
+ icon: "fas fa-exclamation-triangle",
+ color: "yellow",
+ },
+ {
+ title: "This Month",
+ value: stats?.thisMonth || 0,
+ icon: "fas fa-chart-bar",
+ color: "purple",
+ },
+ ];
+
+ return (
+
+
+
Admin Dashboard
+
Manage DJs, events, and system settings
+
+
+
+ {statCards.map((stat) => (
+
+
+
+
+
{stat.title}
+
{stat.value}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/client/src/components/admin/AssignmentTool.tsx b/client/src/components/admin/AssignmentTool.tsx
new file mode 100644
index 0000000..6c948b3
--- /dev/null
+++ b/client/src/components/admin/AssignmentTool.tsx
@@ -0,0 +1,170 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Checkbox } from "@/components/ui/checkbox";
+import { useToast } from "@/hooks/use-toast";
+import { apiRequest } from "@/lib/queryClient";
+import { Wand2 } from "lucide-react";
+
+export function AssignmentTool() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [selectedTemplate, setSelectedTemplate] = useState("");
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+ const [randomize, setRandomize] = useState(true);
+ const [respectFrequency, setRespectFrequency] = useState(true);
+ const [checkAvailability, setCheckAvailability] = useState(true);
+
+ const { data: templates } = useQuery({
+ queryKey: ["/api/schedule-templates"],
+ });
+
+ const generateAssignmentsMutation = useMutation({
+ mutationFn: async (data: any) => {
+ // TODO: Implement assignment generation logic
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Simulate API call
+ return { success: true, message: "Assignments generated successfully" };
+ },
+ onSuccess: () => {
+ toast({
+ title: "Assignments generated",
+ description: "DJ assignments have been generated successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/events"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: "Failed to generate assignments",
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleGenerateAssignments = () => {
+ if (!selectedTemplate || !startDate || !endDate) {
+ toast({
+ title: "Validation Error",
+ description: "Please fill in all required fields",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ const data = {
+ templateId: parseInt(selectedTemplate),
+ startDate,
+ endDate,
+ options: {
+ randomize,
+ respectFrequency,
+ checkAvailability,
+ },
+ };
+
+ generateAssignmentsMutation.mutate(data);
+ };
+
+ return (
+
+
+ Quick Assignment Tool
+ Assign DJs to schedule templates
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/admin/DJManagement.tsx b/client/src/components/admin/DJManagement.tsx
new file mode 100644
index 0000000..db00391
--- /dev/null
+++ b/client/src/components/admin/DJManagement.tsx
@@ -0,0 +1,263 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Badge } from "@/components/ui/badge";
+import { DataTable } from "@/components/ui/data-table";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { useToast } from "@/hooks/use-toast";
+import { apiRequest } from "@/lib/queryClient";
+import { UserPlus, UserCheck, UserX, Shield, ShieldOff } from "lucide-react";
+
+export function DJManagement() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [showInviteModal, setShowInviteModal] = useState(false);
+ const [inviteName, setInviteName] = useState("");
+ const [inviteEmail, setInviteEmail] = useState("");
+
+ const { data: djs, isLoading } = useQuery({
+ queryKey: ["/api/users"],
+ });
+
+ const { data: invitations } = useQuery({
+ queryKey: ["/api/invitations"],
+ });
+
+ const inviteDJMutation = useMutation({
+ mutationFn: async (data: { name: string; email: string }) => {
+ await apiRequest("POST", "/api/invitations", data);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Invitation sent",
+ description: "DJ invitation has been sent successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/invitations"] });
+ setShowInviteModal(false);
+ setInviteName("");
+ setInviteEmail("");
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const toggleUserStatusMutation = useMutation({
+ mutationFn: async (data: { userId: string; action: "activate" | "deactivate" }) => {
+ const endpoint = data.action === "activate" ? "reactivate" : "deactivate";
+ await apiRequest("POST", `/api/users/${data.userId}/${endpoint}`);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Status updated",
+ description: "User status has been updated successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/users"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const toggleAdminMutation = useMutation({
+ mutationFn: async (data: { userId: string; action: "make-admin" | "remove-admin" }) => {
+ await apiRequest("POST", `/api/users/${data.userId}/${data.action}`);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Admin status updated",
+ description: "User admin status has been updated successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/users"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleInvite = () => {
+ if (!inviteName || !inviteEmail) {
+ toast({
+ title: "Validation Error",
+ description: "Please fill in all fields",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ inviteDJMutation.mutate({ name: inviteName, email: inviteEmail });
+ };
+
+ const columns = [
+ {
+ key: "displayName" as const,
+ header: "DJ Name",
+ cell: (dj: any) => (
+
+

+
+
{dj.displayName || dj.firstName}
+
{dj.email}
+
+
+ ),
+ },
+ {
+ key: "role" as const,
+ header: "Role",
+ cell: (dj: any) => (
+
+ {dj.role}
+
+ ),
+ },
+ {
+ key: "isActive" as const,
+ header: "Status",
+ cell: (dj: any) => (
+
+ {dj.isActive ? "Active" : "Inactive"}
+
+ ),
+ },
+ {
+ key: "maxEventsPerMonth" as const,
+ header: "Max Events/Month",
+ },
+ ];
+
+ const actions = (dj: any) => (
+
+
+
+
+
+ );
+
+ return (
+
+
+
+
DJ Management
+
Manage DJ accounts and invitations
+
+
+
+
+
+
+
+ Active DJs
+
+
+
+
+
+
+ {invitations && invitations.length > 0 && (
+
+
+ Pending Invitations
+
+
+
+ {invitations.map((invitation: any) => (
+
+
+
{invitation.name}
+
{invitation.email}
+
+
Pending
+
+ ))}
+
+
+
+ )}
+
+ );
+}
diff --git a/client/src/components/admin/EventTypeManagement.tsx b/client/src/components/admin/EventTypeManagement.tsx
new file mode 100644
index 0000000..fbae876
--- /dev/null
+++ b/client/src/components/admin/EventTypeManagement.tsx
@@ -0,0 +1,251 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { Badge } from "@/components/ui/badge";
+import { DataTable } from "@/components/ui/data-table";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { useToast } from "@/hooks/use-toast";
+import { apiRequest } from "@/lib/queryClient";
+import { Plus, Edit, Trash2 } from "lucide-react";
+
+export function EventTypeManagement() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [showModal, setShowModal] = useState(false);
+ const [selectedEventType, setSelectedEventType] = useState(null);
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+
+ const { data: eventTypes, isLoading } = useQuery({
+ queryKey: ["/api/event-types"],
+ });
+
+ const createEventTypeMutation = useMutation({
+ mutationFn: async (data: { name: string; description: string }) => {
+ await apiRequest("POST", "/api/event-types", data);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Event type created",
+ description: "Event type has been created successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/event-types"] });
+ handleCloseModal();
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const updateEventTypeMutation = useMutation({
+ mutationFn: async (data: { id: number; name: string; description: string }) => {
+ await apiRequest("PATCH", `/api/event-types/${data.id}`, { name: data.name, description: data.description });
+ },
+ onSuccess: () => {
+ toast({
+ title: "Event type updated",
+ description: "Event type has been updated successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/event-types"] });
+ handleCloseModal();
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const deleteEventTypeMutation = useMutation({
+ mutationFn: async (id: number) => {
+ await apiRequest("DELETE", `/api/event-types/${id}`);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Event type deleted",
+ description: "Event type has been deleted successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/event-types"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleOpenModal = (eventType?: any) => {
+ setSelectedEventType(eventType);
+ setName(eventType?.name || "");
+ setDescription(eventType?.description || "");
+ setShowModal(true);
+ };
+
+ const handleCloseModal = () => {
+ setShowModal(false);
+ setSelectedEventType(null);
+ setName("");
+ setDescription("");
+ };
+
+ const handleSubmit = () => {
+ if (!name) {
+ toast({
+ title: "Validation Error",
+ description: "Event type name is required",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ if (selectedEventType) {
+ updateEventTypeMutation.mutate({
+ id: selectedEventType.id,
+ name,
+ description,
+ });
+ } else {
+ createEventTypeMutation.mutate({ name, description });
+ }
+ };
+
+ const handleDelete = (id: number) => {
+ if (window.confirm("Are you sure you want to delete this event type?")) {
+ deleteEventTypeMutation.mutate(id);
+ }
+ };
+
+ const columns = [
+ {
+ key: "name" as const,
+ header: "Name",
+ cell: (eventType: any) => (
+ {eventType.name}
+ ),
+ },
+ {
+ key: "description" as const,
+ header: "Description",
+ cell: (eventType: any) => (
+ {eventType.description || "-"}
+ ),
+ },
+ {
+ key: "isActive" as const,
+ header: "Status",
+ cell: (eventType: any) => (
+
+ {eventType.isActive ? "Active" : "Inactive"}
+
+ ),
+ },
+ ];
+
+ const actions = (eventType: any) => (
+
+
+
+
+ );
+
+ return (
+
+
+
+
Event Types
+
Manage event categories and types
+
+
+
+
+
+
+
+ Event Types
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/admin/PendingRequests.tsx b/client/src/components/admin/PendingRequests.tsx
new file mode 100644
index 0000000..9bc9b1b
--- /dev/null
+++ b/client/src/components/admin/PendingRequests.tsx
@@ -0,0 +1,123 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { useToast } from "@/hooks/use-toast";
+import { apiRequest } from "@/lib/queryClient";
+import { CheckCircle, XCircle } from "lucide-react";
+
+export function PendingRequests() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+
+ const { data: requests, isLoading } = useQuery({
+ queryKey: ["/api/removal-requests"],
+ });
+
+ const approveRequestMutation = useMutation({
+ mutationFn: async (requestId: number) => {
+ await apiRequest("POST", `/api/removal-requests/${requestId}/approve`);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Request approved",
+ description: "The removal request has been approved.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/removal-requests"] });
+ queryClient.invalidateQueries({ queryKey: ["/api/stats/admin"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const denyRequestMutation = useMutation({
+ mutationFn: async (requestId: number) => {
+ await apiRequest("POST", `/api/removal-requests/${requestId}/deny`);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Request denied",
+ description: "The removal request has been denied.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/removal-requests"] });
+ queryClient.invalidateQueries({ queryKey: ["/api/stats/admin"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleApprove = (requestId: number) => {
+ approveRequestMutation.mutate(requestId);
+ };
+
+ const handleDeny = (requestId: number) => {
+ denyRequestMutation.mutate(requestId);
+ };
+
+ return (
+
+
+ Pending Removal Requests
+ DJs requesting removal from assigned events
+
+
+
+ {requests?.map((request: any) => (
+
+
+
+
{request.dj?.displayName}
+
{request.event?.name}
+
+ {new Date(request.event?.date).toLocaleDateString()} • {request.event?.startTime}
+
+ {request.reason && (
+
+ )}
+
+
+
+
+
+
+
+ ))}
+
+ {(!requests || requests.length === 0) && (
+
+
+
No pending requests
+
+ )}
+
+
+
+ );
+}
diff --git a/client/src/components/admin/ScheduleTemplates.tsx b/client/src/components/admin/ScheduleTemplates.tsx
new file mode 100644
index 0000000..5fb7696
--- /dev/null
+++ b/client/src/components/admin/ScheduleTemplates.tsx
@@ -0,0 +1,352 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Badge } from "@/components/ui/badge";
+import { DataTable } from "@/components/ui/data-table";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { useToast } from "@/hooks/use-toast";
+import { apiRequest } from "@/lib/queryClient";
+import { Plus, Edit, Trash2, Clock } from "lucide-react";
+
+export function ScheduleTemplates() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [showTemplateModal, setShowTemplateModal] = useState(false);
+ const [showSlotModal, setShowSlotModal] = useState(false);
+ const [selectedTemplate, setSelectedTemplate] = useState(null);
+ const [templateName, setTemplateName] = useState("");
+ const [templateDescription, setTemplateDescription] = useState("");
+ const [slotData, setSlotData] = useState({
+ dayOfWeek: 0,
+ startTime: "",
+ endTime: "",
+ eventTypeId: 0,
+ });
+
+ const { data: templates, isLoading } = useQuery({
+ queryKey: ["/api/schedule-templates"],
+ });
+
+ const { data: eventTypes } = useQuery({
+ queryKey: ["/api/event-types"],
+ });
+
+ const { data: templateSlots } = useQuery({
+ queryKey: ["/api/schedule-templates", selectedTemplate?.id, "slots"],
+ enabled: !!selectedTemplate,
+ });
+
+ const createTemplateMutation = useMutation({
+ mutationFn: async (data: { name: string; description: string }) => {
+ await apiRequest("POST", "/api/schedule-templates", data);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Template created",
+ description: "Schedule template has been created successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/schedule-templates"] });
+ handleCloseTemplateModal();
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const createSlotMutation = useMutation({
+ mutationFn: async (data: any) => {
+ await apiRequest("POST", `/api/schedule-templates/${selectedTemplate.id}/slots`, data);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Slot created",
+ description: "Template slot has been created successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/schedule-templates", selectedTemplate?.id, "slots"] });
+ handleCloseSlotModal();
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleOpenTemplateModal = (template?: any) => {
+ setSelectedTemplate(template);
+ setTemplateName(template?.name || "");
+ setTemplateDescription(template?.description || "");
+ setShowTemplateModal(true);
+ };
+
+ const handleCloseTemplateModal = () => {
+ setShowTemplateModal(false);
+ setSelectedTemplate(null);
+ setTemplateName("");
+ setTemplateDescription("");
+ };
+
+ const handleOpenSlotModal = (template: any) => {
+ setSelectedTemplate(template);
+ setSlotData({
+ dayOfWeek: 0,
+ startTime: "",
+ endTime: "",
+ eventTypeId: 0,
+ });
+ setShowSlotModal(true);
+ };
+
+ const handleCloseSlotModal = () => {
+ setShowSlotModal(false);
+ setSlotData({
+ dayOfWeek: 0,
+ startTime: "",
+ endTime: "",
+ eventTypeId: 0,
+ });
+ };
+
+ const handleCreateTemplate = () => {
+ if (!templateName) {
+ toast({
+ title: "Validation Error",
+ description: "Template name is required",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ createTemplateMutation.mutate({ name: templateName, description: templateDescription });
+ };
+
+ const handleCreateSlot = () => {
+ if (!slotData.startTime || !slotData.endTime || !slotData.eventTypeId) {
+ toast({
+ title: "Validation Error",
+ description: "All fields are required",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ createSlotMutation.mutate(slotData);
+ };
+
+ const getDayName = (dayOfWeek: number) => {
+ const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
+ return days[dayOfWeek];
+ };
+
+ const getEventTypeName = (eventTypeId: number) => {
+ return eventTypes?.find((type: any) => type.id === eventTypeId)?.name || "Unknown";
+ };
+
+ const templateColumns = [
+ {
+ key: "name" as const,
+ header: "Template Name",
+ cell: (template: any) => (
+ {template.name}
+ ),
+ },
+ {
+ key: "description" as const,
+ header: "Description",
+ cell: (template: any) => (
+ {template.description || "-"}
+ ),
+ },
+ {
+ key: "isActive" as const,
+ header: "Status",
+ cell: (template: any) => (
+
+ {template.isActive ? "Active" : "Inactive"}
+
+ ),
+ },
+ ];
+
+ const templateActions = (template: any) => (
+
+
+
+
+ );
+
+ return (
+
+
+
+
Schedule Templates
+
Manage recurring schedule templates
+
+
+
+
+
+
+
+ Schedule Templates
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/availability/AvailabilityCalendar.tsx b/client/src/components/availability/AvailabilityCalendar.tsx
new file mode 100644
index 0000000..e01585b
--- /dev/null
+++ b/client/src/components/availability/AvailabilityCalendar.tsx
@@ -0,0 +1,268 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { Calendar } from "@/components/ui/calendar";
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { useToast } from "@/hooks/use-toast";
+import { apiRequest } from "@/lib/queryClient";
+import { CalendarX, Plus, Trash2 } from "lucide-react";
+import { format } from "date-fns";
+
+export function AvailabilityCalendar() {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [showModal, setShowModal] = useState(false);
+ const [selectedDate, setSelectedDate] = useState();
+ const [unavailableData, setUnavailableData] = useState({
+ startDate: "",
+ endDate: "",
+ reason: "",
+ });
+
+ const { data: availability, isLoading } = useQuery({
+ queryKey: ["/api/availability"],
+ });
+
+ const createAvailabilityMutation = useMutation({
+ mutationFn: async (data: { startDate: string; endDate: string; reason: string }) => {
+ await apiRequest("POST", "/api/availability", data);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Unavailability added",
+ description: "Your unavailable dates have been saved.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/availability"] });
+ handleCloseModal();
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const deleteAvailabilityMutation = useMutation({
+ mutationFn: async (id: number) => {
+ await apiRequest("DELETE", `/api/availability/${id}`);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Unavailability removed",
+ description: "Your unavailable dates have been removed.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/availability"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const handleOpenModal = (date?: Date) => {
+ const dateStr = date ? format(date, "yyyy-MM-dd") : "";
+ setUnavailableData({
+ startDate: dateStr,
+ endDate: dateStr,
+ reason: "",
+ });
+ setShowModal(true);
+ };
+
+ const handleCloseModal = () => {
+ setShowModal(false);
+ setUnavailableData({
+ startDate: "",
+ endDate: "",
+ reason: "",
+ });
+ };
+
+ const handleSubmit = () => {
+ if (!unavailableData.startDate || !unavailableData.endDate) {
+ toast({
+ title: "Validation Error",
+ description: "Please select start and end dates",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ createAvailabilityMutation.mutate(unavailableData);
+ };
+
+ const isDateUnavailable = (date: Date) => {
+ if (!availability) return false;
+
+ const dateStr = format(date, "yyyy-MM-dd");
+ return availability.some((item: any) =>
+ dateStr >= item.startDate && dateStr <= item.endDate
+ );
+ };
+
+ const getUnavailablePeriodsForDate = (date: Date) => {
+ if (!availability) return [];
+
+ const dateStr = format(date, "yyyy-MM-dd");
+ return availability.filter((item: any) =>
+ dateStr >= item.startDate && dateStr <= item.endDate
+ );
+ };
+
+ return (
+
+
+
+
Availability Management
+
Mark dates when you're unavailable for events
+
+
+
+
+
+
+
+
+ Calendar
+
+
+ isDateUnavailable(date),
+ }}
+ modifiersStyles={{
+ unavailable: {
+ backgroundColor: "#fef3c7",
+ color: "#d97706",
+ },
+ }}
+ onDayClick={(date) => {
+ setSelectedDate(date);
+ if (isDateUnavailable(date)) {
+ // Show details for this date
+ } else {
+ handleOpenModal(date);
+ }
+ }}
+ />
+
+
+
+
Click on a date to mark it as unavailable, or click on an unavailable date to view details.
+
+
+
+
+
+
+ Unavailable Periods
+
+
+
+ {availability?.map((item: any) => (
+
+
+
+
+ {format(new Date(item.startDate), "MMM d, yyyy")} - {format(new Date(item.endDate), "MMM d, yyyy")}
+
+ {item.reason && (
+
{item.reason}
+ )}
+
+
+
+
+ ))}
+
+ {(!availability || availability.length === 0) && (
+
+
+
No unavailable periods set
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/dashboard/CalendarWidget.tsx b/client/src/components/dashboard/CalendarWidget.tsx
new file mode 100644
index 0000000..c45fd96
--- /dev/null
+++ b/client/src/components/dashboard/CalendarWidget.tsx
@@ -0,0 +1,107 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { useQuery } from "@tanstack/react-query";
+import { cn } from "@/lib/utils";
+
+export function CalendarWidget() {
+ const { data: events } = useQuery({
+ queryKey: ["/api/events/upcoming"],
+ });
+
+ const currentDate = new Date();
+ const currentMonth = currentDate.getMonth();
+ const currentYear = currentDate.getFullYear();
+
+ const monthNames = [
+ "January", "February", "March", "April", "May", "June",
+ "July", "August", "September", "October", "November", "December"
+ ];
+
+ const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
+ const firstDayOfMonth = new Date(currentYear, currentMonth, 1).getDay();
+
+ const days = [];
+
+ // Add empty cells for days before the first day of the month
+ for (let i = 0; i < firstDayOfMonth; i++) {
+ days.push(null);
+ }
+
+ // Add days of the month
+ for (let day = 1; day <= daysInMonth; day++) {
+ days.push(day);
+ }
+
+ const getEventForDay = (day: number) => {
+ if (!events || !day) return null;
+
+ const dateStr = `${currentYear}-${(currentMonth + 1).toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`;
+ return events.find((event: any) => event.date === dateStr);
+ };
+
+ const getDayClass = (day: number) => {
+ const today = currentDate.getDate();
+ const event = getEventForDay(day);
+
+ if (!day) return "text-slate-300";
+
+ if (day === today) {
+ return "bg-primary-600 text-white rounded-full";
+ }
+
+ if (event) {
+ if (event.isAssignedByAdmin) {
+ return "bg-primary-100 rounded-full";
+ } else {
+ return "bg-green-100 rounded-full";
+ }
+ }
+
+ return "text-slate-700";
+ };
+
+ return (
+
+
+ {monthNames[currentMonth]} {currentYear}
+
+
+
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
+
+ {day}
+
+ ))}
+
+
+
+ {days.map((day, index) => (
+
+ {day}
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/dashboard/DashboardStats.tsx b/client/src/components/dashboard/DashboardStats.tsx
new file mode 100644
index 0000000..7754124
--- /dev/null
+++ b/client/src/components/dashboard/DashboardStats.tsx
@@ -0,0 +1,70 @@
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent } from "@/components/ui/card";
+import { Skeleton } from "@/components/ui/skeleton";
+
+export function DashboardStats() {
+ const { data: stats, isLoading } = useQuery({
+ queryKey: ["/api/stats/dashboard"],
+ });
+
+ if (isLoading) {
+ return (
+
+ {[...Array(4)].map((_, i) => (
+
+
+
+
+
+ ))}
+
+ );
+ }
+
+ const statCards = [
+ {
+ title: "Upcoming Events",
+ value: stats?.upcomingEvents || 0,
+ icon: "fas fa-calendar-alt",
+ color: "blue",
+ },
+ {
+ title: "This Month",
+ value: stats?.thisMonth || 0,
+ icon: "fas fa-music",
+ color: "green",
+ },
+ {
+ title: "Pending Requests",
+ value: stats?.pendingRequests || 0,
+ icon: "fas fa-clock",
+ color: "yellow",
+ },
+ {
+ title: "Total Events",
+ value: stats?.totalEvents || 0,
+ icon: "fas fa-chart-line",
+ color: "purple",
+ },
+ ];
+
+ return (
+
+ {statCards.map((stat) => (
+
+
+
+
+
{stat.title}
+
{stat.value}
+
+
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/client/src/components/dashboard/QuickActions.tsx b/client/src/components/dashboard/QuickActions.tsx
new file mode 100644
index 0000000..df03866
--- /dev/null
+++ b/client/src/components/dashboard/QuickActions.tsx
@@ -0,0 +1,58 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { useState } from "react";
+import { EventModal } from "../events/EventModal";
+import { ProfileModal } from "../profile/ProfileModal";
+import { Link } from "wouter";
+
+export function QuickActions() {
+ const [showEventModal, setShowEventModal] = useState(false);
+ const [showProfileModal, setShowProfileModal] = useState(false);
+
+ return (
+ <>
+
+
+ Quick Actions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setShowEventModal(false)}
+ />
+
+ setShowProfileModal(false)}
+ />
+ >
+ );
+}
diff --git a/client/src/components/dashboard/UpcomingEvents.tsx b/client/src/components/dashboard/UpcomingEvents.tsx
new file mode 100644
index 0000000..ab5ce77
--- /dev/null
+++ b/client/src/components/dashboard/UpcomingEvents.tsx
@@ -0,0 +1,116 @@
+import { useQuery } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Link } from "wouter";
+
+export function UpcomingEvents() {
+ const { data: events, isLoading } = useQuery({
+ queryKey: ["/api/events/upcoming"],
+ });
+
+ if (isLoading) {
+ return (
+
+
+ Upcoming Events
+
+
+
+ {[...Array(3)].map((_, i) => (
+
+ ))}
+
+
+
+ );
+ }
+
+ const getEventIcon = (eventType: string) => {
+ switch (eventType) {
+ case "radio":
+ return "fas fa-music";
+ case "club":
+ return "fas fa-microphone";
+ case "special":
+ return "fas fa-headphones";
+ default:
+ return "fas fa-calendar";
+ }
+ };
+
+ const getEventColor = (eventType: string) => {
+ switch (eventType) {
+ case "radio":
+ return "primary";
+ case "club":
+ return "purple";
+ case "special":
+ return "green";
+ default:
+ return "blue";
+ }
+ };
+
+ const getEventStatus = (event: any) => {
+ if (event.isAssignedByAdmin) {
+ return { label: "Confirmed", variant: "default" as const };
+ }
+ return { label: "Self-Added", variant: "secondary" as const };
+ };
+
+ return (
+
+
+ Upcoming Events
+
+
+
+ {events?.slice(0, 3).map((event: any) => {
+ const status = getEventStatus(event);
+ const color = getEventColor(event.eventType);
+
+ return (
+
+
+
+
+
+
+
{event.name}
+
+ {new Date(event.date).toLocaleDateString()} • {event.startTime} - {event.endTime}
+
+
{event.locationName}
+
+
+
+ {status.label}
+
+
+
+ );
+ })}
+
+ {(!events || events.length === 0) && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/events/EventList.tsx b/client/src/components/events/EventList.tsx
new file mode 100644
index 0000000..6767483
--- /dev/null
+++ b/client/src/components/events/EventList.tsx
@@ -0,0 +1,198 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { DataTable } from "@/components/ui/data-table";
+import { EventModal } from "./EventModal";
+import { useToast } from "@/hooks/use-toast";
+import { apiRequest } from "@/lib/queryClient";
+import { Edit, Trash2, MapPin, Clock } from "lucide-react";
+
+export function EventList() {
+ const [selectedEvent, setSelectedEvent] = useState(null);
+ const [showEventModal, setShowEventModal] = useState(false);
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+
+ const { data: events, isLoading } = useQuery({
+ queryKey: ["/api/events"],
+ });
+
+ const { data: eventTypes } = useQuery({
+ queryKey: ["/api/event-types"],
+ });
+
+ const deleteEventMutation = useMutation({
+ mutationFn: async (eventId: number) => {
+ await apiRequest("DELETE", `/api/events/${eventId}`);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Event deleted",
+ description: "The event has been deleted successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/events"] });
+ queryClient.invalidateQueries({ queryKey: ["/api/events/upcoming"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const createRemovalRequestMutation = useMutation({
+ mutationFn: async (data: { eventId: number; reason: string }) => {
+ await apiRequest("POST", "/api/removal-requests", data);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Request submitted",
+ description: "Your removal request has been submitted for admin review.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/events"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const getEventTypeName = (eventTypeId: number) => {
+ return eventTypes?.find((type: any) => type.id === eventTypeId)?.name || "Unknown";
+ };
+
+ const handleEditEvent = (event: any) => {
+ setSelectedEvent(event);
+ setShowEventModal(true);
+ };
+
+ const handleDeleteEvent = (eventId: number) => {
+ if (window.confirm("Are you sure you want to delete this event?")) {
+ deleteEventMutation.mutate(eventId);
+ }
+ };
+
+ const handleRequestRemoval = (eventId: number) => {
+ const reason = window.prompt("Please provide a reason for the removal request:");
+ if (reason) {
+ createRemovalRequestMutation.mutate({ eventId, reason });
+ }
+ };
+
+ const columns = [
+ {
+ key: "name" as const,
+ header: "Event Name",
+ cell: (event: any) => (
+
+
{event.name}
+
{getEventTypeName(event.eventTypeId)}
+
+ ),
+ },
+ {
+ key: "date" as const,
+ header: "Date & Time",
+ cell: (event: any) => (
+
+
+
+
{new Date(event.date).toLocaleDateString()}
+
{event.startTime} - {event.endTime}
+
+
+ ),
+ },
+ {
+ key: "locationName" as const,
+ header: "Location",
+ cell: (event: any) => (
+
+
+
+
{event.locationName}
+ {event.locationAddress && (
+
{event.locationAddress}
+ )}
+
+
+ ),
+ },
+ {
+ key: "isAssignedByAdmin" as const,
+ header: "Status",
+ cell: (event: any) => (
+
+ {event.isAssignedByAdmin ? "Admin Assigned" : "Self-Added"}
+
+ ),
+ },
+ ];
+
+ const actions = (event: any) => (
+
+ {!event.isAssignedByAdmin && (
+
+ )}
+
+ {event.isAssignedByAdmin ? (
+
+ ) : (
+
+ )}
+
+ );
+
+ return (
+ <>
+
+
+ My Events
+
+
+
+
+
+
+ {
+ setShowEventModal(false);
+ setSelectedEvent(null);
+ }}
+ event={selectedEvent}
+ />
+ >
+ );
+}
diff --git a/client/src/components/events/EventModal.tsx b/client/src/components/events/EventModal.tsx
new file mode 100644
index 0000000..a80b044
--- /dev/null
+++ b/client/src/components/events/EventModal.tsx
@@ -0,0 +1,256 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { useToast } from "@/hooks/use-toast";
+import { apiRequest } from "@/lib/queryClient";
+
+const eventSchema = z.object({
+ name: z.string().min(1, "Event name is required"),
+ eventTypeId: z.number().min(1, "Event type is required"),
+ date: z.string().min(1, "Date is required"),
+ startTime: z.string().min(1, "Start time is required"),
+ endTime: z.string().min(1, "End time is required"),
+ locationName: z.string().min(1, "Location name is required"),
+ locationAddress: z.string().optional(),
+ description: z.string().optional(),
+});
+
+interface EventModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ event?: any;
+}
+
+export function EventModal({ isOpen, onClose, event }: EventModalProps) {
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+
+ const { data: eventTypes } = useQuery({
+ queryKey: ["/api/event-types"],
+ enabled: isOpen,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(eventSchema),
+ defaultValues: {
+ name: event?.name || "",
+ eventTypeId: event?.eventTypeId || 0,
+ date: event?.date || "",
+ startTime: event?.startTime || "",
+ endTime: event?.endTime || "",
+ locationName: event?.locationName || "",
+ locationAddress: event?.locationAddress || "",
+ description: event?.description || "",
+ },
+ });
+
+ const createEventMutation = useMutation({
+ mutationFn: async (data: any) => {
+ await apiRequest("POST", "/api/events", data);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Event created",
+ description: "Your event has been created successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/events"] });
+ queryClient.invalidateQueries({ queryKey: ["/api/events/upcoming"] });
+ queryClient.invalidateQueries({ queryKey: ["/api/stats/dashboard"] });
+ onClose();
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const updateEventMutation = useMutation({
+ mutationFn: async (data: any) => {
+ await apiRequest("PATCH", `/api/events/${event.id}`, data);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Event updated",
+ description: "Your event has been updated successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/events"] });
+ queryClient.invalidateQueries({ queryKey: ["/api/events/upcoming"] });
+ onClose();
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const onSubmit = (data: any) => {
+ if (event) {
+ updateEventMutation.mutate(data);
+ } else {
+ createEventMutation.mutate(data);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/layout/AppLayout.tsx b/client/src/components/layout/AppLayout.tsx
new file mode 100644
index 0000000..82c190a
--- /dev/null
+++ b/client/src/components/layout/AppLayout.tsx
@@ -0,0 +1,58 @@
+import { useAuth } from "@/hooks/useAuth";
+import { Sidebar } from "./Sidebar";
+
+interface AppLayoutProps {
+ children: React.ReactNode;
+}
+
+export function AppLayout({ children }: AppLayoutProps) {
+ const { user } = useAuth();
+
+ return (
+
+
+
+
+
+ {children}
+
+
+
+ );
+}
+
+function TopBar({ user }: { user: any }) {
+ return (
+
+
+
+
Dashboard
+
+ Welcome back, {user?.displayName || user?.firstName || 'User'}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx
new file mode 100644
index 0000000..2759452
--- /dev/null
+++ b/client/src/components/layout/Sidebar.tsx
@@ -0,0 +1,117 @@
+import { useAuth } from "@/hooks/useAuth";
+import { Link, useLocation } from "wouter";
+import { cn } from "@/lib/utils";
+
+const djNavItems = [
+ { href: "/", icon: "fas fa-home", label: "Dashboard" },
+ { href: "/profile", icon: "fas fa-user", label: "My Profile" },
+ { href: "/events", icon: "fas fa-calendar", label: "My Events" },
+ { href: "/schedule", icon: "fas fa-clock", label: "Schedule" },
+ { href: "/availability", icon: "fas fa-calendar-times", label: "Availability" },
+];
+
+const adminNavItems = [
+ { href: "/admin", icon: "fas fa-tachometer-alt", label: "Admin Dashboard" },
+ { href: "/admin/djs", icon: "fas fa-users", label: "Manage DJs" },
+ { href: "/admin/event-types", icon: "fas fa-tags", label: "Event Types" },
+ { href: "/admin/templates", icon: "fas fa-copy", label: "Templates" },
+ { href: "/admin/assignment", icon: "fas fa-magic", label: "Assignment Tool" },
+];
+
+export function Sidebar() {
+ const { user } = useAuth();
+ const [location] = useLocation();
+
+ const isAdmin = user?.role === "admin";
+
+ return (
+
+ );
+}
diff --git a/client/src/components/profile/ProfileModal.tsx b/client/src/components/profile/ProfileModal.tsx
new file mode 100644
index 0000000..9830b59
--- /dev/null
+++ b/client/src/components/profile/ProfileModal.tsx
@@ -0,0 +1,326 @@
+import { useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Separator } from "@/components/ui/separator";
+import { useToast } from "@/hooks/use-toast";
+import { useAuth } from "@/hooks/useAuth";
+import { apiRequest } from "@/lib/queryClient";
+import { ImageCropper } from "../ui/image-cropper";
+import { Plus, Trash2 } from "lucide-react";
+
+const profileSchema = z.object({
+ displayName: z.string().min(1, "Display name is required"),
+ currentPassword: z.string().optional(),
+ newPassword: z.string().optional(),
+ confirmPassword: z.string().optional(),
+}).refine((data) => {
+ if (data.newPassword && data.newPassword !== data.confirmPassword) {
+ return false;
+ }
+ return true;
+}, {
+ message: "Passwords don't match",
+ path: ["confirmPassword"],
+});
+
+interface ProfileModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+}
+
+export function ProfileModal({ isOpen, onClose }: ProfileModalProps) {
+ const { user } = useAuth();
+ const { toast } = useToast();
+ const queryClient = useQueryClient();
+ const [showImageCropper, setShowImageCropper] = useState(false);
+ const [newSocialLink, setNewSocialLink] = useState({ label: "", url: "" });
+
+ const { data: socialLinks, isLoading: socialLinksLoading } = useQuery({
+ queryKey: ["/api/social-links"],
+ enabled: isOpen,
+ });
+
+ const form = useForm({
+ resolver: zodResolver(profileSchema),
+ defaultValues: {
+ displayName: user?.displayName || "",
+ currentPassword: "",
+ newPassword: "",
+ confirmPassword: "",
+ },
+ });
+
+ const updateProfileMutation = useMutation({
+ mutationFn: async (data: any) => {
+ const updates: any = { displayName: data.displayName };
+
+ if (data.newPassword) {
+ updates.password = data.newPassword;
+ }
+
+ await apiRequest("PATCH", `/api/users/${user?.id}`, updates);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Profile updated",
+ description: "Your profile has been updated successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/auth/user"] });
+ onClose();
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const addSocialLinkMutation = useMutation({
+ mutationFn: async (link: { label: string; url: string }) => {
+ await apiRequest("POST", "/api/social-links", link);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Social link added",
+ description: "Your social link has been added successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/social-links"] });
+ setNewSocialLink({ label: "", url: "" });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const deleteSocialLinkMutation = useMutation({
+ mutationFn: async (id: number) => {
+ await apiRequest("DELETE", `/api/social-links/${id}`);
+ },
+ onSuccess: () => {
+ toast({
+ title: "Social link deleted",
+ description: "Your social link has been deleted successfully.",
+ });
+ queryClient.invalidateQueries({ queryKey: ["/api/social-links"] });
+ },
+ onError: (error) => {
+ toast({
+ title: "Error",
+ description: error.message,
+ variant: "destructive",
+ });
+ },
+ });
+
+ const onSubmit = (data: any) => {
+ updateProfileMutation.mutate(data);
+ };
+
+ const handleAddSocialLink = () => {
+ if (newSocialLink.label && newSocialLink.url) {
+ addSocialLinkMutation.mutate(newSocialLink);
+ }
+ };
+
+ const handleImageCrop = async (croppedImage: File) => {
+ try {
+ const formData = new FormData();
+ formData.append("profileImage", croppedImage);
+
+ // TODO: Implement image upload endpoint
+ toast({
+ title: "Image uploaded",
+ description: "Your profile image has been updated successfully.",
+ });
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: "Failed to upload image",
+ variant: "destructive",
+ });
+ }
+ };
+
+ return (
+ <>
+
+
+ setShowImageCropper(false)}
+ onCrop={handleImageCrop}
+ />
+ >
+ );
+}
diff --git a/client/src/components/ui/accordion.tsx b/client/src/components/ui/accordion.tsx
new file mode 100644
index 0000000..e6a723d
--- /dev/null
+++ b/client/src/components/ui/accordion.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import * as AccordionPrimitive from "@radix-ui/react-accordion"
+import { ChevronDown } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Accordion = AccordionPrimitive.Root
+
+const AccordionItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AccordionItem.displayName = "AccordionItem"
+
+const AccordionTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ svg]:rotate-180",
+ className
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+))
+AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
+
+const AccordionContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+ {children}
+
+))
+
+AccordionContent.displayName = AccordionPrimitive.Content.displayName
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/client/src/components/ui/alert-dialog.tsx b/client/src/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..8722561
--- /dev/null
+++ b/client/src/components/ui/alert-dialog.tsx
@@ -0,0 +1,139 @@
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+const AlertDialog = AlertDialogPrimitive.Root
+
+const AlertDialogTrigger = AlertDialogPrimitive.Trigger
+
+const AlertDialogPortal = AlertDialogPrimitive.Portal
+
+const AlertDialogOverlay = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
+
+const AlertDialogContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+
+))
+AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
+
+const AlertDialogHeader = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogHeader.displayName = "AlertDialogHeader"
+
+const AlertDialogFooter = ({
+ className,
+ ...props
+}: React.HTMLAttributes) => (
+
+)
+AlertDialogFooter.displayName = "AlertDialogFooter"
+
+const AlertDialogTitle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
+
+const AlertDialogDescription = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogDescription.displayName =
+ AlertDialogPrimitive.Description.displayName
+
+const AlertDialogAction = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
+
+const AlertDialogCancel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/client/src/components/ui/alert.tsx b/client/src/components/ui/alert.tsx
new file mode 100644
index 0000000..41fa7e0
--- /dev/null
+++ b/client/src/components/ui/alert.tsx
@@ -0,0 +1,59 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
+ {
+ variants: {
+ variant: {
+ default: "bg-background text-foreground",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+))
+Alert.displayName = "Alert"
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertTitle.displayName = "AlertTitle"
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+AlertDescription.displayName = "AlertDescription"
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/client/src/components/ui/aspect-ratio.tsx b/client/src/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c4abbf3
--- /dev/null
+++ b/client/src/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/client/src/components/ui/avatar.tsx b/client/src/components/ui/avatar.tsx
new file mode 100644
index 0000000..51e507b
--- /dev/null
+++ b/client/src/components/ui/avatar.tsx
@@ -0,0 +1,50 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+const Avatar = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+Avatar.displayName = AvatarPrimitive.Root.displayName
+
+const AvatarImage = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarImage.displayName = AvatarPrimitive.Image.displayName
+
+const AvatarFallback = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/client/src/components/ui/badge.tsx b/client/src/components/ui/badge.tsx
new file mode 100644
index 0000000..f000e3e
--- /dev/null
+++ b/client/src/components/ui/badge.tsx
@@ -0,0 +1,36 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ destructive:
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
+ outline: "text-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+export interface BadgeProps
+ extends React.HTMLAttributes,
+ VariantProps {}
+
+function Badge({ className, variant, ...props }: BadgeProps) {
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/client/src/components/ui/breadcrumb.tsx b/client/src/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..60e6c96
--- /dev/null
+++ b/client/src/components/ui/breadcrumb.tsx
@@ -0,0 +1,115 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { ChevronRight, MoreHorizontal } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+const Breadcrumb = React.forwardRef<
+ HTMLElement,
+ React.ComponentPropsWithoutRef<"nav"> & {
+ separator?: React.ReactNode
+ }
+>(({ ...props }, ref) => )
+Breadcrumb.displayName = "Breadcrumb"
+
+const BreadcrumbList = React.forwardRef<
+ HTMLOListElement,
+ React.ComponentPropsWithoutRef<"ol">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbList.displayName = "BreadcrumbList"
+
+const BreadcrumbItem = React.forwardRef<
+ HTMLLIElement,
+ React.ComponentPropsWithoutRef<"li">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbItem.displayName = "BreadcrumbItem"
+
+const BreadcrumbLink = React.forwardRef<
+ HTMLAnchorElement,
+ React.ComponentPropsWithoutRef<"a"> & {
+ asChild?: boolean
+ }
+>(({ asChild, className, ...props }, ref) => {
+ const Comp = asChild ? Slot : "a"
+
+ return (
+
+ )
+})
+BreadcrumbLink.displayName = "BreadcrumbLink"
+
+const BreadcrumbPage = React.forwardRef<
+ HTMLSpanElement,
+ React.ComponentPropsWithoutRef<"span">
+>(({ className, ...props }, ref) => (
+
+))
+BreadcrumbPage.displayName = "BreadcrumbPage"
+
+const BreadcrumbSeparator = ({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<"li">) => (
+ svg]:w-3.5 [&>svg]:h-3.5", className)}
+ {...props}
+ >
+ {children ?? }
+
+)
+BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
+
+const BreadcrumbEllipsis = ({
+ className,
+ ...props
+}: React.ComponentProps<"span">) => (
+
+
+ More
+
+)
+BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
new file mode 100644
index 0000000..36496a2
--- /dev/null
+++ b/client/src/components/ui/button.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const buttonVariants = cva(
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
+ destructive:
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
+ outline:
+ "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
+ secondary:
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
+ ghost: "hover:bg-accent hover:text-accent-foreground",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ size: {
+ default: "h-10 px-4 py-2",
+ sm: "h-9 rounded-md px-3",
+ lg: "h-11 rounded-md px-8",
+ icon: "h-10 w-10",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+export interface ButtonProps
+ extends React.ButtonHTMLAttributes,
+ VariantProps {
+ asChild?: boolean
+}
+
+const Button = React.forwardRef(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button"
+ return (
+
+ )
+ }
+)
+Button.displayName = "Button"
+
+export { Button, buttonVariants }
diff --git a/client/src/components/ui/calendar.tsx b/client/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..2174f71
--- /dev/null
+++ b/client/src/components/ui/calendar.tsx
@@ -0,0 +1,68 @@
+import * as React from "react"
+import { ChevronLeft, ChevronRight } from "lucide-react"
+import { DayPicker } from "react-day-picker"
+
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
+
+export type CalendarProps = React.ComponentProps
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ (
+
+ ),
+ IconRight: ({ className, ...props }) => (
+
+ ),
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/client/src/components/ui/card.tsx b/client/src/components/ui/card.tsx
new file mode 100644
index 0000000..f62edea
--- /dev/null
+++ b/client/src/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
diff --git a/client/src/components/ui/carousel.tsx b/client/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..9c2b9bf
--- /dev/null
+++ b/client/src/components/ui/carousel.tsx
@@ -0,0 +1,260 @@
+import * as React from "react"
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react"
+import { ArrowLeft, ArrowRight } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+import { Button } from "@/components/ui/button"
+
+type CarouselApi = UseEmblaCarouselType[1]
+type UseCarouselParameters = Parameters
+type CarouselOptions = UseCarouselParameters[0]
+type CarouselPlugin = UseCarouselParameters[1]
+
+type CarouselProps = {
+ opts?: CarouselOptions
+ plugins?: CarouselPlugin
+ orientation?: "horizontal" | "vertical"
+ setApi?: (api: CarouselApi) => void
+}
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0]
+ api: ReturnType[1]
+ scrollPrev: () => void
+ scrollNext: () => void
+ canScrollPrev: boolean
+ canScrollNext: boolean
+} & CarouselProps
+
+const CarouselContext = React.createContext(null)
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext)
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ")
+ }
+
+ return context
+}
+
+const Carousel = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & CarouselProps
+>(
+ (
+ {
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+ },
+ ref
+ ) => {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins
+ )
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false)
+ const [canScrollNext, setCanScrollNext] = React.useState(false)
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) {
+ return
+ }
+
+ setCanScrollPrev(api.canScrollPrev())
+ setCanScrollNext(api.canScrollNext())
+ }, [])
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev()
+ }, [api])
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext()
+ }, [api])
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault()
+ scrollPrev()
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault()
+ scrollNext()
+ }
+ },
+ [scrollPrev, scrollNext]
+ )
+
+ React.useEffect(() => {
+ if (!api || !setApi) {
+ return
+ }
+
+ setApi(api)
+ }, [api, setApi])
+
+ React.useEffect(() => {
+ if (!api) {
+ return
+ }
+
+ onSelect(api)
+ api.on("reInit", onSelect)
+ api.on("select", onSelect)
+
+ return () => {
+ api?.off("select", onSelect)
+ }
+ }, [api, onSelect])
+
+ return (
+
+
+ {children}
+
+
+ )
+ }
+)
+Carousel.displayName = "Carousel"
+
+const CarouselContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { carouselRef, orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselContent.displayName = "CarouselContent"
+
+const CarouselItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { orientation } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselItem.displayName = "CarouselItem"
+
+const CarouselPrevious = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselPrevious.displayName = "CarouselPrevious"
+
+const CarouselNext = React.forwardRef<
+ HTMLButtonElement,
+ React.ComponentProps
+>(({ className, variant = "outline", size = "icon", ...props }, ref) => {
+ const { orientation, scrollNext, canScrollNext } = useCarousel()
+
+ return (
+
+ )
+})
+CarouselNext.displayName = "CarouselNext"
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+}
diff --git a/client/src/components/ui/chart.tsx b/client/src/components/ui/chart.tsx
new file mode 100644
index 0000000..39fba6d
--- /dev/null
+++ b/client/src/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+