+
{tabs.map((tab, index) => (
-
setActive(index)}
+ className={`px-6 py-3 text-sm font-medium transition-all duration-200 ${
+ active === index
+ ? "text-blue-400 border-b-2 border-blue-400 bg-blue-400/5"
+ : "text-gray-400 hover:text-gray-300 hover:bg-gray-800/30"
+ }`}
>
{tab}
-
+
))}
+
{/* Tab Content */}
-
diff --git a/frontend/src/components/form/CustomRichTextEditor.jsx b/frontend/src/components/form/CustomRichTextEditor.jsx
new file mode 100644
index 0000000..e359b20
--- /dev/null
+++ b/frontend/src/components/form/CustomRichTextEditor.jsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import MDEditor from "@uiw/react-md-editor";
+
+const CustomRichTextEditor = ({
+ value = "",
+ onChange,
+ placeholder = "Describe your event...",
+ height = "600px"
+}) => {
+ const handleChange = (val) => {
+ onChange(val || '');
+ };
+
+ return (
+
+ {/* Editor Header */}
+
+
+ = 100 && value.length <= 2000 ? 'text-green-400' : 'text-gray-400'}`}>
+ {value.length} characters
+
+
+ {value.split(/\s+/).filter(word => word.length > 0).length} words
+
+
+
+
+ {/* Editor */}
+
+
+
+
+ );
+};
+
+export default CustomRichTextEditor;
diff --git a/frontend/src/components/form/DescriptionField.jsx b/frontend/src/components/form/DescriptionField.jsx
new file mode 100644
index 0000000..bcc8b26
--- /dev/null
+++ b/frontend/src/components/form/DescriptionField.jsx
@@ -0,0 +1,106 @@
+import React, { useState, useEffect, useCallback } from "react";
+
+const DescriptionField = ({
+ value = "",
+ onChange,
+ placeholder = "Describe your event...",
+ storageKey = "event-description-draft",
+ enablePreview = true
+}) => {
+ const [localValue, setLocalValue] = useState(value);
+ const [showPreview, setShowPreview] = useState(false);
+ const [lastSaved, setLastSaved] = useState(null);
+ const [isSaving, setIsSaving] = useState(false);
+
+ // Load draft from localStorage on mount
+ useEffect(() => {
+ const saved = localStorage.getItem(storageKey);
+ if (saved && !value) {
+ setLocalValue(saved);
+ onChange?.(saved);
+ }
+ }, [storageKey, value, onChange]);
+
+ // Debounced autosave
+ const saveToStorage = useCallback(
+ (() => {
+ let timeoutId;
+ return (text) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => {
+ localStorage.setItem(storageKey, text);
+ setLastSaved(new Date());
+ setIsSaving(false);
+ }, 1000);
+ setIsSaving(true);
+ };
+ })(),
+ [storageKey]
+ );
+
+ // Handle text changes
+ const handleChange = (e) => {
+ const newValue = e.target.value;
+ setLocalValue(newValue);
+ onChange?.(newValue);
+ saveToStorage(newValue);
+ };
+
+ const characterCount = localValue.length;
+ const isGoodLength = characterCount >= 100 && characterCount <= 1000;
+
+ return (
+
+ {/* Toolbar */}
+
+
+
+ {characterCount} characters
+
+ {lastSaved && (
+
+ Draft saved {lastSaved.toLocaleTimeString()}
+
+ )}
+ {isSaving && (
+ Saving...
+ )}
+
+
+ {enablePreview && (
+
setShowPreview(!showPreview)}
+ className="px-3 py-1 text-sm text-secondary hover:text-white border border-white/20 rounded-md hover:bg-white/10 transition-colors"
+ >
+ {showPreview ? 'Edit' : 'Preview'}
+
+ )}
+
+
+ {/* Textarea or Preview */}
+ {!showPreview ? (
+
+ ) : (
+
+ {localValue || (
+ No content to preview
+ )}
+
+ )}
+
+ {/* Help text */}
+
+ Recommended length: 100-1000 characters. Include event details, requirements, and what participants can expect.
+
+
+ );
+};
+
+export default DescriptionField;
diff --git a/frontend/src/components/form/FormActions.jsx b/frontend/src/components/form/FormActions.jsx
new file mode 100644
index 0000000..456b382
--- /dev/null
+++ b/frontend/src/components/form/FormActions.jsx
@@ -0,0 +1,62 @@
+import React from "react";
+
+const FormActions = ({
+ onBack,
+ onNext,
+ onSubmit,
+ isFirst = false,
+ isLast = false,
+ loading = false,
+ isEditMode = false
+}) => {
+ return (
+
+
+
+ Back
+
+
+
+
+ {isLast ? (
+
+ {loading ? "Submitting..." : (isEditMode ? "Update Event" : "Create Event")}
+
+ ) : (
+
+ Next
+
+ )}
+
+
+ );
+};
+
+export default FormActions;
diff --git a/frontend/src/components/form/FormField.jsx b/frontend/src/components/form/FormField.jsx
new file mode 100644
index 0000000..a774b6b
--- /dev/null
+++ b/frontend/src/components/form/FormField.jsx
@@ -0,0 +1,102 @@
+import React from "react";
+
+const FormField = ({
+ label,
+ name,
+ type = "text",
+ value,
+ onChange,
+ error,
+ placeholder,
+ required = false,
+ disabled = false,
+ className = "",
+ helperText,
+ ...props
+}) => {
+ const id = `field-${name}`;
+
+ return (
+
+
+ {label}
+ {required && * }
+
+
+ {type === "textarea" ? (
+
+ ) : type === "file" ? (
+
+
+
+ {value?.name || placeholder || "Choose a file"}
+
+ {value?.name && (
+
✓ {value.name}
+ )}
+
+ ) : (
+
+ )}
+
+ {error && (
+
+ {error}
+
+ )}
+ {helperText && !error && (
+
+ {helperText}
+
+ )}
+
+ );
+};
+
+export default FormField;
diff --git a/frontend/src/components/form/StepProgress.jsx b/frontend/src/components/form/StepProgress.jsx
new file mode 100644
index 0000000..3c5c716
--- /dev/null
+++ b/frontend/src/components/form/StepProgress.jsx
@@ -0,0 +1,141 @@
+import React, { useState } from "react";
+
+const StepProgress = ({ steps, currentStep, onStepClick, stepData = {} }) => {
+ const [hoveredStep, setHoveredStep] = useState(null);
+
+ const getStepStatus = (index) => {
+ if (index < currentStep) return "completed";
+ if (index === currentStep) return "current";
+ return "pending";
+ };
+
+ const isStepComplete = (index) => {
+ const data = stepData;
+ switch (index) {
+ case 0:
+ return data.name?.trim() && data.date && data.venue?.trim() &&
+ data.numberOfMember > 0 && data.poster &&
+ data.ruleBook?.trim() && data.amount > 0;
+ case 1:
+ return data.description?.trim();
+ case 2:
+ return data.coordinators?.length > 0;
+ default:
+ return false;
+ }
+ };
+
+ const isStepClickable = (index) => {
+ return true;
+ };
+
+ const getStepSummary = (index) => {
+ const data = stepData;
+ switch (index) {
+ case 0:
+ return data.name ? `${data.name}` : "Event name required";
+ case 1:
+ return data.description ? `${data.description.length} characters` : "Description needed";
+ case 2:
+ return data.coordinators?.length || data.usefulLinks?.length ? "Team & links added" : "Team & links needed";
+ default:
+ return "";
+ }
+ };
+
+ return (
+
+ {/* Desktop Progress Bar */}
+
+
+ {steps.map((step, index) => {
+ const status = getStepStatus(index);
+ const isClickable = isStepClickable(index);
+
+ return (
+
+
+
isClickable && onStepClick(index)}
+ onMouseEnter={() => setHoveredStep(index)}
+ onMouseLeave={() => setHoveredStep(null)}
+ disabled={!isClickable}
+ className={`group relative w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-all duration-200 ${
+ isStepComplete(index)
+ ? "bg-green-500 text-white hover:bg-green-600 cursor-pointer"
+ : status === "current"
+ ? "bg-blue-500 text-white ring-2 ring-blue-200"
+ : "bg-gray-300 text-gray-600 cursor-pointer"
+ }`}
+ >
+ {isStepComplete(index) ? "✓" : index + 1}
+
+
+ {/* Step Label */}
+
+
+ {/* Hover Card */}
+ {hoveredStep === index && isClickable && (
+
+
+ {step.title}
+
+
+ {step.subtitle}
+
+
+ {getStepSummary(index)}
+
+
+
+ )}
+
+
+ {index < steps.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+
+
+ {/* Mobile Progress Bar */}
+
+
+ {steps.map((step, index) => {
+ const status = getStepStatus(index);
+ const isClickable = isStepClickable(index);
+
+ return (
+ isClickable && onStepClick(index)}
+ disabled={!isClickable}
+ className={`flex-1 mx-1 px-2 py-1.5 rounded text-xs font-medium transition-all duration-200 ${
+ isStepComplete(index)
+ ? "bg-green-500/20 text-green-400 border border-green-500/30"
+ : status === "current"
+ ? "bg-blue-500/20 text-blue-400 border border-blue-500/30"
+ : "bg-white/5 text-gray-400 border border-white/10"
+ }`}
+ >
+ {step.title}
+
+ );
+ })}
+
+
+
+ );
+};
+
+export default StepProgress;
diff --git a/frontend/src/hooks/useApiRequest.jsx b/frontend/src/hooks/useApiRequest.jsx
index 2e0f752..73bcb99 100644
--- a/frontend/src/hooks/useApiRequest.jsx
+++ b/frontend/src/hooks/useApiRequest.jsx
@@ -1,8 +1,7 @@
import { useState } from "react";
-import { useToast } from "@/hooks/use-toast";
+import { showToast } from "../utils/toast";
export const useApiRequest = ({ enableToast }) => {
- const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
@@ -13,56 +12,57 @@ export const useApiRequest = ({ enableToast }) => {
try {
const response = await fetchData();
-
- // If response is successful (status 2xx)
- if (response?.status >= 200 && response?.status < 300) {
- setData(response.data);
- enableToast
- ? toast({
- title: "Success",
- description: successMessage,
- variant: "success",
- className:
- "bg-green-800 text-white shadow-lg border border-green-500",
- })
- : "";
- return response.json();
+ setData(response.data);
+
+ if (enableToast) {
+ showToast.success(successMessage);
}
-
- // If response indicates an error (status 4xx, 5xx)
- throw response;
+
+ return response;
} catch (err) {
let errorMessage = "Something went wrong.";
let errorDetails = "";
- const { status } = err;
- const res = await err?.json();
-
- if (status >= 400 && status < 500) {
- // Client-side validation errors
- if (!Array.isArray(res.errors) && !res?.success) {
- errorDetails = res.message;
- } else {
- errorDetails = (
-
- {" "}
- {res.errors?.map((e, index) => (
-
{`${e.path}: ${e.msg}`}
- ))}
-
- );
+ // Handle different types of errors
+ if (err?.response) {
+ // Axios error with response
+ const { status, data } = err.response;
+
+ if (status >= 400 && status < 500) {
+ // Client-side validation errors
+ if (data?.errors && Array.isArray(data.errors)) {
+ errorDetails = data.errors.map(e => `${e.path}: ${e.msg}`).join(', ');
+ } else if (data?.message) {
+ errorDetails = data.message;
+ }
+ } else if (status >= 500) {
+ errorMessage = "Server error. Please try again later.";
+ }
+ } else if (err?.status) {
+ // Fetch error with status
+ const { status } = err;
+
+ if (status >= 400 && status < 500) {
+ try {
+ const res = await err.json();
+ if (res?.errors && Array.isArray(res.errors)) {
+ errorDetails = res.errors.map(e => `${e.path}: ${e.msg}`).join(', ');
+ } else if (res?.message) {
+ errorDetails = res.message;
+ }
+ } catch (parseError) {
+ errorDetails = "Invalid response format";
+ }
+ } else if (status >= 500) {
+ errorMessage = "Server error. Please try again later.";
}
- } else if (status >= 500) {
- // Server-side errors
- errorMessage = "Server error. Please try again later.";
+ } else {
+ // Generic error (network, etc.)
+ errorMessage = err?.message || "Network error. Please check your connection.";
}
setError(errorMessage);
- toast({
- title: "Error",
- description: errorDetails || errorMessage,
- variant: "destructive",
- });
+ showToast.error(errorDetails || errorMessage);
return null;
} finally {
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 134b62f..00326ec 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -221,3 +221,163 @@
--radius: 0.5rem;
}
}
+
+/* Custom styles for markdown editor */
+.w-md-editor {
+ background-color: transparent !important;
+ border: none !important;
+ height: 100% !important;
+}
+
+.w-md-editor-content {
+ background-color: transparent !important;
+ height: 100% !important;
+}
+
+.w-md-editor-text {
+ background-color: transparent !important;
+ color: white !important;
+ height: 100% !important;
+}
+
+.w-md-editor-text-input {
+ background-color: transparent !important;
+ color: white !important;
+ height: 100% !important;
+}
+
+.w-md-editor-text-pre > code,
+.w-md-editor-text-input {
+ color: white !important;
+ background-color: transparent !important;
+}
+
+.w-md-editor-toolbar {
+ background-color: rgba(255, 255, 255, 0.1) !important;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2) !important;
+ padding: 8px 12px !important;
+}
+
+.w-md-editor-toolbar-divider {
+ background-color: rgba(255, 255, 255, 0.2) !important;
+}
+
+.w-md-editor-toolbar li > button {
+ color: white !important;
+ padding: 4px 8px !important;
+ border-radius: 4px !important;
+}
+
+.w-md-editor-toolbar li > button:hover {
+ background-color: rgba(255, 255, 255, 0.1) !important;
+}
+
+.w-md-editor-toolbar li > button:active {
+ background-color: rgba(255, 255, 255, 0.2) !important;
+}
+
+/* Markdown preview styles */
+.w-md-editor-preview {
+ background-color: transparent !important;
+ color: white !important;
+ padding: 16px !important;
+}
+
+.w-md-editor-preview h1,
+.w-md-editor-preview h2,
+.w-md-editor-preview h3,
+.w-md-editor-preview h4,
+.w-md-editor-preview h5,
+.w-md-editor-preview h6 {
+ color: white !important;
+ margin-bottom: 12px !important;
+}
+
+.w-md-editor-preview p {
+ color: #e5e7eb !important;
+ margin-bottom: 12px !important;
+ line-height: 1.6 !important;
+}
+
+.w-md-editor-preview code {
+ background-color: rgba(255, 255, 255, 0.1) !important;
+ color: #fbbf24 !important;
+ padding: 2px 6px !important;
+ border-radius: 4px !important;
+ font-size: 0.875em !important;
+}
+
+.w-md-editor-preview pre {
+ background-color: rgba(0, 0, 0, 0.3) !important;
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
+ border-radius: 6px !important;
+ padding: 12px !important;
+ margin: 12px 0 !important;
+ overflow-x: auto !important;
+}
+
+.w-md-editor-preview blockquote {
+ border-left: 4px solid #3b82f6 !important;
+ background-color: rgba(59, 130, 246, 0.1) !important;
+ padding: 12px 16px !important;
+ margin: 12px 0 !important;
+ border-radius: 0 6px 6px 0 !important;
+}
+
+.w-md-editor-preview a {
+ color: #60a5fa !important;
+ text-decoration: underline !important;
+}
+
+.w-md-editor-preview a:hover {
+ color: #93c5fd !important;
+}
+
+.w-md-editor-preview ul,
+.w-md-editor-preview ol {
+ margin: 12px 0 !important;
+ padding-left: 24px !important;
+}
+
+.w-md-editor-preview li {
+ margin-bottom: 4px !important;
+ color: #e5e7eb !important;
+}
+
+/* Fullscreen mode */
+.w-md-editor-fullscreen {
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ right: 0 !important;
+ bottom: 0 !important;
+ z-index: 9999 !important;
+ background-color: rgba(0, 0, 0, 0.95) !important;
+}
+
+/* Form input focus styles */
+input:focus,
+textarea:focus {
+ outline: none !important;
+ ring: 2px !important;
+ ring-color: #3b82f6 !important;
+}
+
+/* Custom scrollbar for webkit browsers */
+::-webkit-scrollbar {
+ width: 8px;
+}
+
+::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.3);
+ border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: rgba(255, 255, 255, 0.5);
+}
diff --git a/frontend/src/pages/CreateEventPage.jsx b/frontend/src/pages/CreateEventPage.jsx
index 705e0ad..380bc8f 100644
--- a/frontend/src/pages/CreateEventPage.jsx
+++ b/frontend/src/pages/CreateEventPage.jsx
@@ -1,13 +1,11 @@
-import React, { useState } from "react";
+import React from "react";
import PageLayout from "../components/PageLayout";
-import { EventForm } from "../components/EventForm";
+import MultiPageEventForm from "../components/MultiPageEventForm";
const CreateEventPage = () => {
return (
-
-
-
-
+
+
);
};
diff --git a/frontend/src/pages/EventDetailsPage.jsx b/frontend/src/pages/EventDetailsPage.jsx
index 7408eb6..14cde84 100644
--- a/frontend/src/pages/EventDetailsPage.jsx
+++ b/frontend/src/pages/EventDetailsPage.jsx
@@ -1,29 +1,49 @@
import React from "react";
-import { useLocation } from "react-router-dom";
-import Tabs from "../components/Tabs";
-import ParticipantsDashboard from "../components/ParticipantsDashboard";
-import TeamDashboard from "../components/TeamDashboard";
+import { useLocation, useNavigate } from "react-router-dom";
import PageLayout from "../components/PageLayout";
-import { EventForm } from "../components/EventForm";
+import EventDetails from "../components/EventDetails";
const EventDetailsPage = () => {
const location = useLocation();
- const { event } = location.state;
+ const navigate = useNavigate();
+ const { event } = location.state || {};
- const tabs = ["Details", "Participants", "Teams", "Leaderboard"];
- const content = [
- ,
-
- ,
-
- ,
- {/* Submit Content */}
,
- ];
+ // Handle case when no event is passed
+ if (!event) {
+ return (
+
+
+
Event not found
+
The event you're looking for doesn't exist or has been removed.
+
navigate("/events/manage")}
+ className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-xl transition-colors duration-200"
+ >
+ Back to Events
+
+
+
+ );
+ }
return (
-
-
+
+ {/* Back Button */}
+
+
navigate("/events/manage")}
+ className="flex items-center gap-2 px-4 py-2 text-gray-400 hover:text-white transition-colors duration-200"
+ >
+
+
+
+ Back to Events
+
+
+
+ {/* Event Details */}
+
);
diff --git a/frontend/src/pages/ManageEventsPage.jsx b/frontend/src/pages/ManageEventsPage.jsx
index 2789399..91b910f 100644
--- a/frontend/src/pages/ManageEventsPage.jsx
+++ b/frontend/src/pages/ManageEventsPage.jsx
@@ -1,40 +1,218 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import PageLayout from "../components/PageLayout";
import EventsSection from "../components/EventSection";
+import SearchBar from "../components/SearchBar";
import { useApiRequest } from "@/hooks/useApiRequest";
import { getAllEvents } from "@/api/apiService";
+import {
+ getEventStatus,
+ searchEvents,
+ filterEventsByStatus
+} from "../utils/eventUtils";
export default function ManageEventsPage() {
const navigate = useNavigate();
const [events, setEvents] = useState([]);
+ const [searchTerm, setSearchTerm] = useState("");
+ const [selectedStatus, setSelectedStatus] = useState("All");
const { request, loading } = useApiRequest({ enableToast: false });
useEffect(() => {
async function fetch() {
const response = await request(getAllEvents, "");
- console.log(response);
- setEvents(response.events);
+ if (response && response.events) {
+ setEvents(response.events);
+ }
}
fetch();
- }, []);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []); // Only run once on mount
+
+ // Categorize events by status
+ const categorizedEvents = useMemo(() => {
+ const categorized = {
+ Live: [],
+ Upcoming: [],
+ Past: []
+ };
+
+ events.forEach(event => {
+ const status = getEventStatus(event.date);
+ categorized[status].push(event);
+ });
+
+ return categorized;
+ }, [events]);
+
+ // Filter and search events
+ const filteredEvents = useMemo(() => {
+ let filtered = events;
+
+ // Apply search
+ if (searchTerm) {
+ filtered = searchEvents(filtered, searchTerm);
+ }
+
+ // Apply status filter
+ if (selectedStatus !== "All") {
+ filtered = filterEventsByStatus(filtered, selectedStatus);
+ }
+
+ return filtered;
+ }, [events, searchTerm, selectedStatus]);
+
+ // Get filtered categorized events
+ const filteredCategorizedEvents = useMemo(() => {
+ const categorized = {
+ Live: [],
+ Upcoming: [],
+ Past: []
+ };
+
+ filteredEvents.forEach(event => {
+ const status = getEventStatus(event.date);
+ categorized[status].push(event);
+ });
+
+ return categorized;
+ }, [filteredEvents]);
+
+ const handleSearch = (term) => {
+ setSearchTerm(term);
+ };
+
+ const handleStatusFilter = (status) => {
+ setSelectedStatus(status);
+ };
+
+ const statusFilters = [
+ { label: "All Events", value: "All", count: events.length },
+ { label: "Live", value: "Live", count: categorizedEvents.Live.length },
+ { label: "Upcoming", value: "Upcoming", count: categorizedEvents.Upcoming.length },
+ { label: "Past", value: "Past", count: categorizedEvents.Past.length }
+ ];
return (
-
- event.isLive)}
- isLive={true}
- navigate={navigate}
- />
-
- !event.isLive)}
- isLive={false}
- navigate={navigate}
- />
+
+
+ {/* Header Section */}
+
+
+ Event Management
+
+
+ Discover, manage, and organize all your events in one place.
+ Find events by name, date, or status with our smart search.
+
+
+
+ {/* Search Bar */}
+
+
+ {/* Status Filter Tabs */}
+
+ {statusFilters.map((filter) => (
+ handleStatusFilter(filter.value)}
+ className={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
+ selectedStatus === filter.value
+ ? "bg-blue-600 text-white shadow-lg shadow-blue-500/25"
+ : "bg-gray-800/50 text-gray-300 hover:bg-gray-700/50 hover:text-white border border-gray-700/50"
+ }`}
+ >
+ {filter.label}
+
+ {filter.count}
+
+
+ ))}
+
+
+ {/* Loading State */}
+ {loading && (
+
+
+
+
+
+
+ Loading events...
+
+
+ )}
+
+ {/* Events Display */}
+ {!loading && (
+ <>
+ {/* Show filtered results when search or filter is active */}
+ {(searchTerm || selectedStatus !== "All") ? (
+ <>
+ {/* Search/Filter Results Summary */}
+
+
+ {searchTerm && selectedStatus !== "All" ? (
+ <>Found {filteredEvents.length} events matching "{searchTerm}" in {selectedStatus} events>
+ ) : searchTerm ? (
+ <>Found {filteredEvents.length} events matching "{searchTerm}">
+ ) : (
+ <>Showing {filteredEvents.length} {selectedStatus} events>
+ )}
+
+
+
+ {/* Single Filtered Results Section */}
+
+ >
+ ) : (
+ <>
+ {/* Show categorized sections when no filters are active */}
+ {/* Live Events */}
+
+
+ {/* Upcoming Events */}
+
+
+ {/* Past Events */}
+
+
+ {/* No Results */}
+ {filteredEvents.length === 0 && (
+
+
No events found
+
Try adjusting your search or filters
+
+ )}
+ >
+ )}
+ >
+ )}
+
);
}
diff --git a/frontend/src/utils/eventUtils.js b/frontend/src/utils/eventUtils.js
new file mode 100644
index 0000000..7defc8e
--- /dev/null
+++ b/frontend/src/utils/eventUtils.js
@@ -0,0 +1,98 @@
+// Event categorization and utility functions
+
+export const getEventStatus = (eventDate) => {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0); // Reset time to start of day
+
+ const eventDateObj = new Date(eventDate);
+ eventDateObj.setHours(0, 0, 0, 0); // Reset time to start of day
+
+ if (eventDateObj < today) {
+ return 'Past';
+ } else if (eventDateObj.getTime() === today.getTime()) {
+ return 'Live';
+ } else {
+ return 'Upcoming';
+ }
+};
+
+export const getStatusColor = (status) => {
+ switch (status) {
+ case 'Live':
+ return 'bg-red-500';
+ case 'Upcoming':
+ return 'bg-green-500';
+ case 'Past':
+ return 'bg-gray-500';
+ default:
+ return 'bg-blue-500';
+ }
+};
+
+export const getStatusTextColor = (status) => {
+ switch (status) {
+ case 'Live':
+ return 'text-red-500';
+ case 'Upcoming':
+ return 'text-green-500';
+ case 'Past':
+ return 'text-gray-400';
+ default:
+ return 'text-blue-500';
+ }
+};
+
+export const formatDate = (dateString) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('en-US', {
+ weekday: 'short',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric'
+ });
+};
+
+export const formatTime = (dateString) => {
+ const date = new Date(dateString);
+ return date.toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+};
+
+// Smart search function
+export const searchEvents = (events, searchTerm) => {
+ if (!searchTerm.trim()) return events;
+
+ const term = searchTerm.toLowerCase().trim();
+
+ return events.filter(event => {
+ // Check if it's a date search (YYYY-MM-DD or MM/DD/YYYY format)
+ const dateRegex = /^\d{4}-\d{2}-\d{2}$|^\d{1,2}\/\d{1,2}\/\d{4}$/;
+ if (dateRegex.test(term)) {
+ const eventDate = new Date(event.date).toISOString().split('T')[0];
+ const searchDate = term.includes('/')
+ ? new Date(term).toISOString().split('T')[0]
+ : term;
+ return eventDate === searchDate;
+ }
+
+ // Check if it's a status search
+ const statusTerms = ['past', 'live', 'upcoming'];
+ if (statusTerms.includes(term)) {
+ const eventStatus = getEventStatus(event.date);
+ return eventStatus.toLowerCase() === term;
+ }
+
+ // Default to name search
+ return event.name.toLowerCase().includes(term) ||
+ (event.description && event.description.toLowerCase().includes(term)) ||
+ (event.venue && event.venue.toLowerCase().includes(term));
+ });
+};
+
+// Filter events by status
+export const filterEventsByStatus = (events, status) => {
+ if (!status || status === 'All') return events;
+ return events.filter(event => getEventStatus(event.date) === status);
+};
diff --git a/frontend/src/utils/toast.js b/frontend/src/utils/toast.js
new file mode 100644
index 0000000..b1e2bbd
--- /dev/null
+++ b/frontend/src/utils/toast.js
@@ -0,0 +1,188 @@
+import toast from 'react-hot-toast';
+
+// Custom toast styles that match EMR theme
+const toastStyles = {
+ style: {
+ background: '#050816',
+ color: '#f3f3f3',
+ border: '1px solid #aaa6c3',
+ borderRadius: '12px',
+ padding: '16px 20px',
+ fontSize: '16px',
+ fontWeight: '500',
+ boxShadow: '0px 35px 120px -15px #211e35',
+ backdropFilter: 'blur(10px)',
+ },
+ success: {
+ style: {
+ background: 'linear-gradient(135deg, #050816 0%, #151030 100%)',
+ color: '#f3f3f3',
+ border: '1px solid #10b981',
+ borderRadius: '12px',
+ padding: '16px 20px',
+ fontSize: '16px',
+ fontWeight: '500',
+ boxShadow: '0px 35px 120px -15px #211e35',
+ backdropFilter: 'blur(10px)',
+ },
+ iconTheme: {
+ primary: '#10b981',
+ secondary: '#f3f3f3',
+ },
+ },
+ error: {
+ style: {
+ background: 'linear-gradient(135deg, #050816 0%, #151030 100%)',
+ color: '#f3f3f3',
+ border: '1px solid #ef4444',
+ borderRadius: '12px',
+ padding: '16px 20px',
+ fontSize: '16px',
+ fontWeight: '500',
+ boxShadow: '0px 35px 120px -15px #211e35',
+ backdropFilter: 'blur(10px)',
+ },
+ iconTheme: {
+ primary: '#ef4444',
+ secondary: '#f3f3f3',
+ },
+ },
+ loading: {
+ style: {
+ background: 'linear-gradient(135deg, #050816 0%, #151030 100%)',
+ color: '#f3f3f3',
+ border: '1px solid #3b82f6',
+ borderRadius: '12px',
+ padding: '16px 20px',
+ fontSize: '16px',
+ fontWeight: '500',
+ boxShadow: '0px 35px 120px -15px #211e35',
+ backdropFilter: 'blur(10px)',
+ },
+ iconTheme: {
+ primary: '#3b82f6',
+ secondary: '#f3f3f3',
+ },
+ },
+};
+
+// Large toast styles for important messages
+const largeToastStyles = {
+ style: {
+ background: '#050816',
+ color: '#f3f3f3',
+ border: '1px solid #aaa6c3',
+ borderRadius: '16px',
+ padding: '24px 28px',
+ fontSize: '18px',
+ fontWeight: '600',
+ boxShadow: '0px 35px 120px -15px #211e35',
+ backdropFilter: 'blur(10px)',
+ minWidth: '400px',
+ },
+ success: {
+ style: {
+ background: 'linear-gradient(135deg, #050816 0%, #151030 100%)',
+ color: '#f3f3f3',
+ border: '1px solid #10b981',
+ borderRadius: '16px',
+ padding: '24px 28px',
+ fontSize: '18px',
+ fontWeight: '600',
+ boxShadow: '0px 35px 120px -15px #211e35',
+ backdropFilter: 'blur(10px)',
+ minWidth: '400px',
+ },
+ iconTheme: {
+ primary: '#10b981',
+ secondary: '#f3f3f3',
+ },
+ },
+ error: {
+ style: {
+ background: 'linear-gradient(135deg, #050816 0%, #151030 100%)',
+ color: '#f3f3f3',
+ border: '1px solid #ef4444',
+ borderRadius: '16px',
+ padding: '24px 28px',
+ fontSize: '18px',
+ fontWeight: '600',
+ boxShadow: '0px 35px 120px -15px #211e35',
+ backdropFilter: 'blur(10px)',
+ minWidth: '400px',
+ },
+ iconTheme: {
+ primary: '#ef4444',
+ secondary: '#f3f3f3',
+ },
+ },
+};
+
+// Toast functions
+export const showToast = {
+ // Regular toasts
+ success: (message, options = {}) => {
+ return toast.success(message, {
+ duration: 4000,
+ position: 'bottom-right',
+ ...toastStyles.success,
+ ...options,
+ });
+ },
+
+ error: (message, options = {}) => {
+ return toast.error(message, {
+ duration: 5000,
+ position: 'bottom-right',
+ ...toastStyles.error,
+ ...options,
+ });
+ },
+
+ loading: (message, options = {}) => {
+ return toast.loading(message, {
+ position: 'bottom-right',
+ ...toastStyles.loading,
+ ...options,
+ });
+ },
+
+ // Large toasts for important messages
+ largeSuccess: (message, options = {}) => {
+ return toast.success(message, {
+ duration: 6000,
+ position: 'bottom-right',
+ ...largeToastStyles.success,
+ ...options,
+ });
+ },
+
+ largeError: (message, options = {}) => {
+ return toast.error(message, {
+ duration: 7000,
+ position: 'bottom-right',
+ ...largeToastStyles.error,
+ ...options,
+ });
+ },
+
+ // Custom toast
+ custom: (message, options = {}) => {
+ return toast(message, {
+ duration: 4000,
+ position: 'bottom-right',
+ ...toastStyles.style,
+ ...options,
+ });
+ },
+};
+
+// Dismiss toast
+export const dismissToast = (toastId) => {
+ toast.dismiss(toastId);
+};
+
+// Dismiss all toasts
+export const dismissAllToasts = () => {
+ toast.dismiss();
+};