mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-07 01:56:53 +00:00
feat(ui): improve mobile responsive layouts
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Languages, Loader2 } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { TitledCard } from '@/components/ui/titled-card'
|
||||
import { updateUserLanguage } from '../api'
|
||||
import { parseUserSettings } from '../lib'
|
||||
import type { UserProfile } from '../types'
|
||||
|
||||
const LANGUAGE_OPTIONS = [
|
||||
{ value: 'zh', label: '简体中文' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'ru', label: 'Русский' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'vi', label: 'Tiếng Việt' },
|
||||
] as const
|
||||
|
||||
function normalizeLanguage(value?: string | null): string {
|
||||
if (!value) return 'en'
|
||||
const normalized = value.trim().replace(/_/g, '-').toLowerCase()
|
||||
if (normalized.startsWith('zh')) return 'zh'
|
||||
return LANGUAGE_OPTIONS.some((lang) => lang.value === normalized)
|
||||
? normalized
|
||||
: 'en'
|
||||
}
|
||||
|
||||
type LanguagePreferencesCardProps = {
|
||||
profile: UserProfile | null
|
||||
onProfileUpdate: () => void
|
||||
}
|
||||
|
||||
export function LanguagePreferencesCard(props: LanguagePreferencesCardProps) {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { auth } = useAuthStore()
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const savedLanguage = useMemo(() => {
|
||||
const settings = parseUserSettings(props.profile?.setting)
|
||||
return normalizeLanguage(settings.language || i18n.language)
|
||||
}, [props.profile?.setting, i18n.language])
|
||||
|
||||
const [currentLanguage, setCurrentLanguage] = useState(savedLanguage)
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentLanguage(savedLanguage)
|
||||
}, [savedLanguage])
|
||||
|
||||
const handleLanguageChange = async (language: string) => {
|
||||
const nextLanguage = normalizeLanguage(language)
|
||||
if (nextLanguage === currentLanguage) return
|
||||
|
||||
const previousLanguage = currentLanguage
|
||||
setCurrentLanguage(nextLanguage)
|
||||
setSaving(true)
|
||||
await i18n.changeLanguage(nextLanguage)
|
||||
|
||||
try {
|
||||
const response = await updateUserLanguage(nextLanguage)
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || t('Failed to update settings'))
|
||||
}
|
||||
|
||||
if (auth.user) {
|
||||
const existingSetting =
|
||||
typeof auth.user.setting === 'string'
|
||||
? parseUserSettings(auth.user.setting)
|
||||
: (auth.user.setting ?? {})
|
||||
auth.setUser({
|
||||
...auth.user,
|
||||
setting: JSON.stringify({
|
||||
...existingSetting,
|
||||
language: nextLanguage,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
props.onProfileUpdate()
|
||||
toast.success(t('Language preference saved'))
|
||||
} catch (_error) {
|
||||
setCurrentLanguage(previousLanguage)
|
||||
await i18n.changeLanguage(previousLanguage)
|
||||
toast.error(t('Failed to update settings'))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TitledCard
|
||||
title={t('Language Preferences')}
|
||||
description={t('Set the language used across the interface')}
|
||||
icon={<Languages className='h-4 w-4' />}
|
||||
>
|
||||
<div className='flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4'>
|
||||
<div className='space-y-1'>
|
||||
<div className='text-sm font-medium'>{t('Interface Language')}</div>
|
||||
<p className='text-muted-foreground line-clamp-2 text-xs sm:text-sm'>
|
||||
{t(
|
||||
'Language preferences sync across your signed-in devices and affect API error messages.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 sm:min-w-48'>
|
||||
<Select
|
||||
value={currentLanguage}
|
||||
onValueChange={handleLanguageChange}
|
||||
disabled={saving}
|
||||
>
|
||||
<SelectTrigger className='w-full sm:w-48'>
|
||||
<SelectValue placeholder={t('Select language')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LANGUAGE_OPTIONS.map((language) => (
|
||||
<SelectItem key={language.value} value={language.value}>
|
||||
{language.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{saving && (
|
||||
<Loader2 className='text-muted-foreground size-4 animate-spin' />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TitledCard>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user