564 lines
19 KiB
Text
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>
|