264 lines
9.2 KiB
TypeScript
Executable File
264 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}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|