Compare commits

...
Author SHA1 Message Date
Manuel 639cbbd032 feat: add manage nav footer 2026-03-23 20:44:23 +01:00
4 changed files with 120 additions and 8 deletions

No files matched your search

@@ -36,6 +36,7 @@ import { homarrLogoPath } from "~/components/layout/logo/homarr-logo";
import type { NavigationLink } from "~/components/layout/navigation";
import { MainNavigation } from "~/components/layout/navigation";
import { ClientShell } from "~/components/layout/shell";
import { ManageNavigationFooter } from "~/components/manage/manage-navigation-footer";
export default async function ManageLayout({ children }: PropsWithChildren) {
const t = await getScopedI18n("management.navbar");
@@ -194,7 +195,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
return (
<ClientShell hasNavigation>
<MainHeader></MainHeader>
<MainNavigation links={navigationLinks}></MainNavigation>
<MainNavigation links={navigationLinks} footerSection={<ManageNavigationFooter />}></MainNavigation>
<AppShellMain>{children}</AppShellMain>
</ClientShell>
);
@@ -6,11 +6,20 @@ import { LanguageCombobox } from "./language-combobox";
interface CurrentLanguageComboboxProps {
width?: string;
showTranslatedName?: boolean;
}
export const CurrentLanguageCombobox = ({ width }: CurrentLanguageComboboxProps) => {
export const CurrentLanguageCombobox = ({ width, showTranslatedName }: CurrentLanguageComboboxProps) => {
const currentLocale = useCurrentLocale();
const { changeLocale, isPending } = useChangeLocale();
return <LanguageCombobox value={currentLocale} onChange={changeLocale} isPending={isPending} width={width} />;
return (
<LanguageCombobox
showTranslatedName={showTranslatedName}
value={currentLocale}
onChange={changeLocale}
isPending={isPending}
width={width}
/>
);
};
@@ -16,9 +16,17 @@ interface LanguageComboboxProps {
onChange: (value: SupportedLanguage) => void;
isPending?: boolean;
width?: string;
showTranslatedName?: boolean;
}
export const LanguageCombobox = ({ label, value, onChange, isPending, width }: LanguageComboboxProps) => {
export const LanguageCombobox = ({
label,
value,
onChange,
isPending,
width,
showTranslatedName,
}: LanguageComboboxProps) => {
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
});
@@ -53,7 +61,7 @@ export const LanguageCombobox = ({ label, value, onChange, isPending, width }: L
variant="filled"
w={width}
>
<OptionItem currentLocale={value} localeKey={value} />
<OptionItem currentLocale={value} localeKey={value} showTranslatedName={showTranslatedName} />
</InputBase>
</Combobox.Target>
<Combobox.Dropdown>
@@ -75,10 +83,12 @@ const OptionItem = ({
currentLocale,
localeKey,
showCheck,
showTranslatedName = true,
}: {
currentLocale: SupportedLanguage;
localeKey: SupportedLanguage;
showCheck?: boolean;
showTranslatedName?: boolean;
}) => {
return (
<Group wrap="nowrap" justify="space-between">
@@ -86,9 +96,11 @@ const OptionItem = ({
<LanguageIcon icon={localeConfigurations[localeKey].icon} />
<Group wrap="nowrap" gap="xs">
<Text>{localeConfigurations[localeKey].name}</Text>
<Text size="xs" c="dimmed" inherit>
({localeConfigurations[localeKey].translatedName})
</Text>
{showTranslatedName && (
<Text size="xs" c="dimmed" inherit>
({localeConfigurations[localeKey].translatedName})
</Text>
)}
</Group>
</Group>
{showCheck && localeKey === currentLocale && <IconCheck color="currentColor" size={16} />}
@@ -0,0 +1,90 @@
"use client";
import { useCallback } from "react";
import { useRouter } from "next/navigation";
import { Button, Stack, useMantineColorScheme } from "@mantine/core";
import { IconLogout, IconMoon, IconSun } from "@tabler/icons-react";
import { signOut, useSession } from "@homarr/auth/client";
import { createModal, useModalAction } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
import { useAuthContext } from "~/app/[locale]/_client-providers/session";
import { CurrentLanguageCombobox } from "../language/current-language-combobox";
const LogoutModal = createModal(() => {
const t = useScopedI18n("common.userAvatar.menu.logoutModal");
return ({ onTimeout }: { onTimeout: () => void }) => (
<Stack>
<div>{t("description")}</div>
<Button onClick={onTimeout} fullWidth>
{t("action")}
</Button>
</Stack>
);
}).withOptions({
centered: true,
defaultTitle: (t) => t("common.userAvatar.menu.logoutModal.title"),
});
export const ManageNavigationFooter = () => {
const t = useScopedI18n("common.userAvatar.menu");
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const ColorSchemeIcon = colorScheme === "dark" ? IconSun : IconMoon;
const colorSchemeText = colorScheme === "dark" ? t("switchToLightMode") : t("switchToDarkMode");
const session = useSession();
const router = useRouter();
const { logoutUrl } = useAuthContext();
const { openModal } = useModalAction(LogoutModal);
const handleSignout = useCallback(async () => {
await signOut({
redirect: false,
});
openModal({
onTimeout: () => {
if (logoutUrl) {
window.location.assign(logoutUrl);
return;
}
router.refresh();
},
});
}, [logoutUrl, openModal, router]);
return (
<Stack gap="xs">
<Button
variant="subtle"
leftSection={<ColorSchemeIcon size="1rem" />}
onClick={toggleColorScheme}
justify="flex-start"
fullWidth
>
{colorSchemeText}
</Button>
<CurrentLanguageCombobox width="100%" showTranslatedName={false} />
{session.status === "authenticated" && (
<Button
variant="subtle"
leftSection={<IconLogout size="1rem" />}
onClick={handleSignout}
color="red"
justify="flex-start"
fullWidth
>
{t("logout")}
</Button>
)}
</Stack>
);
};