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 type { NavigationLink } from "~/components/layout/navigation";
import { MainNavigation } from "~/components/layout/navigation"; import { MainNavigation } from "~/components/layout/navigation";
import { ClientShell } from "~/components/layout/shell"; import { ClientShell } from "~/components/layout/shell";
import { ManageNavigationFooter } from "~/components/manage/manage-navigation-footer";
export default async function ManageLayout({ children }: PropsWithChildren) { export default async function ManageLayout({ children }: PropsWithChildren) {
const t = await getScopedI18n("management.navbar"); const t = await getScopedI18n("management.navbar");
@@ -194,7 +195,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
return ( return (
<ClientShell hasNavigation> <ClientShell hasNavigation>
<MainHeader></MainHeader> <MainHeader></MainHeader>
<MainNavigation links={navigationLinks}></MainNavigation> <MainNavigation links={navigationLinks} footerSection={<ManageNavigationFooter />}></MainNavigation>
<AppShellMain>{children}</AppShellMain> <AppShellMain>{children}</AppShellMain>
</ClientShell> </ClientShell>
); );
@@ -6,11 +6,20 @@ import { LanguageCombobox } from "./language-combobox";
interface CurrentLanguageComboboxProps { interface CurrentLanguageComboboxProps {
width?: string; width?: string;
showTranslatedName?: boolean;
} }
export const CurrentLanguageCombobox = ({ width }: CurrentLanguageComboboxProps) => { export const CurrentLanguageCombobox = ({ width, showTranslatedName }: CurrentLanguageComboboxProps) => {
const currentLocale = useCurrentLocale(); const currentLocale = useCurrentLocale();
const { changeLocale, isPending } = useChangeLocale(); 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; onChange: (value: SupportedLanguage) => void;
isPending?: boolean; isPending?: boolean;
width?: string; 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({ const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(), onDropdownClose: () => combobox.resetSelectedOption(),
}); });
@@ -53,7 +61,7 @@ export const LanguageCombobox = ({ label, value, onChange, isPending, width }: L
variant="filled" variant="filled"
w={width} w={width}
> >
<OptionItem currentLocale={value} localeKey={value} /> <OptionItem currentLocale={value} localeKey={value} showTranslatedName={showTranslatedName} />
</InputBase> </InputBase>
</Combobox.Target> </Combobox.Target>
<Combobox.Dropdown> <Combobox.Dropdown>
@@ -75,10 +83,12 @@ const OptionItem = ({
currentLocale, currentLocale,
localeKey, localeKey,
showCheck, showCheck,
showTranslatedName = true,
}: { }: {
currentLocale: SupportedLanguage; currentLocale: SupportedLanguage;
localeKey: SupportedLanguage; localeKey: SupportedLanguage;
showCheck?: boolean; showCheck?: boolean;
showTranslatedName?: boolean;
}) => { }) => {
return ( return (
<Group wrap="nowrap" justify="space-between"> <Group wrap="nowrap" justify="space-between">
@@ -86,9 +96,11 @@ const OptionItem = ({
<LanguageIcon icon={localeConfigurations[localeKey].icon} /> <LanguageIcon icon={localeConfigurations[localeKey].icon} />
<Group wrap="nowrap" gap="xs"> <Group wrap="nowrap" gap="xs">
<Text>{localeConfigurations[localeKey].name}</Text> <Text>{localeConfigurations[localeKey].name}</Text>
<Text size="xs" c="dimmed" inherit> {showTranslatedName && (
({localeConfigurations[localeKey].translatedName}) <Text size="xs" c="dimmed" inherit>
</Text> ({localeConfigurations[localeKey].translatedName})
</Text>
)}
</Group> </Group>
</Group> </Group>
{showCheck && localeKey === currentLocale && <IconCheck color="currentColor" size={16} />} {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>
);
};