ieeeucsd-org/src/components/dashboard/EventsSection/EventLoad.tsx
2025-04-28 13:33:28 -07:00

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;