Fix toast hydration

This commit is contained in:
chark1es 2025-03-01 04:37:33 -08:00
parent eb77c00540
commit ef3e8f38d6
9 changed files with 855 additions and 931 deletions

View file

@ -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>

View file

@ -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">

View file

@ -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

View file

@ -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>

View file

@ -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"

View file

@ -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"

View file

@ -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',
},
}}
/>
);
}

View 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,
},
}}
/>
);
}

View file

@ -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>