feat: Move language option to profile setting (#387)
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user