271 lines
9.2 KiB
TypeScript
Executable File

import { useEffect, useState } from "preact/hooks";
import { WelcomeBarProps, WelcomeBarState } from "./welcome-types.ts";
import { useEncryption } from "./useEncryption.ts";
import { useLogoutCleanup } from "./useServiceWorker.ts";
import EncryptionModal from "./EncryptionModal.tsx";
import NavigationMenu from "./NavigationMenu.tsx";
import LockIcon from "./LockIcon.tsx";
function normalizeRoute(path: string): string {
if (!path || path === "/") return "/";
return path.endsWith("/") ? path.slice(0, -1) : path;
}
/**
* WelcomeBar Component
* Main navigation bar with user info, encryption status, and menu.
*/
export default function WelcomeBar(
{
publicId,
user_name,
currentRoute,
isEncrypted,
stateId,
serverCalendarPath,
}: WelcomeBarProps,
) {
const [activeRoute, setActiveRoute] = useState(normalizeRoute(currentRoute));
const [state] = useState<WelcomeBarState>({
photoUrl: "",
});
const hasValidUserId = publicId.length > 0;
const displayName = user_name?.trim() || (hasValidUserId ? "Member" : "");
const manageStateIdInStorage = () => {
try {
const storedStateId = localStorage.getItem("mystate");
if (storedStateId === null || storedStateId !== stateId) {
localStorage.setItem("mystate", stateId);
}
} catch (error) {
console.error("Error managing stateId in localStorage:", error);
}
};
useLogoutCleanup();
useEffect(() => {
const syncRoute = () => {
const pathname = globalThis.location?.pathname ?? currentRoute;
const nextRoute = normalizeRoute(pathname);
setActiveRoute((prev) => (prev === nextRoute ? prev : nextRoute));
};
const handlePotentialNavigation = (event: MouseEvent) => {
const target = event.target as HTMLElement | null;
if (!target?.closest("a[href]")) return;
globalThis.setTimeout(syncRoute, 0);
};
syncRoute();
globalThis.addEventListener("popstate", syncRoute);
globalThis.addEventListener("hashchange", syncRoute);
document.addEventListener("click", handlePotentialNavigation, true);
return () => {
globalThis.removeEventListener("popstate", syncRoute);
globalThis.removeEventListener("hashchange", syncRoute);
document.removeEventListener("click", handlePotentialNavigation, true);
};
}, [currentRoute]);
useEffect(() => {
manageStateIdInStorage();
}, [stateId]);
const encryption = useEncryption(
hasValidUserId ? publicId : "",
isEncrypted,
);
return (
<>
<div class="fixed top-0 left-0 right-0 w-full theme-navbar welcomebar-shell">
<div class="welcomebar-inner">
<div class="welcomebar-left">
{hasValidUserId
? (
<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-${displayName || "default"}`,
);
if (fallback) fallback.classList.remove("is-hidden");
}}
/>
)
: null}
<div
id={`initials-${displayName || "default"}`}
class={`profile-avatar profile-avatar--fallback ${
state.photoUrl ? "is-hidden" : ""
}`}
>
{displayName.slice(0, 2).toUpperCase()}
</div>
<span class="profile-name">{displayName}</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="welcomebar-actions">
<details class="navigation-details">
<summary
class="theme-navbar-accent welcomebar-menu-button transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-cyan-500 rounded menu-button"
title="Menu"
aria-label="Toggle navigation menu"
>
<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>
</summary>
<NavigationMenu
isLoggedIn={hasValidUserId}
currentRoute={activeRoute}
serverCalendarPath={serverCalendarPath}
/>
</details>
<button
type="button"
class="welcomebar-theme-toggle"
title="Switch to light mode"
aria-label="Switch to light mode"
data-theme-toggle="true"
>
<svg
class="welcomebar-theme-icon welcomebar-theme-icon--sun"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle cx="12" cy="12" r="4" stroke-width="2" />
<path
stroke-linecap="round"
stroke-width="2"
d="M12 2v2.5M12 19.5V22M4.93 4.93l1.77 1.77M17.3 17.3l1.77 1.77M2 12h2.5M19.5 12H22M4.93 19.07l1.77-1.77M17.3 6.7l1.77-1.77"
/>
</svg>
<svg
class="welcomebar-theme-icon welcomebar-theme-icon--moon"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
{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}
/>
)}
{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}
/>
)}
</>
);
}