297 lines
10 KiB
TypeScript
Executable File
297 lines
10 KiB
TypeScript
Executable File
import { useEffect, useRef, useState } from "preact/hooks";
|
|
import { WelcomeBarProps, WelcomeBarState } from "./welcome-types.ts";
|
|
import { useEncryption } from "./useEncryption.ts";
|
|
import {
|
|
useLogoutCleanup,
|
|
useServiceWorker as _useServiceWorker,
|
|
} from "./useServiceWorker.ts";
|
|
import EncryptionModal from "./EncryptionModal.tsx";
|
|
import NavigationMenu from "./NavigationMenu.tsx";
|
|
import LockIcon from "./LockIcon.tsx";
|
|
|
|
/**
|
|
* WelcomeBar Component
|
|
* Main navigation bar with user info, encryption status, and menu
|
|
* Refactored with consolidated state for V8 optimization
|
|
*
|
|
* Features:
|
|
* - Consolidated state management (single state object)
|
|
* - Modular component architecture
|
|
* - Custom hooks for reusable logic
|
|
* - Service worker management
|
|
* - Encryption status display
|
|
*
|
|
* @param stateData - Session cookie data with user info
|
|
* @param currentRoute - Current route path
|
|
*/
|
|
export default function WelcomeBar(
|
|
{ publicId, user_name, currentRoute, isEncrypted, stateId }: WelcomeBarProps,
|
|
) {
|
|
// Use ref for menu button to avoid state-based re-renders
|
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
|
|
|
// Track current route dynamically (props only set on initial hydration)
|
|
const [activeRoute, setActiveRoute] = useState(currentRoute);
|
|
|
|
// Consolidated state for better V8 optimization
|
|
const [state, setState] = useState<Omit<WelcomeBarState, "menuButtonRef">>({
|
|
showPasswordModal: false,
|
|
isPasswordSet: false,
|
|
isSkipped: false,
|
|
tempPassword: "",
|
|
passwordError: "",
|
|
showMenu: false,
|
|
photoUrl: "",
|
|
});
|
|
|
|
// Helper function for partial state updates
|
|
const updateState = (
|
|
partial: Partial<Omit<WelcomeBarState, "menuButtonRef">>,
|
|
) => {
|
|
setState((prev) => ({ ...prev, ...partial }));
|
|
};
|
|
|
|
// Function to manage localStorage stateId tracking
|
|
const manageStateIdInStorage = () => {
|
|
try {
|
|
const storedStateId = localStorage.getItem("mystate");
|
|
|
|
if (storedStateId === null) {
|
|
// No existing stateId, save the current one
|
|
localStorage.setItem("mystate", stateId);
|
|
} else if (storedStateId !== stateId) {
|
|
// Existing stateId doesn't match, log it before saving
|
|
//console.log(`StateId changed from ${storedStateId} to ${stateId}`);
|
|
localStorage.setItem("mystate", stateId);
|
|
}
|
|
// If they match, no action needed
|
|
} catch (error) {
|
|
console.error("Error managing stateId in localStorage:", error);
|
|
}
|
|
};
|
|
|
|
// Custom hooks for modular logic
|
|
//useServiceWorker();
|
|
useLogoutCleanup();
|
|
|
|
// Update activeRoute when browser location changes (client-side navigation)
|
|
useEffect(() => {
|
|
// Set initial route from globalThis.location if different from prop
|
|
if (
|
|
typeof globalThis !== "undefined" &&
|
|
globalThis.location?.pathname !== activeRoute
|
|
) {
|
|
setActiveRoute(globalThis.location.pathname);
|
|
}
|
|
|
|
// Listen for popstate (back/forward navigation)
|
|
const handlePopState = () => {
|
|
setActiveRoute(globalThis.location.pathname);
|
|
};
|
|
|
|
// Listen for custom navigation events (if using client-side routing)
|
|
const handleNavigation = () => {
|
|
setActiveRoute(globalThis.location.pathname);
|
|
};
|
|
|
|
globalThis.addEventListener("popstate", handlePopState);
|
|
// Also check on click events that might trigger navigation
|
|
document.addEventListener("click", handleNavigation);
|
|
|
|
return () => {
|
|
globalThis.removeEventListener("popstate", handlePopState);
|
|
document.removeEventListener("click", handleNavigation);
|
|
};
|
|
}, []);
|
|
|
|
// useEffect to run localStorage management only when stateId changes
|
|
useEffect(() => {
|
|
manageStateIdInStorage();
|
|
}, [stateId]);
|
|
|
|
// Only use encryption hook if user is logged in (has valid user_id)
|
|
const hasValidUserId = publicId && publicId.length > 0;
|
|
const encryption = useEncryption(
|
|
hasValidUserId ? publicId : "",
|
|
isEncrypted,
|
|
);
|
|
|
|
// Menu handlers with error boundaries
|
|
const handleMenuToggle = (event?: Event) => {
|
|
try {
|
|
// Prevent event bubbling to avoid triggering click outside handlers
|
|
if (event && typeof event.stopPropagation === "function") {
|
|
event.stopPropagation();
|
|
}
|
|
updateState({ showMenu: !state.showMenu });
|
|
} catch (error) {
|
|
console.error("Error toggling menu:", error);
|
|
}
|
|
};
|
|
|
|
const handleMenuClose = () => {
|
|
try {
|
|
updateState({ showMenu: false });
|
|
} catch (error) {
|
|
console.error("Error closing menu:", error);
|
|
}
|
|
};
|
|
|
|
// Allow rendering even for guest users - don't block on missing stateData
|
|
// If stateData is completely missing, create a safe default for guest state
|
|
|
|
return (
|
|
<>
|
|
<div class="fixed top-0 left-0 right-0 w-full theme-navbar welcomebar-shell z-50">
|
|
<div class="welcomebar-inner">
|
|
<div class="welcomebar-left">
|
|
{user_name
|
|
? (
|
|
<div class="profile-pill">
|
|
{state.photoUrl
|
|
? (
|
|
<img
|
|
src={state.photoUrl}
|
|
alt="User profile"
|
|
class="profile-avatar"
|
|
onError={(e) => {
|
|
const target = e.currentTarget as HTMLImageElement;
|
|
target.classList.add("is-hidden");
|
|
const fallback = document.getElementById(
|
|
`initials-${user_name || "default"}`,
|
|
);
|
|
if (fallback) fallback.classList.remove("is-hidden");
|
|
}}
|
|
/>
|
|
)
|
|
: null}
|
|
<div
|
|
id={`initials-${user_name || "default"}`}
|
|
class={`profile-avatar profile-avatar--fallback ${
|
|
state.photoUrl ? "is-hidden" : ""
|
|
}`}
|
|
>
|
|
{user_name.substring(0, 2).toUpperCase()}
|
|
</div>
|
|
<span class="profile-name">{user_name}</span>
|
|
</div>
|
|
)
|
|
: (
|
|
<form method="get" action="/auth/login">
|
|
<button
|
|
type="submit"
|
|
class="theme-navbar-button welcomebar-signin"
|
|
>
|
|
Start
|
|
</button>
|
|
</form>
|
|
)}
|
|
</div>
|
|
|
|
<div class="welcomebar-center">
|
|
{hasValidUserId && (
|
|
<div class="welcomebar-lock">
|
|
<LockIcon
|
|
isPasswordSet={encryption.isPasswordSet}
|
|
isSkipped={encryption.isSkipped}
|
|
onClearSkip={encryption.handleClearSkip}
|
|
onChangePin={encryption.handleChangePin}
|
|
isEncrypted={isEncrypted}
|
|
onRequestPin={encryption.handleOpenPasswordModal}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div class="welcomebar-75-percent-from-left">
|
|
{activeRoute !== "/" && (
|
|
<a
|
|
href="/"
|
|
title="Home"
|
|
class="welcomebar-logo-link"
|
|
aria-label="Home"
|
|
>
|
|
<img
|
|
src="/logo.svg"
|
|
alt="Cyber Hybrid Hub"
|
|
class="welcomebar-logo-image"
|
|
/>
|
|
</a>
|
|
)}
|
|
</div>
|
|
<div class="welcomebar-right">
|
|
<div class="flex items-center gap-3 relative hamburger-container">
|
|
<button
|
|
ref={menuButtonRef}
|
|
type="button"
|
|
onClick={handleMenuToggle}
|
|
class="theme-navbar-accent welcomebar-menu-button transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-cyan-500 rounded menu-button relative z-[9999]"
|
|
title="Menu"
|
|
aria-label="Toggle navigation menu"
|
|
aria-expanded={state.showMenu}
|
|
>
|
|
<svg
|
|
class="w-7 h-7"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M4 6h16M4 12h16M4 18h16"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<NavigationMenu
|
|
showMenu={state.showMenu}
|
|
menuButtonRef={menuButtonRef.current}
|
|
onClose={handleMenuClose}
|
|
isLoggedIn={!!user_name}
|
|
currentRoute={activeRoute}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Encryption Modal Component - only for logged in users */}
|
|
{hasValidUserId && encryption.showPasswordModal && (
|
|
<EncryptionModal
|
|
showModal={encryption.showPasswordModal}
|
|
tempPassword={encryption.tempPassword}
|
|
passwordError={encryption.passwordError}
|
|
persistAcrossDevices={encryption.persistAcrossDevices}
|
|
onPasswordChange={encryption.handlePasswordChange}
|
|
onSetPassword={encryption.handleSetPassword}
|
|
onSkip={encryption.handleSkipEncryption}
|
|
onPersistAcrossDevicesChange={encryption
|
|
.handlePersistAcrossDevicesChange}
|
|
/>
|
|
)}
|
|
|
|
{/* Change PIN Modal Component - only for logged in users */}
|
|
{hasValidUserId && (
|
|
<EncryptionModal
|
|
showModal={encryption.showChangePinModal}
|
|
tempPassword={encryption.tempPassword}
|
|
passwordError={encryption.passwordError}
|
|
isChangeMode
|
|
showRemovePinConfirm={encryption.showRemovePinConfirm}
|
|
persistAcrossDevices={encryption.persistAcrossDevices}
|
|
onPasswordChange={encryption.handlePasswordChange}
|
|
onSetPassword={encryption.handleChangePinSubmit}
|
|
onSkip={encryption.handleCancelRemovePin}
|
|
onClose={encryption.handleCloseChangePin}
|
|
onRemovePinConfirm={encryption.handleRemovePinConfirm}
|
|
onPersistAcrossDevicesChange={encryption
|
|
.handlePersistAcrossDevicesChange}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|