191 lines
8.4 KiB
TypeScript
191 lines
8.4 KiB
TypeScript
import { Fragment, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useLocation } from "react-router";
|
|
import { Transition } from "@headlessui/react";
|
|
import clsx from "clsx";
|
|
import { MenuIcon, XIcon } from "~/components/ui/icons/vendor";
|
|
import Icon from "~/components/brand/Icon";
|
|
import Logo from "~/components/brand/Logo";
|
|
import ProfileButton from "~/components/layouts/buttons/ProfileButton";
|
|
import ButtonEvent from "~/components/ui/buttons/ButtonEvent";
|
|
import ButtonPrimary from "~/components/ui/buttons/ButtonPrimary";
|
|
import ButtonTertiary from "~/components/ui/buttons/ButtonTertiary";
|
|
import LocaleSelector from "~/components/ui/selectors/LocaleSelector";
|
|
import ThemeSelector from "~/components/ui/selectors/ThemeSelector";
|
|
import DarkModeToggle from "~/components/ui/toggles/DarkModeToggle";
|
|
import { useRootData } from "~/utils/data/useRootData";
|
|
import { absoluteAppRootPath } from "~/utils/app/CrossHostUrls";
|
|
|
|
type PublicHeaderLink = {
|
|
title: string;
|
|
path: string;
|
|
target?: "_blank";
|
|
};
|
|
|
|
type Props = {
|
|
links?: PublicHeaderLink[];
|
|
width?: "screen-2xl" | "7xl";
|
|
withLogo?: boolean;
|
|
withSignInAndSignUp?: boolean;
|
|
withDarkModeToggle?: boolean;
|
|
withLanguageSelector?: boolean;
|
|
withThemeSelector?: boolean;
|
|
};
|
|
|
|
export default function PublicHeader({
|
|
links,
|
|
width = "7xl",
|
|
withLogo = true,
|
|
withSignInAndSignUp = true,
|
|
withDarkModeToggle = true,
|
|
withLanguageSelector = true,
|
|
withThemeSelector = true,
|
|
}: Props) {
|
|
const { authenticated, appConfiguration, user, routing } = useRootData();
|
|
const { t } = useTranslation();
|
|
const location = useLocation();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const navLinks = links ?? [];
|
|
const hasProfileButton = appConfiguration?.app.features.tenantHome === "/";
|
|
const loginOrEnterRoute = authenticated ? absoluteAppRootPath(routing) : "/login";
|
|
const canPublicSignUp = Boolean(appConfiguration?.subscription.allowSignUpBeforeSubscribe);
|
|
|
|
return (
|
|
<header className="pb-6">
|
|
<div className="relative pt-6">
|
|
<div className={clsx("mx-auto px-4 sm:px-6", width === "screen-2xl" ? "max-w-(--breakpoint-2xl)" : "max-w-7xl")}>
|
|
<nav className="relative flex items-center justify-between sm:h-10 md:justify-center" aria-label={t("components.publicHeader.globalNavigation")}>
|
|
<div className="flex flex-1 items-center md:absolute md:inset-y-0 md:left-0">
|
|
<div className="flex w-full items-center justify-between md:w-auto">
|
|
{withLogo ? (
|
|
<>
|
|
<Logo className="hidden lg:block" size="h-9" />
|
|
<Icon className="lg:hidden" size="h-9" />
|
|
</>
|
|
) : (
|
|
<div />
|
|
)}
|
|
<div className="-mr-1 flex min-w-0 items-center gap-2 md:hidden">
|
|
{withSignInAndSignUp && (
|
|
<div className="flex min-w-0 flex-wrap justify-end gap-2 rounded-md">
|
|
{!authenticated && canPublicSignUp && (
|
|
<ButtonTertiary to="/register" className="h-auto min-h-9 min-w-0 text-center whitespace-normal">
|
|
{t("account.shared.signUp")}
|
|
</ButtonTertiary>
|
|
)}
|
|
{(!hasProfileButton || !authenticated) && (
|
|
<ButtonPrimary to={loginOrEnterRoute} className="h-auto min-h-9 min-w-0 text-center whitespace-normal">
|
|
{authenticated ? t("shared.enter") : t("account.shared.signIn")}
|
|
</ButtonPrimary>
|
|
)}
|
|
</div>
|
|
)}
|
|
{navLinks.length > 0 && (
|
|
<button
|
|
onClick={() => setOpen(!open)}
|
|
type="button"
|
|
className="text-muted-foreground hover:bg-secondary hover:text-secondary-foreground inline-flex items-center justify-center rounded-md p-2 focus:outline-hidden"
|
|
aria-expanded={open}
|
|
aria-label={open ? t("shared.closeMenu") : t("shared.openMenu")}
|
|
>
|
|
<span className="sr-only">{open ? t("shared.closeMenu") : t("shared.openMenu")}</span>
|
|
<MenuIcon className="size-6" />
|
|
</button>
|
|
)}
|
|
{hasProfileButton && authenticated && <ProfileButton user={user} layout="/" />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="hidden items-center gap-2 sm:gap-4 md:flex md:gap-6">
|
|
{navLinks.map((link) => (
|
|
<ButtonEvent
|
|
key={link.path}
|
|
to={link.path}
|
|
target={link.target}
|
|
className={clsx(
|
|
"truncate rounded-sm px-3 py-1 text-base leading-6 font-medium transition duration-150 ease-in-out focus:outline-hidden",
|
|
location.pathname !== link.path && "text-muted-foreground"
|
|
)}
|
|
event={{ action: "click", category: "header", label: link.title, value: link.path }}
|
|
>
|
|
{link.title}
|
|
</ButtonEvent>
|
|
))}
|
|
{withLanguageSelector && <LocaleSelector className="hidden lg:flex" />}
|
|
{withDarkModeToggle && <DarkModeToggle className="hidden lg:flex" />}
|
|
{withThemeSelector && <ThemeSelector className="hidden lg:flex" />}
|
|
</div>
|
|
|
|
<div className="hidden md:absolute md:inset-y-0 md:right-0 md:flex md:items-center md:justify-end">
|
|
<span className="inline-flex min-w-0 gap-2">
|
|
{hasProfileButton && authenticated ? (
|
|
<ProfileButton user={user} layout="/" />
|
|
) : (
|
|
withSignInAndSignUp && (
|
|
<>
|
|
{!authenticated && canPublicSignUp && (
|
|
<ButtonTertiary to="/register" className="h-auto min-h-9 min-w-0 text-center whitespace-normal">
|
|
{t("account.shared.signUp")}
|
|
</ButtonTertiary>
|
|
)}
|
|
<ButtonPrimary to={loginOrEnterRoute} className="h-auto min-h-9 min-w-0 text-center whitespace-normal">
|
|
{authenticated ? t("shared.enter") : t("account.shared.signIn")}
|
|
</ButtonPrimary>
|
|
</>
|
|
)
|
|
)}
|
|
</span>
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
|
|
<Transition
|
|
show={open}
|
|
as={Fragment}
|
|
enter="duration-150 ease-out"
|
|
enterFrom="opacity-0 scale-95"
|
|
enterTo="opacity-100 scale-100"
|
|
leave="duration-100 ease-in"
|
|
leaveFrom="opacity-100 scale-100"
|
|
leaveTo="opacity-0 scale-95"
|
|
>
|
|
<div className="absolute inset-x-0 top-0 z-40 origin-top-right transform p-2 transition md:hidden">
|
|
<div className="bg-background border-border overflow-visible rounded-lg border shadow-xl">
|
|
<div className="flex items-center justify-between px-5 pt-4">
|
|
<div>{withLogo && <Icon />}</div>
|
|
<button
|
|
onClick={() => setOpen(false)}
|
|
type="button"
|
|
className="hover:bg-secondary hover:text-secondary-foreground inline-flex items-center justify-center rounded-md p-2 focus:outline-hidden"
|
|
>
|
|
<span className="sr-only">{t("shared.close")}</span>
|
|
<XIcon className="size-6" />
|
|
</button>
|
|
</div>
|
|
<div className="px-2 py-3">
|
|
{navLinks.map((link) => (
|
|
<ButtonEvent
|
|
key={link.path}
|
|
to={link.path}
|
|
target={link.target}
|
|
role="menuitem"
|
|
className={clsx(
|
|
"hover:bg-secondary hover:text-secondary-foreground block rounded-md px-3 py-2 text-base font-medium",
|
|
location.pathname === link.path && "bg-secondary"
|
|
)}
|
|
event={{ action: "click", category: "header", label: link.title, value: link.path }}
|
|
>
|
|
{link.title}
|
|
</ButtonEvent>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|