1100 lines
51 KiB
Text
1100 lines
51 KiB
Text
---
|
|
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";
|
|
import "../styles/global.css";
|
|
const title = "Dashboard";
|
|
|
|
// Load environment variables
|
|
const logtoApiEndpoint = import.meta.env.LOGTO_API_ENDPOINT || "";
|
|
|
|
// Load and parse dashboard config
|
|
const configPath = path.join(process.cwd(), "src", "config", "dashboard.yaml");
|
|
const dashboardConfig = yaml.load(fs.readFileSync(configPath, "utf8")) as any;
|
|
|
|
// Dynamically import all dashboard components
|
|
const components = Object.fromEntries(
|
|
await Promise.all(
|
|
Object.values(dashboardConfig.sections)
|
|
.filter((section: any) => section.component) // Only process sections with components
|
|
.map(async (section: any) => {
|
|
const component = await import(
|
|
`../components/dashboard/${section.component}.astro`
|
|
);
|
|
// console.log(`Loaded component: ${section.component}`); // Debug log
|
|
return [section.component, component.default];
|
|
})
|
|
)
|
|
);
|
|
|
|
// console.log("Available components:", Object.keys(components)); // Debug log
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang="en" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>{title} | IEEE UCSD</title>
|
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
<script
|
|
src="https://code.iconify.design/iconify-icon/2.3.0/iconify-icon.min.js"
|
|
></script>
|
|
</head>
|
|
<body class="bg-base-200">
|
|
<!-- First Time Login Manager - This handles the onboarding popup for new users -->
|
|
<FirstTimeLoginManager
|
|
client:load
|
|
logtoApiEndpoint={logtoApiEndpoint}
|
|
/>
|
|
|
|
<div class="flex h-screen">
|
|
<!-- Sidebar -->
|
|
<aside
|
|
class="bg-base-100 w-80 flex flex-col shadow-xl border-r border-base-200 transition-all duration-300 fixed xl:relative h-full z-50 -translate-x-full xl:translate-x-0"
|
|
>
|
|
<!-- Logo -->
|
|
<div class="p-6 border-b border-base-200">
|
|
<div class="flex items-center justify-center">
|
|
<span
|
|
class="text-4xl font-bold text-[#06659d] select-none tracking-wide"
|
|
>IEEEUCSD</span
|
|
>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Profile -->
|
|
<div class="p-6 border-b border-base-200">
|
|
<!-- Loading State -->
|
|
<div
|
|
id="userProfileSkeleton"
|
|
class="flex items-center gap-4"
|
|
>
|
|
<div class="avatar flex items-center justify-center">
|
|
<div
|
|
class="w-12 h-12 rounded-xl bg-base-300 animate-pulse"
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div
|
|
class="h-6 w-32 bg-base-300 animate-pulse rounded-sm mb-2"
|
|
>
|
|
</div>
|
|
<div
|
|
class="h-5 w-20 bg-base-300 animate-pulse rounded-sm"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Signed Out State -->
|
|
<div
|
|
id="userProfileSignedOut"
|
|
class="flex items-center gap-4 hidden"
|
|
>
|
|
<div class="avatar flex items-center justify-center">
|
|
<div
|
|
class="w-12 h-12 rounded-xl bg-base-300 text-base-content/30 flex items-center justify-center"
|
|
>
|
|
<Icon name="heroicons:user" class="h-6 w-6" />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3
|
|
class="font-medium text-lg text-base-content/70"
|
|
>
|
|
Signed Out
|
|
</h3>
|
|
<div class="badge badge-outline mt-1 opacity-50">
|
|
Guest
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actual Profile -->
|
|
<div
|
|
id="userProfileSummary"
|
|
class="flex items-center gap-4 hidden"
|
|
>
|
|
<div class="avatar flex items-center justify-center">
|
|
<div
|
|
class="w-12 h-12 rounded-xl bg-[#06659d] text-white ring-3 ring-base-200 ring-offset-base-100 ring-offset-2 inline-flex items-center justify-center"
|
|
>
|
|
<span
|
|
class="text-xl font-semibold select-none inline-flex items-center justify-center w-full h-full"
|
|
id="userInitials">?</span
|
|
>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 class="font-medium text-lg" id="userName">
|
|
Loading...
|
|
</h3>
|
|
<div
|
|
class="badge badge-outline mt-1 border-[#06659d] text-[#06659d]"
|
|
id="userRole"
|
|
>
|
|
Member
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<nav
|
|
class="flex-1 overflow-y-auto scrollbar-hide py-6 flex flex-col"
|
|
>
|
|
<ul
|
|
class="menu gap-2 px-4 text-base-content/80 flex-1 flex flex-col"
|
|
>
|
|
<!-- Loading Skeleton -->
|
|
<div id="menuLoadingSkeleton">
|
|
{
|
|
[1, 2, 3].map((group) => (
|
|
<>
|
|
<li class="menu-title font-medium opacity-70">
|
|
<div class="h-4 w-24 bg-base-300 animate-pulse rounded-sm" />
|
|
</li>
|
|
{[1, 2, 3].map((item) => (
|
|
<li>
|
|
<div class="flex items-center gap-4 py-2">
|
|
<div class="h-5 w-5 bg-base-300 animate-pulse rounded-sm" />
|
|
<div class="h-4 w-32 bg-base-300 animate-pulse rounded-sm" />
|
|
</div>
|
|
</li>
|
|
))}
|
|
</>
|
|
))
|
|
}
|
|
</div>
|
|
|
|
<!-- Actual Menu -->
|
|
<div id="actualMenu" class="hidden">
|
|
{
|
|
Object.entries(dashboardConfig.categories).map(
|
|
([categoryKey, category]: [
|
|
string,
|
|
any,
|
|
]) => (
|
|
<>
|
|
<li
|
|
class={`menu-title font-medium opacity-70 ${
|
|
category.role &&
|
|
category.role !== "none"
|
|
? "hidden"
|
|
: ""
|
|
}`}
|
|
data-role-required={
|
|
category.role || "none"
|
|
}
|
|
>
|
|
<span>{category.title}</span>
|
|
</li>
|
|
{category.sections.map(
|
|
(sectionKey: string) => {
|
|
const section =
|
|
dashboardConfig
|
|
.sections[
|
|
sectionKey
|
|
];
|
|
return (
|
|
<li
|
|
class={
|
|
section.role &&
|
|
section.role !==
|
|
"none"
|
|
? "hidden"
|
|
: ""
|
|
}
|
|
data-role-required={
|
|
section.role
|
|
}
|
|
>
|
|
<button
|
|
class={`dashboard-nav-btn gap-4 transition-all duration-200 outline-hidden focus:outline-hidden hover:bg-opacity-5 ${section.class || ""}`}
|
|
data-section={
|
|
sectionKey
|
|
}
|
|
>
|
|
<Icon
|
|
name={
|
|
section.icon
|
|
}
|
|
class="h-5 w-5"
|
|
/>
|
|
{section.title}
|
|
</button>
|
|
</li>
|
|
);
|
|
}
|
|
)}
|
|
</>
|
|
)
|
|
)
|
|
}
|
|
|
|
{/* Add Logout Button to the bottom of the menu */}
|
|
<li class="mt-auto">
|
|
<button
|
|
class="dashboard-nav-btn gap-4 transition-all duration-200 outline-hidden focus:outline-hidden hover:bg-opacity-5 text-error"
|
|
data-section="logout"
|
|
>
|
|
<Icon
|
|
name="heroicons:arrow-left-on-rectangle"
|
|
class="h-5 w-5"
|
|
/>
|
|
Logout
|
|
</button>
|
|
</li>
|
|
</div>
|
|
</ul>
|
|
</nav>
|
|
</aside>
|
|
|
|
<!-- Main Content -->
|
|
<main
|
|
class="flex-1 overflow-x-hidden overflow-y-auto bg-base-200 w-full xl:w-[calc(100%-20rem)]"
|
|
>
|
|
<!-- Mobile Header -->
|
|
<header
|
|
class="bg-base-100 p-4 shadow-md xl:hidden sticky top-0 z-40"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
id="mobileSidebarToggle"
|
|
class="btn btn-square btn-ghost"
|
|
>
|
|
<Icon name="heroicons:bars-3" class="h-6 w-6" />
|
|
</button>
|
|
<h1 class="text-xl font-bold">IEEE UCSD</h1>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Page Content -->
|
|
<div class="p-4 md:p-6 max-w-[1600px] mx-auto">
|
|
<!-- Loading State -->
|
|
<div id="pageLoadingState" class="w-full">
|
|
<div
|
|
class="flex flex-col items-center justify-center p-4 sm:p-8"
|
|
>
|
|
<div class="loading loading-spinner loading-lg">
|
|
</div>
|
|
<p class="mt-4 opacity-70">Loading dashboard...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="pageErrorState" class="hidden w-full">
|
|
<div class="alert alert-error mx-2 sm:mx-0">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-6 w-6"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
|
|
clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span>Failed to load dashboard content</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Not Authenticated State -->
|
|
<div id="notAuthenticatedState" class="hidden w-full">
|
|
<div class="card bg-base-100 shadow-xl mx-2 sm:mx-0">
|
|
<div
|
|
class="card-body items-center text-center p-4 sm:p-8"
|
|
>
|
|
<div class="mb-4 sm:mb-6">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-12 w-12 sm:h-16 sm:w-16 opacity-30"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"
|
|
clip-rule="evenodd"></path>
|
|
</svg>
|
|
</div>
|
|
<h2 class="card-title text-xl sm:text-2xl mb-2">
|
|
Sign in to Access Dashboard
|
|
</h2>
|
|
<p
|
|
class="opacity-70 mb-4 sm:mb-6 text-sm sm:text-base"
|
|
>
|
|
Please sign in with your IEEE UCSD account
|
|
to access the dashboard.
|
|
</p>
|
|
<button
|
|
class="login-button btn btn-primary btn-lg gap-2 w-full sm:w-auto"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-5 w-5"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fill-rule="evenodd"
|
|
d="M3 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1zm7.707 3.293a1 1 0 010 1.414L9.414 9H17a1 1 0 110 2H9.414l1.293 1.293a1 1 0 01-1.414 1.414l-3-3a1 1 0 010-1.414l3-3a1 1 0 011.414 0z"
|
|
clip-rule="evenodd"></path>
|
|
</svg>
|
|
<span class="whitespace-nowrap"
|
|
>Sign in with IEEEUCSD SSO</span
|
|
>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div id="mainContent" class="space-y-4 sm:space-y-6">
|
|
{
|
|
Object.entries(dashboardConfig.sections).map(
|
|
([sectionKey, section]: [string, any]) => {
|
|
// Skip if no component is defined
|
|
if (!section.component) return null;
|
|
|
|
const Component =
|
|
components[section.component];
|
|
return (
|
|
<div
|
|
id={`${sectionKey}Section`}
|
|
class={`dashboard-section hidden ${
|
|
section.role &&
|
|
section.role !== "none"
|
|
? "role-restricted"
|
|
: ""
|
|
}`}
|
|
data-role-required={section.role}
|
|
>
|
|
<Component />
|
|
</div>
|
|
);
|
|
}
|
|
)
|
|
}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Centralized Toast Provider -->
|
|
<ToastProvider client:load />
|
|
|
|
<script>
|
|
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";
|
|
|
|
const auth = Authentication.getInstance();
|
|
const get = Get.getInstance();
|
|
const logger = SendLog.getInstance();
|
|
|
|
// Ensure toast function is defined
|
|
if (typeof window !== "undefined" && !window.toast) {
|
|
window.toast = () => {};
|
|
}
|
|
|
|
// Initialize page state
|
|
const pageLoadingState =
|
|
document.getElementById("pageLoadingState");
|
|
const pageErrorState = document.getElementById("pageErrorState");
|
|
const notAuthenticatedState = document.getElementById(
|
|
"notAuthenticatedState"
|
|
);
|
|
const mainContent = document.getElementById("mainContent");
|
|
const sidebar = document.querySelector("aside");
|
|
|
|
// User profile elements
|
|
const userInitials = document.getElementById("userInitials");
|
|
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");
|
|
});
|
|
|
|
// Only show sponsor sections
|
|
document
|
|
.querySelectorAll('[data-role-required="sponsor"]')
|
|
.forEach((element) => {
|
|
element.classList.remove("hidden");
|
|
});
|
|
return;
|
|
}
|
|
|
|
// For non-sponsor roles, handle normally
|
|
document
|
|
.querySelectorAll("[data-role-required]")
|
|
.forEach((element) => {
|
|
const requiredRole = element.getAttribute(
|
|
"data-role-required"
|
|
) as OfficerStatus;
|
|
|
|
// 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);
|
|
});
|
|
};
|
|
|
|
// Function to delete all cookies (to handle Logto logout)
|
|
const deleteAllCookies = () => {
|
|
// Get all cookies
|
|
const cookies = document.cookie.split(";");
|
|
|
|
// Common paths that might have cookies
|
|
const paths = ["/", "/dashboard", "/auth", "/api"];
|
|
|
|
// Domains to target
|
|
const domains = [
|
|
"", // current domain
|
|
"auth.ieeeucsd.org",
|
|
".auth.ieeeucsd.org",
|
|
"ieeeucsd.org",
|
|
".ieeeucsd.org",
|
|
"dev.ieeeucsd.org",
|
|
".dev.ieeeucsd.org",
|
|
];
|
|
|
|
// Delete each cookie with all combinations of paths and domains
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
const cookie = cookies[i];
|
|
const eqPos = cookie.indexOf("=");
|
|
const name =
|
|
eqPos > -1
|
|
? cookie.substring(0, eqPos).trim()
|
|
: cookie.trim();
|
|
|
|
if (!name) continue; // Skip empty cookie names
|
|
|
|
// Try all combinations of paths and domains
|
|
for (const path of paths) {
|
|
// Delete from current domain (no domain specified)
|
|
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${path}`;
|
|
|
|
// Try with specific domains
|
|
for (const domain of domains) {
|
|
if (domain) {
|
|
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${path};domain=${domain}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Specifically target known Logto cookies
|
|
const logtoSpecificCookies = [
|
|
"logto",
|
|
"logto.signin",
|
|
"logto.session",
|
|
"logto.callback",
|
|
];
|
|
|
|
for (const cookieName of logtoSpecificCookies) {
|
|
for (const path of paths) {
|
|
document.cookie = `${cookieName}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${path}`;
|
|
|
|
for (const domain of domains) {
|
|
if (domain) {
|
|
document.cookie = `${cookieName}=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=${path};domain=${domain}`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Function to create and show a logout confirmation modal
|
|
const showLogoutConfirmation = () => {
|
|
// Create modal if it doesn't exist
|
|
let modal = document.getElementById("logoutConfirmModal");
|
|
if (!modal) {
|
|
modal = document.createElement("dialog");
|
|
modal.id = "logoutConfirmModal";
|
|
modal.className = "modal modal-bottom sm:modal-middle";
|
|
|
|
modal.innerHTML = `
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg">Confirm Logout</h3>
|
|
<p class="py-4">Are you sure you want to log out of your account?</p>
|
|
<div class="modal-action">
|
|
<button id="cancelLogout" class="btn btn-outline">Cancel</button>
|
|
<button id="confirmLogout" class="btn btn-error">
|
|
<span id="logoutSpinner" class="loading loading-spinner loading-sm hidden"></span>
|
|
<span id="logoutText">Log Out</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>Close</button>
|
|
</form>
|
|
`;
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// Add event listeners
|
|
document
|
|
.getElementById("cancelLogout")
|
|
?.addEventListener("click", () => {
|
|
(modal as HTMLDialogElement).close();
|
|
});
|
|
|
|
document
|
|
.getElementById("confirmLogout")
|
|
?.addEventListener("click", async () => {
|
|
// Show loading state
|
|
const spinner =
|
|
document.getElementById("logoutSpinner");
|
|
const text = document.getElementById("logoutText");
|
|
const confirmBtn =
|
|
document.getElementById("confirmLogout");
|
|
const cancelBtn =
|
|
document.getElementById("cancelLogout");
|
|
|
|
if (spinner) spinner.classList.remove("hidden");
|
|
if (text) text.textContent = "Logging out...";
|
|
if (confirmBtn)
|
|
confirmBtn.setAttribute("disabled", "true");
|
|
if (cancelBtn)
|
|
cancelBtn.setAttribute("disabled", "true");
|
|
|
|
try {
|
|
// Log the logout action
|
|
await logger.send(
|
|
"logout",
|
|
"auth",
|
|
"User logged out from dashboard menu"
|
|
);
|
|
|
|
// Log out from PocketBase using the Authentication class
|
|
await auth.logout();
|
|
|
|
// For extra safety, also directly clear the PocketBase auth store
|
|
const pb = auth.getPocketBase();
|
|
pb.authStore.clear();
|
|
|
|
// Delete all cookies to ensure Logto is logged out
|
|
deleteAllCookies();
|
|
|
|
// Redirect to our API logout endpoint which will properly sign out from Logto
|
|
window.location.href = "/api/logout";
|
|
return; // Stop execution here as we're redirecting
|
|
} catch (error) {
|
|
console.error("Error during logout:", error);
|
|
|
|
// Show error message if toast is available
|
|
if (
|
|
window.toast &&
|
|
typeof window.toast === "function"
|
|
) {
|
|
window.toast(
|
|
"Failed to log out. Please try again.",
|
|
{
|
|
type: "error",
|
|
}
|
|
);
|
|
}
|
|
|
|
// Reset button state
|
|
if (spinner) spinner.classList.add("hidden");
|
|
if (text) text.textContent = "Log Out";
|
|
if (confirmBtn)
|
|
confirmBtn.removeAttribute("disabled");
|
|
if (cancelBtn)
|
|
cancelBtn.removeAttribute("disabled");
|
|
|
|
// Close the modal
|
|
if (modal) (modal as HTMLDialogElement).close();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Show the modal
|
|
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");
|
|
|
|
// 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;
|
|
}
|
|
|
|
// 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");
|
|
// 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 {
|
|
// Use fields from the User interface in the schema
|
|
const extendedUser = await get.getOne("users", user.id, {
|
|
fields: [
|
|
"id",
|
|
"name",
|
|
"email",
|
|
"verified",
|
|
"avatar",
|
|
"pid",
|
|
"member_id",
|
|
"graduation_year",
|
|
"major",
|
|
"signed_up",
|
|
],
|
|
});
|
|
|
|
const displayName = extendedUser.name || "Unknown User";
|
|
// Default role is Member
|
|
let displayRole = "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
|
|
// Use fields from the Officer interface in the schema
|
|
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;
|
|
const officerRole = officerRecords.items[0].role;
|
|
|
|
// Use the role field from the officer's collection
|
|
if (officerRole) {
|
|
displayRole = officerRole;
|
|
}
|
|
|
|
// 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";
|
|
displayRole = "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);
|
|
}
|
|
};
|
|
|
|
// 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");
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|