ieeeucsd-org/src/components/dashboard/Officer_EventManagement.astro
2025-02-13 05:30:07 -08:00

2363 lines
97 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";
import FilePreview from "../modals/FilePreview";
// 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;
has_food: boolean;
attendees: AttendeeEntry[];
}
interface AttendeeEntry {
user_id: string;
time_checked_in: string;
food: string;
}
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>;
resetAndCloseModal: () => void;
previewFileInEditModal: (url: string, filename: string) => 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" id="quarterLabel">
Current Academic Term
</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">
Unique Attendees
</div>
<div class="stat-value text-secondary" id="uniqueAttendees">
-
</div>
<div class="stat-desc flex items-center gap-2 mt-1">
<div class="badge badge-secondary badge-sm">
Current Academic Term
</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">
Recurring Attendees
</div>
<div class="stat-value text-accent" id="recurringAttendees">
-
</div>
<div class="stat-desc flex items-center gap-2 mt-1">
<div class="badge badge-accent badge-sm">
Current Quarter (<span id="quarterName">-</span>)
</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>
<!-- Filter Controls -->
<div class="mb-4">
<!-- All Filters in One Line -->
<div class="flex flex-wrap items-end gap-4">
<div class="form-control">
<label class="label">
<span class="label-text font-medium"
>Time Filter</span
>
</label>
<div class="join m-1">
<input
type="radio"
name="timeFilter"
value="all"
class="join-item btn btn-sm"
checked
aria-label="All Years"
/>
<input
type="radio"
name="timeFilter"
value="upcoming"
class="join-item btn btn-sm"
aria-label="Upcoming"
/>
<input
type="radio"
name="timeFilter"
value="past"
class="join-item btn btn-sm"
aria-label="Past"
/>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Year</span>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[180px] justify-between"
>
<span id="yearFilterLabel">All Years</span>
<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="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">All Years</span
>
<input
type="checkbox"
class="checkbox"
value="all"
checked
/>
</label>
</div>
<div id="yearCheckboxes" class="space-y-1">
<!-- Year checkboxes will be populated dynamically -->
</div>
</div>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Quarter</span>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[180px] justify-between"
>
<span id="quarterFilterLabel">All Quarters</span
>
<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="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
id="quarterDropdownContent"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text"
>All Quarters</span
>
<input
type="checkbox"
class="checkbox"
value="all"
checked
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Fall</span>
<input
type="checkbox"
class="checkbox"
value="fall"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Winter</span>
<input
type="checkbox"
class="checkbox"
value="winter"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Spring</span>
<input
type="checkbox"
class="checkbox"
value="spring"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Summer</span>
<input
type="checkbox"
class="checkbox"
value="summer"
/>
</label>
</div>
</div>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Published</span
>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[140px] justify-between"
>
<span id="publishedFilterLabel">All</span>
<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="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">All</span>
<input
type="radio"
name="publishedFilter"
class="radio"
value="all"
checked
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Yes</span>
<input
type="radio"
name="publishedFilter"
class="radio"
value="yes"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">No</span>
<input
type="radio"
name="publishedFilter"
class="radio"
value="no"
/>
</label>
</div>
</div>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Has Files</span
>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[140px] justify-between"
>
<span id="hasFilesFilterLabel">All</span>
<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="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">All</span>
<input
type="radio"
name="hasFilesFilter"
class="radio"
value="all"
checked
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Yes</span>
<input
type="radio"
name="hasFilesFilter"
class="radio"
value="yes"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">No</span>
<input
type="radio"
name="hasFilesFilter"
class="radio"
value="no"
/>
</label>
</div>
</div>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text font-medium">Has Food</span>
</label>
<div class="dropdown">
<label
tabindex="0"
class="btn btn-sm m-1 w-[140px] justify-between"
>
<span id="hasFoodFilterLabel">All</span>
<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="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"></path>
</svg>
</label>
<div
tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">All</span>
<input
type="radio"
name="hasFoodFilter"
class="radio"
value="all"
checked
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Yes</span>
<input
type="radio"
name="hasFoodFilter"
class="radio"
value="yes"
/>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">No</span>
<input
type="radio"
name="hasFoodFilter"
class="radio"
value="no"
/>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Search and Per Page Controls -->
<div class="flex flex-col md:flex-row gap-4 mb-4">
<div class="form-control flex-1">
<div class="join w-full">
<div
class="join-item bg-base-200 flex items-center px-3"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 opacity-70"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
clip-rule="evenodd"></path>
</svg>
</div>
<input
type="text"
id="searchInput"
placeholder="Search events..."
class="input input-bordered join-item w-full"
/>
</div>
</div>
<div class="form-control w-full md:w-auto">
<div class="join">
<div
class="join-item bg-base-200 flex items-center px-3"
>
Per Page
</div>
<select
id="perPageSelect"
class="select select-bordered join-item"
>
<option value="5">5</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
</select>
</div>
</div>
</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">
<!-- Main Edit Form Section -->
<div id="editFormSection">
<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>
<!-- Has Food -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
id="editEventHasFood"
name="editEventHasFood"
class="toggle"
/>
<span class="label-text">Has Food</span>
</label>
<label class="label">
<span class="label-text-alt text-info"
>Check this if food will be provided at the event</span
>
</label>
</div>
</form>
</div>
<div class="modal-action">
<button type="submit" class="btn btn-primary" form="editEventForm"
>Save Changes</button
>
<button type="button" class="btn" onclick="editEventModal.close()"
>Cancel</button
>
</div>
</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">
<h3 class="font-bold text-lg">Event Details</h3>
<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">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Event Attendees</h3>
<button
id="downloadAttendeesCSV"
class="btn btn-primary btn-sm gap-2"
>
<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"></path>
</svg>
Download CSV
</button>
</div>
<!-- Attendees list will be populated here -->
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- Universal File Preview Modal -->
<dialog id="filePreviewModal" 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">
<button
class="btn btn-ghost btn-sm"
onclick="window.closeFilePreview()"
>
← Back
</button>
<h3 class="font-bold text-lg truncate" id="previewFileName">
</h3>
</div>
</div>
<div class="relative" id="previewContainer">
<div
id="previewLoadingSpinner"
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">
<FilePreview
client:load
url=""
filename=""
id="universalFilePreview"
/>
</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";
import FilePreview from "../modals/FilePreview";
// Add file storage
const selectedFileStorage = new Map<string, File>();
interface AttendeeEntry {
user_id: string;
time_checked_in: string;
food: string;
}
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;
has_food: boolean;
attendees: AttendeeEntry[];
}
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;
let searchQuery = "";
let perPage = 5;
let quarterlyStats = {
totalEvents: 0,
uniqueAttendees: 0,
recurringAttendees: 0,
};
// Add filter state
let filterState = {
time: "all",
year: ["all"],
quarter: ["all"],
published: "all",
hasFiles: "all",
hasFood: "all",
};
// Add cache for events
let cachedEvents: Event[] = [];
let lastCacheUpdate = 0; // Will force a refresh on page load
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
// Function to refresh cache
async function refreshCache() {
try {
const now = Date.now();
if (
lastCacheUpdate === 0 ||
now - lastCacheUpdate >= CACHE_DURATION ||
cachedEvents.length === 0
) {
auth.setUpdating(true);
const response = await get.getAll<Event>("events");
cachedEvents = response.map((event) =>
Get.convertUTCToLocal(event)
);
lastCacheUpdate = now;
// Initialize year filter options from cache
const years = new Set<number>();
cachedEvents.forEach((event) => {
const year = new Date(event.start_date).getFullYear();
years.add(year);
});
const yearCheckboxes =
document.getElementById("yearCheckboxes");
if (yearCheckboxes) {
const sortedYears = Array.from(years).sort((a, b) => b - a);
yearCheckboxes.innerHTML = sortedYears
.map(
(year) => `
<label class="label cursor-pointer">
<span class="label-text">${year}</span>
<input type="checkbox" class="checkbox" value="${year}" />
</label>
`
)
.join("");
// Add event listeners to checkboxes
const allYearsCheckbox = document.querySelector(
'input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const yearInputs = Array.from(
yearCheckboxes.querySelectorAll(
'input[type="checkbox"]'
)
) as HTMLInputElement[];
if (allYearsCheckbox) {
allYearsCheckbox.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
yearInputs.forEach((input) => {
input.checked = false;
});
if (target.checked) {
filterState.year = ["all"];
document.getElementById(
"yearFilterLabel"
)!.textContent = "All Years";
}
currentPage = 1;
fetchEvents();
});
}
yearInputs.forEach((input) => {
input.addEventListener("change", () => {
const checkedYears = yearInputs
.filter((inp) => inp.checked)
.map((inp) => inp.value);
if (checkedYears.length === 0) {
allYearsCheckbox.checked = true;
filterState.year = ["all"];
document.getElementById(
"yearFilterLabel"
)!.textContent = "All Years";
} else {
allYearsCheckbox.checked = false;
filterState.year = checkedYears;
document.getElementById(
"yearFilterLabel"
)!.textContent =
checkedYears.length === 1
? checkedYears[0]
: `${checkedYears.length} Years Selected`;
}
currentPage = 1;
fetchEvents();
});
});
}
}
} catch (error) {
console.error("Failed to refresh cache:", error);
throw error;
} finally {
auth.setUpdating(false);
}
}
// Helper function to determine current academic term
function getCurrentTerm(): { start: Date; end: Date } {
const now = new Date();
const year = now.getFullYear();
let start: Date, end: Date;
// If we're before September, term started last year
if (now.getMonth() < 8) {
// Before September
start = new Date(year - 1, 8, 1); // Sept 1 of last year
end = new Date(year, 8, 0); // Aug 31 of current year
} else {
start = new Date(year, 8, 1); // Sept 1 of current year
end = new Date(year + 1, 8, 0); // Aug 31 of next year
}
return { start, end };
}
// Helper function to determine current quarter
function getCurrentQuarter(): { start: Date; end: Date } {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
let start: Date, end: Date;
// Determine quarter (0-based months: 0-11)
// Q1: Sept-Dec (8-11)
// Q2: Jan-Mar (0-2)
// Q3: Mar-Jun (2-5)
// Q4: Jun-Sept (5-8)
if (month >= 8) {
// Q1: Sept-Dec
start = new Date(year, 8, 1);
end = new Date(year, 11, 31);
} else if (month < 2) {
// Q2: Jan-Mar
start = new Date(year, 0, 1);
end = new Date(year, 2, 31);
} else if (month < 5) {
// Q3: Mar-Jun
start = new Date(year, 2, 1);
end = new Date(year, 5, 30);
} else {
// Q4: Jun-Sept
start = new Date(year, 5, 1);
end = new Date(year, 8, 0); // End on Aug 31
}
return { start, end };
}
// Helper function to get quarter name
function getQuarterName(): string {
const month = new Date().getMonth();
if (month >= 8) {
// Sept-Dec
return "Fall";
} else if (month < 2) {
// Jan-Mar
return "Winter";
} else if (month < 5) {
// Mar-Jun
return "Spring";
} else {
// Jun-Sept
return "Summer";
}
}
// Function to apply filters to cached data
function filterEvents(events: Event[]): Event[] {
return events.filter((event) => {
const now = new Date().toISOString();
// Time filter
if (filterState.time === "upcoming" && event.start_date < now)
return false;
if (filterState.time === "past" && event.start_date >= now)
return false;
// Year filter
if (!filterState.year.includes("all")) {
const eventYear = new Date(event.start_date)
.getFullYear()
.toString();
if (!filterState.year.includes(eventYear)) return false;
}
// Quarter filter
if (!filterState.quarter.includes("all")) {
const eventDate = new Date(event.start_date);
const month = eventDate.getMonth();
let isInSelectedQuarter = false;
for (const quarter of filterState.quarter) {
let isInQuarter = false;
switch (quarter) {
case "fall":
isInQuarter = month >= 8 && month <= 11; // Sept-Dec
break;
case "winter":
isInQuarter = month >= 0 && month <= 2; // Jan-Mar
break;
case "spring":
isInQuarter = month >= 2 && month <= 5; // Mar-Jun
break;
case "summer":
isInQuarter = month >= 5 && month <= 8; // Jun-Sept
break;
}
if (isInQuarter) {
isInSelectedQuarter = true;
break;
}
}
if (!isInSelectedQuarter) return false;
}
// Published filter
if (filterState.published !== "all") {
if ((filterState.published === "yes") !== event.published)
return false;
}
// Has Files filter
if (filterState.hasFiles !== "all") {
const hasFiles = event.files && event.files.length > 0;
if ((filterState.hasFiles === "yes") !== hasFiles) return false;
}
// Has Food filter
if (filterState.hasFood !== "all") {
if ((filterState.hasFood === "yes") !== event.has_food)
return false;
}
// Search query
if (searchQuery) {
const searchLower = searchQuery.toLowerCase();
const searchTerms = searchLower.split(/\s+/).filter(Boolean);
return (
searchTerms.length === 0 || // If no search terms, return true
searchTerms.every(
(term) =>
event.event_name.toLowerCase().includes(term) ||
event.event_code.toLowerCase().includes(term) ||
event.location.toLowerCase().includes(term) ||
event.event_description.toLowerCase().includes(term)
)
);
}
return true;
});
}
// Fetch and display events using cached data
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;
}
// Refresh cache if needed
await refreshCache();
// Apply filters to cached data
const filteredEvents = filterEvents(cachedEvents);
// Sort events by start date (newest first)
filteredEvents.sort(
(a, b) =>
new Date(b.start_date).getTime() -
new Date(a.start_date).getTime()
);
// Calculate pagination
const totalItems = filteredEvents.length;
totalPages = Math.ceil(totalItems / perPage);
const startIndex = (currentPage - 1) * perPage;
const endIndex = startIndex + perPage;
const paginatedEvents = filteredEvents.slice(startIndex, endIndex);
// Update pagination UI
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;
const currentPageNumber =
document.getElementById("currentPageNumber");
if (firstPageBtn) firstPageBtn.disabled = currentPage <= 1;
if (prevPageBtn) prevPageBtn.disabled = currentPage <= 1;
if (nextPageBtn) nextPageBtn.disabled = currentPage >= totalPages;
if (lastPageBtn) lastPageBtn.disabled = currentPage >= totalPages;
if (currentPageNumber)
currentPageNumber.textContent = currentPage.toString();
// Show/hide pagination based on total pages
if (totalPages <= 1) {
paginationContainer.classList.add("hidden");
} else {
paginationContainer.classList.remove("hidden");
}
// Update events list
if (paginatedEvents.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 = paginatedEvents
.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: ");
// Store event data in a global map using the event ID as key
const eventDataId = `event_${event.id}`;
window[eventDataId] = event;
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(window['${eventDataId}'])">
<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(window['${eventDataId}'])">
<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.replace(/'/g, "\\'")}')">
<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("");
// Calculate and update stats using cached data
await calculateQuarterlyStats();
} 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");
}
}
// Calculate quarterly stats using cached data
async function calculateQuarterlyStats() {
try {
const { start: termStart, end: termEnd } = getCurrentTerm();
const { start: quarterStart, end: quarterEnd } =
getCurrentQuarter();
// Update quarter name in UI
const quarterNameEl = document.getElementById("quarterName");
if (quarterNameEl) {
quarterNameEl.textContent = getQuarterName();
}
// Filter events for current term and quarter
const termEvents = cachedEvents.filter((event) => {
const eventDate = new Date(event.start_date);
return eventDate >= termStart && eventDate <= termEnd;
});
const quarterEvents = cachedEvents.filter((event) => {
const eventDate = new Date(event.start_date);
return eventDate >= quarterStart && eventDate <= quarterEnd;
});
// Calculate stats
quarterlyStats.totalEvents = termEvents.length;
const termAttendees = new Map<string, number>();
termEvents.forEach((event) => {
event.attendees?.forEach((attendee) => {
const count = termAttendees.get(attendee.user_id) || 0;
termAttendees.set(attendee.user_id, count + 1);
});
});
quarterlyStats.uniqueAttendees = termAttendees.size;
const quarterAttendees = new Map<string, number>();
quarterEvents.forEach((event) => {
event.attendees?.forEach((attendee) => {
const count = quarterAttendees.get(attendee.user_id) || 0;
quarterAttendees.set(attendee.user_id, count + 1);
});
});
quarterlyStats.recurringAttendees = Array.from(
quarterAttendees.values()
).filter((count) => count > 1).length;
// Update the UI
const totalEventsEl = document.getElementById("totalEvents");
const uniqueAttendeesEl =
document.getElementById("uniqueAttendees");
const recurringAttendeesEl =
document.getElementById("recurringAttendees");
if (totalEventsEl)
totalEventsEl.textContent =
quarterlyStats.totalEvents.toString();
if (uniqueAttendeesEl)
uniqueAttendeesEl.textContent =
quarterlyStats.uniqueAttendees.toString();
if (recurringAttendeesEl)
recurringAttendeesEl.textContent =
quarterlyStats.recurringAttendees.toString();
} catch (error) {
console.error("Failed to calculate quarterly stats:", error);
}
}
// 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();
}
});
// Add filter event listeners
// Time filter
document.querySelectorAll('input[name="timeFilter"]').forEach((radio) => {
radio.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
filterState.time = target.value;
currentPage = 1;
fetchEvents();
});
});
// Year filter
document
.getElementById("yearCheckboxes")
?.addEventListener("change", () => {
const checkedYears = Array.from(
document.querySelectorAll('input[type="checkbox"]:checked')
).map((inp) => (inp as HTMLInputElement).value);
if (checkedYears.length === 0 || checkedYears.includes("all")) {
document.getElementById("yearFilterLabel")!.textContent =
"All Years";
filterState.year = ["all"];
} else {
document.getElementById("yearFilterLabel")!.textContent =
checkedYears.length === 1
? checkedYears[0]
: `${checkedYears.length} Years Selected`;
filterState.year = checkedYears;
}
currentPage = 1;
fetchEvents();
});
// Quarter filter
document
.getElementById("quarterDropdownContent")
?.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
if (!target.matches('input[type="checkbox"]')) return;
const allQuartersCheckbox = target
.closest("#quarterDropdownContent")
?.querySelector(
'input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const quarterInputs = Array.from(
target
.closest("#quarterDropdownContent")
?.querySelectorAll('input[type="checkbox"]') || []
).filter(
(inp) => (inp as HTMLInputElement).value !== "all"
) as HTMLInputElement[];
if (target.value === "all" && target.checked) {
quarterInputs.forEach((input) => {
input.checked = false;
});
filterState.quarter = ["all"];
document.getElementById("quarterFilterLabel")!.textContent =
"All Quarters";
} else {
const checkedQuarters = quarterInputs
.filter((inp) => inp.checked)
.map((inp) => inp.value);
if (checkedQuarters.length === 0) {
allQuartersCheckbox.checked = true;
filterState.quarter = ["all"];
document.getElementById("quarterFilterLabel")!.textContent =
"All Quarters";
} else {
allQuartersCheckbox.checked = false;
filterState.quarter = checkedQuarters;
document.getElementById("quarterFilterLabel")!.textContent =
checkedQuarters.length === 1
? checkedQuarters[0].charAt(0).toUpperCase() +
checkedQuarters[0].slice(1)
: `${checkedQuarters.length} Quarters Selected`;
}
}
currentPage = 1;
fetchEvents();
});
// Published filter
document
.querySelectorAll('input[name="publishedFilter"]')
.forEach((radio: Element) => {
(radio as HTMLInputElement).addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
filterState.published = target.value;
document.getElementById("publishedFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")
?.textContent || "All";
currentPage = 1;
fetchEvents();
});
});
// Has Files filter
document
.querySelectorAll('input[name="hasFilesFilter"]')
.forEach((radio: Element) => {
(radio as HTMLInputElement).addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
filterState.hasFiles = target.value;
document.getElementById("hasFilesFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")
?.textContent || "All";
currentPage = 1;
fetchEvents();
});
});
// Has Food filter
document
.querySelectorAll('input[name="hasFoodFilter"]')
.forEach((radio: Element) => {
(radio as HTMLInputElement).addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
filterState.hasFood = target.value;
document.getElementById("hasFoodFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")
?.textContent || "All";
currentPage = 1;
fetchEvents();
});
});
// Search input with instant search (no debounce needed for cached data)
const searchInput = document.getElementById("searchInput");
if (searchInput) {
// Add input event for instant search
searchInput.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement;
searchQuery = target.value.trim();
currentPage = 1;
fetchEvents();
});
// Clear search on Escape key
searchInput.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
(e.target as HTMLInputElement).value = "";
searchQuery = "";
currentPage = 1;
fetchEvents();
}
});
}
// Per page select
document
.getElementById("perPageSelect")
?.addEventListener("change", (e) => {
const target = e.target as HTMLSelectElement;
perPage = parseInt(target.value);
currentPage = 1;
fetchEvents();
});
// Initial setup
if (typeof window !== "undefined") {
window.addEventListener("DOMContentLoaded", () => {
// Reset all filters to defaults
const timeFilterAll = document.querySelector(
'input[name="timeFilter"][value="all"]'
) as HTMLInputElement;
if (timeFilterAll) {
// Ensure the radio button is properly checked
timeFilterAll.checked = true;
filterState.time = "all";
}
// Reset year filter
const yearAllCheckbox = document.querySelector(
'input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const yearCheckboxes = document.querySelectorAll(
'#yearCheckboxes input[type="checkbox"]'
);
if (yearAllCheckbox && yearCheckboxes) {
yearAllCheckbox.checked = true;
yearCheckboxes.forEach((checkbox: Element) => {
(checkbox as HTMLInputElement).checked = false;
});
filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent =
"All Years";
}
// Reset quarter filter
const quarterAllCheckbox = document.querySelector(
'#quarterDropdownContent input[type="checkbox"][value="all"]'
) as HTMLInputElement;
const quarterCheckboxes = document.querySelectorAll(
'#quarterDropdownContent input[type="checkbox"]:not([value="all"])'
);
if (quarterAllCheckbox && quarterCheckboxes) {
quarterAllCheckbox.checked = true;
quarterCheckboxes.forEach((checkbox: Element) => {
(checkbox as HTMLInputElement).checked = false;
});
filterState.quarter = ["all"];
document.getElementById("quarterFilterLabel")!.textContent =
"All Quarters";
}
const publishedFilter = document.getElementById(
"publishedFilter"
) as HTMLSelectElement;
if (publishedFilter) publishedFilter.value = "all";
const hasFilesFilter = document.getElementById(
"hasFilesFilter"
) as HTMLSelectElement;
if (hasFilesFilter) hasFilesFilter.value = "all";
const hasFoodFilter = document.getElementById(
"hasFoodFilter"
) as HTMLSelectElement;
if (hasFoodFilter) hasFoodFilter.value = "all";
const searchInput = document.getElementById(
"searchInput"
) as HTMLInputElement;
if (searchInput) searchInput.value = "";
const perPageSelect = document.getElementById(
"perPageSelect"
) as HTMLSelectElement;
if (perPageSelect) perPageSelect.value = "5";
// Reset variables
searchQuery = "";
perPage = 5;
currentPage = 1;
// Reset filter state
filterState = {
time: "all",
year: ["all"],
quarter: ["all"],
published: "all",
hasFiles: "all",
hasFood: "all",
};
// Fetch events with reset filters
fetchEvents();
});
}
// Initial fetch
fetchEvents();
// Add openEditModal function
window.openEditModal = async function (event?: any) {
const modal = document.getElementById(
"editEventModal"
) as HTMLDialogElement;
const form = document.getElementById(
"editEventForm"
) as HTMLFormElement;
const modalTitle = document.getElementById("editModalTitle");
const currentFiles = document.getElementById("currentFiles");
const newFiles = document.getElementById("newFiles");
if (!modal || !form) return;
try {
// Clear previous form data
form.reset();
if (currentFiles) currentFiles.innerHTML = "";
if (newFiles) newFiles.innerHTML = "";
// Set modal title based on whether we're editing or creating
if (modalTitle) {
modalTitle.textContent = event ? "Edit Event" : "Add New Event";
}
if (event) {
// Fetch fresh data from PocketBase for the event
const freshEventData = await get.getOne<Event>(
"events",
event.id
);
// Populate form with fresh data
const idInput = document.getElementById(
"editEventId"
) as HTMLInputElement;
const nameInput = document.getElementById(
"editEventName"
) as HTMLInputElement;
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 descriptionInput = document.getElementById(
"editEventDescription"
) as HTMLTextAreaElement;
const publishedInput = document.getElementById(
"editEventPublished"
) as HTMLInputElement;
const hasFoodInput = document.getElementById(
"editEventHasFood"
) as HTMLInputElement;
if (idInput) idInput.value = freshEventData.id;
if (nameInput) nameInput.value = freshEventData.event_name;
if (codeInput) codeInput.value = freshEventData.event_code;
if (locationInput)
locationInput.value = freshEventData.location;
if (pointsInput)
pointsInput.value =
freshEventData.points_to_reward.toString();
if (startDateInput)
startDateInput.value = new Date(freshEventData.start_date)
.toISOString()
.slice(0, 16);
if (endDateInput)
endDateInput.value = new Date(freshEventData.end_date)
.toISOString()
.slice(0, 16);
if (descriptionInput)
descriptionInput.value = freshEventData.event_description;
if (publishedInput)
publishedInput.checked = freshEventData.published;
if (hasFoodInput)
hasFoodInput.checked = freshEventData.has_food;
// Display current files if any
if (
currentFiles &&
freshEventData.files &&
freshEventData.files.length > 0
) {
currentFiles.innerHTML = updateFilePreviewButtons(
freshEventData.files,
freshEventData.id
);
}
}
// Show the modal
modal.showModal();
} catch (error) {
console.error("Failed to open edit modal:", error);
// Show error toast or alert
}
};
// Update the previewFileInEditModal function
window.previewFileInEditModal = async function (
url: string,
filename: string
) {
const editFormSection = document.getElementById("editFormSection");
const previewSection = document.getElementById(
"editModalPreviewSection"
);
const editFilePreview = document.getElementById("editFilePreview");
const previewFileName = document.getElementById("editPreviewFileName");
const loadingSpinner = document.getElementById("editLoadingSpinner");
if (
!editFormSection ||
!previewSection ||
!editFilePreview ||
!previewFileName ||
!loadingSpinner
)
return;
// Hide form and show preview section
editFormSection.classList.add("hidden");
previewSection.classList.remove("hidden");
previewFileName.textContent = filename;
// Show loading spinner
loadingSpinner.classList.remove("hidden");
try {
// Dispatch a custom event to update the FilePreview
const event = new CustomEvent("updateFilePreview", {
detail: { url, filename },
});
editFilePreview.dispatchEvent(event);
} finally {
// Hide loading spinner
loadingSpinner.classList.add("hidden");
}
};
// Update the showFilePreview function
window.showFilePreview = function (file: { url: string; name: string }) {
const fileListSection = document.getElementById("filesContent");
const previewSection = document.getElementById("filePreviewSection");
const mainFilePreview = document.getElementById("mainFilePreview");
const previewFileName = document.getElementById("previewFileName");
if (
!fileListSection ||
!previewSection ||
!mainFilePreview ||
!previewFileName
)
return;
// Hide file list and show preview section
fileListSection.classList.add("hidden");
previewSection.classList.remove("hidden");
previewFileName.textContent = file.name;
// Dispatch a custom event to update the FilePreview
const event = new CustomEvent("updateFilePreview", {
detail: { url: file.url, filename: file.name },
});
mainFilePreview.dispatchEvent(event);
};
// Add backToEditForm function
window.backToEditForm = function () {
const editFormSection = document.getElementById("editFormSection");
const previewSection = document.getElementById(
"editModalPreviewSection"
);
const editFilePreview = document.getElementById("editFilePreview");
const previewFileName = document.getElementById("editPreviewFileName");
if (
editFormSection &&
previewSection &&
editFilePreview &&
previewFileName
) {
editFormSection.classList.remove("hidden");
previewSection.classList.add("hidden");
// Reset the preview
const event = new CustomEvent("updateFilePreview", {
detail: { url: "", filename: "" },
});
editFilePreview.dispatchEvent(event);
previewFileName.textContent = "";
}
};
// Universal file preview function
window.previewFile = function (url: string, filename: string) {
const modal = document.getElementById(
"filePreviewModal"
) as HTMLDialogElement;
const previewFileName = document.getElementById("previewFileName");
const filePreview = document.getElementById("universalFilePreview");
const loadingSpinner = document.getElementById("previewLoadingSpinner");
if (!modal || !previewFileName || !filePreview || !loadingSpinner)
return;
// Show modal and update filename
modal.showModal();
previewFileName.textContent = filename;
// Show loading spinner
loadingSpinner.classList.remove("hidden");
try {
// Update the FilePreview component
const event = new CustomEvent("updateFilePreview", {
detail: { url, filename },
});
filePreview.dispatchEvent(event);
} finally {
// Hide loading spinner
loadingSpinner.classList.add("hidden");
}
};
// Close file preview
window.closeFilePreview = function () {
const modal = document.getElementById(
"filePreviewModal"
) as HTMLDialogElement;
const filePreview = document.getElementById("universalFilePreview");
if (modal && filePreview) {
// Reset the preview
const event = new CustomEvent("updateFilePreview", {
detail: { url: "", filename: "" },
});
filePreview.dispatchEvent(event);
modal.close();
}
};
// Update all file preview buttons to use the universal preview
function updateFilePreviewButtons(files: string[], eventId: string) {
return files
.map(
(filename) => `
<div class="flex items-center justify-between p-2 bg-base-200 rounded-lg">
<span class="truncate">${filename}</span>
<div class="flex gap-2">
<button type="button" class="btn btn-ghost btn-xs" onclick="window.previewFile('${fileManager.getFileUrl("events", eventId, filename)}', '${filename}')">
<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>
<button type="button" class="btn btn-ghost btn-xs text-error" onclick="window.deleteFile('${eventId}', '${filename}')">
<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="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("");
}
// Update the openDetailsModal function to use the universal preview
window.openDetailsModal = function (event: Event) {
const modal = document.getElementById(
"eventDetailsModal"
) as HTMLDialogElement;
const filesContent = document.getElementById("filesContent");
const attendeesContent = document.getElementById("attendeesContent");
if (!modal || !filesContent || !attendeesContent) return;
// Show modal
modal.showModal();
// Update files list
if (event.files && event.files.length > 0) {
filesContent.innerHTML = `
<div class="space-y-2">
${updateFilePreviewButtons(event.files, event.id)}
</div>
`;
} else {
filesContent.innerHTML = `
<div class="text-center py-8 text-base-content/70">
<p>No files attached to this event</p>
</div>
`;
}
};
// Add file input change handler to show selected files
document
.getElementById("editEventFiles")
?.addEventListener("change", function (e) {
const fileInput = e.target as HTMLInputElement;
const newFiles = document.getElementById("newFiles");
if (newFiles && fileInput.files) {
// Get existing files if any
const existingFiles = newFiles.querySelectorAll(".file-item");
const existingFilesArray = Array.from(existingFiles).map(
(item) => {
const nameSpan = item.querySelector(".file-name");
return nameSpan ? nameSpan.textContent : "";
}
);
// Store new files in the storage and update UI
Array.from(fileInput.files)
.filter((file) => !existingFilesArray.includes(file.name))
.forEach((file) => {
selectedFileStorage.set(file.name, file);
const fileDiv = document.createElement("div");
fileDiv.className =
"flex items-center justify-between p-2 bg-base-200 rounded-lg file-item";
fileDiv.innerHTML = `
<span class="truncate file-name">${file.name}</span>
<div class="flex gap-2">
<div class="badge badge-primary">New</div>
<button type="button" class="btn btn-ghost btn-xs text-error" onclick="this.closest('.file-item').remove(); selectedFileStorage.delete('${file.name}');">
<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="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>
`;
newFiles.appendChild(fileDiv);
});
}
// Clear the file input
fileInput.value = "";
});
// Modify form submission handler to use selectedFileStorage
document
.getElementById("editEventForm")
?.addEventListener("submit", async function (e) {
e.preventDefault();
const form = e.target as HTMLFormElement;
// Get the submit and cancel buttons from the modal-action div
const modalAction = document.querySelector(".modal-action");
const submitButton = modalAction?.querySelector(
".btn-primary"
) as HTMLButtonElement;
const cancelButton = modalAction?.querySelector(
".btn:not(.btn-primary)"
) as HTMLButtonElement;
if (!submitButton || !cancelButton) {
console.error("Could not find submit or cancel buttons");
return;
}
const originalText = submitButton.innerHTML;
try {
const formData = new FormData(form);
const eventId = (
document.getElementById("editEventId") as HTMLInputElement
)?.value;
// Get files from storage
const selectedFiles = Array.from(selectedFileStorage.values());
// Disable buttons and show loading state
submitButton.disabled = true;
cancelButton.disabled = true;
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Saving...</span>
</div>
`;
window.showLoading?.();
// Prepare event data
const eventData = {
event_name: formData.get("editEventName"),
event_code: formData.get("editEventCode"),
event_description: formData.get("editEventDescription"),
location: formData.get("editEventLocation"),
points_to_reward: Number(formData.get("editEventPoints")),
start_date: new Date(
formData.get("editEventStartDate") as string
).toISOString(),
end_date: new Date(
formData.get("editEventEndDate") as string
).toISOString(),
published: formData.get("editEventPublished") === "on",
has_food: formData.get("editEventHasFood") === "on",
};
let updatedEvent;
try {
if (eventId) {
// Update existing event
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Updating event...</span>
</div>
`;
updatedEvent = await update.updateFields(
"events",
eventId,
eventData
);
// Handle file uploads if any
if (selectedFiles.length > 0) {
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Uploading files (0/${selectedFiles.length})...</span>
</div>
`;
await fileManager.appendFiles(
"events",
eventId,
"files",
selectedFiles
);
}
await sendLog.send(
"update",
"event",
`Updated event: ${eventData.event_name}`
);
} else {
// Create new event
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Creating event...</span>
</div>
`;
const pb = auth.getPocketBase();
const newEvent = await pb
.collection("events")
.create(eventData);
// Handle file uploads if any
if (selectedFiles.length > 0) {
submitButton.innerHTML = `
<div class="flex items-center gap-2">
<span class="loading loading-spinner loading-sm"></span>
<span>Uploading files (0/${selectedFiles.length})...</span>
</div>
`;
await fileManager.uploadFiles(
"events",
newEvent.id,
"files",
selectedFiles
);
}
await sendLog.send(
"create",
"event",
`Created event: ${eventData.event_name}`
);
}
// Show success state briefly
submitButton.innerHTML = `
<div class="flex items-center gap-2 text-success">
<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="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
<span>Saved!</span>
</div>
`;
await new Promise((resolve) => setTimeout(resolve, 1000));
// Close modal and refresh events list
const modal = document.getElementById(
"editEventModal"
) as HTMLDialogElement;
modal?.close();
// Force cache refresh and update events list
lastCacheUpdate = 0; // Reset cache timestamp to force refresh
await refreshCache(); // Refresh the cache
await fetchEvents(); // Update the UI
// Clear form inputs
const formFileInput = document.getElementById(
"editEventFiles"
) as HTMLInputElement;
const newFiles = document.getElementById("newFiles");
if (formFileInput) formFileInput.value = "";
if (newFiles) newFiles.innerHTML = "";
// Clear storage after successful upload
selectedFileStorage.clear();
} catch (error) {
throw error; // Re-throw to be caught by outer try-catch
}
} catch (error) {
console.error("Failed to save event:", error);
// Show error message in the button with icon
submitButton.innerHTML = `
<div class="flex items-center gap-2 text-error">
<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="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
<span>Failed</span>
</div>
`;
await new Promise((resolve) => setTimeout(resolve, 2000));
// Show detailed error to user
alert("Failed to save event. Please try again.");
} finally {
// Reset button state
submitButton.disabled = false;
cancelButton.disabled = false;
submitButton.innerHTML = originalText;
window.hideLoading?.();
}
});
// Clear storage when modal is closed
document.getElementById("editEventModal")?.addEventListener("close", () => {
selectedFileStorage.clear();
});
// Add delete file handler
window.deleteFile = async function (eventId: string, filename: string) {
if (!confirm("Are you sure you want to delete this file?")) return;
try {
window.showLoading?.();
const pb = auth.getPocketBase();
// Get current event data
const event = await pb.collection("events").getOne(eventId);
// Filter out the file to delete
const updatedFiles = event.files.filter(
(f: string) => f !== filename
);
// Update the event with the new files array
await pb.collection("events").update(eventId, {
files: updatedFiles,
});
await sendLog.send(
"delete",
"event_file",
`Deleted file ${filename} from event ${event.event_name}`
);
// Refresh the current files display
const currentFiles = document.getElementById("currentFiles");
if (currentFiles && updatedFiles.length > 0) {
currentFiles.innerHTML = updateFilePreviewButtons(
updatedFiles,
eventId
);
} else if (currentFiles) {
currentFiles.innerHTML = `
<div class="text-center py-4 text-base-content/70">
<p>No files attached</p>
</div>
`;
}
} catch (error) {
console.error("Failed to delete file:", error);
alert("Failed to delete file. Please try again.");
} finally {
window.hideLoading?.();
}
};
</script>