2026-02-14 02:20:19 +00:00

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