feat: Move language option to profile setting (#387)
This commit is contained in:
@@ -101,6 +101,7 @@
|
||||
"confirm_password": "Passwort (Bestätigung)",
|
||||
"invalid_email": "Ungültige E-Mail",
|
||||
"invalid_password": "Passwort muss mindestens 8 Zeichen haben",
|
||||
"invalid_locale": "Ungültige Sprache",
|
||||
"username_empty": "Benutzername ist leer",
|
||||
"password_empty": "Passwort ist leer",
|
||||
"password_not_match": "Passwörter stimmen nicht überein",
|
||||
@@ -125,6 +126,9 @@
|
||||
"confirm_new_password": "Neues Passwort bestätigen",
|
||||
"update_password": "Passwort aktualisieren",
|
||||
"password_updated": "Passwort erfolgreich geändert",
|
||||
"change_locale": "Sprache ändern",
|
||||
"update_locale": "Sprache aktualisieren",
|
||||
"locale_updated": "Sprache erfolgreich geändert",
|
||||
"change_avatar": "Avatar ändern",
|
||||
"upload_avatar": "Avatar hochladen",
|
||||
"remove_avatar": "Avatar entfernen",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"confirm_password": "Password (confirm)",
|
||||
"invalid_email": "Invalid email",
|
||||
"invalid_password": "Password must be at least 8 characters",
|
||||
"invalid_locale": "Invalid language",
|
||||
"username_empty": "username is empty",
|
||||
"password_empty": "password is empty",
|
||||
"password_not_match": "Pasword does not match",
|
||||
@@ -125,6 +126,9 @@
|
||||
"confirm_new_password": "Confirm New Password",
|
||||
"update_password": "Update Password",
|
||||
"password_updated": "Password updated successfully",
|
||||
"change_locale": "Change Language",
|
||||
"update_locale": "Update Language",
|
||||
"locale_updated": "Language updated successfully",
|
||||
"change_avatar": "Change Avatar",
|
||||
"upload_avatar": "Upload Avatar",
|
||||
"remove_avatar": "Remove Avatar",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"confirm_password": "パスワード(確認)",
|
||||
"invalid_email": "無効なメールアドレスです",
|
||||
"invalid_password": "パスワードは8文字以上である必要があります",
|
||||
"invalid_locale": "無効な言語です",
|
||||
"username_empty": "ユーザー名が入力されていません",
|
||||
"password_empty": "パスワードが入力されていません",
|
||||
"password_not_match": "パスワードが一致しません",
|
||||
@@ -125,6 +126,9 @@
|
||||
"confirm_new_password": "新しいパスワード(確認)",
|
||||
"update_password": "パスワードを更新",
|
||||
"password_updated": "パスワードが正常に更新されました",
|
||||
"change_locale": "言語の変更",
|
||||
"update_locale": "言語を更新",
|
||||
"locale_updated": "言語が正常に更新されました",
|
||||
"change_avatar": "アバターの変更",
|
||||
"upload_avatar": "アバターをアップロード",
|
||||
"remove_avatar": "アバターを削除",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"confirm_password": "Senha (confirmação)",
|
||||
"invalid_email": "E-mail inválido",
|
||||
"invalid_password": "A senha deve ter pelo menos 8 caracteres",
|
||||
"invalid_locale": "Idioma inválido",
|
||||
"username_empty": "O nome de usuário está vazio",
|
||||
"password_empty": "A senha está vazia",
|
||||
"password_not_match": "A senha não corresponde",
|
||||
@@ -125,6 +126,9 @@
|
||||
"confirm_new_password": "Confirmar Nova Senha",
|
||||
"update_password": "Atualizar Senha",
|
||||
"password_updated": "Senha atualizada com sucesso",
|
||||
"change_locale": "Alterar Idioma",
|
||||
"update_locale": "Atualizar Idioma",
|
||||
"locale_updated": "Idioma atualizado com sucesso",
|
||||
"change_avatar": "Alterar Avatar",
|
||||
"upload_avatar": "Enviar Avatar",
|
||||
"remove_avatar": "Remover Avatar",
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
"confirm_password": "确认密码",
|
||||
"invalid_email": "无效的邮箱地址",
|
||||
"invalid_password": "密码必须至少包含 8 个字符",
|
||||
"invalid_locale": "无效的语言",
|
||||
"username_empty": "用户名不能为空",
|
||||
"password_empty": "密码不能为空",
|
||||
"password_not_match": "密码不匹配",
|
||||
@@ -125,6 +126,9 @@
|
||||
"confirm_new_password": "确认新密码",
|
||||
"update_password": "更新密码",
|
||||
"password_updated": "密码更新成功",
|
||||
"change_locale": "修改语言",
|
||||
"update_locale": "更新语言",
|
||||
"locale_updated": "语言更新成功",
|
||||
"change_avatar": "修改头像",
|
||||
"upload_avatar": "上传头像",
|
||||
"remove_avatar": "移除头像",
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function DropdownLanguage({ locale, onChangeLocale }: Props) {
|
||||
{locales.find((entry) => entry.code === locale)?.name || locale}
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="lacales">
|
||||
<DropdownMenu aria-label="locales">
|
||||
{locales.map((entry) => (
|
||||
<DropdownItem key={entry.code} onPress={() => onChangeLocale(entry.code)}>
|
||||
{entry.name}
|
||||
|
||||
@@ -136,7 +136,7 @@ export default function HeaderNavbarMenu({ messages, locale }: Props) {
|
||||
<ThemeSwitch />
|
||||
<div className="hidden md:block">
|
||||
<DropdownAccount messages={messages} locale={locale} onItemPress={() => {}} />
|
||||
<DropdownLanguage locale={locale} onChangeLocale={changeLocale} />
|
||||
{!context.isSignedIn() && <DropdownLanguage locale={locale} onChangeLocale={changeLocale} />}
|
||||
</div>
|
||||
<NavbarMenuToggle className="md:hidden" onChange={() => setIsMenuOpen(!isMenuOpen)} />
|
||||
</NavbarContent>
|
||||
@@ -224,51 +224,53 @@ export default function HeaderNavbarMenu({ messages, locale }: Props) {
|
||||
/>
|
||||
</Listbox>
|
||||
) : (
|
||||
<Listbox
|
||||
aria-label="Account links"
|
||||
itemClasses={{
|
||||
base: 'h-10 text-large',
|
||||
}}
|
||||
>
|
||||
<ListboxItem
|
||||
key="signin"
|
||||
startContent={<ArrowRightToLine size={16} />}
|
||||
title={messages.signIn}
|
||||
onPress={() => {
|
||||
router.push('/account/signin', { locale: locale });
|
||||
setIsMenuOpen(false);
|
||||
<>
|
||||
<Listbox
|
||||
aria-label="Account links"
|
||||
itemClasses={{
|
||||
base: 'h-10 text-large',
|
||||
}}
|
||||
/>
|
||||
<ListboxItem
|
||||
key="signup"
|
||||
title={messages.signUp}
|
||||
startContent={<PenTool size={16} />}
|
||||
onPress={() => {
|
||||
router.push('/account/signup', { locale: locale });
|
||||
setIsMenuOpen(false);
|
||||
>
|
||||
<ListboxItem
|
||||
key="signin"
|
||||
startContent={<ArrowRightToLine size={16} />}
|
||||
title={messages.signIn}
|
||||
onPress={() => {
|
||||
router.push('/account/signin', { locale: locale });
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
<ListboxItem
|
||||
key="signup"
|
||||
title={messages.signUp}
|
||||
startContent={<PenTool size={16} />}
|
||||
onPress={() => {
|
||||
router.push('/account/signup', { locale: locale });
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
</Listbox>
|
||||
<p className="font-bold">{messages.languages}</p>
|
||||
<Listbox
|
||||
aria-label="Language links"
|
||||
itemClasses={{
|
||||
base: 'h-10 text-large',
|
||||
}}
|
||||
/>
|
||||
</Listbox>
|
||||
>
|
||||
{locales.map((entry) => (
|
||||
<ListboxItem
|
||||
key={entry.code}
|
||||
startContent={<Globe size={16} />}
|
||||
title={entry.name}
|
||||
onPress={() => {
|
||||
changeLocale(entry.code);
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Listbox>
|
||||
</>
|
||||
)}
|
||||
<p className="font-bold">{messages.languages}</p>
|
||||
<Listbox
|
||||
aria-label="Language links"
|
||||
itemClasses={{
|
||||
base: 'h-10 text-large',
|
||||
}}
|
||||
>
|
||||
{locales.map((entry) => (
|
||||
<ListboxItem
|
||||
key={entry.code}
|
||||
startContent={<Globe size={16} />}
|
||||
title={entry.name}
|
||||
onPress={() => {
|
||||
changeLocale(entry.code);
|
||||
setIsMenuOpen(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Listbox>
|
||||
</div>
|
||||
</NavbarMenu>
|
||||
</Navbar>
|
||||
|
||||
@@ -60,6 +60,7 @@ async function signInAsGuest() {
|
||||
username: 'Guest',
|
||||
role: roles.findIndex((entry) => entry.uid === 'user'),
|
||||
avatarPath: '',
|
||||
locale: null,
|
||||
};
|
||||
const token = await signUp(guestUser);
|
||||
return token;
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
||||
username: '',
|
||||
role: roles.findIndex((entry) => entry.uid === 'user'),
|
||||
avatarPath: '',
|
||||
locale: null,
|
||||
});
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
@@ -83,14 +84,14 @@ export default function AuthPage({ isSignup, messages, locale }: Props) {
|
||||
|
||||
context.setToken(token);
|
||||
context.storeTokenToLocalStorage(token);
|
||||
router.push('/account', { locale: locale });
|
||||
router.push('/account', { locale: token.user?.locale ?? locale });
|
||||
};
|
||||
|
||||
const handleSignInAsGuest = async () => {
|
||||
const token = await signInAsGuest();
|
||||
context.setToken(token);
|
||||
context.storeTokenToLocalStorage(token);
|
||||
router.push('/account', { locale: locale });
|
||||
router.push('/account', { locale: token.user?.locale ?? locale });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
'use client';
|
||||
import { useState, useContext, useRef } from 'react';
|
||||
import { Button, Input, Card, CardHeader, CardBody, addToast, CardFooter } from '@heroui/react';
|
||||
import { Button, Input, Card, CardHeader, CardBody, addToast, CardFooter, Select, SelectItem } from '@heroui/react';
|
||||
import { Globe } from 'lucide-react';
|
||||
import { TokenContext } from '@/utils/TokenProvider';
|
||||
import { updateUsername, updatePassword, uploadAvatar, deleteAvatar } from '@/utils/usersControl';
|
||||
import { updateUsername, updatePassword, uploadAvatar, deleteAvatar, updateLocale } from '@/utils/usersControl';
|
||||
import { LocaleCodeType } from '@/types/locale';
|
||||
import { logError } from '@/utils/errorHandler';
|
||||
import UserAvatar from '@/components/UserAvatar';
|
||||
import { useRouter, usePathname } from '@/src/i18n/routing';
|
||||
import { locales } from '@/config/selection';
|
||||
import { LocaleType } from '@/types/locale';
|
||||
|
||||
type ProfileSettingsPageMessages = {
|
||||
profileSettings: string;
|
||||
@@ -19,6 +23,9 @@ type ProfileSettingsPageMessages = {
|
||||
confirmNewPassword: string;
|
||||
updatePassword: string;
|
||||
passwordUpdated: string;
|
||||
changeLocale: string;
|
||||
updateLocale: string;
|
||||
localeUpdated: string;
|
||||
changeAvatar: string;
|
||||
uploadAvatar: string;
|
||||
removeAvatar: string;
|
||||
@@ -31,6 +38,7 @@ type ProfileSettingsPageMessages = {
|
||||
invalidPassword: string;
|
||||
passwordNotMatch: string;
|
||||
usernameEmpty: string;
|
||||
invalidLocale: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@@ -38,15 +46,21 @@ type Props = {
|
||||
locale: LocaleCodeType;
|
||||
};
|
||||
|
||||
export default function ProfileSettingsPage({ messages }: Props) {
|
||||
export default function ProfileSettingsPage({ messages, locale: defaultLocale }: Props) {
|
||||
const context = useContext(TokenContext);
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [username, setUsername] = useState('');
|
||||
const [currentPassword, setCurrentPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [locale, setLocale] = useState<LocaleCodeType>(context.token?.user?.locale ?? defaultLocale);
|
||||
const [isUpdatingUsername, setIsUpdatingUsername] = useState(false);
|
||||
const [isUpdatingPassword, setIsUpdatingPassword] = useState(false);
|
||||
const [isUpdatingLocale, setIsUpdatingLocale] = useState(false);
|
||||
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
|
||||
|
||||
const handleUsernameUpdate = async () => {
|
||||
@@ -150,6 +164,53 @@ export default function ProfileSettingsPage({ messages }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleLocaleUpdate = async () => {
|
||||
if (!locales.some((l) => l.code === locale)) {
|
||||
addToast({
|
||||
title: 'Warning',
|
||||
color: 'warning',
|
||||
description: messages.invalidLocale,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUpdatingLocale(true);
|
||||
try {
|
||||
const result = await updateLocale(context.token.access_token, locale);
|
||||
if (result && result.user) {
|
||||
// refresh locale
|
||||
const newToken = { ...context.token };
|
||||
if (newToken.user) {
|
||||
newToken.user.locale = result.user.locale;
|
||||
}
|
||||
context.setToken(newToken);
|
||||
context.storeTokenToLocalStorage(newToken);
|
||||
|
||||
addToast({
|
||||
title: 'Success',
|
||||
color: 'success',
|
||||
description: messages.localeUpdated,
|
||||
});
|
||||
const nextLocale = result.user.locale ?? locale;
|
||||
setLocale(nextLocale);
|
||||
changeLocale(nextLocale);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Error updating locale:', error);
|
||||
addToast({
|
||||
title: 'Error',
|
||||
color: 'danger',
|
||||
description: messages.updateError,
|
||||
});
|
||||
} finally {
|
||||
setIsUpdatingLocale(false);
|
||||
}
|
||||
};
|
||||
|
||||
async function changeLocale(nextLocale: LocaleCodeType) {
|
||||
router.push(pathname, { locale: nextLocale });
|
||||
}
|
||||
|
||||
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
@@ -333,6 +394,46 @@ export default function ProfileSettingsPage({ messages }: Props) {
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Change Locale */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<Globe size={16} />
|
||||
<h2 className="text-large font-semibold ml-2">{messages.changeLocale}</h2>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<form>
|
||||
<div className="space-y-4">
|
||||
<Select<LocaleType>
|
||||
fullWidth
|
||||
aria-label="change locale"
|
||||
selectedKeys={[locale]}
|
||||
disabledKeys={[locale]}
|
||||
onSelectionChange={(value) => {
|
||||
const selectedLocale = locales.find((locale) => locale.code === value.currentKey);
|
||||
if (!selectedLocale) return;
|
||||
setLocale(selectedLocale.code);
|
||||
}}
|
||||
>
|
||||
{locales.map((locale) => (
|
||||
<SelectItem key={locale.code}>{locale.name}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</form>
|
||||
</CardBody>
|
||||
<CardFooter className="flex justify-end">
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleLocaleUpdate}
|
||||
isLoading={isUpdatingLocale}
|
||||
isDisabled={locale === context.token?.user?.locale}
|
||||
size="sm"
|
||||
>
|
||||
{messages.updateLocale}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Change Avatar */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
|
||||
@@ -17,6 +17,9 @@ export default function Page({ params }: PageType) {
|
||||
confirmNewPassword: t('confirm_new_password'),
|
||||
updatePassword: t('update_password'),
|
||||
passwordUpdated: t('password_updated'),
|
||||
changeLocale: t('change_locale'),
|
||||
updateLocale: t('update_locale'),
|
||||
localeUpdated: t('locale_updated'),
|
||||
changeAvatar: t('change_avatar'),
|
||||
uploadAvatar: t('upload_avatar'),
|
||||
removeAvatar: t('remove_avatar'),
|
||||
@@ -29,6 +32,7 @@ export default function Page({ params }: PageType) {
|
||||
invalidPassword: t('invalid_password'),
|
||||
passwordNotMatch: t('password_not_match'),
|
||||
usernameEmpty: t('username_empty'),
|
||||
invalidLocale: t('invalid_locale'),
|
||||
};
|
||||
|
||||
return <ProfileSettingsPage messages={messages} locale={params.locale as LocaleCodeType} />;
|
||||
|
||||
@@ -43,6 +43,7 @@ export default function SettingsPage({ projectId, messages, projectDialogMessage
|
||||
avatarPath: '',
|
||||
role: -1,
|
||||
username: '',
|
||||
locale: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type UserType = {
|
||||
username: string;
|
||||
role: number;
|
||||
avatarPath: string | null;
|
||||
locale: LocaleCodeType | null;
|
||||
};
|
||||
|
||||
export type TokenProps = {
|
||||
|
||||
@@ -219,6 +219,36 @@ async function adminResetPassword(jwt: string, userId: number, newPassword: stri
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLocale(jwt: string, locale: string) {
|
||||
const updateData = {
|
||||
locale,
|
||||
};
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
body: JSON.stringify(updateData),
|
||||
};
|
||||
|
||||
const url = `${apiServer}/users/locale`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, fetchOptions);
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(errorText || `HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error: unknown) {
|
||||
logError('Error updating locale:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
findUser,
|
||||
searchUsers,
|
||||
@@ -228,4 +258,5 @@ export {
|
||||
uploadAvatar,
|
||||
deleteAvatar,
|
||||
adminResetPassword,
|
||||
updateLocale,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user