move some code to its own component
This commit is contained in:
parent
e51dd3d6f8
commit
c5a5166e6a
2 changed files with 326 additions and 418 deletions
|
@ -1,8 +1,9 @@
|
||||||
---
|
---
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "@iconify/react";
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import FilePreview from "./universal/FilePreview";
|
import FilePreview from "./universal/FilePreview";
|
||||||
import EventCheckIn from "./EventsSection/EventCheckIn";
|
import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
|
import EventLoad from "./EventsSection/EventLoad";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="eventsSection" class="dashboard-section hidden">
|
<div id="eventsSection" class="dashboard-section hidden">
|
||||||
|
@ -49,50 +50,7 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ongoing Events -->
|
<EventLoad client:load />
|
||||||
<div
|
|
||||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
|
||||||
>
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title mb-4">Ongoing Events</h3>
|
|
||||||
<div
|
|
||||||
id="ongoingEventsContainer"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
|
||||||
>
|
|
||||||
<!-- Ongoing events will be dynamically inserted here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Upcoming Events -->
|
|
||||||
<div
|
|
||||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"
|
|
||||||
>
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title mb-4">Upcoming Events</h3>
|
|
||||||
<div
|
|
||||||
id="upcomingEventsContainer"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
|
||||||
>
|
|
||||||
<!-- Upcoming events will be dynamically inserted here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Past Events -->
|
|
||||||
<div
|
|
||||||
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
|
|
||||||
>
|
|
||||||
<div class="card-body">
|
|
||||||
<h3 class="card-title mb-4">Past Events</h3>
|
|
||||||
<div
|
|
||||||
id="pastEventsContainer"
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
|
||||||
>
|
|
||||||
<!-- Past events will be dynamically inserted here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Event Details Modal -->
|
<!-- Event Details Modal -->
|
||||||
|
@ -106,17 +64,10 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
class="btn btn-primary btn-sm gap-1"
|
class="btn btn-primary btn-sm gap-1"
|
||||||
onclick="window.downloadAllFiles()"
|
onclick="window.downloadAllFiles()"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
icon="heroicons:arrow-down-tray-20-solid"
|
||||||
class="h-4 w-4"
|
className="h-4 w-4"
|
||||||
viewBox="0 0 20 20"
|
/>
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
|
|
||||||
clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
Download All
|
Download All
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,19 +75,7 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
class="btn btn-circle btn-ghost"
|
class="btn btn-circle btn-ghost"
|
||||||
onclick="window.closeEventDetailsModal()"
|
onclick="window.closeEventDetailsModal()"
|
||||||
>
|
>
|
||||||
<svg
|
<Icon icon="heroicons:x-mark" className="h-6 w-6" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="h-6 w-6"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M6 18L18 6M6 6l12 12"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -180,10 +119,6 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Get } from "../../scripts/pocketbase/Get";
|
|
||||||
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
|
||||||
import { Update } from "../../scripts/pocketbase/Update";
|
|
||||||
import { SendLog } from "../../scripts/pocketbase/SendLog";
|
|
||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
|
|
||||||
// Toast management system
|
// Toast management system
|
||||||
|
@ -234,10 +169,10 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
${
|
${
|
||||||
type === "success"
|
type === "success"
|
||||||
? '<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>'
|
? '<Icon icon="heroicons:check-circle" class="stroke-current shrink-0 h-6 w-6"></Icon>'
|
||||||
: type === "error"
|
: type === "error"
|
||||||
? '<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>'
|
? '<Icon icon="heroicons:x-circle" class="stroke-current shrink-0 h-6 w-6"></Icon>'
|
||||||
: '<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>'
|
: '<Icon icon="heroicons:exclamation-triangle" class="stroke-current shrink-0 h-6 w-6"></Icon>'
|
||||||
}
|
}
|
||||||
<span>${message}</span>
|
<span>${message}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -295,28 +230,6 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
interface Event {
|
|
||||||
id: string;
|
|
||||||
event_name: string;
|
|
||||||
event_code: string;
|
|
||||||
location: string;
|
|
||||||
points_to_reward: number;
|
|
||||||
attendees: AttendeeEntry[];
|
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
|
||||||
has_food: boolean;
|
|
||||||
description: string;
|
|
||||||
files: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AttendeeEntry {
|
|
||||||
user_id: string;
|
|
||||||
time_checked_in: string;
|
|
||||||
food: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentEventId = "";
|
|
||||||
|
|
||||||
// Add helper functions for file preview
|
// Add helper functions for file preview
|
||||||
function getFileType(filename: string): string {
|
function getFileType(filename: string): string {
|
||||||
const extension = filename.split(".").pop()?.toLowerCase();
|
const extension = filename.split(".").pop()?.toLowerCase();
|
||||||
|
@ -339,40 +252,6 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
return mimeTypes[extension || ""] || "application/octet-stream";
|
return mimeTypes[extension || ""] || "application/octet-stream";
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLoading() {
|
|
||||||
const spinner = document.getElementById("loadingSpinner");
|
|
||||||
if (spinner) spinner.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideLoading() {
|
|
||||||
const spinner = document.getElementById("loadingSpinner");
|
|
||||||
if (spinner) spinner.classList.add("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
function showFilePreview(file: {
|
|
||||||
url: string;
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
}) {
|
|
||||||
console.log("showFilePreview called with:", file);
|
|
||||||
window.previewFileEvents(file.url, file.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePreviewError() {
|
|
||||||
hideLoading();
|
|
||||||
const previewContent = document.getElementById("previewContent");
|
|
||||||
if (previewContent) {
|
|
||||||
previewContent.innerHTML = `
|
|
||||||
<div class="alert alert-error">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
<span>Failed to load file preview</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Universal file preview function for events section
|
// Universal file preview function for events section
|
||||||
window.previewFileEvents = function (url: string, filename: string) {
|
window.previewFileEvents = function (url: string, filename: string) {
|
||||||
console.log("previewFileEvents called with:", { url, filename });
|
console.log("previewFileEvents called with:", { url, filename });
|
||||||
|
@ -444,7 +323,7 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
) as HTMLDivElement;
|
) as HTMLDivElement;
|
||||||
|
|
||||||
// Reset state
|
// Reset state
|
||||||
currentEventId = event.id;
|
window.currentEventId = event.id;
|
||||||
if (filesContent) filesContent.classList.remove("hidden");
|
if (filesContent) filesContent.classList.remove("hidden");
|
||||||
|
|
||||||
// Populate files content
|
// Populate files content
|
||||||
|
@ -480,15 +359,10 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
<td>${file}</td>
|
<td>${file}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<button class="btn btn-ghost btn-sm" onclick='window.showFilePreviewEvents(${previewData})'>
|
<button class="btn btn-ghost btn-sm" onclick='window.showFilePreviewEvents(${previewData})'>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<Icon icon="heroicons:document" className="h-4 w-4" />
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
<a href="${fileUrl}" download="${file}" class="btn btn-ghost btn-sm">
|
<a href="${fileUrl}" download="${file}" class="btn btn-ghost btn-sm">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<Icon icon="heroicons:arrow-down-tray-20-solid" className="h-4 w-4" />
|
||||||
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -502,9 +376,7 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
} else {
|
} else {
|
||||||
filesContent.innerHTML = `
|
filesContent.innerHTML = `
|
||||||
<div class="text-center py-8 text-base-content/70">
|
<div class="text-center py-8 text-base-content/70">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4 opacity-50" viewBox="0 0 20 20" fill="currentColor">
|
<Icon icon="heroicons:document-duplicate" className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>
|
|
||||||
</svg>
|
|
||||||
<p>No files attached to this event</p>
|
<p>No files attached to this event</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -532,11 +404,11 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
// Get current event files
|
// Get current event files
|
||||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
||||||
const collectionId = "events";
|
const collectionId = "events";
|
||||||
const recordId = currentEventId;
|
const recordId = window.currentEventId;
|
||||||
|
|
||||||
// Get the current event from the window object
|
// Get the current event from the window object
|
||||||
const eventDataId = `event_${currentEventId}`;
|
const eventDataId = `event_${window.currentEventId}`;
|
||||||
const event = window[eventDataId] as Event;
|
const event = window[eventDataId];
|
||||||
|
|
||||||
if (!event || !event.files || event.files.length === 0) {
|
if (!event || !event.files || event.files.length === 0) {
|
||||||
throw new Error("No files available to download");
|
throw new Error("No files available to download");
|
||||||
|
@ -595,7 +467,7 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset any other state if needed
|
// Reset any other state if needed
|
||||||
currentEventId = "";
|
window.currentEventId = "";
|
||||||
|
|
||||||
// Close the modal
|
// Close the modal
|
||||||
modal.close();
|
modal.close();
|
||||||
|
@ -603,276 +475,16 @@ import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make helper functions available globally
|
// Make helper functions available globally
|
||||||
window.showFilePreview = showFilePreview;
|
window.showFilePreview = window.showFilePreviewEvents;
|
||||||
window.handlePreviewError = handlePreviewError;
|
window.handlePreviewError = function () {
|
||||||
window.showLoading = showLoading;
|
const previewContent = document.getElementById("previewContent");
|
||||||
window.hideLoading = hideLoading;
|
if (previewContent) {
|
||||||
|
previewContent.innerHTML = `
|
||||||
// Add TypeScript interface for Window
|
<div class="alert alert-error">
|
||||||
declare global {
|
<Icon icon="heroicons:x-circle" className="h-6 w-6" />
|
||||||
interface Window {
|
<span>Failed to load file preview</span>
|
||||||
downloadAllFiles: () => Promise<void>;
|
</div>
|
||||||
[key: string]: any;
|
`;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async function loadEvents() {
|
|
||||||
try {
|
|
||||||
// Show skeletons first
|
|
||||||
const upcomingEventsContainer = document.getElementById(
|
|
||||||
"upcomingEventsContainer"
|
|
||||||
);
|
|
||||||
const ongoingEventsContainer = document.getElementById(
|
|
||||||
"ongoingEventsContainer"
|
|
||||||
);
|
|
||||||
const pastEventsContainer = document.getElementById(
|
|
||||||
"pastEventsContainer"
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
!upcomingEventsContainer ||
|
|
||||||
!pastEventsContainer ||
|
|
||||||
!ongoingEventsContainer
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Add 3 skeleton cards to each container initially
|
|
||||||
const createSkeletonCard = () => {
|
|
||||||
const skeletonCard = document.createElement("div");
|
|
||||||
skeletonCard.className =
|
|
||||||
"card bg-base-200 shadow-lg animate-pulse";
|
|
||||||
skeletonCard.innerHTML = `
|
|
||||||
<div class="card-body p-5">
|
|
||||||
<div class="flex flex-col h-full">
|
|
||||||
<div class="flex items-start justify-between gap-3 mb-2">
|
|
||||||
<div class="flex-1">
|
|
||||||
<div class="skeleton h-6 w-3/4 mb-2"></div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="skeleton h-5 w-16"></div>
|
|
||||||
<div class="skeleton h-5 w-20"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col items-end">
|
|
||||||
<div class="skeleton h-5 w-24 mb-1"></div>
|
|
||||||
<div class="skeleton h-4 w-16"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="skeleton h-4 w-full mb-3"></div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div class="skeleton h-4 w-4"></div>
|
|
||||||
<div class="skeleton h-4 w-1/2"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return skeletonCard;
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
upcomingEventsContainer.appendChild(createSkeletonCard());
|
|
||||||
ongoingEventsContainer.appendChild(createSkeletonCard());
|
|
||||||
pastEventsContainer.appendChild(createSkeletonCard());
|
|
||||||
}
|
|
||||||
|
|
||||||
const get = Get.getInstance();
|
|
||||||
const events = await get.getAll<Event>(
|
|
||||||
"events",
|
|
||||||
"published = true",
|
|
||||||
"-start_date"
|
|
||||||
); // Sort by start date descending
|
|
||||||
|
|
||||||
// Clear skeletons
|
|
||||||
upcomingEventsContainer.innerHTML = "";
|
|
||||||
ongoingEventsContainer.innerHTML = "";
|
|
||||||
pastEventsContainer.innerHTML = "";
|
|
||||||
|
|
||||||
// Split events into upcoming, ongoing, and past based on start and end dates
|
|
||||||
const now = new Date();
|
|
||||||
const { upcoming, ongoing, past } = events.reduce(
|
|
||||||
(acc, event) => {
|
|
||||||
// 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) {
|
|
||||||
// If start date is in future, it's upcoming
|
|
||||||
acc.upcoming.push(event);
|
|
||||||
} else if (endLocal < nowLocal) {
|
|
||||||
// If end date is in past, it's past
|
|
||||||
acc.past.push(event);
|
|
||||||
} else {
|
|
||||||
// If start date is past but end date is future, it's ongoing
|
|
||||||
acc.ongoing.push(event);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
upcoming: [] as Event[],
|
|
||||||
ongoing: [] as Event[],
|
|
||||||
past: [] as Event[],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort upcoming events by start date (closest first)
|
|
||||||
upcoming.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(a.start_date).getTime() -
|
|
||||||
new Date(b.start_date).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort ongoing events by end date (ending soonest first)
|
|
||||||
ongoing.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(a.end_date).getTime() -
|
|
||||||
new Date(b.end_date).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sort past events by end date (most recent first)
|
|
||||||
past.sort(
|
|
||||||
(a, b) =>
|
|
||||||
new Date(b.end_date).getTime() -
|
|
||||||
new Date(a.end_date).getTime()
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderEventCard = (event: Event, container: HTMLElement) => {
|
|
||||||
const startDate = new Date(event.start_date);
|
|
||||||
const endDate = new Date(event.end_date);
|
|
||||||
const now = new Date();
|
|
||||||
const isPastEvent = endDate < now;
|
|
||||||
|
|
||||||
// Store event data in window object with unique ID
|
|
||||||
const eventDataId = `event_${event.id}`;
|
|
||||||
window[eventDataId] = event;
|
|
||||||
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className =
|
|
||||||
"card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-300 relative overflow-hidden";
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="card-body p-5">
|
|
||||||
<div class="flex flex-col h-full">
|
|
||||||
<div class="flex items-start justify-between gap-3 mb-2">
|
|
||||||
<div class="flex-1">
|
|
||||||
<h3 class="card-title text-lg font-semibold mb-1 line-clamp-2">${event.event_name}</h3>
|
|
||||||
<div class="flex items-center gap-2 text-sm text-base-content/70">
|
|
||||||
<div class="badge badge-primary badge-sm">${event.points_to_reward} pts</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-right shrink-0 text-base-content/80">
|
|
||||||
<div class="text-sm font-medium">
|
|
||||||
${startDate.toLocaleDateString(
|
|
||||||
"en-US",
|
|
||||||
{
|
|
||||||
weekday: "short",
|
|
||||||
month: "short",
|
|
||||||
day: "numeric",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs mt-0.5 opacity-75">
|
|
||||||
${startDate.toLocaleTimeString(
|
|
||||||
"en-US",
|
|
||||||
{
|
|
||||||
hour: "numeric",
|
|
||||||
minute: "2-digit",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm text-base-content/70 mb-3 line-clamp-2">
|
|
||||||
${event.description || "No description available"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-start gap-2.5 text-base-content/80">
|
|
||||||
<Icon name="mdi:map-marker" class="w-4 h-4 text-primary shrink-0 mt-0.5" />
|
|
||||||
<span class="text-sm leading-tight truncate max-w-[200px]">${event.location}</span>
|
|
||||||
</div>
|
|
||||||
${
|
|
||||||
isPastEvent &&
|
|
||||||
event.files &&
|
|
||||||
event.files.length > 0
|
|
||||||
? `
|
|
||||||
<button onclick="window.openDetailsModal(window['event_${event.id}'])" class="btn btn-sm btn-primary w-[90px] inline-flex items-center justify-center">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
||||||
<polyline points="14 2 14 8 20 8" />
|
|
||||||
</svg>
|
|
||||||
<span>Files</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.appendChild(card);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render all event types
|
|
||||||
upcoming.forEach((event) =>
|
|
||||||
renderEventCard(event, upcomingEventsContainer)
|
|
||||||
);
|
|
||||||
ongoing.forEach((event) =>
|
|
||||||
renderEventCard(event, ongoingEventsContainer)
|
|
||||||
);
|
|
||||||
past.forEach((event) =>
|
|
||||||
renderEventCard(event, pastEventsContainer)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hide sections if they have no events
|
|
||||||
if (upcoming.length === 0)
|
|
||||||
upcomingEventsContainer.parentElement?.classList.add("hidden");
|
|
||||||
if (ongoing.length === 0)
|
|
||||||
ongoingEventsContainer.parentElement?.classList.add("hidden");
|
|
||||||
if (past.length === 0)
|
|
||||||
pastEventsContainer.parentElement?.classList.add("hidden");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load events:", error);
|
|
||||||
// You might want to show an error message to the user here
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load events when the section becomes visible
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
loadEvents();
|
|
||||||
observer.disconnect(); // Only load once
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const eventsSection = document.getElementById("eventsSection");
|
|
||||||
if (eventsSection) {
|
|
||||||
observer.observe(eventsSection);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
296
src/components/dashboard/EventsSection/EventLoad.tsx
Normal file
296
src/components/dashboard/EventsSection/EventLoad.tsx
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { Get } from "../../../scripts/pocketbase/Get";
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
id: string;
|
||||||
|
event_name: string;
|
||||||
|
event_code: string;
|
||||||
|
location: string;
|
||||||
|
points_to_reward: number;
|
||||||
|
attendees: AttendeeEntry[];
|
||||||
|
start_date: string;
|
||||||
|
end_date: string;
|
||||||
|
has_food: boolean;
|
||||||
|
description: string;
|
||||||
|
files: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AttendeeEntry {
|
||||||
|
user_id: string;
|
||||||
|
time_checked_in: string;
|
||||||
|
food: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
openDetailsModal: (event: Event) => void;
|
||||||
|
downloadAllFiles: () => Promise<void>;
|
||||||
|
currentEventId: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventLoad = () => {
|
||||||
|
const [events, setEvents] = useState<{
|
||||||
|
upcoming: Event[];
|
||||||
|
ongoing: Event[];
|
||||||
|
past: Event[];
|
||||||
|
}>({
|
||||||
|
upcoming: [],
|
||||||
|
ongoing: [],
|
||||||
|
past: [],
|
||||||
|
});
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const startDate = new Date(event.start_date);
|
||||||
|
const endDate = new Date(event.end_date);
|
||||||
|
const now = new Date();
|
||||||
|
const isPastEvent = endDate < now;
|
||||||
|
|
||||||
|
// Store event data in window object with unique ID
|
||||||
|
const eventDataId = `event_${event.id}`;
|
||||||
|
window[eventDataId] = event;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div 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-5">
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex items-start justify-between gap-3 mb-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="card-title text-lg font-semibold mb-1 line-clamp-2">{event.event_name}</h3>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-base-content/70">
|
||||||
|
<div className="badge badge-primary badge-sm">{event.points_to_reward} pts</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right shrink-0 text-base-content/80">
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{startDate.toLocaleDateString("en-US", {
|
||||||
|
weekday: "short",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs mt-0.5 opacity-75">
|
||||||
|
{startDate.toLocaleTimeString("en-US", {
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-base-content/70 mb-3 line-clamp-2">
|
||||||
|
{event.description || "No description available"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-base-content/80">
|
||||||
|
<Icon icon="mdi:map-marker" className="w-4 h-4 text-primary shrink-0" />
|
||||||
|
<span className="text-sm">{event.location}</span>
|
||||||
|
</div>
|
||||||
|
{isPastEvent && event.files && event.files.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => window.openDetailsModal(event)}
|
||||||
|
className="btn btn-sm btn-primary w-[90px] inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Icon icon="mdi:file-document-outline" className="w-4 h-4" />
|
||||||
|
<span>Files</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadEvents = async () => {
|
||||||
|
try {
|
||||||
|
const get = Get.getInstance();
|
||||||
|
const allEvents = await get.getAll<Event>(
|
||||||
|
"events",
|
||||||
|
"published = true",
|
||||||
|
"-start_date"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Split events into upcoming, ongoing, and past based on start and end dates
|
||||||
|
const now = new Date();
|
||||||
|
const { upcoming, ongoing, past } = allEvents.reduce(
|
||||||
|
(acc, event) => {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
upcoming: [] as Event[],
|
||||||
|
ongoing: [] as Event[],
|
||||||
|
past: [] as Event[],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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, ongoing, past });
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load events:", error);
|
||||||
|
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-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title mb-4">Ongoing Events</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title mb-4">Upcoming Events</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title mb-4">Past Events</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<div key={`past-skeleton-${i}`}>{createSkeletonCard()}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* 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-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title mb-4">Ongoing Events</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title mb-4">Upcoming Events</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 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">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title mb-4">Past Events</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{events.past.map(renderEventCard)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventLoad;
|
Loading…
Reference in a new issue