Fix toast hydration
This commit is contained in:
parent
eb77c00540
commit
ef3e8f38d6
9 changed files with 855 additions and 931 deletions
|
@ -2,362 +2,297 @@
|
||||||
import FilePreview from "./universal/FilePreview";
|
import FilePreview from "./universal/FilePreview";
|
||||||
import EventCheckIn from "./EventsSection/EventCheckIn";
|
import EventCheckIn from "./EventsSection/EventCheckIn";
|
||||||
import EventLoad from "./EventsSection/EventLoad";
|
import EventLoad from "./EventsSection/EventLoad";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
import { Get } from "../../scripts/pocketbase/Get";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="" class="">
|
<div id="" class="">
|
||||||
<div class="mb-4 sm:mb-6 px-4 sm:px-6">
|
<div class="mb-4 sm:mb-6 px-4 sm:px-6">
|
||||||
<h2 class="text-xl sm:text-2xl font-bold">Events</h2>
|
<h2 class="text-xl sm:text-2xl font-bold">Events</h2>
|
||||||
<p class="opacity-70 text-sm sm:text-base">
|
<p class="opacity-70 text-sm sm:text-base">
|
||||||
View and manage your IEEE UCSD events
|
View and manage your IEEE UCSD events
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 mb-4 sm:mb-6 px-4 sm:px-6"
|
|
||||||
>
|
|
||||||
<!-- Event Check-in Card -->
|
|
||||||
<div class="w-full">
|
|
||||||
<EventCheckIn client:load />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Event Registration Card -->
|
<div
|
||||||
<div class="w-full">
|
class="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 mb-4 sm:mb-6 px-4 sm:px-6"
|
||||||
<div
|
>
|
||||||
class="card bg-base-100 shadow-xl border border-base-200 opacity-50 cursor-not-allowed relative group h-full"
|
<!-- Event Check-in Card -->
|
||||||
>
|
<div class="w-full">
|
||||||
<div
|
<EventCheckIn client:load />
|
||||||
class="absolute inset-0 bg-base-100 opacity-0 group-hover:opacity-90 transition-opacity duration-300 flex items-center justify-center z-10"
|
|
||||||
>
|
|
||||||
<span class="text-base-content font-medium text-sm sm:text-base"
|
|
||||||
>Coming Soon</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-4 sm:p-6">
|
|
||||||
<h3 class="card-title text-base sm:text-lg mb-3 sm:mb-4">
|
<!-- Event Registration Card -->
|
||||||
Event Registration
|
<div class="w-full">
|
||||||
</h3>
|
<div
|
||||||
<div class="form-control w-full">
|
class="card bg-base-100 shadow-xl border border-base-200 opacity-50 cursor-not-allowed relative group h-full"
|
||||||
<label class="label">
|
>
|
||||||
<span class="label-text text-sm sm:text-base"
|
<div
|
||||||
>Select an event to register</span
|
class="absolute inset-0 bg-base-100 opacity-0 group-hover:opacity-90 transition-opacity duration-300 flex items-center justify-center z-10"
|
||||||
>
|
>
|
||||||
</label>
|
<span
|
||||||
<div class="flex flex-col sm:flex-row gap-2">
|
class="text-base-content font-medium text-sm sm:text-base"
|
||||||
<select
|
>Coming Soon</span
|
||||||
class="select select-bordered flex-1 text-sm sm:text-base h-10 min-h-[2.5rem] w-full"
|
>
|
||||||
disabled
|
</div>
|
||||||
>
|
<div class="card-body p-4 sm:p-6">
|
||||||
<option disabled selected>Pick an event</option>
|
<h3 class="card-title text-base sm:text-lg mb-3 sm:mb-4">
|
||||||
<option>Technical Workshop - Web Development</option>
|
Event Registration
|
||||||
<option>Professional Development Workshop</option>
|
</h3>
|
||||||
<option>Social Event - Game Night</option>
|
<div class="form-control w-full">
|
||||||
</select>
|
<label class="label">
|
||||||
<button
|
<span class="label-text text-sm sm:text-base"
|
||||||
class="btn btn-primary text-sm sm:text-base h-10 min-h-[2.5rem] w-full sm:w-[100px]"
|
>Select an event to register</span
|
||||||
disabled>Register</button
|
>
|
||||||
>
|
</label>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-2">
|
||||||
|
<select
|
||||||
|
class="select select-bordered flex-1 text-sm sm:text-base h-10 min-h-[2.5rem] w-full"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<option disabled selected>Pick an event</option>
|
||||||
|
<option
|
||||||
|
>Technical Workshop - Web Development</option
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
>Professional Development Workshop</option
|
||||||
|
>
|
||||||
|
<option>Social Event - Game Night</option>
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary text-sm sm:text-base h-10 min-h-[2.5rem] w-full sm:w-[100px]"
|
||||||
|
disabled>Register</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<EventLoad client:load />
|
<EventLoad client:load />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Event Details Modal -->
|
<!-- Event Details Modal -->
|
||||||
<dialog id="eventDetailsModal" class="modal">
|
<dialog id="eventDetailsModal" class="modal">
|
||||||
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
|
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
|
||||||
<div class="flex justify-between items-center mb-3 sm:mb-4">
|
<div class="flex justify-between items-center mb-3 sm:mb-4">
|
||||||
<div class="flex items-center gap-2 sm:gap-3">
|
<div class="flex items-center gap-2 sm:gap-3">
|
||||||
<h3 class="font-bold text-base sm:text-lg" id="modalTitle">
|
<h3 class="font-bold text-base sm:text-lg" id="modalTitle">
|
||||||
Event Files
|
Event Files
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
id="downloadAllBtn"
|
id="downloadAllBtn"
|
||||||
class="btn btn-primary btn-sm gap-1 text-xs sm:text-sm"
|
class="btn btn-primary btn-sm gap-1 text-xs sm:text-sm"
|
||||||
onclick="window.downloadAllFiles()"
|
onclick="window.downloadAllFiles()"
|
||||||
>
|
>
|
||||||
<iconify-icon
|
<iconify-icon
|
||||||
icon="heroicons:arrow-down-tray-20-solid"
|
icon="heroicons:arrow-down-tray-20-solid"
|
||||||
className="h-3 w-3 sm:h-4 sm:w-4"></iconify-icon>
|
className="h-3 w-3 sm:h-4 sm:w-4"></iconify-icon>
|
||||||
Download All
|
Download All
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn btn-circle btn-ghost btn-sm sm:btn-md"
|
class="btn btn-circle btn-ghost btn-sm sm:btn-md"
|
||||||
onclick="window.closeEventDetailsModal()"
|
onclick="window.closeEventDetailsModal()"
|
||||||
>
|
>
|
||||||
<iconify-icon icon="heroicons:x-mark" className="h-4 w-4 sm:h-6 sm:w-6"
|
<iconify-icon
|
||||||
></iconify-icon>
|
icon="heroicons:x-mark"
|
||||||
</button>
|
className="h-4 w-4 sm:h-6 sm:w-6"></iconify-icon>
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="filesContent" class="space-y-3 sm:space-y-4">
|
<div id="filesContent" class="space-y-3 sm:space-y-4">
|
||||||
<!-- Files list will be populated here -->
|
<!-- Files list will be populated here -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<form method="dialog" class="modal-backdrop">
|
||||||
<form method="dialog" class="modal-backdrop">
|
<button onclick="window.closeEventDetailsModal()">close</button>
|
||||||
<button onclick="window.closeEventDetailsModal()">close</button>
|
</form>
|
||||||
</form>
|
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<!-- Universal File Preview Modal -->
|
<!-- Universal File Preview Modal -->
|
||||||
<dialog id="filePreviewModal" class="modal">
|
<dialog id="filePreviewModal" class="modal">
|
||||||
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
|
<div class="modal-box max-w-[90vw] sm:max-w-4xl p-4 sm:p-6">
|
||||||
<div class="flex justify-between items-center mb-3 sm:mb-4">
|
<div class="flex justify-between items-center mb-3 sm:mb-4">
|
||||||
<div class="flex items-center gap-2 sm:gap-3">
|
<div class="flex items-center gap-2 sm:gap-3">
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-sm text-xs sm:text-sm"
|
class="btn btn-ghost btn-sm text-xs sm:text-sm"
|
||||||
onclick="window.closeFilePreviewEvents()">Close</button
|
onclick="window.closeFilePreviewEvents()">Close</button
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
class="font-bold text-base sm:text-lg truncate"
|
class="font-bold text-base sm:text-lg truncate"
|
||||||
id="previewFileName"
|
id="previewFileName"
|
||||||
>
|
>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</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-md sm:loading-lg"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div id="previewContent" class="w-full">
|
||||||
|
<FilePreview client:load isModal={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative" id="previewContainer">
|
<form method="dialog" class="modal-backdrop">
|
||||||
<div
|
<button onclick="window.closeFilePreviewEvents()">close</button>
|
||||||
id="previewLoadingSpinner"
|
</form>
|
||||||
class="absolute inset-0 flex items-center justify-center bg-base-200 bg-opacity-50 hidden"
|
|
||||||
>
|
|
||||||
<span class="loading loading-spinner loading-md sm:loading-lg"></span>
|
|
||||||
</div>
|
|
||||||
<div id="previewContent" class="w-full">
|
|
||||||
<FilePreview client:load isModal={true} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button onclick="window.closeFilePreviewEvents()">close</button>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import JSZip from "jszip";
|
import { toast } from "react-hot-toast";
|
||||||
|
import JSZip from "jszip";
|
||||||
|
|
||||||
// Toast management system
|
// Add styles to the document
|
||||||
const createToast = (
|
const style = document.createElement("style");
|
||||||
message: string,
|
style.textContent = `
|
||||||
type: "success" | "error" | "warning" = "success",
|
/* Custom styles for the event details modal */
|
||||||
) => {
|
.event-details-grid {
|
||||||
let toastContainer = document.querySelector(".toast-container");
|
display: grid;
|
||||||
if (!toastContainer) {
|
grid-template-columns: 1fr 1fr;
|
||||||
toastContainer = document.createElement("div");
|
gap: 1rem;
|
||||||
toastContainer.className = "toast-container fixed bottom-4 right-4 z-50";
|
|
||||||
document.body.appendChild(toastContainer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingToasts = document.querySelectorAll(".toast-container .toast");
|
@media (max-width: 640px) {
|
||||||
if (existingToasts.length >= 2) {
|
.event-details-grid {
|
||||||
const oldestToast = existingToasts[0];
|
grid-template-columns: 1fr;
|
||||||
oldestToast.classList.add("toast-exit");
|
}
|
||||||
setTimeout(() => oldestToast.remove(), 150);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update positions of existing toasts
|
|
||||||
existingToasts.forEach((t) => {
|
|
||||||
const toast = t as HTMLElement;
|
|
||||||
const currentIndex = parseInt(toast.getAttribute("data-index") || "0");
|
|
||||||
toast.setAttribute("data-index", (currentIndex + 1).toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
const toast = document.createElement("div");
|
|
||||||
toast.className = "toast translate-x-full";
|
|
||||||
toast.setAttribute("data-index", "0");
|
|
||||||
|
|
||||||
// Update alert styling based on type
|
|
||||||
const alertClass =
|
|
||||||
type === "success"
|
|
||||||
? "alert-success bg-success text-success-content"
|
|
||||||
: type === "error"
|
|
||||||
? "alert-error bg-error text-error-content"
|
|
||||||
: "alert-warning bg-warning text-warning-content";
|
|
||||||
|
|
||||||
toast.innerHTML = `
|
|
||||||
<div class="alert ${alertClass} shadow-lg min-w-[300px]">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
${
|
|
||||||
type === "success"
|
|
||||||
? '<iconify-icon icon="heroicons:check-circle" class="stroke-current shrink-0 h-6 w-6"></iconify-icon>'
|
|
||||||
: type === "error"
|
|
||||||
? '<iconify-icon icon="heroicons:x-circle" class="stroke-current shrink-0 h-6 w-6"></iconify-icon>'
|
|
||||||
: '<iconify-icon icon="heroicons:exclamation-triangle" class="stroke-current shrink-0 w-6"></iconify-icon>'
|
|
||||||
}
|
|
||||||
<span>${message}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
toastContainer.appendChild(toast);
|
|
||||||
|
|
||||||
// Force a reflow to ensure the animation triggers
|
|
||||||
toast.offsetHeight;
|
|
||||||
|
|
||||||
// Add the transition class and remove transform
|
|
||||||
toast.classList.add("transition-all", "duration-300", "ease-out");
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
toast.classList.remove("translate-x-full");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup exit animation
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.add("toast-exit");
|
|
||||||
setTimeout(() => toast.remove(), 150);
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add styles to the document
|
|
||||||
const style = document.createElement("style");
|
|
||||||
style.textContent = `
|
|
||||||
.toast-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.toast {
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: translateX(0);
|
|
||||||
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.toast-exit {
|
|
||||||
transform: translateX(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.toast.translate-x-full {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
.toast-container .toast {
|
|
||||||
transform: translateY(calc((1 - attr(data-index number)) * -0.25rem));
|
|
||||||
}
|
|
||||||
.toast-container .toast[data-index="0"] {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
.toast-container .toast[data-index="1"] {
|
|
||||||
transform: translateY(-0.025rem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Remove custom toast styles since we're using react-hot-toast */
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
// Add helper functions for file preview
|
// Add helper functions for file preview
|
||||||
function getFileType(filename: string): string {
|
function getFileType(filename: string): string {
|
||||||
const extension = filename.split(".").pop()?.toLowerCase();
|
const extension = filename.split(".").pop()?.toLowerCase();
|
||||||
const mimeTypes: { [key: string]: string } = {
|
const mimeTypes: { [key: string]: string } = {
|
||||||
pdf: "application/pdf",
|
pdf: "application/pdf",
|
||||||
jpg: "image/jpeg",
|
jpg: "image/jpeg",
|
||||||
jpeg: "image/jpeg",
|
jpeg: "image/jpeg",
|
||||||
png: "image/png",
|
png: "image/png",
|
||||||
gif: "image/gif",
|
gif: "image/gif",
|
||||||
mp4: "video/mp4",
|
mp4: "video/mp4",
|
||||||
mp3: "audio/mpeg",
|
mp3: "audio/mpeg",
|
||||||
txt: "text/plain",
|
txt: "text/plain",
|
||||||
doc: "application/msword",
|
doc: "application/msword",
|
||||||
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
xls: "application/vnd.ms-excel",
|
xls: "application/vnd.ms-excel",
|
||||||
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
json: "application/json",
|
json: "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
return mimeTypes[extension || ""] || "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Universal file preview function for events section
|
||||||
|
window.previewFileEvents = function (url: string, filename: string) {
|
||||||
|
console.log("previewFileEvents called with:", { url, filename });
|
||||||
|
const modal = document.getElementById(
|
||||||
|
"filePreviewModal"
|
||||||
|
) as HTMLDialogElement;
|
||||||
|
const previewFileName = document.getElementById("previewFileName");
|
||||||
|
const previewContent = document.getElementById("previewContent");
|
||||||
|
|
||||||
|
if (modal && previewFileName && previewContent) {
|
||||||
|
console.log("Found all required elements");
|
||||||
|
// Update the filename display
|
||||||
|
previewFileName.textContent = filename;
|
||||||
|
|
||||||
|
// Show the modal
|
||||||
|
modal.showModal();
|
||||||
|
|
||||||
|
// Dispatch state change event
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("filePreviewStateChange", {
|
||||||
|
detail: { url, filename },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return mimeTypes[extension || ""] || "application/octet-stream";
|
// Close file preview for events section
|
||||||
}
|
window.closeFilePreviewEvents = function () {
|
||||||
|
console.log("closeFilePreviewEvents called");
|
||||||
|
const modal = document.getElementById(
|
||||||
|
"filePreviewModal"
|
||||||
|
) as HTMLDialogElement;
|
||||||
|
const previewFileName = document.getElementById("previewFileName");
|
||||||
|
const previewContent = document.getElementById("previewContent");
|
||||||
|
|
||||||
// Universal file preview function for events section
|
if (modal && previewFileName && previewContent) {
|
||||||
window.previewFileEvents = function (url: string, filename: string) {
|
console.log("Resetting preview and closing modal");
|
||||||
console.log("previewFileEvents called with:", { url, filename });
|
// Reset the preview state
|
||||||
const modal = document.getElementById(
|
window.dispatchEvent(
|
||||||
"filePreviewModal",
|
new CustomEvent("filePreviewStateChange", {
|
||||||
) as HTMLDialogElement;
|
detail: { url: "", filename: "" },
|
||||||
const previewFileName = document.getElementById("previewFileName");
|
})
|
||||||
const previewContent = document.getElementById("previewContent");
|
);
|
||||||
|
|
||||||
if (modal && previewFileName && previewContent) {
|
// Reset the UI
|
||||||
console.log("Found all required elements");
|
previewFileName.textContent = "";
|
||||||
// Update the filename display
|
|
||||||
previewFileName.textContent = filename;
|
|
||||||
|
|
||||||
// Show the modal
|
// Close the modal
|
||||||
modal.showModal();
|
modal.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Dispatch state change event
|
// Update the showFilePreview function for events section
|
||||||
window.dispatchEvent(
|
window.showFilePreviewEvents = function (file: {
|
||||||
new CustomEvent("filePreviewStateChange", {
|
url: string;
|
||||||
detail: { url, filename },
|
name: string;
|
||||||
}),
|
}) {
|
||||||
);
|
console.log("showFilePreviewEvents called with:", file);
|
||||||
}
|
window.previewFileEvents(file.url, file.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close file preview for events section
|
// Update the openDetailsModal function to use the events-specific preview
|
||||||
window.closeFilePreviewEvents = function () {
|
window.openDetailsModal = function (event: any) {
|
||||||
console.log("closeFilePreviewEvents called");
|
const modal = document.getElementById(
|
||||||
const modal = document.getElementById(
|
"eventDetailsModal"
|
||||||
"filePreviewModal",
|
) as HTMLDialogElement;
|
||||||
) as HTMLDialogElement;
|
const filesContent = document.getElementById(
|
||||||
const previewFileName = document.getElementById("previewFileName");
|
"filesContent"
|
||||||
const previewContent = document.getElementById("previewContent");
|
) as HTMLDivElement;
|
||||||
|
|
||||||
if (modal && previewFileName && previewContent) {
|
// Check if event has ended
|
||||||
console.log("Resetting preview and closing modal");
|
const eventEndDate = new Date(event.end_date);
|
||||||
// Reset the preview state
|
const now = new Date();
|
||||||
window.dispatchEvent(
|
|
||||||
new CustomEvent("filePreviewStateChange", {
|
|
||||||
detail: { url: "", filename: "" },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset the UI
|
if (eventEndDate > now) {
|
||||||
previewFileName.textContent = "";
|
toast("Files are only available after the event has ended.", {
|
||||||
|
icon: "⚠️",
|
||||||
|
style: {
|
||||||
|
borderRadius: "10px",
|
||||||
|
background: "#FFC107",
|
||||||
|
color: "#000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Close the modal
|
// Reset state
|
||||||
modal.close();
|
window.currentEventId = event.id;
|
||||||
}
|
if (filesContent) filesContent.classList.remove("hidden");
|
||||||
};
|
|
||||||
|
|
||||||
// Update the showFilePreview function for events section
|
// Populate files content
|
||||||
window.showFilePreviewEvents = function (file: {
|
if (
|
||||||
url: string;
|
event.files &&
|
||||||
name: string;
|
Array.isArray(event.files) &&
|
||||||
}) {
|
event.files.length > 0
|
||||||
console.log("showFilePreviewEvents called with:", file);
|
) {
|
||||||
window.previewFileEvents(file.url, file.name);
|
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
||||||
};
|
const collectionId = "events";
|
||||||
|
const recordId = event.id;
|
||||||
|
|
||||||
// Update the openDetailsModal function to use the events-specific preview
|
filesContent.innerHTML = `
|
||||||
window.openDetailsModal = function (event: any) {
|
|
||||||
const modal = document.getElementById(
|
|
||||||
"eventDetailsModal",
|
|
||||||
) as HTMLDialogElement;
|
|
||||||
const filesContent = document.getElementById(
|
|
||||||
"filesContent",
|
|
||||||
) as HTMLDivElement;
|
|
||||||
|
|
||||||
// Check if event has ended
|
|
||||||
const eventEndDate = new Date(event.end_date);
|
|
||||||
const now = new Date();
|
|
||||||
|
|
||||||
if (eventEndDate > now) {
|
|
||||||
createToast(
|
|
||||||
"Files are only available after the event has ended.",
|
|
||||||
"warning",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset state
|
|
||||||
window.currentEventId = event.id;
|
|
||||||
if (filesContent) filesContent.classList.remove("hidden");
|
|
||||||
|
|
||||||
// Populate files content
|
|
||||||
if (event.files && Array.isArray(event.files) && event.files.length > 0) {
|
|
||||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
|
||||||
const collectionId = "events";
|
|
||||||
const recordId = event.id;
|
|
||||||
|
|
||||||
filesContent.innerHTML = `
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-zebra w-full">
|
<table class="table table-zebra w-full">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -368,14 +303,14 @@ import EventLoad from "./EventsSection/EventLoad";
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${event.files
|
${event.files
|
||||||
.map((file: string) => {
|
.map((file: string) => {
|
||||||
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
|
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${file}`;
|
||||||
const fileType = getFileType(file);
|
const fileType = getFileType(file);
|
||||||
const previewData = JSON.stringify({
|
const previewData = JSON.stringify({
|
||||||
url: fileUrl,
|
url: fileUrl,
|
||||||
name: file,
|
name: file,
|
||||||
}).replace(/'/g, "\\'");
|
}).replace(/'/g, "\\'");
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${file}</td>
|
<td>${file}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
@ -388,124 +323,123 @@ import EventLoad from "./EventsSection/EventLoad";
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
.join("")}
|
.join("")}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
filesContent.innerHTML = `
|
filesContent.innerHTML = `
|
||||||
<div class="text-center py-8 text-base-content/70">
|
<div class="text-center py-8 text-base-content/70">
|
||||||
<iconify-icon icon="heroicons:document-duplicate" className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
<iconify-icon icon="heroicons:document-duplicate" className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||||
<p>No files attached to this event</p>
|
<p>No files attached to this event</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
|
||||||
|
|
||||||
modal.showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add downloadAllFiles function
|
|
||||||
window.downloadAllFiles = async function () {
|
|
||||||
const downloadBtn = document.getElementById(
|
|
||||||
"downloadAllBtn",
|
|
||||||
) as HTMLButtonElement;
|
|
||||||
if (!downloadBtn) return;
|
|
||||||
const originalBtnContent = downloadBtn.innerHTML;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Show loading state
|
|
||||||
downloadBtn.innerHTML =
|
|
||||||
'<span class="loading loading-spinner loading-xs"></span> Preparing...';
|
|
||||||
downloadBtn.disabled = true;
|
|
||||||
|
|
||||||
const zip = new JSZip();
|
|
||||||
|
|
||||||
// Get current event files
|
|
||||||
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
|
||||||
const collectionId = "events";
|
|
||||||
const recordId = window.currentEventId;
|
|
||||||
|
|
||||||
// Get the current event from the window object
|
|
||||||
const eventDataId = `event_${window.currentEventId}`;
|
|
||||||
const event = window[eventDataId];
|
|
||||||
|
|
||||||
if (!event || !event.files || event.files.length === 0) {
|
|
||||||
throw new Error("No files available to download");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download each file and add to zip
|
|
||||||
const filePromises = event.files.map(async (filename: string) => {
|
|
||||||
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${filename}`;
|
|
||||||
const response = await fetch(fileUrl);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to download ${filename}`);
|
|
||||||
}
|
}
|
||||||
const blob = await response.blob();
|
|
||||||
zip.file(filename, blob);
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(filePromises);
|
modal.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
// Generate and download zip
|
// Add downloadAllFiles function
|
||||||
const zipBlob = await zip.generateAsync({ type: "blob" });
|
window.downloadAllFiles = async function () {
|
||||||
const downloadUrl = URL.createObjectURL(zipBlob);
|
const downloadBtn = document.getElementById(
|
||||||
const link = document.createElement("a");
|
"downloadAllBtn"
|
||||||
link.href = downloadUrl;
|
) as HTMLButtonElement;
|
||||||
link.download = `${event.event_name}_files.zip`;
|
if (!downloadBtn) return;
|
||||||
document.body.appendChild(link);
|
const originalBtnContent = downloadBtn.innerHTML;
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(downloadUrl);
|
|
||||||
|
|
||||||
// Show success message
|
try {
|
||||||
createToast("Files downloaded successfully!", "success");
|
// Show loading state
|
||||||
} catch (error: any) {
|
downloadBtn.innerHTML =
|
||||||
console.error("Failed to download files:", error);
|
'<span class="loading loading-spinner loading-xs"></span> Preparing...';
|
||||||
createToast(
|
downloadBtn.disabled = true;
|
||||||
error?.message || "Failed to download files. Please try again.",
|
|
||||||
"error",
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
// Reset button state
|
|
||||||
downloadBtn.innerHTML = originalBtnContent;
|
|
||||||
downloadBtn.disabled = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close event details modal
|
const zip = new JSZip();
|
||||||
window.closeEventDetailsModal = function () {
|
|
||||||
const modal = document.getElementById(
|
|
||||||
"eventDetailsModal",
|
|
||||||
) as HTMLDialogElement;
|
|
||||||
const filesContent = document.getElementById("filesContent");
|
|
||||||
|
|
||||||
if (modal) {
|
// Get current event files
|
||||||
// Reset the files content
|
const baseUrl = "https://pocketbase.ieeeucsd.org";
|
||||||
if (filesContent) {
|
const collectionId = "events";
|
||||||
filesContent.innerHTML = "";
|
const recordId = window.currentEventId;
|
||||||
}
|
|
||||||
|
|
||||||
// Reset any other state if needed
|
// Get the current event from the window object
|
||||||
window.currentEventId = "";
|
const eventDataId = `event_${window.currentEventId}`;
|
||||||
|
const event = window[eventDataId];
|
||||||
|
|
||||||
// Close the modal
|
if (!event || !event.files || event.files.length === 0) {
|
||||||
modal.close();
|
throw new Error("No files available to download");
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Make helper functions available globally
|
// Download each file and add to zip
|
||||||
window.showFilePreview = window.showFilePreviewEvents;
|
const filePromises = event.files.map(async (filename: string) => {
|
||||||
window.handlePreviewError = function () {
|
const fileUrl = `${baseUrl}/api/files/${collectionId}/${recordId}/${filename}`;
|
||||||
const previewContent = document.getElementById("previewContent");
|
const response = await fetch(fileUrl);
|
||||||
if (previewContent) {
|
if (!response.ok) {
|
||||||
previewContent.innerHTML = `
|
throw new Error(`Failed to download ${filename}`);
|
||||||
|
}
|
||||||
|
const blob = await response.blob();
|
||||||
|
zip.file(filename, blob);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(filePromises);
|
||||||
|
|
||||||
|
// Generate and download zip
|
||||||
|
const zipBlob = await zip.generateAsync({ type: "blob" });
|
||||||
|
const downloadUrl = URL.createObjectURL(zipBlob);
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.download = `${event.event_name}_files.zip`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(downloadUrl);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
toast.success("Files downloaded successfully!");
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to download files:", error);
|
||||||
|
toast.error(
|
||||||
|
error?.message || "Failed to download files. Please try again."
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
// Reset button state
|
||||||
|
downloadBtn.innerHTML = originalBtnContent;
|
||||||
|
downloadBtn.disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close event details modal
|
||||||
|
window.closeEventDetailsModal = function () {
|
||||||
|
const modal = document.getElementById(
|
||||||
|
"eventDetailsModal"
|
||||||
|
) as HTMLDialogElement;
|
||||||
|
const filesContent = document.getElementById("filesContent");
|
||||||
|
|
||||||
|
if (modal) {
|
||||||
|
// Reset the files content
|
||||||
|
if (filesContent) {
|
||||||
|
filesContent.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset any other state if needed
|
||||||
|
window.currentEventId = "";
|
||||||
|
|
||||||
|
// Close the modal
|
||||||
|
modal.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make helper functions available globally
|
||||||
|
window.showFilePreview = window.showFilePreviewEvents;
|
||||||
|
window.handlePreviewError = function () {
|
||||||
|
const previewContent = document.getElementById("previewContent");
|
||||||
|
if (previewContent) {
|
||||||
|
previewContent.innerHTML = `
|
||||||
<div class="alert alert-error">
|
<div class="alert alert-error">
|
||||||
<Icon icon="heroicons:x-circle" className="h-6 w-6" />
|
<Icon icon="heroicons:x-circle" className="h-6 w-6" />
|
||||||
<span>Failed to load file preview</span>
|
<span>Failed to load file preview</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { SendLog } from "../../../scripts/pocketbase/SendLog";
|
||||||
import { DataSyncService } from "../../../scripts/database/DataSyncService";
|
import { DataSyncService } from "../../../scripts/database/DataSyncService";
|
||||||
import { Collections } from "../../../schemas/pocketbase/schema";
|
import { Collections } from "../../../schemas/pocketbase/schema";
|
||||||
import { Icon } from "@iconify/react";
|
import { Icon } from "@iconify/react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
import type { Event, AttendeeEntry } from "../../../schemas/pocketbase";
|
import type { Event, AttendeeEntry } from "../../../schemas/pocketbase";
|
||||||
|
|
||||||
// Extended Event interface with additional properties needed for this component
|
// Extended Event interface with additional properties needed for this component
|
||||||
|
@ -17,77 +18,6 @@ interface ExtendedEvent extends Event {
|
||||||
// When fetching events, UTC dates are converted to local time.
|
// When fetching events, UTC dates are converted to local time.
|
||||||
// When saving events, local dates are converted back to UTC.
|
// When saving events, local dates are converted back to UTC.
|
||||||
|
|
||||||
// Toast management system
|
|
||||||
const createToast = (
|
|
||||||
message: string,
|
|
||||||
type: "success" | "error" | "warning" = "success"
|
|
||||||
) => {
|
|
||||||
let toastContainer = document.querySelector(".toast-container");
|
|
||||||
if (!toastContainer) {
|
|
||||||
toastContainer = document.createElement("div");
|
|
||||||
toastContainer.className = "toast-container fixed bottom-4 right-4 z-50";
|
|
||||||
document.body.appendChild(toastContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingToasts = document.querySelectorAll(".toast-container .toast");
|
|
||||||
if (existingToasts.length >= 2) {
|
|
||||||
const oldestToast = existingToasts[0];
|
|
||||||
oldestToast.classList.add("toast-exit");
|
|
||||||
setTimeout(() => oldestToast.remove(), 150);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update positions of existing toasts
|
|
||||||
existingToasts.forEach((t) => {
|
|
||||||
const toast = t as HTMLElement;
|
|
||||||
const currentIndex = parseInt(toast.getAttribute("data-index") || "0");
|
|
||||||
toast.setAttribute("data-index", (currentIndex + 1).toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
const toast = document.createElement("div");
|
|
||||||
toast.className = "toast translate-x-full";
|
|
||||||
toast.setAttribute("data-index", "0");
|
|
||||||
|
|
||||||
// Update alert styling based on type
|
|
||||||
const alertClass =
|
|
||||||
type === "success"
|
|
||||||
? "alert-success bg-success text-success-content"
|
|
||||||
: type === "error"
|
|
||||||
? "alert-error bg-error text-error-content"
|
|
||||||
: "alert-warning bg-warning text-warning-content";
|
|
||||||
|
|
||||||
const iconName = type === "success"
|
|
||||||
? "heroicons:check-circle"
|
|
||||||
: type === "error"
|
|
||||||
? "heroicons:x-circle"
|
|
||||||
: "heroicons:exclamation-triangle";
|
|
||||||
|
|
||||||
toast.innerHTML = `
|
|
||||||
<div class="alert ${alertClass} shadow-lg min-w-[300px]">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<iconify-icon icon="${iconName}" width="20" height="20"></iconify-icon>
|
|
||||||
<span>${message}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
toastContainer.appendChild(toast);
|
|
||||||
|
|
||||||
// Force a reflow to ensure the animation triggers
|
|
||||||
toast.offsetHeight;
|
|
||||||
|
|
||||||
// Add the transition class and remove transform
|
|
||||||
toast.classList.add("transition-all", "duration-300", "ease-out");
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
toast.classList.remove("translate-x-full");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup exit animation
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.classList.add("toast-exit");
|
|
||||||
setTimeout(() => toast.remove(), 150);
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const EventCheckIn = () => {
|
const EventCheckIn = () => {
|
||||||
const [currentCheckInEvent, setCurrentCheckInEvent] = useState<Event | null>(null);
|
const [currentCheckInEvent, setCurrentCheckInEvent] = useState<Event | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
@ -101,7 +31,7 @@ const EventCheckIn = () => {
|
||||||
|
|
||||||
const currentUser = auth.getCurrentUser();
|
const currentUser = auth.getCurrentUser();
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
createToast("You must be logged in to check in to events", "error");
|
toast.error("You must be logged in to check in to events");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +81,7 @@ const EventCheckIn = () => {
|
||||||
await completeCheckIn(event, null);
|
await completeCheckIn(event, null);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
createToast(error?.message || "Failed to check in to event", "error");
|
toast.error(error?.message || "Failed to check in to event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,7 +101,7 @@ const EventCheckIn = () => {
|
||||||
const userId = auth.getUserId();
|
const userId = auth.getUserId();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
createToast("You must be logged in to check in to an event", "error");
|
toast.error("You must be logged in to check in to an event");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +114,14 @@ const EventCheckIn = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isAlreadyCheckedIn) {
|
if (isAlreadyCheckedIn) {
|
||||||
createToast("You are already checked in to this event", "warning");
|
toast("You are already checked in to this event", {
|
||||||
|
icon: '⚠️',
|
||||||
|
style: {
|
||||||
|
borderRadius: '10px',
|
||||||
|
background: '#FFC107',
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,12 +180,11 @@ const EventCheckIn = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show success message with points if awarded
|
// Show success message with points if awarded
|
||||||
createToast(
|
toast.success(
|
||||||
`Successfully checked in to ${event.event_name}${event.points_to_reward > 0
|
`Successfully checked in to ${event.event_name}${event.points_to_reward > 0
|
||||||
? ` (+${event.points_to_reward} points!)`
|
? ` (+${event.points_to_reward} points!)`
|
||||||
: ""
|
: ""
|
||||||
}`,
|
}`
|
||||||
"success"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Log the check-in
|
// Log the check-in
|
||||||
|
@ -265,7 +201,7 @@ const EventCheckIn = () => {
|
||||||
setFoodInput("");
|
setFoodInput("");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
createToast(error?.message || "Failed to check in to event", "error");
|
toast.error(error?.message || "Failed to check in to event");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,7 +232,14 @@ const EventCheckIn = () => {
|
||||||
input.value = "";
|
input.value = "";
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
createToast("Please enter an event code", "warning");
|
toast("Please enter an event code", {
|
||||||
|
icon: '⚠️',
|
||||||
|
style: {
|
||||||
|
borderRadius: '10px',
|
||||||
|
background: '#FFC107',
|
||||||
|
color: '#000',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
|
|
@ -4,7 +4,10 @@ import { Get } from "../../scripts/pocketbase/Get";
|
||||||
import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm";
|
import EventRequestForm from "./Officer_EventRequestForm/EventRequestForm";
|
||||||
import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests";
|
import UserEventRequests from "./Officer_EventRequestForm/UserEventRequests";
|
||||||
import { Collections } from "../../schemas/pocketbase/schema";
|
import { Collections } from "../../schemas/pocketbase/schema";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Icon } from "astro-icon/components";
|
||||||
|
import { Create } from "../../scripts/pocketbase/Create";
|
||||||
|
import { Update } from "../../scripts/pocketbase/Update";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
|
||||||
// Import the EventRequest type from UserEventRequests to ensure consistency
|
// Import the EventRequest type from UserEventRequests to ensure consistency
|
||||||
import type { EventRequest as UserEventRequest } from "./Officer_EventRequestForm/UserEventRequests";
|
import type { EventRequest as UserEventRequest } from "./Officer_EventRequestForm/UserEventRequests";
|
||||||
|
@ -118,8 +121,9 @@ if (auth.isAuthenticated()) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast container for notifications -->
|
<div class="dashboard-section hidden" id="eventRequestFormSection">
|
||||||
<Toaster client:load position="bottom-right" />
|
<!-- ... existing code ... -->
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Import the DataSyncService for client-side use
|
// Import the DataSyncService for client-side use
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
---
|
---
|
||||||
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
import { Authentication } from "../../scripts/pocketbase/Authentication";
|
||||||
import { Get } from "../../scripts/pocketbase/Get";
|
import { Get } from "../../scripts/pocketbase/Get";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import EventRequestManagementTable from "./Officer_EventRequestManagement/EventRequestManagementTable";
|
import EventRequestManagementTable from "./Officer_EventRequestManagement/EventRequestManagementTable";
|
||||||
import type { EventRequest } from "../../schemas/pocketbase";
|
import type { EventRequest } from "../../schemas/pocketbase";
|
||||||
import { Collections } from "../../schemas/pocketbase/schema";
|
import { Collections } from "../../schemas/pocketbase/schema";
|
||||||
|
import { Icon } from "astro-icon/components";
|
||||||
|
|
||||||
// Get instances
|
// Get instances
|
||||||
const get = Get.getInstance();
|
const get = Get.getInstance();
|
||||||
|
@ -152,9 +153,6 @@ try {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Toast container for notifications -->
|
|
||||||
<Toaster client:load position="bottom-right" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
import { Authentication } from '../../../scripts/pocketbase/Authentication';
|
||||||
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
||||||
|
@ -7,7 +7,6 @@ import ReceiptForm from './ReceiptForm';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import FilePreview from '../universal/FilePreview';
|
import FilePreview from '../universal/FilePreview';
|
||||||
import ToastProvider from './ToastProvider';
|
|
||||||
import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase';
|
import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase';
|
||||||
|
|
||||||
interface ReceiptFormData {
|
interface ReceiptFormData {
|
||||||
|
@ -310,7 +309,6 @@ export default function ReimbursementForm() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToastProvider />
|
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { FileManager } from '../../../scripts/pocketbase/FileManager';
|
||||||
import FilePreview from '../universal/FilePreview';
|
import FilePreview from '../universal/FilePreview';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import ToastProvider from './ToastProvider';
|
|
||||||
import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase';
|
import type { ItemizedExpense, Reimbursement, Receipt } from '../../../schemas/pocketbase';
|
||||||
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
import { DataSyncService } from '../../../scripts/database/DataSyncService';
|
||||||
import { Collections } from '../../../schemas/pocketbase/schema';
|
import { Collections } from '../../../schemas/pocketbase/schema';
|
||||||
|
@ -355,7 +354,6 @@ export default function ReimbursementList() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ToastProvider />
|
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={containerVariants}
|
variants={containerVariants}
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { Toaster } from 'react-hot-toast';
|
|
||||||
|
|
||||||
export default function ToastProvider() {
|
|
||||||
return (
|
|
||||||
<Toaster
|
|
||||||
position="top-center"
|
|
||||||
reverseOrder={false}
|
|
||||||
toastOptions={{
|
|
||||||
duration: 5000,
|
|
||||||
style: {
|
|
||||||
background: '#333',
|
|
||||||
color: '#fff',
|
|
||||||
padding: '16px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
31
src/components/dashboard/universal/ToastProvider.tsx
Normal file
31
src/components/dashboard/universal/ToastProvider.tsx
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
|
||||||
|
// Centralized toast provider to ensure consistent rendering
|
||||||
|
export default function ToastProvider() {
|
||||||
|
return (
|
||||||
|
<Toaster
|
||||||
|
position="bottom-right"
|
||||||
|
toastOptions={{
|
||||||
|
duration: 4000,
|
||||||
|
style: {
|
||||||
|
background: '#333',
|
||||||
|
color: '#fff',
|
||||||
|
borderRadius: '8px',
|
||||||
|
padding: '12px',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
style: {
|
||||||
|
background: 'green',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
style: {
|
||||||
|
background: 'red',
|
||||||
|
},
|
||||||
|
duration: 5000,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import { SendLog } from "../scripts/pocketbase/SendLog";
|
||||||
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
|
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
|
||||||
import { OfficerTypes } from "../schemas/pocketbase/schema";
|
import { OfficerTypes } from "../schemas/pocketbase/schema";
|
||||||
import { initAuthSync } from "../scripts/database/initAuthSync";
|
import { initAuthSync } from "../scripts/database/initAuthSync";
|
||||||
|
import ToastProvider from "../components/dashboard/universal/ToastProvider";
|
||||||
|
|
||||||
const title = "Dashboard";
|
const title = "Dashboard";
|
||||||
|
|
||||||
|
@ -365,449 +366,485 @@ console.log("Available components:", Object.keys(components)); // Debug log
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<script>
|
<!-- Centralized Toast Provider -->
|
||||||
import { Authentication } from "../scripts/pocketbase/Authentication";
|
<ToastProvider client:load />
|
||||||
import { Get } from "../scripts/pocketbase/Get";
|
|
||||||
import { SendLog } from "../scripts/pocketbase/SendLog";
|
|
||||||
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
|
|
||||||
import { OfficerTypes } from "../schemas/pocketbase/schema";
|
|
||||||
import { initAuthSync } from "../scripts/database/initAuthSync";
|
|
||||||
|
|
||||||
const auth = Authentication.getInstance();
|
<script>
|
||||||
const get = Get.getInstance();
|
import { Authentication } from "../scripts/pocketbase/Authentication";
|
||||||
const logger = SendLog.getInstance();
|
import { Get } from "../scripts/pocketbase/Get";
|
||||||
|
import { SendLog } from "../scripts/pocketbase/SendLog";
|
||||||
|
import { hasAccess, type OfficerStatus } from "../utils/roleAccess";
|
||||||
|
import { OfficerTypes } from "../schemas/pocketbase/schema";
|
||||||
|
import { initAuthSync } from "../scripts/database/initAuthSync";
|
||||||
|
|
||||||
// Initialize page state
|
const auth = Authentication.getInstance();
|
||||||
const pageLoadingState = document.getElementById("pageLoadingState");
|
const get = Get.getInstance();
|
||||||
const pageErrorState = document.getElementById("pageErrorState");
|
const logger = SendLog.getInstance();
|
||||||
const notAuthenticatedState = document.getElementById(
|
|
||||||
"notAuthenticatedState"
|
|
||||||
);
|
|
||||||
const mainContent = document.getElementById("mainContent");
|
|
||||||
const sidebar = document.querySelector("aside");
|
|
||||||
|
|
||||||
// User profile elements
|
// Initialize page state
|
||||||
const userInitials = document.getElementById("userInitials");
|
const pageLoadingState =
|
||||||
const userName = document.getElementById("userName");
|
document.getElementById("pageLoadingState");
|
||||||
const userRole = document.getElementById("userRole");
|
const pageErrorState = document.getElementById("pageErrorState");
|
||||||
|
const notAuthenticatedState = document.getElementById(
|
||||||
|
"notAuthenticatedState"
|
||||||
|
);
|
||||||
|
const mainContent = document.getElementById("mainContent");
|
||||||
|
const sidebar = document.querySelector("aside");
|
||||||
|
|
||||||
// Function to update section visibility based on role
|
// User profile elements
|
||||||
const updateSectionVisibility = (officerStatus: OfficerStatus) => {
|
const userInitials = document.getElementById("userInitials");
|
||||||
// Special handling for sponsor role
|
const userName = document.getElementById("userName");
|
||||||
if (officerStatus === "sponsor") {
|
const userRole = document.getElementById("userRole");
|
||||||
// Hide all sections first
|
|
||||||
document
|
|
||||||
.querySelectorAll("[data-role-required]")
|
|
||||||
.forEach((element) => {
|
|
||||||
element.classList.add("hidden");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only show sponsor sections
|
// Function to update section visibility based on role
|
||||||
document
|
const updateSectionVisibility = (officerStatus: OfficerStatus) => {
|
||||||
.querySelectorAll('[data-role-required="sponsor"]')
|
// Special handling for sponsor role
|
||||||
.forEach((element) => {
|
if (officerStatus === "sponsor") {
|
||||||
element.classList.remove("hidden");
|
// Hide all sections first
|
||||||
});
|
document
|
||||||
return;
|
.querySelectorAll("[data-role-required]")
|
||||||
}
|
.forEach((element) => {
|
||||||
|
element.classList.add("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
// For non-sponsor roles, handle normally
|
// Only show sponsor sections
|
||||||
document.querySelectorAll("[data-role-required]").forEach((element) => {
|
document
|
||||||
const requiredRole = element.getAttribute(
|
.querySelectorAll('[data-role-required="sponsor"]')
|
||||||
"data-role-required"
|
.forEach((element) => {
|
||||||
) as OfficerStatus;
|
element.classList.remove("hidden");
|
||||||
|
});
|
||||||
// Skip elements that don't have a role requirement
|
|
||||||
if (!requiredRole || requiredRole === "none") {
|
|
||||||
element.classList.remove("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user has permission for this role
|
|
||||||
const hasPermission = hasAccess(officerStatus, requiredRole);
|
|
||||||
|
|
||||||
// Only show elements if user has permission
|
|
||||||
element.classList.toggle("hidden", !hasPermission);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle navigation
|
|
||||||
const handleNavigation = () => {
|
|
||||||
const navButtons = document.querySelectorAll(".dashboard-nav-btn");
|
|
||||||
const sections = document.querySelectorAll(".dashboard-section");
|
|
||||||
const mainContentDiv = document.getElementById("mainContent");
|
|
||||||
|
|
||||||
// Ensure mainContent is visible
|
|
||||||
if (mainContentDiv) {
|
|
||||||
mainContentDiv.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
|
|
||||||
navButtons.forEach((button) => {
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
const sectionKey = button.getAttribute("data-section");
|
|
||||||
|
|
||||||
// Handle logout button
|
|
||||||
if (sectionKey === "logout") {
|
|
||||||
auth.logout();
|
|
||||||
window.location.reload();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove active class from all buttons
|
// For non-sponsor roles, handle normally
|
||||||
navButtons.forEach((btn) => {
|
document
|
||||||
btn.classList.remove("active", "bg-base-200");
|
.querySelectorAll("[data-role-required]")
|
||||||
});
|
.forEach((element) => {
|
||||||
|
const requiredRole = element.getAttribute(
|
||||||
|
"data-role-required"
|
||||||
|
) as OfficerStatus;
|
||||||
|
|
||||||
// Add active class to clicked button
|
// Skip elements that don't have a role requirement
|
||||||
button.classList.add("active", "bg-base-200");
|
if (!requiredRole || requiredRole === "none") {
|
||||||
|
element.classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Hide all sections
|
// Check if user has permission for this role
|
||||||
sections.forEach((section) => {
|
const hasPermission = hasAccess(
|
||||||
section.classList.add("hidden");
|
officerStatus,
|
||||||
});
|
requiredRole
|
||||||
|
);
|
||||||
|
|
||||||
// Show selected section
|
// Only show elements if user has permission
|
||||||
const sectionId = `${sectionKey}Section`;
|
element.classList.toggle("hidden", !hasPermission);
|
||||||
const targetSection = document.getElementById(sectionId);
|
});
|
||||||
if (targetSection) {
|
|
||||||
targetSection.classList.remove("hidden");
|
|
||||||
console.log(`Showing section: ${sectionId}`); // Debug log
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close mobile sidebar if needed
|
|
||||||
if (window.innerWidth < 1024 && sidebar) {
|
|
||||||
sidebar.classList.add("-translate-x-full");
|
|
||||||
document.body.classList.remove("overflow-hidden");
|
|
||||||
const overlay = document.getElementById("sidebarOverlay");
|
|
||||||
overlay?.remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Display user profile information and handle role-based access
|
|
||||||
const updateUserProfile = async (user: { id: string }) => {
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const extendedUser = await get.getOne("users", user.id, {
|
|
||||||
fields: [
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"member_type",
|
|
||||||
"officer_status",
|
|
||||||
"expand.member_type",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const displayName = extendedUser.name || "Unknown User";
|
|
||||||
const displayRole = extendedUser.member_type || "Member";
|
|
||||||
|
|
||||||
// Map the officer type from the database to our OfficerStatus type
|
|
||||||
let officerStatus: OfficerStatus = "";
|
|
||||||
|
|
||||||
// Get the officer record for this user if it exists
|
|
||||||
const officerRecords = await get.getList(
|
|
||||||
"officers",
|
|
||||||
1,
|
|
||||||
50,
|
|
||||||
`user="${user.id}"`,
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
fields: ["id", "type"],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (officerRecords && officerRecords.items.length > 0) {
|
|
||||||
const officerType = officerRecords.items[0].type;
|
|
||||||
|
|
||||||
// Map the officer type to our OfficerStatus
|
|
||||||
switch (officerType) {
|
|
||||||
case OfficerTypes.ADMINISTRATOR:
|
|
||||||
officerStatus = "administrator";
|
|
||||||
break;
|
|
||||||
case OfficerTypes.EXECUTIVE:
|
|
||||||
officerStatus = "executive";
|
|
||||||
break;
|
|
||||||
case OfficerTypes.GENERAL:
|
|
||||||
officerStatus = "general";
|
|
||||||
break;
|
|
||||||
case OfficerTypes.HONORARY:
|
|
||||||
officerStatus = "honorary";
|
|
||||||
break;
|
|
||||||
case OfficerTypes.PAST:
|
|
||||||
officerStatus = "past";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
officerStatus = "";
|
|
||||||
}
|
|
||||||
} else if (extendedUser.member_type === "Sponsor") {
|
|
||||||
officerStatus = "sponsor";
|
|
||||||
} else {
|
|
||||||
officerStatus = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
const initials = (extendedUser.name || "U")
|
|
||||||
.split(" ")
|
|
||||||
.map((n: string) => n[0])
|
|
||||||
.join("")
|
|
||||||
.toUpperCase();
|
|
||||||
|
|
||||||
// Update profile display
|
|
||||||
if (userName) userName.textContent = displayName;
|
|
||||||
if (userRole) userRole.textContent = displayRole;
|
|
||||||
if (userInitials) userInitials.textContent = initials;
|
|
||||||
|
|
||||||
// Update section visibility based on role
|
|
||||||
updateSectionVisibility(officerStatus);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching user profile:", error);
|
|
||||||
const fallbackValues = {
|
|
||||||
name: "Unknown User",
|
|
||||||
role: "Member",
|
|
||||||
initials: "?",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (userName) userName.textContent = fallbackValues.name;
|
// Handle navigation
|
||||||
if (userRole) userRole.textContent = fallbackValues.role;
|
const handleNavigation = () => {
|
||||||
if (userInitials)
|
const navButtons =
|
||||||
userInitials.textContent = fallbackValues.initials;
|
document.querySelectorAll(".dashboard-nav-btn");
|
||||||
|
const sections =
|
||||||
|
document.querySelectorAll(".dashboard-section");
|
||||||
|
const mainContentDiv = document.getElementById("mainContent");
|
||||||
|
|
||||||
updateSectionVisibility("" as OfficerStatus);
|
// Ensure mainContent is visible
|
||||||
}
|
if (mainContentDiv) {
|
||||||
};
|
mainContentDiv.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
// Mobile sidebar toggle
|
navButtons.forEach((button) => {
|
||||||
const mobileSidebarToggle = document.getElementById("mobileSidebarToggle");
|
button.addEventListener("click", () => {
|
||||||
if (mobileSidebarToggle && sidebar) {
|
const sectionKey = button.getAttribute("data-section");
|
||||||
const toggleSidebar = () => {
|
|
||||||
const isOpen = !sidebar.classList.contains("-translate-x-full");
|
|
||||||
|
|
||||||
if (isOpen) {
|
// Handle logout button
|
||||||
sidebar.classList.add("-translate-x-full");
|
if (sectionKey === "logout") {
|
||||||
document.body.classList.remove("overflow-hidden");
|
auth.logout();
|
||||||
const overlay = document.getElementById("sidebarOverlay");
|
window.location.reload();
|
||||||
overlay?.remove();
|
return;
|
||||||
} else {
|
}
|
||||||
sidebar.classList.remove("-translate-x-full");
|
|
||||||
document.body.classList.add("overflow-hidden");
|
|
||||||
const overlay = document.createElement("div");
|
|
||||||
overlay.id = "sidebarOverlay";
|
|
||||||
overlay.className =
|
|
||||||
"fixed inset-0 bg-black bg-opacity-50 z-40 xl:hidden";
|
|
||||||
overlay.addEventListener("click", toggleSidebar);
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mobileSidebarToggle.addEventListener("click", toggleSidebar);
|
// Remove active class from all buttons
|
||||||
}
|
navButtons.forEach((btn) => {
|
||||||
|
btn.classList.remove("active", "bg-base-200");
|
||||||
|
});
|
||||||
|
|
||||||
// Function to initialize the page
|
// Add active class to clicked button
|
||||||
const initializePage = async () => {
|
button.classList.add("active", "bg-base-200");
|
||||||
try {
|
|
||||||
// Initialize auth sync for IndexedDB
|
|
||||||
await initAuthSync();
|
|
||||||
|
|
||||||
// Check if user is authenticated
|
// Hide all sections
|
||||||
if (!auth.isAuthenticated()) {
|
sections.forEach((section) => {
|
||||||
console.log("User not authenticated");
|
section.classList.add("hidden");
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
});
|
||||||
if (notAuthenticatedState)
|
|
||||||
notAuthenticatedState.classList.remove("hidden");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageLoadingState) pageLoadingState.classList.remove("hidden");
|
// Show selected section
|
||||||
if (pageErrorState) pageErrorState.classList.add("hidden");
|
const sectionId = `${sectionKey}Section`;
|
||||||
if (notAuthenticatedState)
|
const targetSection =
|
||||||
notAuthenticatedState.classList.add("hidden");
|
document.getElementById(sectionId);
|
||||||
|
if (targetSection) {
|
||||||
|
targetSection.classList.remove("hidden");
|
||||||
|
console.log(`Showing section: ${sectionId}`); // Debug log
|
||||||
|
}
|
||||||
|
|
||||||
// Show loading states
|
// Close mobile sidebar if needed
|
||||||
const userProfileSkeleton = document.getElementById(
|
if (window.innerWidth < 1024 && sidebar) {
|
||||||
"userProfileSkeleton"
|
sidebar.classList.add("-translate-x-full");
|
||||||
);
|
document.body.classList.remove("overflow-hidden");
|
||||||
const userProfileSignedOut = document.getElementById(
|
const overlay =
|
||||||
"userProfileSignedOut"
|
document.getElementById("sidebarOverlay");
|
||||||
);
|
overlay?.remove();
|
||||||
const userProfileSummary =
|
}
|
||||||
document.getElementById("userProfileSummary");
|
});
|
||||||
const menuLoadingSkeleton = document.getElementById(
|
|
||||||
"menuLoadingSkeleton"
|
|
||||||
);
|
|
||||||
const actualMenu = document.getElementById("actualMenu");
|
|
||||||
|
|
||||||
if (userProfileSkeleton)
|
|
||||||
userProfileSkeleton.classList.remove("hidden");
|
|
||||||
if (userProfileSummary) userProfileSummary.classList.add("hidden");
|
|
||||||
if (userProfileSignedOut)
|
|
||||||
userProfileSignedOut.classList.add("hidden");
|
|
||||||
if (menuLoadingSkeleton)
|
|
||||||
menuLoadingSkeleton.classList.remove("hidden");
|
|
||||||
if (actualMenu) actualMenu.classList.add("hidden");
|
|
||||||
|
|
||||||
const user = auth.getCurrentUser();
|
|
||||||
await updateUserProfile(user);
|
|
||||||
|
|
||||||
// Show actual profile and hide skeleton
|
|
||||||
if (userProfileSkeleton)
|
|
||||||
userProfileSkeleton.classList.add("hidden");
|
|
||||||
if (userProfileSummary)
|
|
||||||
userProfileSummary.classList.remove("hidden");
|
|
||||||
|
|
||||||
// Hide all sections first
|
|
||||||
document
|
|
||||||
.querySelectorAll(".dashboard-section")
|
|
||||||
.forEach((section) => {
|
|
||||||
section.classList.add("hidden");
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Show appropriate default section based on role
|
// Display user profile information and handle role-based access
|
||||||
// Get the officer record for this user if it exists
|
const updateUserProfile = async (user: { id: string }) => {
|
||||||
let officerStatus: OfficerStatus = "none";
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const officerRecords = await get.getList(
|
|
||||||
"officers",
|
|
||||||
1,
|
|
||||||
50,
|
|
||||||
`user="${user.id}"`,
|
|
||||||
"",
|
|
||||||
{
|
|
||||||
fields: ["id", "type"],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (officerRecords && officerRecords.items.length > 0) {
|
|
||||||
const officerType = officerRecords.items[0].type;
|
|
||||||
|
|
||||||
// Map the officer type to our OfficerStatus
|
|
||||||
switch (officerType) {
|
|
||||||
case OfficerTypes.ADMINISTRATOR:
|
|
||||||
officerStatus = "administrator";
|
|
||||||
break;
|
|
||||||
case OfficerTypes.EXECUTIVE:
|
|
||||||
officerStatus = "executive";
|
|
||||||
break;
|
|
||||||
case OfficerTypes.GENERAL:
|
|
||||||
officerStatus = "general";
|
|
||||||
break;
|
|
||||||
case OfficerTypes.HONORARY:
|
|
||||||
officerStatus = "honorary";
|
|
||||||
break;
|
|
||||||
case OfficerTypes.PAST:
|
|
||||||
officerStatus = "past";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
officerStatus = "none";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const extendedUser = await get.getOne("users", user.id, {
|
const extendedUser = await get.getOne("users", user.id, {
|
||||||
fields: ["member_type"],
|
fields: [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"member_type",
|
||||||
|
"officer_status",
|
||||||
|
"expand.member_type",
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (extendedUser.member_type === "Sponsor") {
|
const displayName = extendedUser.name || "Unknown User";
|
||||||
|
const displayRole = extendedUser.member_type || "Member";
|
||||||
|
|
||||||
|
// Map the officer type from the database to our OfficerStatus type
|
||||||
|
let officerStatus: OfficerStatus = "";
|
||||||
|
|
||||||
|
// Get the officer record for this user if it exists
|
||||||
|
const officerRecords = await get.getList(
|
||||||
|
"officers",
|
||||||
|
1,
|
||||||
|
50,
|
||||||
|
`user="${user.id}"`,
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
fields: ["id", "type"],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (officerRecords && officerRecords.items.length > 0) {
|
||||||
|
const officerType = officerRecords.items[0].type;
|
||||||
|
|
||||||
|
// Map the officer type to our OfficerStatus
|
||||||
|
switch (officerType) {
|
||||||
|
case OfficerTypes.ADMINISTRATOR:
|
||||||
|
officerStatus = "administrator";
|
||||||
|
break;
|
||||||
|
case OfficerTypes.EXECUTIVE:
|
||||||
|
officerStatus = "executive";
|
||||||
|
break;
|
||||||
|
case OfficerTypes.GENERAL:
|
||||||
|
officerStatus = "general";
|
||||||
|
break;
|
||||||
|
case OfficerTypes.HONORARY:
|
||||||
|
officerStatus = "honorary";
|
||||||
|
break;
|
||||||
|
case OfficerTypes.PAST:
|
||||||
|
officerStatus = "past";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
officerStatus = "";
|
||||||
|
}
|
||||||
|
} else if (extendedUser.member_type === "Sponsor") {
|
||||||
officerStatus = "sponsor";
|
officerStatus = "sponsor";
|
||||||
|
} else {
|
||||||
|
officerStatus = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initials = (extendedUser.name || "U")
|
||||||
|
.split(" ")
|
||||||
|
.map((n: string) => n[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
|
// Update profile display
|
||||||
|
if (userName) userName.textContent = displayName;
|
||||||
|
if (userRole) userRole.textContent = displayRole;
|
||||||
|
if (userInitials) userInitials.textContent = initials;
|
||||||
|
|
||||||
|
// Update section visibility based on role
|
||||||
|
updateSectionVisibility(officerStatus);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching user profile:", error);
|
||||||
|
const fallbackValues = {
|
||||||
|
name: "Unknown User",
|
||||||
|
role: "Member",
|
||||||
|
initials: "?",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (userName) userName.textContent = fallbackValues.name;
|
||||||
|
if (userRole) userRole.textContent = fallbackValues.role;
|
||||||
|
if (userInitials)
|
||||||
|
userInitials.textContent = fallbackValues.initials;
|
||||||
|
|
||||||
|
updateSectionVisibility("" as OfficerStatus);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
};
|
||||||
console.error("Error determining officer status:", error);
|
|
||||||
officerStatus = "none";
|
// Mobile sidebar toggle
|
||||||
|
const mobileSidebarToggle = document.getElementById(
|
||||||
|
"mobileSidebarToggle"
|
||||||
|
);
|
||||||
|
if (mobileSidebarToggle && sidebar) {
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
const isOpen =
|
||||||
|
!sidebar.classList.contains("-translate-x-full");
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
sidebar.classList.add("-translate-x-full");
|
||||||
|
document.body.classList.remove("overflow-hidden");
|
||||||
|
const overlay =
|
||||||
|
document.getElementById("sidebarOverlay");
|
||||||
|
overlay?.remove();
|
||||||
|
} else {
|
||||||
|
sidebar.classList.remove("-translate-x-full");
|
||||||
|
document.body.classList.add("overflow-hidden");
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.id = "sidebarOverlay";
|
||||||
|
overlay.className =
|
||||||
|
"fixed inset-0 bg-black bg-opacity-50 z-40 xl:hidden";
|
||||||
|
overlay.addEventListener("click", toggleSidebar);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mobileSidebarToggle.addEventListener("click", toggleSidebar);
|
||||||
}
|
}
|
||||||
|
|
||||||
let defaultSection;
|
// Function to initialize the page
|
||||||
let defaultButton;
|
const initializePage = async () => {
|
||||||
|
try {
|
||||||
|
// Initialize auth sync for IndexedDB
|
||||||
|
await initAuthSync();
|
||||||
|
|
||||||
if (officerStatus === "sponsor") {
|
// Check if user is authenticated
|
||||||
defaultSection = document.getElementById(
|
if (!auth.isAuthenticated()) {
|
||||||
"sponsorDashboardSection"
|
console.log("User not authenticated");
|
||||||
);
|
if (pageLoadingState)
|
||||||
defaultButton = document.querySelector(
|
pageLoadingState.classList.add("hidden");
|
||||||
'[data-section="sponsorDashboard"]'
|
if (notAuthenticatedState)
|
||||||
);
|
notAuthenticatedState.classList.remove("hidden");
|
||||||
} else if (officerStatus === "administrator") {
|
return;
|
||||||
defaultSection = document.getElementById(
|
}
|
||||||
"adminDashboardSection"
|
|
||||||
);
|
|
||||||
defaultButton = document.querySelector(
|
|
||||||
'[data-section="adminDashboard"]'
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
defaultSection = document.getElementById("profileSection");
|
|
||||||
defaultButton = document.querySelector(
|
|
||||||
'[data-section="profile"]'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultSection) {
|
if (pageLoadingState)
|
||||||
defaultSection.classList.remove("hidden");
|
pageLoadingState.classList.remove("hidden");
|
||||||
}
|
if (pageErrorState) pageErrorState.classList.add("hidden");
|
||||||
if (defaultButton) {
|
if (notAuthenticatedState)
|
||||||
defaultButton.classList.add("active", "bg-base-200");
|
notAuthenticatedState.classList.add("hidden");
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize navigation
|
// Show loading states
|
||||||
handleNavigation();
|
const userProfileSkeleton = document.getElementById(
|
||||||
|
"userProfileSkeleton"
|
||||||
|
);
|
||||||
|
const userProfileSignedOut = document.getElementById(
|
||||||
|
"userProfileSignedOut"
|
||||||
|
);
|
||||||
|
const userProfileSummary =
|
||||||
|
document.getElementById("userProfileSummary");
|
||||||
|
const menuLoadingSkeleton = document.getElementById(
|
||||||
|
"menuLoadingSkeleton"
|
||||||
|
);
|
||||||
|
const actualMenu = document.getElementById("actualMenu");
|
||||||
|
|
||||||
// Show actual menu and hide skeleton
|
if (userProfileSkeleton)
|
||||||
if (menuLoadingSkeleton)
|
userProfileSkeleton.classList.remove("hidden");
|
||||||
menuLoadingSkeleton.classList.add("hidden");
|
if (userProfileSummary)
|
||||||
if (actualMenu) actualMenu.classList.remove("hidden");
|
userProfileSummary.classList.add("hidden");
|
||||||
|
if (userProfileSignedOut)
|
||||||
|
userProfileSignedOut.classList.add("hidden");
|
||||||
|
if (menuLoadingSkeleton)
|
||||||
|
menuLoadingSkeleton.classList.remove("hidden");
|
||||||
|
if (actualMenu) actualMenu.classList.add("hidden");
|
||||||
|
|
||||||
// Show main content and hide loading
|
const user = auth.getCurrentUser();
|
||||||
if (mainContent) mainContent.classList.remove("hidden");
|
await updateUserProfile(user);
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error initializing dashboard:", error);
|
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
|
||||||
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize when DOM is loaded
|
// Show actual profile and hide skeleton
|
||||||
document.addEventListener("DOMContentLoaded", initializePage);
|
if (userProfileSkeleton)
|
||||||
|
userProfileSkeleton.classList.add("hidden");
|
||||||
|
if (userProfileSummary)
|
||||||
|
userProfileSummary.classList.remove("hidden");
|
||||||
|
|
||||||
// Handle login button click
|
// Hide all sections first
|
||||||
document
|
document
|
||||||
.querySelector(".login-button")
|
.querySelectorAll(".dashboard-section")
|
||||||
?.addEventListener("click", async () => {
|
.forEach((section) => {
|
||||||
try {
|
section.classList.add("hidden");
|
||||||
if (pageLoadingState)
|
});
|
||||||
pageLoadingState.classList.remove("hidden");
|
|
||||||
if (notAuthenticatedState)
|
|
||||||
notAuthenticatedState.classList.add("hidden");
|
|
||||||
await auth.login();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Login error:", error);
|
|
||||||
if (pageLoadingState) pageLoadingState.classList.add("hidden");
|
|
||||||
if (pageErrorState) pageErrorState.classList.remove("hidden");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle logout button click
|
// Show appropriate default section based on role
|
||||||
document.getElementById("logoutButton")?.addEventListener("click", () => {
|
// Get the officer record for this user if it exists
|
||||||
auth.logout();
|
let officerStatus: OfficerStatus = "none";
|
||||||
window.location.reload();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle responsive sidebar
|
try {
|
||||||
if (sidebar) {
|
const officerRecords = await get.getList(
|
||||||
if (window.innerWidth < 1024) {
|
"officers",
|
||||||
sidebar.classList.add("-translate-x-full");
|
1,
|
||||||
}
|
50,
|
||||||
|
`user="${user.id}"`,
|
||||||
|
"",
|
||||||
|
{
|
||||||
|
fields: ["id", "type"],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
if (officerRecords && officerRecords.items.length > 0) {
|
||||||
if (window.innerWidth >= 1024) {
|
const officerType = officerRecords.items[0].type;
|
||||||
const overlay = document.getElementById("sidebarOverlay");
|
|
||||||
if (overlay) {
|
// Map the officer type to our OfficerStatus
|
||||||
overlay.remove();
|
switch (officerType) {
|
||||||
document.body.classList.remove("overflow-hidden");
|
case OfficerTypes.ADMINISTRATOR:
|
||||||
|
officerStatus = "administrator";
|
||||||
|
break;
|
||||||
|
case OfficerTypes.EXECUTIVE:
|
||||||
|
officerStatus = "executive";
|
||||||
|
break;
|
||||||
|
case OfficerTypes.GENERAL:
|
||||||
|
officerStatus = "general";
|
||||||
|
break;
|
||||||
|
case OfficerTypes.HONORARY:
|
||||||
|
officerStatus = "honorary";
|
||||||
|
break;
|
||||||
|
case OfficerTypes.PAST:
|
||||||
|
officerStatus = "past";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
officerStatus = "none";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const extendedUser = await get.getOne(
|
||||||
|
"users",
|
||||||
|
user.id,
|
||||||
|
{
|
||||||
|
fields: ["member_type"],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extendedUser.member_type === "Sponsor") {
|
||||||
|
officerStatus = "sponsor";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"Error determining officer status:",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
officerStatus = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultSection;
|
||||||
|
let defaultButton;
|
||||||
|
|
||||||
|
if (officerStatus === "sponsor") {
|
||||||
|
defaultSection = document.getElementById(
|
||||||
|
"sponsorDashboardSection"
|
||||||
|
);
|
||||||
|
defaultButton = document.querySelector(
|
||||||
|
'[data-section="sponsorDashboard"]'
|
||||||
|
);
|
||||||
|
} else if (officerStatus === "administrator") {
|
||||||
|
defaultSection = document.getElementById(
|
||||||
|
"adminDashboardSection"
|
||||||
|
);
|
||||||
|
defaultButton = document.querySelector(
|
||||||
|
'[data-section="adminDashboard"]'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
defaultSection =
|
||||||
|
document.getElementById("profileSection");
|
||||||
|
defaultButton = document.querySelector(
|
||||||
|
'[data-section="profile"]'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (defaultSection) {
|
||||||
|
defaultSection.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
if (defaultButton) {
|
||||||
|
defaultButton.classList.add("active", "bg-base-200");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize navigation
|
||||||
|
handleNavigation();
|
||||||
|
|
||||||
|
// Show actual menu and hide skeleton
|
||||||
|
if (menuLoadingSkeleton)
|
||||||
|
menuLoadingSkeleton.classList.add("hidden");
|
||||||
|
if (actualMenu) actualMenu.classList.remove("hidden");
|
||||||
|
|
||||||
|
// Show main content and hide loading
|
||||||
|
if (mainContent) mainContent.classList.remove("hidden");
|
||||||
|
if (pageLoadingState)
|
||||||
|
pageLoadingState.classList.add("hidden");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initializing dashboard:", error);
|
||||||
|
if (pageLoadingState)
|
||||||
|
pageLoadingState.classList.add("hidden");
|
||||||
|
if (pageErrorState)
|
||||||
|
pageErrorState.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
sidebar.classList.remove("-translate-x-full");
|
};
|
||||||
|
|
||||||
|
// Initialize when DOM is loaded
|
||||||
|
document.addEventListener("DOMContentLoaded", initializePage);
|
||||||
|
|
||||||
|
// Handle login button click
|
||||||
|
document
|
||||||
|
.querySelector(".login-button")
|
||||||
|
?.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
if (pageLoadingState)
|
||||||
|
pageLoadingState.classList.remove("hidden");
|
||||||
|
if (notAuthenticatedState)
|
||||||
|
notAuthenticatedState.classList.add("hidden");
|
||||||
|
await auth.login();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Login error:", error);
|
||||||
|
if (pageLoadingState)
|
||||||
|
pageLoadingState.classList.add("hidden");
|
||||||
|
if (pageErrorState)
|
||||||
|
pageErrorState.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle logout button click
|
||||||
|
document
|
||||||
|
.getElementById("logoutButton")
|
||||||
|
?.addEventListener("click", () => {
|
||||||
|
auth.logout();
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle responsive sidebar
|
||||||
|
if (sidebar) {
|
||||||
|
if (window.innerWidth < 1024) {
|
||||||
|
sidebar.classList.add("-translate-x-full");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
if (window.innerWidth >= 1024) {
|
||||||
|
const overlay =
|
||||||
|
document.getElementById("sidebarOverlay");
|
||||||
|
if (overlay) {
|
||||||
|
overlay.remove();
|
||||||
|
document.body.classList.remove("overflow-hidden");
|
||||||
|
}
|
||||||
|
sidebar.classList.remove("-translate-x-full");
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
</script>
|
||||||
}
|
</body>
|
||||||
</script>
|
</html>
|
||||||
|
|
Loading…
Reference in a new issue