reformat event checkin code

This commit is contained in:
chark1es 2025-02-17 14:22:40 -08:00
parent 79b34b97de
commit 371fb98d4d
2 changed files with 670 additions and 595 deletions

View file

@ -2,6 +2,7 @@
import { Icon } from "astro-icon/components"; import { Icon } from "astro-icon/components";
import JSZip from "jszip"; import JSZip from "jszip";
import FilePreview from "./universal/FilePreview"; import FilePreview from "./universal/FilePreview";
import EventCheckIn from "./EventsSection/EventCheckIn";
--- ---
<div id="eventsSection" class="dashboard-section hidden"> <div id="eventsSection" class="dashboard-section hidden">
@ -12,74 +13,17 @@ import FilePreview from "./universal/FilePreview";
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<!-- Event Check-in Card --> <!-- Event Check-in Card -->
<div class="card bg-base-100 shadow-xl border border-base-200"> <EventCheckIn client:load />
<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="password"
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 --> <!-- Event Registration Card -->
<div class="card bg-base-100 shadow-xl border border-base-200"> <div
class="card bg-base-100 shadow-xl border border-base-200 opacity-50 cursor-not-allowed relative group"
>
<div
class="absolute inset-0 bg-base-100 opacity-0 group-hover:opacity-90 transition-opacity duration-300 flex items-center justify-center z-10"
>
<span class="text-base-content font-medium">Coming Soon</span>
</div>
<div class="card-body"> <div class="card-body">
<h3 class="card-title text-lg mb-4">Event Registration</h3> <h3 class="card-title text-lg mb-4">Event Registration</h3>
<div class="form-control w-full"> <div class="form-control w-full">
@ -89,14 +33,16 @@ import FilePreview from "./universal/FilePreview";
> >
</label> </label>
<div class="flex gap-2"> <div class="flex gap-2">
<select class="select select-bordered flex-1"> <select class="select select-bordered flex-1" disabled>
<option disabled selected>Pick an event</option> <option disabled selected>Pick an event</option>
<option>Technical Workshop - Web Development</option <option>Technical Workshop - Web Development</option
> >
<option>Professional Development Workshop</option> <option>Professional Development Workshop</option>
<option>Social Event - Game Night</option> <option>Social Event - Game Night</option>
</select> </select>
<button class="btn btn-primary">Register</button> <button class="btn btn-primary" disabled
>Register</button
>
</div> </div>
</div> </div>
</div> </div>
@ -210,10 +156,8 @@ import FilePreview from "./universal/FilePreview";
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
class="btn btn-ghost btn-sm" class="btn btn-ghost btn-sm"
onclick="window.closeFilePreviewEvents()" onclick="window.closeFilePreviewEvents()">Close</button
> >
Close
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName"> <h3 class="font-bold text-lg truncate" id="previewFileName">
</h3> </h3>
</div> </div>
@ -371,227 +315,306 @@ import FilePreview from "./universal/FilePreview";
food: string; food: string;
} }
let currentCheckInEvent: Event | null = null; let currentEventId = "";
async function handleEventCheckIn(eventCode: string): Promise<void> { // Add helper functions for file preview
try { function getFileType(filename: string): string {
const get = Get.getInstance(); const extension = filename.split(".").pop()?.toLowerCase();
const auth = Authentication.getInstance(); const mimeTypes: { [key: string]: string } = {
pdf: "application/pdf",
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
mp4: "video/mp4",
mp3: "audio/mpeg",
txt: "text/plain",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
json: "application/json",
};
const currentUser = auth.getCurrentUser(); return mimeTypes[extension || ""] || "application/octet-stream";
if (!currentUser) { }
throw new Error("You must be logged in to check in to events");
}
// Find the event with the given code function showLoading() {
const event = await get.getFirst<Event>( const spinner = document.getElementById("loadingSpinner");
"events", if (spinner) spinner.classList.remove("hidden");
`event_code = "${eventCode}"` }
);
if (!event) {
throw new Error("Invalid event code");
}
// Check if user is already checked in function hideLoading() {
if ( const spinner = document.getElementById("loadingSpinner");
event.attendees.some( if (spinner) spinner.classList.add("hidden");
(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 function showFilePreview(file: {
const eventEndDate = new Date(event.end_date); url: string;
if (eventEndDate < new Date()) { type: string;
throw new Error("This event has already ended"); name: string;
} }) {
console.log("showFilePreview called with:", file);
window.previewFileEvents(file.url, file.name);
}
// If event has food, show food selection modal function handlePreviewError() {
if (event.has_food) { hideLoading();
currentCheckInEvent = event; const previewContent = document.getElementById("previewContent");
const modal = document.getElementById( if (previewContent) {
"foodSelectionModal" previewContent.innerHTML = `
) as HTMLDialogElement; <div class="alert alert-error">
modal.showModal(); <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
} else { <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" />
// If no food, complete check-in directly </svg>
await completeCheckIn(event, null); <span>Failed to load file preview</span>
} </div>
} catch (error: any) { `;
createToast(
error?.message || "Failed to check in to event",
"error"
);
} }
} }
// Add food selection form handler // Universal file preview function for events section
const foodSelectionForm = document.getElementById( window.previewFileEvents = function (url: string, filename: string) {
"foodSelectionForm" console.log("previewFileEvents called with:", { url, filename });
) as HTMLFormElement; const modal = document.getElementById(
if (foodSelectionForm) { "filePreviewModal"
foodSelectionForm.addEventListener("submit", async (e) => { ) as HTMLDialogElement;
e.preventDefault(); const previewFileName = document.getElementById("previewFileName");
const modal = document.getElementById( const previewContent = document.getElementById("previewContent");
"foodSelectionModal"
) as HTMLDialogElement;
const foodInput = document.getElementById(
"foodInput"
) as HTMLInputElement;
try { if (modal && previewFileName && previewContent) {
if (currentCheckInEvent) { console.log("Found all required elements");
await completeCheckIn( // Update the filename display
currentCheckInEvent, previewFileName.textContent = filename;
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( // Show the modal
event: Event, modal.showModal();
foodSelection: string | null
): Promise<void> {
try {
const auth = Authentication.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
const currentUser = auth.getCurrentUser(); // Dispatch state change event
if (!currentUser) { window.dispatchEvent(
throw new Error("You must be logged in to check in to events"); new CustomEvent("filePreviewStateChange", {
} detail: { url, filename },
})
// 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 // Close file preview for events section
document.addEventListener("DOMContentLoaded", () => { window.closeFilePreviewEvents = function () {
const checkInForm = document.querySelector(".form-control"); console.log("closeFilePreviewEvents called");
const checkInInput = checkInForm?.querySelector("input"); const modal = document.getElementById(
const checkInButton = checkInForm?.querySelector("button"); "filePreviewModal"
) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent");
if (checkInForm && checkInInput && checkInButton) { if (modal && previewFileName && previewContent) {
checkInButton.addEventListener("click", async () => { console.log("Resetting preview and closing modal");
const eventCode = checkInInput.value.trim(); // Reset the preview state
if (!eventCode) { window.dispatchEvent(
createToast("Please enter an event code", "warning"); new CustomEvent("filePreviewStateChange", {
return; detail: { url: "", filename: "" },
})
);
// Reset the UI
previewFileName.textContent = "";
// Close the modal
modal.close();
}
};
// Update the showFilePreview function for events section
window.showFilePreviewEvents = function (file: {
url: string;
name: string;
}) {
console.log("showFilePreviewEvents called with:", file);
window.previewFileEvents(file.url, file.name);
};
// Update the openDetailsModal function to use the events-specific preview
window.openDetailsModal = function (event: any) {
const modal = document.getElementById(
"eventDetailsModal"
) as HTMLDialogElement;
const filesContent = document.getElementById(
"filesContent"
) as HTMLDivElement;
// Reset state
currentEventId = event.id;
if (filesContent) filesContent.classList.remove("hidden");
// Populate files content
if (
event.files &&
Array.isArray(event.files) &&
event.files.length > 0
) {
const baseUrl = "https://pocketbase.ieeeucsd.org";
const collectionId = "events";
const recordId = event.id;
filesContent.innerHTML = `
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>File Name</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
${event.files
.map((file: string) => {
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
const fileType = getFileType(file);
const previewData = JSON.stringify({
url: fileUrl,
name: file,
}).replace(/'/g, "\\'");
return `
<tr>
<td>${file}</td>
<td class="text-right">
<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">
<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>
<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">
<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>
</td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
`;
} else {
filesContent.innerHTML = `
<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">
<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>
</div>
`;
}
modal.showModal();
};
// Add downloadAllFiles function
window.downloadAllFiles = async function () {
const downloadBtn = document.getElementById(
"downloadAllBtn"
) as HTMLButtonElement;
if (!downloadBtn) return;
const originalBtnContent = downloadBtn.innerHTML;
try {
// Show loading state
downloadBtn.innerHTML =
'<span class="loading loading-spinner loading-xs"></span> Preparing...';
downloadBtn.disabled = true;
const zip = new JSZip();
// Get current event files
const baseUrl = "https://pocketbase.ieeeucsd.org";
const collectionId = "events";
const recordId = currentEventId;
// Get the current event from the window object
const eventDataId = `event_${currentEventId}`;
const event = window[eventDataId] as Event;
if (!event || !event.files || event.files.length === 0) {
throw new Error("No files available to download");
}
// Download each file and add to zip
const filePromises = event.files.map(async (filename: string) => {
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${filename}`;
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error(`Failed to download ${filename}`);
} }
const blob = await response.blob();
checkInButton.classList.add("btn-disabled"); zip.file(filename, blob);
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 await Promise.all(filePromises);
checkInInput.addEventListener("keypress", async (e) => {
if (e.key === "Enter") { // Generate and download zip
e.preventDefault(); const zipBlob = await zip.generateAsync({ type: "blob" });
checkInButton.click(); const downloadUrl = URL.createObjectURL(zipBlob);
} const link = document.createElement("a");
}); link.href = downloadUrl;
link.download = `${event.event_name}_files.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
// Show success message
createToast("Files downloaded successfully!", "success");
} catch (error: any) {
console.error("Failed to download files:", error);
createToast(
error?.message || "Failed to download files. Please try again.",
"error"
);
} finally {
// Reset button state
downloadBtn.innerHTML = originalBtnContent;
downloadBtn.disabled = false;
} }
}); };
// Close event details modal
window.closeEventDetailsModal = function () {
const modal = document.getElementById(
"eventDetailsModal"
) as HTMLDialogElement;
const filesContent = document.getElementById("filesContent");
if (modal) {
// Reset the files content
if (filesContent) {
filesContent.innerHTML = "";
}
// Reset any other state if needed
currentEventId = "";
// Close the modal
modal.close();
}
};
// Make helper functions available globally
window.showFilePreview = showFilePreview;
window.handlePreviewError = handlePreviewError;
window.showLoading = showLoading;
window.hideLoading = hideLoading;
// Add TypeScript interface for Window
declare global {
interface Window {
downloadAllFiles: () => Promise<void>;
[key: string]: any;
}
}
async function loadEvents() { async function loadEvents() {
try { try {
@ -852,327 +875,4 @@ import FilePreview from "./universal/FilePreview";
if (eventsSection) { if (eventsSection) {
observer.observe(eventsSection); observer.observe(eventsSection);
} }
// Add helper functions for file preview
function getFileType(filename: string): string {
const extension = filename.split(".").pop()?.toLowerCase();
const mimeTypes: { [key: string]: string } = {
pdf: "application/pdf",
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
gif: "image/gif",
mp4: "video/mp4",
mp3: "audio/mpeg",
txt: "text/plain",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
json: "application/json",
};
return mimeTypes[extension || ""] || "application/octet-stream";
}
let currentEventId = "";
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 isPreviewableType(fileType: string): boolean {
return (
fileType.startsWith("image/") ||
fileType.startsWith("video/") ||
fileType.startsWith("audio/") ||
fileType === "application/pdf" ||
fileType.startsWith("text/") ||
fileType === "application/json"
);
}
function backToFileList() {
const filePreviewSection =
document.getElementById("filePreviewSection");
const filesContent = document.getElementById("filesContent");
const modalTitle = document.getElementById("modalTitle");
if (filePreviewSection) filePreviewSection.classList.add("hidden");
if (filesContent) filesContent.classList.remove("hidden");
if (modalTitle) modalTitle.textContent = "Event Files";
}
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
window.previewFileEvents = function (url: string, filename: string) {
console.log("previewFileEvents called with:", { url, filename });
const modal = document.getElementById(
"filePreviewModal"
) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent");
if (modal && previewFileName && previewContent) {
console.log("Found all required elements");
// Update the filename display
previewFileName.textContent = filename;
// Show the modal
modal.showModal();
// Dispatch state change event
window.dispatchEvent(
new CustomEvent("filePreviewStateChange", {
detail: { url, filename },
})
);
}
};
// Close file preview for events section
window.closeFilePreviewEvents = function () {
console.log("closeFilePreviewEvents called");
const modal = document.getElementById(
"filePreviewModal"
) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const previewContent = document.getElementById("previewContent");
if (modal && previewFileName && previewContent) {
console.log("Resetting preview and closing modal");
// Reset the preview state
window.dispatchEvent(
new CustomEvent("filePreviewStateChange", {
detail: { url: "", filename: "" },
})
);
// Reset the UI
previewFileName.textContent = "";
// Close the modal
modal.close();
}
};
// Update the showFilePreview function for events section
window.showFilePreviewEvents = function (file: {
url: string;
name: string;
}) {
console.log("showFilePreviewEvents called with:", file);
window.previewFileEvents(file.url, file.name);
};
// Update the openDetailsModal function to use the events-specific preview
window.openDetailsModal = function (event: any) {
const modal = document.getElementById(
"eventDetailsModal"
) as HTMLDialogElement;
const filesContent = document.getElementById(
"filesContent"
) as HTMLDivElement;
// Reset state
currentEventId = event.id;
if (filesContent) filesContent.classList.remove("hidden");
// Populate files content
if (
event.files &&
Array.isArray(event.files) &&
event.files.length > 0
) {
const baseUrl = "https://pocketbase.ieeeucsd.org";
const collectionId = "events";
const recordId = event.id;
filesContent.innerHTML = `
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>File Name</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
${event.files
.map((file: string) => {
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
const fileType = getFileType(file);
const previewData = JSON.stringify({
url: fileUrl,
name: file,
}).replace(/'/g, "\\'");
return `
<tr>
<td>${file}</td>
<td class="text-right">
<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">
<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>
<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">
<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>
</td>
</tr>
`;
})
.join("")}
</tbody>
</table>
</div>
`;
} else {
filesContent.innerHTML = `
<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">
<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>
</div>
`;
}
modal.showModal();
};
// Add downloadAllFiles function
window.downloadAllFiles = async function () {
const downloadBtn = document.getElementById(
"downloadAllBtn"
) as HTMLButtonElement;
if (!downloadBtn) return;
const originalBtnContent = downloadBtn.innerHTML;
try {
// Show loading state
downloadBtn.innerHTML =
'<span class="loading loading-spinner loading-xs"></span> Preparing...';
downloadBtn.disabled = true;
const zip = new JSZip();
// Get current event files
const baseUrl = "https://pocketbase.ieeeucsd.org";
const collectionId = "events";
const recordId = currentEventId;
// Get the current event from the window object
const eventDataId = `event_${currentEventId}`;
const event = window[eventDataId] as Event;
if (!event || !event.files || event.files.length === 0) {
throw new Error("No files available to download");
}
// Download each file and add to zip
const filePromises = event.files.map(async (filename: string) => {
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${filename}`;
const response = await fetch(fileUrl);
if (!response.ok) {
throw new Error(`Failed to download ${filename}`);
}
const blob = await response.blob();
zip.file(filename, blob);
});
await Promise.all(filePromises);
// Generate and download zip
const zipBlob = await zip.generateAsync({ type: "blob" });
const downloadUrl = URL.createObjectURL(zipBlob);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = `${event.event_name}_files.zip`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(downloadUrl);
// Show success message
createToast("Files downloaded successfully!", "success");
} catch (error: any) {
console.error("Failed to download files:", error);
createToast(
error?.message || "Failed to download files. Please try again.",
"error"
);
} finally {
// Reset button state
downloadBtn.innerHTML = originalBtnContent;
downloadBtn.disabled = false;
}
};
// Close event details modal
window.closeEventDetailsModal = function () {
const modal = document.getElementById(
"eventDetailsModal"
) as HTMLDialogElement;
const filesContent = document.getElementById("filesContent");
if (modal) {
// Reset the files content
if (filesContent) {
filesContent.innerHTML = "";
}
// Reset any other state if needed
currentEventId = "";
// Close the modal
modal.close();
}
};
// Make helper functions available globally
window.showFilePreview = showFilePreview;
window.handlePreviewError = handlePreviewError;
window.showLoading = showLoading;
window.hideLoading = hideLoading;
// Add TypeScript interface for Window
declare global {
interface Window {
downloadAllFiles: () => Promise<void>;
[key: string]: any;
}
}
</script> </script>

View file

@ -0,0 +1,375 @@
import { useEffect, useState } from "react";
import { Get } from "../../../scripts/pocketbase/Get";
import { Authentication } from "../../../scripts/pocketbase/Authentication";
import { Update } from "../../../scripts/pocketbase/Update";
import { SendLog } from "../../../scripts/pocketbase/SendLog";
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;
}
// 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");
// Update alert styling based on type
const alertClass =
type === "success"
? "alert-success bg-success text-success-content"
: type === "error"
? "alert-error bg-error text-error-content"
: "alert-warning bg-warning text-warning-content";
toast.innerHTML = `
<div class="alert ${alertClass} shadow-lg min-w-[300px]">
<div class="flex items-center gap-2">
${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>'
: 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>'
: '<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>'
}
<span>${message}</span>
</div>
</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);
};
const EventCheckIn = () => {
const [currentCheckInEvent, setCurrentCheckInEvent] = useState<Event | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [foodInput, setFoodInput] = useState("");
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) {
setCurrentCheckInEvent(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");
}
}
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}`
);
// Close the food selection modal if it's open
const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
if (modal) {
modal.close();
setFoodInput("");
}
} catch (error: any) {
createToast(error?.message || "Failed to check in to event", "error");
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (currentCheckInEvent) {
await completeCheckIn(currentCheckInEvent, foodInput.trim());
setCurrentCheckInEvent(null);
}
};
return (
<>
<div className="card bg-base-100 shadow-xl border border-base-200">
<div className="card-body">
<h3 className="card-title text-lg mb-4">Event Check-in</h3>
<div className="form-control w-full">
<label className="label">
<span className="label-text">Enter event code to check in</span>
</label>
<div className="flex gap-2">
<input
type="password"
placeholder="Enter code"
className="input input-bordered flex-1"
onKeyPress={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.target as HTMLInputElement;
if (input.value.trim()) {
setIsLoading(true);
handleEventCheckIn(input.value.trim()).finally(() => {
setIsLoading(false);
input.value = "";
});
}
}
}}
/>
<button
className={`btn btn-primary min-w-[90px] ${isLoading ? "loading" : ""}`}
onClick={(e) => {
const input = e.currentTarget.previousElementSibling as HTMLInputElement;
if (input.value.trim()) {
setIsLoading(true);
handleEventCheckIn(input.value.trim()).finally(() => {
setIsLoading(false);
input.value = "";
});
} else {
createToast("Please enter an event code", "warning");
}
}}
disabled={isLoading}
>
{isLoading ? (
<span className="loading loading-spinner loading-xs"></span>
) : (
"Check In"
)}
</button>
</div>
</div>
</div>
</div>
<dialog id="foodSelectionModal" className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg mb-4">Food Selection</h3>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text">What food would you like?</span>
<span className="label-text-alt text-error">*</span>
</label>
<input
type="text"
value={foodInput}
onChange={(e) => setFoodInput(e.target.value)}
className="input input-bordered"
placeholder="Enter your food choice or 'none'"
required
/>
<label className="label">
<span className="label-text-alt text-info">
Enter 'none' if you don't want any food
</span>
</label>
</div>
<div className="modal-action">
<button type="submit" className="btn btn-primary">
Submit
</button>
<button
type="button"
className="btn"
onClick={() => {
const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
modal.close();
setCurrentCheckInEvent(null);
setFoodInput("");
}}
>
Cancel
</button>
</div>
</form>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
<style>{`
.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);
}
`}</style>
</>
);
};
export default EventCheckIn;