ieeeucsd-org/src/components/dashboard/EventsSection.astro
2025-02-11 01:36:36 -08:00

564 lines
19 KiB
Text

---
import { Icon } from "astro-icon/components";
---
<div id="eventsSection" class="dashboard-section hidden">
<div class="mb-6">
<h2 class="text-2xl font-bold">Events</h2>
<p class="opacity-70">View and manage your IEEE UCSD events</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Event Check-in Card -->
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body">
<h3 class="card-title text-lg mb-4">Event Check-in</h3>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Enter event code to check in</span>
</label>
<div class="flex gap-2">
<input
type="text"
placeholder="Enter code"
class="input input-bordered flex-1"
/>
<button class="btn btn-primary min-w-[90px]">Check In</button>
</div>
</div>
</div>
</div>
<!-- Food Selection Modal -->
<dialog id="foodSelectionModal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Food Selection</h3>
<form id="foodSelectionForm" class="space-y-4">
<div class="form-control">
<label class="label">
<span class="label-text">What food would you like?</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="foodInput"
name="foodInput"
class="input input-bordered"
placeholder="Enter your food choice or 'none'"
required
/>
<label class="label">
<span class="label-text-alt text-info"
>Enter 'none' if you don't want any food</span
>
</label>
</div>
<div class="modal-action">
<button type="submit" class="btn btn-primary">Submit</button>
<button
type="button"
class="btn"
onclick="foodSelectionModal.close()">Cancel</button
>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- Event Registration Card -->
<div class="card bg-base-100 shadow-xl border border-base-200">
<div class="card-body">
<h3 class="card-title text-lg mb-4">Event Registration</h3>
<div class="form-control w-full">
<label class="label">
<span class="label-text">Select an event to register</span>
</label>
<div class="flex gap-2">
<select class="select select-bordered flex-1">
<option disabled selected>Pick an event</option>
<option>Technical Workshop - Web Development</option>
<option>Professional Development Workshop</option>
<option>Social Event - Game Night</option>
</select>
<button class="btn btn-primary">Register</button>
</div>
</div>
</div>
</div>
</div>
<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">Upcoming Events</h3>
<div
id="eventsContainer"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
>
<!-- Events will be dynamically inserted here -->
</div>
</div>
</div>
</div>
<script>
import { Get } from "../pocketbase/Get";
import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update";
import { SendLog } from "../pocketbase/SendLog";
// Toast management system
const createToast = (
message: string,
type: "success" | "error" | "warning" = "success",
) => {
let toastContainer = document.querySelector(".toast-container");
if (!toastContainer) {
toastContainer = document.createElement("div");
toastContainer.className = "toast-container fixed bottom-4 right-4 z-50";
document.body.appendChild(toastContainer);
}
const existingToasts = document.querySelectorAll(".toast-container .toast");
if (existingToasts.length >= 2) {
const oldestToast = existingToasts[0];
oldestToast.classList.add("toast-exit");
setTimeout(() => oldestToast.remove(), 150);
}
// Update positions of existing toasts
existingToasts.forEach((t) => {
const toast = t as HTMLElement;
const currentIndex = parseInt(toast.getAttribute("data-index") || "0");
toast.setAttribute("data-index", (currentIndex + 1).toString());
});
const toast = document.createElement("div");
toast.className = "toast translate-x-full";
toast.setAttribute("data-index", "0");
toast.innerHTML = `
<div class="alert alert-${type} shadow-lg min-w-[300px]">
<span>${message}</span>
</div>
`;
toastContainer.appendChild(toast);
// Force a reflow to ensure the animation triggers
toast.offsetHeight;
// Add the transition class and remove transform
toast.classList.add("transition-all", "duration-300", "ease-out");
requestAnimationFrame(() => {
toast.classList.remove("translate-x-full");
});
// Setup exit animation
setTimeout(() => {
toast.classList.add("toast-exit");
setTimeout(() => toast.remove(), 150);
}, 3000);
};
// Add styles to the document
const style = document.createElement("style");
style.textContent = `
.toast-container {
display: flex;
flex-direction: column;
pointer-events: none;
}
.toast {
pointer-events: auto;
transform: translateX(0);
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.toast-exit {
transform: translateX(100%);
opacity: 0;
}
.toast.translate-x-full {
transform: translateX(100%);
}
.toast-container .toast {
transform: translateY(calc((1 - attr(data-index number)) * -0.25rem));
}
.toast-container .toast[data-index="0"] {
transform: translateY(0);
}
.toast-container .toast[data-index="1"] {
transform: translateY(-0.025rem);
}
`;
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 currentCheckInEvent: Event | null = null;
async function handleEventCheckIn(eventCode: string): Promise<void> {
try {
const get = Get.getInstance();
const auth = Authentication.getInstance();
const currentUser = auth.getCurrentUser();
if (!currentUser) {
throw new Error("You must be logged in to check in to events");
}
// Find the event with the given code
const event = await get.getFirst<Event>(
"events",
`event_code = "${eventCode}"`,
);
if (!event) {
throw new Error("Invalid event code");
}
// Check if user is already checked in
if (event.attendees.some((entry) => entry.user_id === currentUser.id)) {
throw new Error("You have already checked in to this event");
}
// Check if the event hasn't ended yet
const eventEndDate = new Date(event.end_date);
if (eventEndDate < new Date()) {
throw new Error("This event has already ended");
}
// If event has food, show food selection modal
if (event.has_food) {
currentCheckInEvent = event;
const modal = document.getElementById(
"foodSelectionModal",
) as HTMLDialogElement;
modal.showModal();
} else {
// If no food, complete check-in directly
await completeCheckIn(event, null);
}
} catch (error: any) {
createToast(error?.message || "Failed to check in to event", "error");
}
}
// Add food selection form handler
const foodSelectionForm = document.getElementById(
"foodSelectionForm",
) as HTMLFormElement;
if (foodSelectionForm) {
foodSelectionForm.addEventListener("submit", async (e) => {
e.preventDefault();
const modal = document.getElementById(
"foodSelectionModal",
) as HTMLDialogElement;
const foodInput = document.getElementById(
"foodInput",
) as HTMLInputElement;
try {
if (currentCheckInEvent) {
await completeCheckIn(currentCheckInEvent, foodInput.value.trim());
modal.close();
foodInput.value = ""; // Reset input
currentCheckInEvent = null;
}
} catch (error: any) {
createToast(error?.message || "Failed to check in to event", "error");
}
});
}
async function completeCheckIn(
event: Event,
foodSelection: string | null,
): Promise<void> {
try {
const auth = Authentication.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
const currentUser = auth.getCurrentUser();
if (!currentUser) {
throw new Error("You must be logged in to check in to events");
}
// Create attendee entry with check-in details
const attendeeEntry: AttendeeEntry = {
user_id: currentUser.id,
time_checked_in: new Date().toISOString(),
food: foodSelection || "none",
};
// Get existing attendees or initialize empty array
const existingAttendees = event.attendees || [];
// Check if user is already checked in
if (existingAttendees.some((entry) => entry.user_id === currentUser.id)) {
throw new Error("You have already checked in to this event");
}
// Add new attendee entry to the array
const updatedAttendees = [...existingAttendees, attendeeEntry];
// Update attendees array with the new entry
await update.updateField(
"events",
event.id,
"attendees",
updatedAttendees,
);
// If food selection was made, log it
if (foodSelection) {
await logger.send(
"update",
"event check-in",
`Food selection for ${event.event_name}: ${foodSelection}`,
);
}
// Award points to user if available
if (event.points_to_reward > 0) {
const userPoints = currentUser.points || 0;
await update.updateField(
"users",
currentUser.id,
"points",
userPoints + event.points_to_reward,
);
// Log the points award
await logger.send(
"update",
"event check-in",
`Awarded ${event.points_to_reward} points for checking in to ${event.event_name}`,
);
}
// Show success message with points if awarded
createToast(
`Successfully checked in to ${event.event_name}${
event.points_to_reward > 0
? ` (+${event.points_to_reward} points!)`
: ""
}`,
"success",
);
// Log the check-in
await logger.send(
"check_in",
"events",
`User ${currentUser.name} (${currentUser.graduation_year}) checked in to event ${event.event_name}`,
);
} catch (error: any) {
createToast(error?.message || "Failed to check in to event", "error");
}
}
// Add event listener to check-in button
document.addEventListener("DOMContentLoaded", () => {
const checkInForm = document.querySelector(".form-control");
const checkInInput = checkInForm?.querySelector("input");
const checkInButton = checkInForm?.querySelector("button");
if (checkInForm && checkInInput && checkInButton) {
checkInButton.addEventListener("click", async () => {
const eventCode = checkInInput.value.trim();
if (!eventCode) {
createToast("Please enter an event code", "warning");
return;
}
checkInButton.classList.add("btn-disabled");
const loadingSpinner = document.createElement("span");
loadingSpinner.className = "loading loading-spinner loading-xs";
const originalText = checkInButton.textContent;
checkInButton.textContent = "";
checkInButton.appendChild(loadingSpinner);
await handleEventCheckIn(eventCode);
checkInButton.classList.remove("btn-disabled");
checkInButton.removeChild(loadingSpinner);
checkInButton.textContent = originalText;
checkInInput.value = "";
});
// Allow Enter key to submit
checkInInput.addEventListener("keypress", async (e) => {
if (e.key === "Enter") {
e.preventDefault();
checkInButton.click();
}
});
}
});
async function loadEvents() {
try {
// Show skeletons first
const eventsContainer = document.getElementById("eventsContainer");
if (!eventsContainer) return;
// Add 6 skeleton cards initially
for (let i = 0; i < 6; i++) {
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>
`;
eventsContainer.appendChild(skeletonCard);
}
const get = Get.getInstance();
const events = await get.getAll<Event>(
"events",
undefined,
"-start_date",
); // Sort by start date descending
// Clear skeletons
eventsContainer.innerHTML = "";
events.forEach((event) => {
const startDate = new Date(event.start_date);
const endDate = new Date(event.end_date);
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>
${(() => {
const endDate = Get.isUTCDateString(
event.end_date,
)
? new Date(event.end_date)
: new Date();
const now = new Date();
return endDate < now &&
event.files &&
event.files.length > 0
? `
<button onclick="window.openDetailsModal(window['${eventDataId}'])" class="btn btn-ghost btn-xs gap-1">
<Icon name="heroicons:folder-open" class="w-4 h-4" />
Files (${event.files.length})
</button>
`
: "";
})()}
</div>
</div>
</div>
`;
eventsContainer.appendChild(card);
});
} 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>