ieeeucsd-org/src/components/dashboard/Officer_EventManagement.astro
2025-02-10 22:59:37 -08:00

1265 lines
44 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import { Icon } from "astro-icon/components";
import { Get } from "../pocketbase/Get";
import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update";
import { FileManager } from "../pocketbase/FileManager";
import { SendLog } from "../pocketbase/SendLog";
// Get instances
const get = Get.getInstance();
const auth = Authentication.getInstance();
const update = Update.getInstance();
const fileManager = FileManager.getInstance();
const sendLog = SendLog.getInstance();
// Interface for Event type
interface Event {
id: string;
event_name: string;
event_description: string;
event_code: string;
location: string;
files: string[];
points_to_reward: number;
start_date: string;
end_date: string;
published: boolean;
}
interface ListResponse<T> {
page: number;
perPage: number;
totalItems: number;
totalPages: number;
items: T[];
}
// Initialize variables
let eventResponse: ListResponse<Event> = {
page: 1,
perPage: 5,
totalItems: 0,
totalPages: 0,
items: [],
};
let upcomingEvents: Event[] = [];
// Fetch events
try {
if (auth.isAuthenticated()) {
eventResponse = await get.getList<Event>("events", 1, 5, "", "-start_date");
upcomingEvents = eventResponse.items;
}
} catch (error) {
console.error("Failed to fetch events:", error);
}
const totalEvents = eventResponse.totalItems;
const totalPages = eventResponse.totalPages;
const currentPage = eventResponse.page;
// Add type declaration for window
declare global {
interface Window {
[key: string]: any;
openEditModal: (event?: any) => void;
deleteFile: (eventId: string, filename: string) => void;
previewFile: (url: string, filename: string) => void;
openDetailsModal: (event: Event) => void;
showFilePreview: (file: {
url: string;
type: string;
name: string;
}) => void;
backToFileList: () => void;
handlePreviewError: () => void;
showLoading: () => void;
hideLoading: () => void;
deleteEvent: (eventId: string, eventName: string) => Promise<void>;
}
}
---
<div id="eventManagementSection" class="dashboard-section hidden">
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold">Event Management</h2>
<p class="opacity-70">Manage and create IEEE UCSD events</p>
</div>
<button class="btn btn-primary gap-2" onclick="window.openEditModal()">
<Icon name="heroicons:plus" class="h-5 w-5" />
Add New Event
</button>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
>
<div class="stat">
<div class="stat-title font-medium opacity-80">Total Events</div>
<div class="stat-value text-primary" id="totalEvents">-</div>
<div class="stat-desc flex items-center gap-2 mt-1">
<div class="badge badge-primary badge-sm">All Time</div>
</div>
</div>
</div>
<div
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-secondary transition-all duration-300 hover:-translate-y-1 transform"
>
<div class="stat">
<div class="stat-title font-medium opacity-80">Showing</div>
<div class="stat-value text-secondary" id="showingEvents">-</div>
<div class="stat-desc flex items-center gap-2 mt-1">
<div class="badge badge-secondary badge-sm" id="totalEventsLabel">
of - Events
</div>
</div>
</div>
</div>
<div
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-accent transition-all duration-300 hover:-translate-y-1 transform"
>
<div class="stat">
<div class="stat-title font-medium opacity-80">Pages</div>
<div class="stat-value text-accent" id="currentPage">-</div>
<div class="stat-desc flex items-center gap-2 mt-1">
<div class="badge badge-accent badge-sm" id="totalPagesLabel">
of -
</div>
</div>
</div>
</div>
</div>
<!-- Events List -->
<div
class="card bg-base-100 shadow-lg border border-base-200 hover:border-primary transition-all duration-300"
>
<div class="card-body">
<h3 class="card-title text-xl font-bold flex items-center gap-3">
<div class="badge badge-primary p-3">
<Icon name="heroicons:calendar" class="h-5 w-5" />
</div>
Events List
</h3>
<div class="divider"></div>
<!-- Event Items -->
<div class="space-y-4" id="eventsList">
<div class="text-center py-8 text-base-content/70">
<Icon
name="heroicons:calendar"
class="h-12 w-12 mx-auto mb-4 opacity-50"
/>
<p>Loading events...</p>
</div>
</div>
<!-- Pagination -->
<div class="flex justify-center mt-6" id="paginationContainer">
<div class="join">
<button class="join-item btn btn-sm" id="firstPageBtn">«</button>
<button class="join-item btn btn-sm" id="prevPageBtn"></button>
<button class="join-item btn btn-sm"
>Page <span id="currentPageNumber">1</span></button
>
<button class="join-item btn btn-sm" id="nextPageBtn"></button>
<button class="join-item btn btn-sm" id="lastPageBtn">»</button>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Event Modal -->
<dialog id="editEventModal" class="modal">
<div class="modal-box max-w-2xl">
<h3 class="font-bold text-lg mb-4" id="editModalTitle">Edit Event</h3>
<form id="editEventForm" class="space-y-4">
<input type="hidden" id="editEventId" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Event Name -->
<div class="form-control">
<label class="label">
<span class="label-text">Event Name</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventName"
name="editEventName"
class="input input-bordered"
required
/>
</div>
<!-- Event Code -->
<div class="form-control">
<label class="label">
<span class="label-text">Event Code</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventCode"
name="editEventCode"
class="input input-bordered"
required
/>
</div>
<!-- Location -->
<div class="form-control">
<label class="label">
<span class="label-text">Location</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="text"
id="editEventLocation"
name="editEventLocation"
class="input input-bordered"
required
/>
</div>
<!-- Points to Reward -->
<div class="form-control">
<label class="label">
<span class="label-text">Points to Reward</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="number"
id="editEventPoints"
name="editEventPoints"
class="input input-bordered"
min="0"
required
/>
</div>
<!-- Start Date -->
<div class="form-control">
<label class="label">
<span class="label-text">Start Date</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="datetime-local"
id="editEventStartDate"
name="editEventStartDate"
class="input input-bordered"
required
/>
</div>
<!-- End Date -->
<div class="form-control">
<label class="label">
<span class="label-text">End Date</span>
<span class="label-text-alt text-error">*</span>
</label>
<input
type="datetime-local"
id="editEventEndDate"
name="editEventEndDate"
class="input input-bordered"
required
/>
</div>
</div>
<!-- Description -->
<div class="form-control">
<label class="label">
<span class="label-text">Description</span>
<span class="label-text-alt text-error">*</span>
</label>
<textarea
id="editEventDescription"
name="editEventDescription"
class="textarea textarea-bordered"
rows="3"
required></textarea>
</div>
<!-- Files -->
<div class="form-control">
<label class="label">
<span class="label-text">Upload Files</span>
</label>
<input
type="file"
id="editEventFiles"
class="file-input file-input-bordered"
multiple
/>
<div class="mt-4 space-y-2">
<div id="newFiles" class="space-y-2">
<!-- New files will be listed here -->
</div>
<div class="divider">Current Files</div>
<div id="currentFiles" class="space-y-2">
<!-- Current files will be listed here -->
</div>
</div>
</div>
<!-- Published -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
id="editEventPublished"
name="editEventPublished"
class="toggle"
/>
<span class="label-text">Publish Event</span>
</label>
<label class="label">
<span class="label-text-alt text-info"
>This has to be clicked if you want to make this event available to
the public</span
>
</label>
</div>
<div class="modal-action">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="button" class="btn" onclick="editEventModal.close()"
>Cancel</button
>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- Event Details Modal -->
<dialog id="eventDetailsModal" class="modal">
<div class="modal-box max-w-4xl">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<h3 class="font-bold text-lg" id="modalTitle">Event Details</h3>
</div>
<button
class="btn btn-circle btn-ghost"
onclick="eventDetailsModal.close()"
>
<svg
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>
</div>
<div class="tabs tabs-boxed mb-4">
<button class="tab tab-active" data-tab="files">Files</button>
<button class="tab" data-tab="attendees">Attendees</button>
</div>
<div id="filesContent" class="space-y-4">
<!-- Files list will be populated here -->
</div>
<div id="attendeesContent" class="space-y-4 hidden">
<!-- Attendees list will be populated here -->
</div>
<!-- File Preview Section -->
<div id="filePreviewSection" class="hidden">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-3">
<button class="btn btn-ghost btn-sm" onclick="backToFileList()">
← Back
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName"></h3>
</div>
</div>
<div class="relative" id="previewContainer">
<div
id="loadingSpinner"
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
>
<span class="loading loading-spinner loading-lg"></span>
</div>
<div id="previewContent" class="w-full"></div>
</div>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<script>
import { Get } from "../pocketbase/Get";
import { Authentication } from "../pocketbase/Authentication";
import { Update } from "../pocketbase/Update";
import { FileManager } from "../pocketbase/FileManager";
import { SendLog } from "../pocketbase/SendLog";
const get = Get.getInstance();
const auth = Authentication.getInstance();
const update = Update.getInstance();
const fileManager = FileManager.getInstance();
const sendLog = SendLog.getInstance();
let currentPage = 1;
let totalPages = 0;
// Store temporary files
let tempFiles: File[] = [];
// Make openEditModal available globally
window.openEditModal = function (event?: any) {
// Convert event times to local time if event exists
const localEvent = event ? Get.convertUTCToLocal(event) : null;
const modal = document.getElementById(
"editEventModal",
) as HTMLDialogElement;
const modalTitle = document.getElementById("editModalTitle");
const form = document.getElementById("editEventForm") as HTMLFormElement;
const idInput = document.getElementById("editEventId") as HTMLInputElement;
const nameInput = document.getElementById(
"editEventName",
) as HTMLInputElement;
const descInput = document.getElementById(
"editEventDescription",
) as HTMLTextAreaElement;
const codeInput = document.getElementById(
"editEventCode",
) as HTMLInputElement;
const locationInput = document.getElementById(
"editEventLocation",
) as HTMLInputElement;
const pointsInput = document.getElementById(
"editEventPoints",
) as HTMLInputElement;
const startDateInput = document.getElementById(
"editEventStartDate",
) as HTMLInputElement;
const endDateInput = document.getElementById(
"editEventEndDate",
) as HTMLInputElement;
const publishedInput = document.getElementById(
"editEventPublished",
) as HTMLInputElement;
const currentFilesDiv = document.getElementById(
"currentFiles",
) as HTMLDivElement;
// Update modal title based on whether we're editing or creating
if (modalTitle) {
modalTitle.textContent = localEvent ? "Edit Event" : "Create New Event";
}
// Set values
idInput.value = localEvent?.id || "";
nameInput.value = localEvent?.event_name || "";
descInput.value = localEvent?.event_description || "";
codeInput.value = localEvent?.event_code || "";
locationInput.value = localEvent?.location || "";
pointsInput.value = localEvent?.points_to_reward?.toString() || "0";
// Format dates properly for datetime-local input
try {
const now = new Date();
const startDate = localEvent ? new Date(localEvent.start_date) : now;
const endDate = localEvent
? new Date(localEvent.end_date)
: new Date(now.getTime() + 60 * 60 * 1000); // Default to 1 hour duration
if (!isNaN(startDate.getTime())) {
startDateInput.value = new Date(
startDate.getTime() - startDate.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 16);
}
if (!isNaN(endDate.getTime())) {
endDateInput.value = new Date(
endDate.getTime() - endDate.getTimezoneOffset() * 60000,
)
.toISOString()
.slice(0, 16);
}
} catch (e) {
console.error("Error formatting dates:", e);
}
publishedInput.checked = localEvent?.published || false;
// Reset temp files
tempFiles = [];
const newFilesDiv = document.getElementById("newFiles") as HTMLDivElement;
newFilesDiv.innerHTML = "";
// Display current files if any
if (localEvent?.files && localEvent.files.length > 0) {
const baseUrl = "https://pocketbase.ieeeucsd.org";
const collectionId = "events";
const recordId = localEvent.id;
currentFilesDiv.innerHTML = localEvent.files
.map(
(file: string) => `
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg">
<span class="text-sm truncate flex-1">${file}</span>
<div class="flex gap-2">
<button type="button" class="btn btn-ghost btn-sm text-error" onclick="deleteFile('${localEvent.id}', '${file}')">
<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="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
`,
)
.join("");
} else {
currentFilesDiv.innerHTML =
'<p class="text-sm opacity-70">No files attached</p>';
}
modal.showModal();
};
// Preview file using FileViewerModal
window.previewFile = function (url: string, filename: string) {
const fileType = getFileType(filename);
const fileData = {
url,
type: fileType,
name: filename,
};
// Create and dispatch custom event for FileViewerModal
const event = new CustomEvent("showFileViewer", {
detail: { files: fileData },
});
window.dispatchEvent(event);
};
// Helper function to determine file type
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";
}
// Handle file deletion
window.deleteFile = async function (eventId: string, filename: string) {
try {
await fileManager.deleteFile("events", eventId, "files");
await sendLog.send(
"delete",
"event_files",
`Deleted file ${filename} from event ${eventId}`,
);
await fetchEvents(); // Refresh the list
} catch (error) {
console.error("Failed to delete file:", error);
}
};
// Handle file input change
const fileInput = document.getElementById(
"editEventFiles",
) as HTMLInputElement;
if (fileInput) {
fileInput.addEventListener("change", function () {
if (this.files && this.files.length > 0) {
const newFilesDiv = document.getElementById(
"newFiles",
) as HTMLDivElement;
// Add new files to temp storage
Array.from(this.files).forEach((file) => {
tempFiles.push(file);
});
// Update preview
newFilesDiv.innerHTML = tempFiles
.map(
(file, index) => `
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg">
<span class="text-sm truncate flex-1">${file.name}</span>
<div class="flex gap-2">
<button type="button" class="btn btn-ghost btn-sm text-error" onclick="removeTempFile(${index})">
<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="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
`,
)
.join("");
// Clear the file input
this.value = "";
}
});
}
// Remove temp file
window.removeTempFile = function (index: number) {
tempFiles.splice(index, 1);
const newFilesDiv = document.getElementById("newFiles") as HTMLDivElement;
if (tempFiles.length === 0) {
newFilesDiv.innerHTML = "";
return;
}
// Update preview
newFilesDiv.innerHTML = tempFiles
.map(
(file, idx) => `
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg">
<span class="text-sm truncate flex-1">${file.name}</span>
<div class="flex gap-2">
<button type="button" class="btn btn-ghost btn-sm text-error" onclick="removeTempFile(${idx})">
<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="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
`,
)
.join("");
};
// Update form submission to handle both create and edit
const editForm = document.getElementById("editEventForm") as HTMLFormElement;
if (editForm) {
editForm.addEventListener("submit", async (e) => {
e.preventDefault();
const modal = document.getElementById(
"editEventModal",
) as HTMLDialogElement;
try {
const formData = new FormData(editForm);
const eventId = (
document.getElementById("editEventId") as HTMLInputElement
).value;
// Get the date strings from the form
const startDateStr = formData.get("editEventStartDate") as string;
const endDateStr = formData.get("editEventEndDate") as string;
// Convert local dates to UTC ISO strings
const startDate = new Date(startDateStr);
const endDate = new Date(endDateStr);
// Get points value and ensure it's a number
const pointsValue = parseInt(formData.get("editEventPoints") as string);
const points = isNaN(pointsValue) ? 0 : pointsValue;
// Prepare base event data without attendees
const baseEventData = {
event_name: formData.get("editEventName"),
event_description: formData.get("editEventDescription"),
event_code: formData.get("editEventCode"),
location: formData.get("editEventLocation"),
points_to_reward: points,
start_date: startDate.toISOString(),
end_date: endDate.toISOString(),
published: formData.get("editEventPublished") === "on",
};
// For new events, add empty attendees list
const eventData = eventId
? baseEventData
: { ...baseEventData, attendees: [] };
// Update event details
auth.setUpdating(true);
try {
const pb = auth.getPocketBase();
let result;
if (eventId) {
// Update existing event
result = await update.updateFields("events", eventId, eventData);
} else {
// Create new event
result = await pb.collection("events").create(eventData);
}
// Upload temp files if any
if (tempFiles.length > 0) {
await fileManager.uploadFiles(
"events",
result.id,
"files",
tempFiles,
);
}
// Log the action
await sendLog.send(
eventId ? "update" : "create",
"events",
`${eventId ? "Updated" : "Created"} event ${eventData.event_name}`,
);
modal.close();
await fetchEvents(); // Refresh the list
} finally {
auth.setUpdating(false);
}
} catch (error) {
console.error("Failed to save event:", error);
alert("Failed to save event. Please try again.");
}
});
}
// File preview functionality
let currentFiles = [];
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 Details";
}
function showFilePreview(file: { url: string; type: string; name: string }) {
const filePreviewSection = document.getElementById("filePreviewSection");
const filesContent = document.getElementById("filesContent");
const previewContent = document.getElementById("previewContent");
const previewFileName = document.getElementById("previewFileName");
const modalTitle = document.getElementById("modalTitle");
if (
!filePreviewSection ||
!filesContent ||
!previewContent ||
!previewFileName
)
return;
filePreviewSection.classList.remove("hidden");
filesContent.classList.add("hidden");
previewFileName.textContent = file.name;
if (modalTitle) modalTitle.textContent = "File Preview";
const fileType = file.type.toLowerCase();
showLoading();
// Create the PocketBase URL
const baseUrl = "https://pocketbase.ieeeucsd.org";
const fileUrl = `${baseUrl}/api/files/events/${currentEventId}/${file.name}`;
if (!isPreviewableType(fileType)) {
previewContent.innerHTML = `
<div class="flex flex-col items-center justify-center p-8">
<div class="text-4xl mb-4">📄</div>
<p class="text-center">
This file type (${file.type}) cannot be previewed.
<br />
<a href="${fileUrl}" download="${file.name}" class="btn btn-primary mt-4" target="_blank" rel="noopener noreferrer">
Open in New Tab
</a>
</p>
</div>
`;
hideLoading();
return;
}
if (fileType.startsWith("image/")) {
previewContent.innerHTML = `
<img
src="${fileUrl}"
alt="${file.name}"
class="max-w-full max-h-[70vh] object-contain"
onload="hideLoading()"
onerror="handlePreviewError()"
/>
`;
} else if (fileType.startsWith("video/")) {
previewContent.innerHTML = `
<video controls class="max-w-full max-h-[70vh]" onloadeddata="hideLoading()" onerror="handlePreviewError()">
<source src="${fileUrl}" type="${file.type}" />
Your browser does not support the video tag.
</video>
`;
} else if (fileType === "application/pdf") {
previewContent.innerHTML = `
<iframe
src="${fileUrl}"
class="w-full h-[70vh]"
onload="hideLoading()"
onerror="handlePreviewError()"
></iframe>
`;
} else if (
fileType.startsWith("text/") ||
fileType === "application/json"
) {
previewContent.innerHTML = `
<iframe
src="${fileUrl}"
class="w-full h-[70vh] font-mono"
onload="hideLoading()"
onerror="handlePreviewError()"
></iframe>
`;
} else if (fileType.startsWith("audio/")) {
previewContent.innerHTML = `
<audio controls class="w-full" onloadeddata="hideLoading()" onerror="handlePreviewError()">
<source src="${fileUrl}" type="${file.type}" />
Your browser does not support the audio element.
</audio>
`;
}
}
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>
`;
}
}
// Update the openDetailsModal function
window.openDetailsModal = function (event: any) {
// Convert event times to local time
const localEvent = Get.convertUTCToLocal(event);
const modal = document.getElementById(
"eventDetailsModal",
) as HTMLDialogElement;
const filesContent = document.getElementById(
"filesContent",
) as HTMLDivElement;
const attendeesContent = document.getElementById(
"attendeesContent",
) as HTMLDivElement;
const filePreviewSection = document.getElementById(
"filePreviewSection",
) as HTMLDivElement;
const tabs = modal.querySelectorAll(
".tab",
) as NodeListOf<HTMLButtonElement>;
// Reset state
currentFiles = [];
currentEventId = localEvent.id;
if (filePreviewSection) filePreviewSection.classList.add("hidden");
if (filesContent) filesContent.classList.remove("hidden");
// Handle tab switching
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
tabs.forEach((t) => t.classList.remove("tab-active"));
tab.classList.add("tab-active");
const tabName = tab.getAttribute("data-tab");
if (tabName === "files") {
filesContent.classList.remove("hidden");
attendeesContent.classList.add("hidden");
filePreviewSection.classList.add("hidden");
} else {
filesContent.classList.add("hidden");
attendeesContent.classList.remove("hidden");
filePreviewSection.classList.add("hidden");
}
});
});
// Populate files content
if (
localEvent.files &&
Array.isArray(localEvent.files) &&
localEvent.files.length > 0
) {
const baseUrl = "https://pocketbase.ieeeucsd.org";
const collectionId = "events";
const recordId = localEvent.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>
${localEvent.files
.map((file: string) => {
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
const fileType = getFileType(file);
return `
<tr>
<td>${file}</td>
<td class="text-right">
<button class="btn btn-ghost btn-sm" onclick='window.showFilePreview(${JSON.stringify(
{
url: fileUrl,
type: fileType,
name: file,
},
)})'>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
</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>
`;
}
// TODO: Implement attendees list in the future
attendeesContent.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 d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
</svg>
<p>Attendees list coming soon</p>
</div>
`;
modal.showModal();
};
// Make helper functions available globally
window.showFilePreview = showFilePreview;
window.backToFileList = backToFileList;
window.handlePreviewError = handlePreviewError;
window.showLoading = showLoading;
window.hideLoading = hideLoading;
// Add deleteEvent function to window
window.deleteEvent = async function (eventId: string, eventName: string) {
if (
!confirm(
`Are you sure you want to delete "${eventName}"? This action cannot be undone.`,
)
) {
return;
}
try {
auth.setUpdating(true);
const pb = auth.getPocketBase();
await pb.collection("events").delete(eventId);
await sendLog.send("delete", "events", `Deleted event ${eventName}`);
await fetchEvents(); // Refresh the list
} catch (error) {
console.error("Failed to delete event:", error);
alert("Failed to delete event. Please try again.");
} finally {
auth.setUpdating(false);
}
};
async function fetchEvents() {
const eventsList = document.getElementById("eventsList");
const paginationContainer = document.getElementById("paginationContainer");
if (!eventsList || !paginationContainer) return;
try {
if (!auth.isAuthenticated()) {
eventsList.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 d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
<p>Please log in to view events</p>
</div>
`;
paginationContainer.classList.add("hidden");
return;
}
// Set updating flag to prevent auto-cancellation
auth.setUpdating(true);
try {
const response = await get.getList(
"events",
currentPage,
5,
"",
"-start_date",
{ disableAutoCancellation: true },
);
// Convert response items to local time
const localEvents = response.items.map((event) =>
Get.convertUTCToLocal(event),
);
// Update stats
const totalEventsEl = document.getElementById("totalEvents");
const showingEventsEl = document.getElementById("showingEvents");
const totalEventsLabelEl = document.getElementById("totalEventsLabel");
const currentPageEl = document.getElementById("currentPage");
const totalPagesLabelEl = document.getElementById("totalPagesLabel");
const currentPageNumber = document.getElementById("currentPageNumber");
if (totalEventsEl)
totalEventsEl.textContent = response.totalItems.toString();
if (showingEventsEl)
showingEventsEl.textContent = localEvents.length.toString();
if (totalEventsLabelEl)
totalEventsLabelEl.textContent = `of ${response.totalItems} Events`;
if (currentPageEl) currentPageEl.textContent = response.page.toString();
if (totalPagesLabelEl)
totalPagesLabelEl.textContent = `of ${response.totalPages}`;
if (currentPageNumber)
currentPageNumber.textContent = response.page.toString();
// Update pagination buttons state
const firstPageBtn = document.getElementById(
"firstPageBtn",
) as HTMLButtonElement;
const prevPageBtn = document.getElementById(
"prevPageBtn",
) as HTMLButtonElement;
const nextPageBtn = document.getElementById(
"nextPageBtn",
) as HTMLButtonElement;
const lastPageBtn = document.getElementById(
"lastPageBtn",
) as HTMLButtonElement;
if (firstPageBtn) firstPageBtn.disabled = response.page <= 1;
if (prevPageBtn) prevPageBtn.disabled = response.page <= 1;
if (nextPageBtn)
nextPageBtn.disabled = response.page >= response.totalPages;
if (lastPageBtn)
lastPageBtn.disabled = response.page >= response.totalPages;
// Show/hide pagination based on total pages
if (response.totalPages <= 1) {
paginationContainer.classList.add("hidden");
} else {
paginationContainer.classList.remove("hidden");
}
// Update events list
if (localEvents.length === 0) {
eventsList.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 d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
<p>No events found</p>
</div>
`;
return;
}
eventsList.innerHTML = localEvents
.map((event) => {
// Safely parse and format the date
let dateStr = "Invalid date";
try {
const date = new Date(event.start_date);
if (!isNaN(date.getTime())) {
dateStr = date.toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
} catch (e) {
console.error("Error formatting date:", e);
}
const locationStr = event.location ? `${event.location}` : "";
const codeStr = event.event_code ? `${event.event_code}` : "";
const detailsStr = [locationStr, codeStr]
.filter(Boolean)
.join(" | code: ");
// Properly escape the event data for use in onclick
const eventJson = JSON.stringify(event)
.replace(/'/g, "\\'")
.replace(/"/g, '\\"');
return `
<div class="flex items-center justify-between p-4 bg-base-200 rounded-xl hover:bg-base-300 transition-all duration-300">
<div class="flex items-center gap-4">
<div class="badge badge-lg p-3 badge-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z" />
</svg>
</div>
<div>
<h4 class="font-semibold">${event.event_name}</h4>
<p class="text-sm opacity-70">${dateStr}${detailsStr ? ` • ${detailsStr}` : ""}</p>
</div>
</div>
<div class="flex gap-2">
<button class="btn btn-ghost btn-sm" onclick='window.openDetailsModal(JSON.parse("${eventJson}"))'>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" />
</svg>
</button>
<button class="btn btn-ghost btn-sm" onclick='window.openEditModal(JSON.parse("${eventJson}"))'>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
</svg>
</button>
<button class="btn btn-ghost btn-sm text-error" onclick='window.deleteEvent("${event.id}", "${event.event_name}")'>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
`;
})
.join("");
totalPages = response.totalPages;
} finally {
auth.setUpdating(false);
}
} catch (error) {
console.error("Failed to fetch events:", error);
eventsList.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 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>
<span>Failed to load events. Please try refreshing the page.</span>
</div>
`;
paginationContainer.classList.add("hidden");
}
}
// Add pagination event listeners
document.getElementById("firstPageBtn")?.addEventListener("click", () => {
if (currentPage > 1) {
currentPage = 1;
fetchEvents();
}
});
document.getElementById("prevPageBtn")?.addEventListener("click", () => {
if (currentPage > 1) {
currentPage--;
fetchEvents();
}
});
document.getElementById("nextPageBtn")?.addEventListener("click", () => {
if (currentPage < totalPages) {
currentPage++;
fetchEvents();
}
});
document.getElementById("lastPageBtn")?.addEventListener("click", () => {
if (currentPage < totalPages) {
currentPage = totalPages;
fetchEvents();
}
});
// Initial fetch - only call once
fetchEvents();
</script>