feat: Move language option to profile setting (#387)

This commit is contained in:
CY
2026-02-26 22:51:12 +08:00
committed by GitHub
parent 3b059f1897
commit a065c1800f
22 changed files with 368 additions and 51 deletions

View File

@@ -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}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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>

View File

@@ -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} />;

View File

@@ -43,6 +43,7 @@ export default function SettingsPage({ projectId, messages, projectDialogMessage
avatarPath: '',
role: -1,
username: '',
locale: null,
});
useEffect(() => {