675 lines
32 KiB
TypeScript
675 lines
32 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Icon } from "@iconify/react";
|
|
import { Get } from "../../../scripts/pocketbase/Get";
|
|
import { Authentication } from "../../../scripts/pocketbase/Authentication";
|
|
import { DataSyncService } from "../../../scripts/database/DataSyncService";
|
|
import { DexieService } from "../../../scripts/database/DexieService";
|
|
import { Collections } from "../../../schemas/pocketbase/schema";
|
|
import type { Event, AttendeeEntry, EventAttendee } from "../../../schemas/pocketbase";
|
|
|
|
// Extended Event interface with additional properties needed for this component
|
|
interface ExtendedEvent extends Event {
|
|
description?: string; // This component uses 'description' but schema has 'event_description'
|
|
event_type: string; // Add event_type field from schema
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
openDetailsModal: (event: ExtendedEvent) => void;
|
|
downloadAllFiles: () => Promise<void>;
|
|
currentEventId: string;
|
|
[key: string]: any;
|
|
}
|
|
}
|
|
// Helper function to validate event data integrity
|
|
const isValidEvent = (event: any): boolean => {
|
|
if (!event || typeof event !== 'object') return false;
|
|
|
|
// Check required fields
|
|
if (!event.id || !event.event_name) return false;
|
|
|
|
// Validate date fields
|
|
try {
|
|
const startDate = new Date(event.start_date);
|
|
const endDate = new Date(event.end_date);
|
|
|
|
// Check if dates are valid
|
|
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
|
|
console.warn(`Event ${event.id} has invalid date format`, {
|
|
start: event.start_date,
|
|
end: event.end_date
|
|
});
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.warn(`Error validating event ${event?.id || 'unknown'}:`, error);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const EventLoad = () => {
|
|
const [events, setEvents] = useState<{
|
|
upcoming: Event[];
|
|
ongoing: Event[];
|
|
past: Event[];
|
|
}>({
|
|
upcoming: [],
|
|
ongoing: [],
|
|
past: [],
|
|
});
|
|
const [loading, setLoading] = useState(true);
|
|
const [syncStatus, setSyncStatus] = useState<'idle' | 'syncing' | 'success' | 'error'>('idle');
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [expandedDescriptions, setExpandedDescriptions] = useState<Set<string>>(new Set());
|
|
|
|
const toggleDescription = (eventId: string) => {
|
|
setExpandedDescriptions(prev => {
|
|
const newSet = new Set(prev);
|
|
if (newSet.has(eventId)) {
|
|
newSet.delete(eventId);
|
|
} else {
|
|
newSet.add(eventId);
|
|
}
|
|
return newSet;
|
|
});
|
|
};
|
|
|
|
// Function to clear the events cache and force a fresh sync
|
|
const refreshEvents = async () => {
|
|
try {
|
|
setRefreshing(true);
|
|
|
|
// Get DexieService instance
|
|
const dexieService = DexieService.getInstance();
|
|
const db = dexieService.getDB();
|
|
|
|
// Clear events table
|
|
if (db && db.events) {
|
|
// console.log("Clearing events cache...");
|
|
await db.events.clear();
|
|
// console.log("Events cache cleared successfully");
|
|
}
|
|
|
|
// Reset sync timestamp for events by updating it to 0
|
|
// First get the current record
|
|
const currentInfo = await dexieService.getLastSync(Collections.EVENTS);
|
|
// Then update it with a timestamp of 0 (forcing a fresh sync)
|
|
await dexieService.updateLastSync(Collections.EVENTS);
|
|
// console.log("Events sync timestamp reset");
|
|
|
|
// Reload events
|
|
setLoading(true);
|
|
await loadEvents();
|
|
|
|
} catch (error) {
|
|
console.error("Error refreshing events:", error);
|
|
setErrorMessage("Failed to refresh events. Please try again.");
|
|
} finally {
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
loadEvents();
|
|
}, []);
|
|
|
|
const createSkeletonCard = () => (
|
|
<div className="card bg-base-200 shadow-lg animate-pulse">
|
|
<div className="card-body p-5">
|
|
<div className="flex flex-col h-full">
|
|
<div className="flex items-start justify-between gap-3 mb-2">
|
|
<div className="flex-1">
|
|
<div className="skeleton h-6 w-3/4 mb-2"></div>
|
|
<div className="flex items-center gap-2">
|
|
<div className="skeleton h-5 w-16"></div>
|
|
<div className="skeleton h-5 w-20"></div>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col items-end">
|
|
<div className="skeleton h-5 w-24 mb-1"></div>
|
|
<div className="skeleton h-4 w-16"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="skeleton h-4 w-full mb-3"></div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<div className="skeleton h-4 w-4"></div>
|
|
<div className="skeleton h-4 w-1/2"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderEventCard = (event: Event) => {
|
|
try {
|
|
// Get authentication instance
|
|
const auth = Authentication.getInstance();
|
|
const currentUser = auth.getCurrentUser();
|
|
|
|
// Check if user has attended this event by querying the event_attendees collection
|
|
let hasAttended = false;
|
|
if (currentUser) {
|
|
// We'll check attendance status when displaying the card
|
|
// This will be done asynchronously after rendering
|
|
setTimeout(async () => {
|
|
try {
|
|
const get = Get.getInstance();
|
|
const attendees = await get.getList<EventAttendee>(
|
|
Collections.EVENT_ATTENDEES,
|
|
1,
|
|
1,
|
|
`user="${currentUser.id}" && event="${event.id}"`
|
|
);
|
|
|
|
const hasAttendedEvent = attendees.totalItems > 0;
|
|
|
|
// Store the attendance status in the window object with the event
|
|
const eventDataId = `event_${event.id}`;
|
|
if (window[eventDataId]) {
|
|
window[eventDataId].hasAttended = hasAttendedEvent;
|
|
}
|
|
|
|
// Update the card UI based on attendance status
|
|
const cardElement = document.getElementById(`event-card-${event.id}`);
|
|
if (cardElement) {
|
|
const attendedBadge = document.getElementById(`attendance-badge-${event.id}`);
|
|
if (attendedBadge && hasAttendedEvent) {
|
|
attendedBadge.classList.remove('badge-ghost');
|
|
attendedBadge.classList.add('badge-success');
|
|
|
|
// Update the icon and text
|
|
const icon = attendedBadge.querySelector('svg');
|
|
if (icon) {
|
|
icon.setAttribute('icon', 'heroicons:check-circle');
|
|
}
|
|
|
|
// Update the text content
|
|
attendedBadge.textContent = '';
|
|
|
|
// Recreate the icon
|
|
const iconElement = document.createElement('span');
|
|
iconElement.className = 'h-3 w-3';
|
|
iconElement.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10zm-.997-6l7.07-7.071l-1.414-1.414l-5.656 5.657l-2.829-2.829l-1.414 1.414L11.003 16z"/></svg>';
|
|
attendedBadge.appendChild(iconElement);
|
|
|
|
// Add the text
|
|
const textNode = document.createTextNode(' Attended');
|
|
attendedBadge.appendChild(textNode);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error checking attendance status:", error);
|
|
}
|
|
}, 0);
|
|
}
|
|
|
|
// Store event data in window object with unique ID
|
|
const eventDataId = `event_${event.id}`;
|
|
window[eventDataId] = event;
|
|
|
|
const startDate = new Date(event.start_date);
|
|
const endDate = new Date(event.end_date);
|
|
const now = new Date();
|
|
const isPastEvent = endDate < now;
|
|
const isExpanded = expandedDescriptions.has(event.id);
|
|
const description = event.event_description || "No description available";
|
|
|
|
return (
|
|
<div id={`event-card-${event.id}`} key={event.id} className="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-300 relative overflow-hidden">
|
|
<div className="card-body p-4">
|
|
{/* Event Header */}
|
|
<div className="flex justify-between items-start mb-2">
|
|
<h3 className="card-title text-base sm:text-lg font-semibold line-clamp-2">{event.event_name}</h3>
|
|
<div className="badge badge-primary badge-sm shrink-0">{event.points_to_reward} pts</div>
|
|
</div>
|
|
|
|
{/* Event Description */}
|
|
<div className="mb-3">
|
|
<p className={`text-xs sm:text-sm text-base-content/70 ${isExpanded ? '' : 'line-clamp-2'}`}>
|
|
{description}
|
|
</p>
|
|
{description.length > 80 && (
|
|
<button
|
|
onClick={() => toggleDescription(event.id)}
|
|
className="text-xs text-primary hover:text-primary-focus mt-1 flex items-center"
|
|
>
|
|
{isExpanded ? (
|
|
<>
|
|
<Icon icon="heroicons:chevron-up" className="h-3 w-3 mr-1" />
|
|
Show less
|
|
</>
|
|
) : (
|
|
<>
|
|
<Icon icon="heroicons:chevron-down" className="h-3 w-3 mr-1" />
|
|
Show more
|
|
</>
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Event Details */}
|
|
<div className="grid grid-cols-1 gap-1 mb-3 text-xs sm:text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<Icon icon="heroicons:calendar" className="h-3.5 w-3.5 text-primary" />
|
|
<span>
|
|
{startDate.toLocaleDateString("en-US", {
|
|
weekday: "short",
|
|
month: "short",
|
|
day: "numeric",
|
|
})}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Icon icon="heroicons:clock" className="h-3.5 w-3.5 text-primary" />
|
|
<span>
|
|
{startDate.toLocaleTimeString("en-US", {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
})}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Icon icon="heroicons:map-pin" className="h-3.5 w-3.5 text-primary" />
|
|
<span className="line-clamp-1">{event.location || "No location specified"}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Icon icon="heroicons:tag" className="h-3.5 w-3.5 text-primary" />
|
|
<span className="line-clamp-1 capitalize">{event.event_type || "Other"}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex flex-wrap items-center gap-2 mt-auto">
|
|
{event.files && event.files.length > 0 && (
|
|
<button
|
|
onClick={() => window.openDetailsModal(event as ExtendedEvent)}
|
|
className="btn btn-accent btn-sm text-xs sm:text-sm gap-1 h-8 min-h-0 px-3 shadow-md hover:shadow-lg transition-all duration-300 rounded-full"
|
|
>
|
|
<Icon icon="heroicons:document-duplicate" className="h-3 w-3 sm:h-4 sm:w-4" />
|
|
Files ({event.files.length})
|
|
</button>
|
|
)}
|
|
{isPastEvent && (
|
|
<div id={`attendance-badge-${event.id}`} className={`badge ${hasAttended ? 'badge-success' : 'badge-ghost'} text-xs gap-1 ml-auto`}>
|
|
<Icon
|
|
icon={hasAttended ? "heroicons:check-circle" : "heroicons:x-circle"}
|
|
className="h-3 w-3"
|
|
/>
|
|
{hasAttended ? 'Attended' : 'Not Attended'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} catch (error) {
|
|
console.error("Error rendering event card:", error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const loadEvents = async () => {
|
|
try {
|
|
const get = Get.getInstance();
|
|
const dataSync = DataSyncService.getInstance();
|
|
const auth = Authentication.getInstance();
|
|
|
|
// console.log("Starting to load events...");
|
|
|
|
// Check if user is authenticated
|
|
if (!auth.isAuthenticated()) {
|
|
// Silently return without error when on dashboard page
|
|
if (window.location.pathname.includes('/dashboard')) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
console.error("User not authenticated, cannot load events");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
// Force sync to ensure we have the latest data
|
|
// console.log("Syncing events collection...");
|
|
let syncSuccess = false;
|
|
let retryCount = 0;
|
|
const maxRetries = 3;
|
|
|
|
while (!syncSuccess && retryCount < maxRetries) {
|
|
try {
|
|
if (retryCount > 0) {
|
|
// console.log(`Retry attempt ${retryCount} of ${maxRetries}...`);
|
|
// Add a small delay between retries
|
|
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
|
|
}
|
|
|
|
await dataSync.syncCollection(Collections.EVENTS, "published = true", "-start_date");
|
|
// console.log("Events collection synced successfully");
|
|
syncSuccess = true;
|
|
} catch (syncError) {
|
|
retryCount++;
|
|
console.error(`Error syncing events collection (attempt ${retryCount}/${maxRetries}):`, syncError);
|
|
|
|
if (retryCount >= maxRetries) {
|
|
console.warn("Max retry attempts reached, continuing with local data");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get events from IndexedDB
|
|
// console.log("Fetching events from IndexedDB...");
|
|
const allEvents = await dataSync.getData<Event>(
|
|
Collections.EVENTS,
|
|
false, // Don't force sync again
|
|
"published = true",
|
|
"-start_date"
|
|
);
|
|
|
|
// console.log(`Retrieved ${allEvents.length} events from IndexedDB`);
|
|
|
|
// Filter out invalid events
|
|
const validEvents = allEvents.filter(event => isValidEvent(event));
|
|
// console.log(`Filtered out ${allEvents.length - validEvents.length} invalid events`);
|
|
|
|
// If no valid events found in IndexedDB, try fetching directly from PocketBase as fallback
|
|
let eventsToProcess = validEvents;
|
|
if (allEvents.length === 0) {
|
|
// console.log("No events found in IndexedDB, trying direct PocketBase fetch...");
|
|
try {
|
|
const pbEvents = await get.getAll<Event>(
|
|
Collections.EVENTS,
|
|
"published = true",
|
|
"-start_date"
|
|
);
|
|
// console.log(`Retrieved ${pbEvents.length} events directly from PocketBase`);
|
|
|
|
// Filter out invalid events from PocketBase results
|
|
const validPbEvents = pbEvents.filter(event => isValidEvent(event));
|
|
// console.log(`Filtered out ${pbEvents.length - validPbEvents.length} invalid events from PocketBase`);
|
|
|
|
eventsToProcess = validPbEvents;
|
|
|
|
// Store these events in IndexedDB for future use
|
|
if (validPbEvents.length > 0) {
|
|
const dexieService = DexieService.getInstance();
|
|
const db = dexieService.getDB();
|
|
if (db && db.events) {
|
|
// console.log(`Storing ${validPbEvents.length} valid PocketBase events in IndexedDB...`);
|
|
await db.events.bulkPut(validPbEvents);
|
|
}
|
|
}
|
|
} catch (pbError) {
|
|
console.error("Error fetching events from PocketBase:", pbError);
|
|
}
|
|
}
|
|
|
|
// Split events into upcoming, ongoing, and past based on start and end dates
|
|
// console.log("Categorizing events...");
|
|
const now = new Date();
|
|
const { upcoming, ongoing, past } = eventsToProcess.reduce(
|
|
(acc, event) => {
|
|
try {
|
|
// Convert UTC dates to local time
|
|
const startDate = new Date(event.start_date);
|
|
const endDate = new Date(event.end_date);
|
|
|
|
// Set both dates and now to midnight for date-only comparison
|
|
const startLocal = new Date(
|
|
startDate.getFullYear(),
|
|
startDate.getMonth(),
|
|
startDate.getDate(),
|
|
startDate.getHours(),
|
|
startDate.getMinutes()
|
|
);
|
|
const endLocal = new Date(
|
|
endDate.getFullYear(),
|
|
endDate.getMonth(),
|
|
endDate.getDate(),
|
|
endDate.getHours(),
|
|
endDate.getMinutes()
|
|
);
|
|
const nowLocal = new Date(
|
|
now.getFullYear(),
|
|
now.getMonth(),
|
|
now.getDate(),
|
|
now.getHours(),
|
|
now.getMinutes()
|
|
);
|
|
|
|
if (startLocal > nowLocal) {
|
|
acc.upcoming.push(event);
|
|
} else if (endLocal < nowLocal) {
|
|
acc.past.push(event);
|
|
} else {
|
|
acc.ongoing.push(event);
|
|
}
|
|
} catch (dateError) {
|
|
console.error("Error processing event dates:", dateError, event);
|
|
// If we can't process dates properly, put in past events as fallback
|
|
acc.past.push(event);
|
|
}
|
|
return acc;
|
|
},
|
|
{
|
|
upcoming: [] as Event[],
|
|
ongoing: [] as Event[],
|
|
past: [] as Event[],
|
|
}
|
|
);
|
|
|
|
// console.log(`Categorized events: ${upcoming.length} upcoming, ${ongoing.length} ongoing, ${past.length} past`);
|
|
|
|
// Sort events
|
|
upcoming.sort((a, b) => new Date(a.start_date).getTime() - new Date(b.start_date).getTime());
|
|
ongoing.sort((a, b) => new Date(a.end_date).getTime() - new Date(b.end_date).getTime());
|
|
past.sort((a, b) => new Date(b.end_date).getTime() - new Date(a.end_date).getTime());
|
|
|
|
setEvents({
|
|
upcoming: upcoming.slice(0, 50), // Limit to 50 events per section
|
|
ongoing: ongoing.slice(0, 50),
|
|
past: past.slice(0, 50)
|
|
});
|
|
setLoading(false);
|
|
} catch (error) {
|
|
console.error("Failed to load events:", error);
|
|
|
|
// Attempt to diagnose the error
|
|
if (error instanceof Error) {
|
|
console.error(`Error type: ${error.name}, Message: ${error.message}`);
|
|
console.error(`Stack trace: ${error.stack}`);
|
|
|
|
// Check for network-related errors
|
|
if (error.message.includes('network') || error.message.includes('fetch') || error.message.includes('connection')) {
|
|
console.error("Network-related error detected");
|
|
|
|
// Try to load from IndexedDB only as a last resort
|
|
try {
|
|
// console.log("Attempting to load events from IndexedDB only...");
|
|
const dexieService = DexieService.getInstance();
|
|
const db = dexieService.getDB();
|
|
if (db && db.events) {
|
|
const allCachedEvents = await db.events.filter(event => event.published === true).toArray();
|
|
// console.log(`Found ${allCachedEvents.length} cached events in IndexedDB`);
|
|
|
|
// Filter out invalid events
|
|
const cachedEvents = allCachedEvents.filter(event => isValidEvent(event));
|
|
// console.log(`Filtered out ${allCachedEvents.length - cachedEvents.length} invalid cached events`);
|
|
|
|
if (cachedEvents.length > 0) {
|
|
// Process these events
|
|
const now = new Date();
|
|
const { upcoming, ongoing, past } = cachedEvents.reduce(
|
|
(acc, event) => {
|
|
try {
|
|
const startDate = new Date(event.start_date);
|
|
const endDate = new Date(event.end_date);
|
|
|
|
if (startDate > now) {
|
|
acc.upcoming.push(event);
|
|
} else if (endDate < now) {
|
|
acc.past.push(event);
|
|
} else {
|
|
acc.ongoing.push(event);
|
|
}
|
|
} catch (e) {
|
|
acc.past.push(event);
|
|
}
|
|
return acc;
|
|
},
|
|
{
|
|
upcoming: [] as Event[],
|
|
ongoing: [] as Event[],
|
|
past: [] as Event[],
|
|
}
|
|
);
|
|
|
|
// Sort and set events
|
|
upcoming.sort((a, b) => new Date(a.start_date).getTime() - new Date(b.start_date).getTime());
|
|
ongoing.sort((a, b) => new Date(a.end_date).getTime() - new Date(b.end_date).getTime());
|
|
past.sort((a, b) => new Date(b.end_date).getTime() - new Date(a.end_date).getTime());
|
|
|
|
setEvents({
|
|
upcoming: upcoming.slice(0, 50),
|
|
ongoing: ongoing.slice(0, 50),
|
|
past: past.slice(0, 50)
|
|
});
|
|
// console.log("Successfully loaded events from cache");
|
|
}
|
|
}
|
|
} catch (cacheError) {
|
|
console.error("Failed to load events from cache:", cacheError);
|
|
}
|
|
}
|
|
}
|
|
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<>
|
|
{/* Ongoing Events */}
|
|
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
|
<div className="card-body p-4 sm:p-6">
|
|
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Ongoing Events</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
{[...Array(3)].map((_, i) => (
|
|
<div key={`ongoing-skeleton-${i}`}>{createSkeletonCard()}</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upcoming Events */}
|
|
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
|
<div className="card-body p-4 sm:p-6">
|
|
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Upcoming Events</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
{[...Array(3)].map((_, i) => (
|
|
<div key={`upcoming-skeleton-${i}`}>{createSkeletonCard()}</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Past Events */}
|
|
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mx-4 sm:mx-6">
|
|
<div className="card-body p-4 sm:p-6">
|
|
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Past Events</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
{[...Array(3)].map((_, i) => (
|
|
<div key={`past-skeleton-${i}`}>{createSkeletonCard()}</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// Check if there are no events at all
|
|
const noEvents = events.ongoing.length === 0 && events.upcoming.length === 0 && events.past.length === 0;
|
|
|
|
return (
|
|
<>
|
|
{/* No Events Message */}
|
|
{noEvents && (
|
|
<div className="card bg-base-100 shadow-xl border border-base-200 mx-4 sm:mx-6 p-8">
|
|
<div className="text-center">
|
|
<Icon icon="heroicons:calendar" className="w-16 h-16 mx-auto text-base-content/30 mb-4" />
|
|
<h3 className="text-xl font-bold mb-2">No Events Found</h3>
|
|
<p className="text-base-content/70 mb-4">
|
|
There are currently no events to display. This could be due to:
|
|
</p>
|
|
<ul className="list-disc text-left max-w-md mx-auto text-base-content/70 mb-6">
|
|
<li className="mb-1">No events have been published yet</li>
|
|
<li className="mb-1">There might be a connection issue with the event database</li>
|
|
<li className="mb-1">The events data might be temporarily unavailable</li>
|
|
</ul>
|
|
<button
|
|
onClick={refreshEvents}
|
|
className="btn btn-primary"
|
|
disabled={refreshing}
|
|
>
|
|
{refreshing ? (
|
|
<>
|
|
<Icon icon="heroicons:arrow-path" className="w-5 h-5 mr-2 animate-spin" />
|
|
Refreshing...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Icon icon="heroicons:arrow-path" className="w-5 h-5 mr-2" />
|
|
Refresh Events
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Ongoing Events */}
|
|
{events.ongoing.length > 0 && (
|
|
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
|
<div className="card-body p-4 sm:p-6">
|
|
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Ongoing Events</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
{events.ongoing.map(renderEventCard)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Upcoming Events */}
|
|
{events.upcoming.length > 0 && (
|
|
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-4 sm:mb-6 mx-4 sm:mx-6">
|
|
<div className="card-body p-4 sm:p-6">
|
|
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Upcoming Events</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
{events.upcoming.map(renderEventCard)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Past Events */}
|
|
{events.past.length > 0 && (
|
|
<div className="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mx-4 sm:mx-6">
|
|
<div className="card-body p-4 sm:p-6">
|
|
<h3 className="card-title text-base sm:text-lg mb-3 sm:mb-4">Past Events</h3>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6">
|
|
{events.past.map(renderEventCard)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default EventLoad;
|