From 31c26941857b4eac8e69ccb68bc4c00cd1073806 Mon Sep 17 00:00:00 2001
From: Manuel <30572287+manuel-rw@users.noreply.github.com>
Date: Sat, 18 May 2024 13:54:43 +0200
Subject: [PATCH] feat: language selector (#484)
* feat: language selector
* refactor: move user general page
* feat: language selector
* refactor: move user general page
* feat: add language combobox in user general
---
apps/nextjs/package.json | 1 +
.../_components}/_delete-user-button.tsx | 0
.../_components}/_profile-avatar-form.tsx | 0
.../_components}/_profile-form.tsx | 0
.../_components/_profile-language-change.tsx | 12 +++
.../users/[userId]/{ => general}/page.tsx | 11 ++-
.../[locale]/manage/users/[userId]/layout.tsx | 2 +-
.../_change-password-form.tsx | 0
.../manage/users/[userId]/security/page.tsx | 2 +-
.../users/_components/user-list.component.tsx | 2 +-
.../language/language-combobox.module.css | 3 +
.../components/language/language-combobox.tsx | 94 +++++++++++++++++++
.../src/components/user-avatar-menu.tsx | 45 +++++----
packages/translation/src/client.ts | 15 +--
packages/translation/src/index.ts | 1 +
packages/translation/src/locale-attributes.ts | 21 +++++
pnpm-lock.yaml | 8 ++
17 files changed, 188 insertions(+), 29 deletions(-)
rename apps/nextjs/src/app/[locale]/manage/users/[userId]/{ => general/_components}/_delete-user-button.tsx (100%)
rename apps/nextjs/src/app/[locale]/manage/users/[userId]/{ => general/_components}/_profile-avatar-form.tsx (100%)
rename apps/nextjs/src/app/[locale]/manage/users/[userId]/{ => general/_components}/_profile-form.tsx (100%)
create mode 100644 apps/nextjs/src/app/[locale]/manage/users/[userId]/general/_components/_profile-language-change.tsx
rename apps/nextjs/src/app/[locale]/manage/users/[userId]/{ => general}/page.tsx (83%)
rename apps/nextjs/src/app/[locale]/manage/users/[userId]/security/{ => _components}/_change-password-form.tsx (100%)
create mode 100644 apps/nextjs/src/components/language/language-combobox.module.css
create mode 100644 apps/nextjs/src/components/language/language-combobox.tsx
create mode 100644 packages/translation/src/locale-attributes.ts
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 (
-