diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro index ebe7ff6..bb5a262 100644 --- a/src/pages/dashboard.astro +++ b/src/pages/dashboard.astro @@ -3,12 +3,6 @@ import yaml from "js-yaml"; import { Icon } from "astro-icon/components"; import fs from "node:fs"; import path from "node:path"; -import { Authentication } from "../scripts/pocketbase/Authentication"; -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"; import ToastProvider from "../components/dashboard/universal/ToastProvider"; import FirstTimeLoginManager from "../components/dashboard/universal/FirstTimeLoginManager"; @@ -57,11 +51,21 @@ const components = Object.fromEntries( logtoApiEndpoint={logtoApiEndpoint} /> -
+
+ + +
@@ -430,51 +435,419 @@ const components = Object.fromEntries( const userName = document.getElementById("userName"); const userRole = document.getElementById("userRole"); - // Function to update section visibility based on role - const updateSectionVisibility = (officerStatus: OfficerStatus) => { - // Special handling for sponsor role - if (officerStatus === "sponsor") { - // Hide all sections first - document - .querySelectorAll("[data-role-required]") - .forEach((element) => { - element.classList.add("hidden"); - }); + // Centralized sidebar management + class SidebarManager { + private sidebar: HTMLElement | null = null; + private overlay: HTMLElement | null = null; + private toggleBtn: HTMLElement | null = null; + private closeBtn: HTMLElement | null = null; + private breakpoint = 1024; // lg breakpoint + private isInitialized = false; - // Only show sponsor sections - document - .querySelectorAll('[data-role-required="sponsor"]') - .forEach((element) => { - element.classList.remove("hidden"); - }); - return; + constructor() { + // Initialize elements + this.sidebar = document.getElementById("dashboardSidebar"); + this.overlay = document.getElementById("sidebarOverlay"); + this.toggleBtn = document.getElementById( + "mobileSidebarToggle" + ); + this.closeBtn = document.getElementById("closeSidebarBtn"); + + if (!this.sidebar || !this.overlay) { + console.error("Sidebar elements not found"); + return; + } + + this.setupEventListeners(); + this.checkViewportSize(); + this.isInitialized = true; } - // For non-sponsor roles, handle normally - document - .querySelectorAll("[data-role-required]") - .forEach((element) => { - const requiredRole = element.getAttribute( - "data-role-required" - ) as OfficerStatus; + private setupEventListeners(): void { + // Toggle button (mobile only) + this.toggleBtn?.addEventListener("click", () => + this.openSidebar() + ); - // Skip elements that don't have a role requirement - if (!requiredRole || requiredRole === "none") { - element.classList.remove("hidden"); + // Close button (mobile only) + this.closeBtn?.addEventListener("click", () => + this.closeSidebar() + ); + + // Overlay click to close + this.overlay?.addEventListener("click", () => + this.closeSidebar() + ); + + // ESC key to close + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") this.closeSidebar(); + }); + + // Handle resize events with debounce + let resizeTimer: ReturnType; + window.addEventListener("resize", () => { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + this.checkViewportSize(); + }, 100); + }); + + // Handle navigation item clicks + document + .querySelectorAll(".dashboard-nav-btn") + .forEach((btn) => { + btn.addEventListener("click", (e) => { + const target = e.currentTarget as HTMLElement; + const sectionKey = + target.getAttribute("data-section"); + + // Skip for logout button + if (sectionKey === "logout") return; + + // Close sidebar on mobile when navigation happens + if (window.innerWidth < this.breakpoint) { + this.closeSidebar(); + } + }); + }); + } + + private checkViewportSize(): void { + if (!this.sidebar) return; + + const isMobile = window.innerWidth < this.breakpoint; + + if (isMobile) { + // On mobile, ensure sidebar starts closed + this.closeSidebar(); + } else { + // On desktop, ensure sidebar is always visible + this.sidebar.classList.remove("-translate-x-full"); + this.sidebar.classList.add("translate-x-0"); + document.body.classList.remove("overflow-hidden"); + this.hideOverlay(); + } + } + + private openSidebar(): void { + if (!this.sidebar) return; + + // Show sidebar + this.sidebar.classList.remove("-translate-x-full"); + this.sidebar.classList.add("translate-x-0"); + + // Prevent body scrolling + document.body.classList.add("overflow-hidden"); + + // Show overlay + this.showOverlay(); + } + + private closeSidebar(): void { + if (!this.sidebar || window.innerWidth >= this.breakpoint) + return; + + // Hide sidebar + this.sidebar.classList.add("-translate-x-full"); + this.sidebar.classList.remove("translate-x-0"); + + // Restore body scrolling + document.body.classList.remove("overflow-hidden"); + + // Hide overlay + this.hideOverlay(); + } + + private showOverlay(): void { + this.overlay?.classList.remove("hidden"); + } + + private hideOverlay(): void { + this.overlay?.classList.add("hidden"); + } + + // Public API + public toggle(): void { + if (!this.sidebar) return; + + if (this.sidebar.classList.contains("-translate-x-full")) { + this.openSidebar(); + } else { + this.closeSidebar(); + } + } + } + + // 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") { + showLogoutConfirmation(); return; } - // Check if user has permission for this role - const hasPermission = hasAccess( - officerStatus, - requiredRole + // Remove active class from all buttons + navButtons.forEach((btn) => { + btn.classList.remove("active", "bg-base-200"); + }); + + // Add active class to clicked button + button.classList.add("active", "bg-base-200"); + + // Hide all sections + sections.forEach((section) => { + section.classList.add("hidden"); + }); + + // Show selected section + const sectionId = `${sectionKey}Section`; + const targetSection = + document.getElementById(sectionId); + if (targetSection) { + targetSection.classList.remove("hidden"); + } + }); + }); + }; + + // Function to initialize the page + const initializePage = async () => { + try { + // Define a temporary toast function that does nothing for unauthenticated users + const originalToast = window.toast; + + // Check if user is authenticated + if (!auth.isAuthenticated()) { + // Temporarily override toast function to prevent notifications for unauthenticated users + window.toast = () => {}; + + // Initialize auth sync for IndexedDB (but toast notifications will be suppressed) + await initAuthSync(); + + // Restore original toast function + window.toast = originalToast; + + // console.log("User not authenticated"); + if (pageLoadingState) + pageLoadingState.classList.add("hidden"); + if (notAuthenticatedState) + notAuthenticatedState.classList.remove("hidden"); + return; + } + + // Initialize auth sync for IndexedDB (for authenticated users) + await initAuthSync(); + + if (pageLoadingState) + pageLoadingState.classList.remove("hidden"); + if (pageErrorState) pageErrorState.classList.add("hidden"); + if (notAuthenticatedState) + notAuthenticatedState.classList.add("hidden"); + + // Show loading states + 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"); + + 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 + // Get the officer record for this user if it exists + let officerStatus: OfficerStatus = "none"; + + try { + const officerRecords = await get.getList( + "officers", + 1, + 50, + `user="${user.id}"`, + "", + { + fields: ["id", "type", "role"], + } ); - // Only show elements if user has permission - element.classList.toggle("hidden", !hasPermission); - }); + if (officerRecords && officerRecords.items.length > 0) { + const officerType = officerRecords.items[0].type; + // We can also get the role here if needed for display elsewhere + const officerRole = officerRecords.items[0].role; + + // 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 { + // Check if user is a sponsor by querying the sponsors collection + const sponsorRecords = await get.getList( + "sponsors", + 1, + 1, + `user="${user.id}"`, + "", + { + fields: ["id", "company"], + } + ); + + if ( + sponsorRecords && + sponsorRecords.items.length > 0 + ) { + officerStatus = "sponsor"; + } else { + officerStatus = "none"; + } + } + } catch (error) { + console.error( + "Error determining officer status:", + error + ); + officerStatus = "none"; + } + + let defaultSection; + let defaultButton; + + // Set default section based on role + // Only sponsors get a different default view + if (officerStatus === "sponsor") { + // For sponsors, show the sponsor dashboard + defaultSection = document.getElementById( + "sponsorDashboardSection" + ); + defaultButton = document.querySelector( + '[data-section="sponsorDashboard"]' + ); + } else { + // For all other users (including administrators), show the profile section + 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"); + } }; + // Initialize when DOM is loaded + document.addEventListener("DOMContentLoaded", () => { + initializePage(); + + // Initialize sidebar manager + new SidebarManager(); + }); + + // 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"); + } + }); + // Function to delete all cookies (to handle Logto logout) const deleteAllCookies = () => { // Get all cookies @@ -649,61 +1022,49 @@ const components = Object.fromEntries( if (modal) (modal as HTMLDialogElement).showModal(); }; - // Handle navigation - const handleNavigation = () => { - const navButtons = - document.querySelectorAll(".dashboard-nav-btn"); - const sections = - document.querySelectorAll(".dashboard-section"); - const mainContentDiv = document.getElementById("mainContent"); + // Function to update section visibility based on role + const updateSectionVisibility = (officerStatus: OfficerStatus) => { + // Special handling for sponsor role + if (officerStatus === "sponsor") { + // Hide all sections first + document + .querySelectorAll("[data-role-required]") + .forEach((element) => { + element.classList.add("hidden"); + }); - // Ensure mainContent is visible - if (mainContentDiv) { - mainContentDiv.classList.remove("hidden"); + // Only show sponsor sections + document + .querySelectorAll('[data-role-required="sponsor"]') + .forEach((element) => { + element.classList.remove("hidden"); + }); + return; } - navButtons.forEach((button) => { - button.addEventListener("click", () => { - const sectionKey = button.getAttribute("data-section"); + // For non-sponsor roles, handle normally + document + .querySelectorAll("[data-role-required]") + .forEach((element) => { + const requiredRole = element.getAttribute( + "data-role-required" + ) as OfficerStatus; - // Handle logout button - if (sectionKey === "logout") { - showLogoutConfirmation(); + // Skip elements that don't have a role requirement + if (!requiredRole || requiredRole === "none") { + element.classList.remove("hidden"); return; } - // Remove active class from all buttons - navButtons.forEach((btn) => { - btn.classList.remove("active", "bg-base-200"); - }); + // Check if user has permission for this role + const hasPermission = hasAccess( + officerStatus, + requiredRole + ); - // Add active class to clicked button - button.classList.add("active", "bg-base-200"); - - // Hide all sections - sections.forEach((section) => { - section.classList.add("hidden"); - }); - - // Show selected section - const sectionId = `${sectionKey}Section`; - 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(); - } + // Only show elements if user has permission + element.classList.toggle("hidden", !hasPermission); }); - }); }; // Display user profile information and handle role-based access @@ -826,275 +1187,6 @@ const components = Object.fromEntries( updateSectionVisibility("" as OfficerStatus); } }; - - // 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); - } - - // Function to initialize the page - const initializePage = async () => { - try { - // Define a temporary toast function that does nothing for unauthenticated users - const originalToast = window.toast; - - // Check if user is authenticated - if (!auth.isAuthenticated()) { - // Temporarily override toast function to prevent notifications for unauthenticated users - window.toast = () => {}; - - // Initialize auth sync for IndexedDB (but toast notifications will be suppressed) - await initAuthSync(); - - // Restore original toast function - window.toast = originalToast; - - // console.log("User not authenticated"); - if (pageLoadingState) - pageLoadingState.classList.add("hidden"); - if (notAuthenticatedState) - notAuthenticatedState.classList.remove("hidden"); - return; - } - - // Initialize auth sync for IndexedDB (for authenticated users) - await initAuthSync(); - - if (pageLoadingState) - pageLoadingState.classList.remove("hidden"); - if (pageErrorState) pageErrorState.classList.add("hidden"); - if (notAuthenticatedState) - notAuthenticatedState.classList.add("hidden"); - - // Show loading states - 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"); - - 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 - // Get the officer record for this user if it exists - let officerStatus: OfficerStatus = "none"; - - try { - const officerRecords = await get.getList( - "officers", - 1, - 50, - `user="${user.id}"`, - "", - { - fields: ["id", "type", "role"], - } - ); - - if (officerRecords && officerRecords.items.length > 0) { - const officerType = officerRecords.items[0].type; - // We can also get the role here if needed for display elsewhere - const officerRole = officerRecords.items[0].role; - - // 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 { - // Check if user is a sponsor by querying the sponsors collection - const sponsorRecords = await get.getList( - "sponsors", - 1, - 1, - `user="${user.id}"`, - "", - { - fields: ["id", "company"], - } - ); - - if ( - sponsorRecords && - sponsorRecords.items.length > 0 - ) { - officerStatus = "sponsor"; - } else { - officerStatus = "none"; - } - } - } catch (error) { - console.error( - "Error determining officer status:", - error - ); - officerStatus = "none"; - } - - let defaultSection; - let defaultButton; - - // Set default section based on role - // Only sponsors get a different default view - if (officerStatus === "sponsor") { - // For sponsors, show the sponsor dashboard - defaultSection = document.getElementById( - "sponsorDashboardSection" - ); - defaultButton = document.querySelector( - '[data-section="sponsorDashboard"]' - ); - } else { - // For all other users (including administrators), show the profile section - defaultSection = - document.getElementById("profileSection"); - defaultButton = document.querySelector( - '[data-section="profile"]' - ); - - // Log the default section for debugging - // console.log(`Setting default section to profile for user with role: ${officerStatus}`); - } - - 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"); - } - }; - - // 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 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"); - } - }); - }