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; 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(null); const [refreshing, setRefreshing] = useState(false); const [expandedDescriptions, setExpandedDescriptions] = useState>(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 = () => (
); 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( 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 = ''; 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 (
{/* Event Header */}

{event.event_name}

{event.points_to_reward} pts
{/* Event Description */}

{description}

{description.length > 80 && ( )}
{/* Event Details */}
{startDate.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric", })}
{startDate.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", })}
{event.location || "No location specified"}
{event.event_type || "Other"}
{/* Action Buttons */}
{event.files && event.files.length > 0 && ( )} {isPastEvent && (
{hasAttended ? 'Attended' : 'Not Attended'}
)}
); } 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( 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( 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 */}

Ongoing Events

{[...Array(3)].map((_, i) => (
{createSkeletonCard()}
))}
{/* Upcoming Events */}

Upcoming Events

{[...Array(3)].map((_, i) => (
{createSkeletonCard()}
))}
{/* Past Events */}

Past Events

{[...Array(3)].map((_, i) => (
{createSkeletonCard()}
))}
); } // 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 && (

No Events Found

There are currently no events to display. This could be due to:

  • No events have been published yet
  • There might be a connection issue with the event database
  • The events data might be temporarily unavailable
)} {/* Ongoing Events */} {events.ongoing.length > 0 && (

Ongoing Events

{events.ongoing.map(renderEventCard)}
)} {/* Upcoming Events */} {events.upcoming.length > 0 && (

Upcoming Events

{events.upcoming.map(renderEventCard)}
)} {/* Past Events */} {events.past.length > 0 && (

Past Events

{events.past.map(renderEventCard)}
)} ); }; export default EventLoad;