diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 0515c0ca7..c7b3e3cf4 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -47,6 +47,7 @@ "chroma-js": "^2.4.2", "dayjs": "^1.11.11", "dotenv": "^16.4.5", + "flag-icons": "^7.2.1", "glob": "^10.3.15", "jotai": "^2.8.0", "next": "^14.2.3", diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/_delete-user-button.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_delete-user-button.tsx similarity index 100% rename from apps/nextjs/src/app/[locale]/manage/users/[userId]/_delete-user-button.tsx rename to apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_delete-user-button.tsx diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/_profile-avatar-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx similarity index 100% rename from apps/nextjs/src/app/[locale]/manage/users/[userId]/_profile-avatar-form.tsx rename to apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-avatar-form.tsx diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/_profile-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx similarity index 100% rename from apps/nextjs/src/app/[locale]/manage/users/[userId]/_profile-form.tsx rename to apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-form.tsx diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-language-change.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-language-change.tsx new file mode 100644 index 000000000..e83b8b170 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-language-change.tsx @@ -0,0 +1,12 @@ +import { Stack, Title } from "@mantine/core"; + +import { LanguageCombobox } from "~/components/language/language-combobox"; + +export const ProfileLanguageChange = () => { + return ( + + Language & Region + + + ); +}; diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx similarity index 83% rename from apps/nextjs/src/app/[locale]/manage/users/[userId]/page.tsx rename to apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx index 9a5c5aade..cb6bdcd6f 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/general/page.tsx @@ -10,10 +10,11 @@ import { DangerZoneRoot, } from "~/components/manage/danger-zone"; import { catchTrpcNotFound } from "~/errors/trpc-not-found"; -import { DeleteUserButton } from "./_delete-user-button"; -import { UserProfileAvatarForm } from "./_profile-avatar-form"; -import { UserProfileForm } from "./_profile-form"; -import { canAccessUserEditPage } from "./access"; +import { canAccessUserEditPage } from "../access"; +import { DeleteUserButton } from "./_components/_delete-user-button"; +import { UserProfileAvatarForm } from "./_components/_profile-avatar-form"; +import { UserProfileForm } from "./_components/_profile-form"; +import { ProfileLanguageChange } from "./_components/_profile-language-change"; interface Props { params: { @@ -67,6 +68,8 @@ export default async function EditUserPage({ params }: Props) { + + } /> diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/_change-password-form.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/_components/_change-password-form.tsx similarity index 100% rename from apps/nextjs/src/app/[locale]/manage/users/[userId]/security/_change-password-form.tsx rename to apps/nextjs/src/app/[locale]/manage/users/[userId]/security/_components/_change-password-form.tsx diff --git a/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/page.tsx index 0ce00f304..f111714a8 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/[userId]/security/page.tsx @@ -7,7 +7,7 @@ import { getScopedI18n } from "@homarr/translation/server"; import { catchTrpcNotFound } from "~/errors/trpc-not-found"; import { canAccessUserEditPage } from "../access"; -import { ChangePasswordForm } from "./_change-password-form"; +import { ChangePasswordForm } from "./_components/_change-password-form"; interface Props { params: { diff --git a/apps/nextjs/src/app/[locale]/manage/users/_components/user-list.component.tsx b/apps/nextjs/src/app/[locale]/manage/users/_components/user-list.component.tsx index 4bda7a355..299c669f1 100644 --- a/apps/nextjs/src/app/[locale]/manage/users/_components/user-list.component.tsx +++ b/apps/nextjs/src/app/[locale]/manage/users/_components/user-list.component.tsx @@ -33,7 +33,7 @@ export const UserListComponent = ({ header: t("user.field.username.label"), grow: 100, Cell: ({ renderedCellValue, row }) => ( - + {renderedCellValue} diff --git a/apps/nextjs/src/components/language/language-combobox.module.css b/apps/nextjs/src/components/language/language-combobox.module.css new file mode 100644 index 000000000..fe98524ad --- /dev/null +++ b/apps/nextjs/src/components/language/language-combobox.module.css @@ -0,0 +1,3 @@ +.flagIcon { + border-radius: 4px; +} diff --git a/apps/nextjs/src/components/language/language-combobox.tsx b/apps/nextjs/src/components/language/language-combobox.tsx new file mode 100644 index 000000000..0b7fbb3fc --- /dev/null +++ b/apps/nextjs/src/components/language/language-combobox.tsx @@ -0,0 +1,94 @@ +"use client"; + +import React from "react"; +import { Combobox, Group, InputBase, Text, useCombobox } from "@mantine/core"; +import { IconCheck } from "@tabler/icons-react"; + +import type { SupportedLanguage } from "@homarr/translation"; +import { localeAttributes, supportedLanguages } from "@homarr/translation"; +import { useChangeLocale, useCurrentLocale } from "@homarr/translation/client"; + +import classes from "./language-combobox.module.css"; + +export const LanguageCombobox = () => { + const combobox = useCombobox({ + onDropdownClose: () => combobox.resetSelectedOption(), + }); + const currentLocale = useCurrentLocale(); + const changeLocale = useChangeLocale(); + + const handleOnOptionSubmit = React.useCallback( + (value: string) => { + if (!value) { + return; + } + changeLocale(value as SupportedLanguage); + combobox.closeDropdown(); + }, + [changeLocale, combobox], + ); + + const handleOnClick = React.useCallback(() => { + combobox.toggleDropdown(); + }, [combobox]); + + return ( + + + } + rightSectionPointerEvents="none" + onClick={handleOnClick} + variant="filled" + > + + + + + + {supportedLanguages.map((languageKey) => ( + + + + ))} + + + + ); +}; + +const OptionItem = ({ + currentLocale, + localeKey, + showCheck, +}: { + currentLocale: SupportedLanguage; + localeKey: SupportedLanguage; + showCheck?: boolean; +}) => { + return ( + + + + + {localeAttributes[localeKey].name} + + ({localeAttributes[localeKey].translatedName}) + + + + {showCheck && localeKey === currentLocale && ( + + )} + + ); +}; diff --git a/apps/nextjs/src/components/user-avatar-menu.tsx b/apps/nextjs/src/components/user-avatar-menu.tsx index 2ffa79c4e..04a6b1480 100644 --- a/apps/nextjs/src/components/user-avatar-menu.tsx +++ b/apps/nextjs/src/components/user-avatar-menu.tsx @@ -27,6 +27,10 @@ import { signOut, useSession } from "@homarr/auth/client"; import { createModal, useModalAction } from "@homarr/modals"; import { useScopedI18n } from "@homarr/translation/client"; +import "flag-icons/css/flag-icons.min.css"; + +import { LanguageCombobox } from "./language/language-combobox"; + interface UserAvatarMenuProps { children: ReactNode; } @@ -57,7 +61,7 @@ export const UserAvatarMenu = ({ children }: UserAvatarMenuProps) => { }, [openModal, router]); return ( - + { > {t("navigateDefaultBoard")} - {Boolean(session.data) && ( - } - > - {t("preferences")} - - )} - } - > - {t("management")} + + + + + {Boolean(session.data) && ( + <> + } + > + {t("preferences")} + + + } + > + {t("management")} + + + )} + {session.status === "authenticated" ? ( = { + de: { + name: "Deutsch", + translatedName: "German", + flagIcon: "de", + }, + en: { + name: "English", + translatedName: "English", + flagIcon: "us", + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6db4e1fbe..8559cd4fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -159,6 +159,9 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 + flag-icons: + specifier: ^7.2.1 + version: 7.2.1 glob: specifier: ^10.3.15 version: 10.3.15 @@ -3423,6 +3426,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + flag-icons@7.2.1: + resolution: {integrity: sha512-EaU4XZmFt1BOilz9nMmJKjma5pOaNjzL7somOhadrrilollh4xj6aaXI2M1sd00VUfVWN0E25Q6xaW3SNt0k/Q==} + flat-cache@3.2.0: resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -8283,6 +8289,8 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + flag-icons@7.2.1: {} + flat-cache@3.2.0: dependencies: flatted: 3.2.9