Configure web display language from profile settings, default is set to
This commit is contained in:
parent
261f21af55
commit
2bc4fcc77f
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
|
@ -9,7 +9,8 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { Save } from 'lucide-react';
|
import { Save } from 'lucide-react';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { useI18n } from '@/locales/client';
|
import { useI18n, useCurrentLocale, useChangeLocale } from '@/locales/client';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
|
||||||
const mockUserProfile = {
|
const mockUserProfile = {
|
||||||
name: 'Alice Wonderland',
|
name: 'Alice Wonderland',
|
||||||
|
|
@ -23,6 +24,8 @@ const mockUserProfile = {
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
const currentLocale = useCurrentLocale();
|
||||||
|
const changeLocale = useChangeLocale();
|
||||||
|
|
||||||
const [name, setName] = useState(mockUserProfile.name);
|
const [name, setName] = useState(mockUserProfile.name);
|
||||||
const [email, setEmail] = useState(mockUserProfile.email);
|
const [email, setEmail] = useState(mockUserProfile.email);
|
||||||
|
|
@ -33,39 +36,58 @@ export default function ProfilePage() {
|
||||||
const [currentPassword, setCurrentPassword] = useState('');
|
const [currentPassword, setCurrentPassword] = useState('');
|
||||||
const [newPassword, setNewPassword] = useState('');
|
const [newPassword, setNewPassword] = useState('');
|
||||||
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
const [confirmNewPassword, setConfirmNewPassword] = useState('');
|
||||||
|
|
||||||
|
const [selectedLanguage, setSelectedLanguage] = useState(currentLocale);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
|
const [isLanguageLoading, setIsLanguageLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedLanguage(currentLocale);
|
||||||
|
}, [currentLocale]);
|
||||||
|
|
||||||
const handleProfileUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleProfileUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
console.log("Updating profile:", { name, avatarUrl, bio, phone, location });
|
console.log("Updating profile:", { name, avatarUrl, bio, phone, location });
|
||||||
toast({ title: "Profile Updated", description: "Your profile information has been saved." }); // Translate
|
toast({ title: t('dashboard.profile.save_button'), description: "Your profile information has been saved." });
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (newPassword !== confirmNewPassword) {
|
if (newPassword !== confirmNewPassword) {
|
||||||
toast({ title: "Password Mismatch", description: "New passwords do not match.", variant: "destructive" }); // Translate
|
toast({ title: "Password Mismatch", description: "New passwords do not match.", variant: "destructive" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (newPassword.length < 6) {
|
if (newPassword.length < 6) {
|
||||||
toast({ title: "Password Too Short", description: "Password must be at least 6 characters.", variant: "destructive" }); // Translate
|
toast({ title: "Password Too Short", description: "Password must be at least 6 characters.", variant: "destructive" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsPasswordLoading(true);
|
setIsPasswordLoading(true);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
console.log("Changing password");
|
console.log("Changing password");
|
||||||
toast({ title: "Password Updated", description: "Your password has been changed successfully." }); // Translate
|
toast({ title: t('dashboard.profile.update_password_button'), description: "Your password has been changed successfully." });
|
||||||
setCurrentPassword('');
|
setCurrentPassword('');
|
||||||
setNewPassword('');
|
setNewPassword('');
|
||||||
setConfirmNewPassword('');
|
setConfirmNewPassword('');
|
||||||
setIsPasswordLoading(false);
|
setIsPasswordLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLanguageChange = (newLang: 'en' | 'zh-TW') => {
|
||||||
|
setIsLanguageLoading(true);
|
||||||
|
changeLocale(newLang);
|
||||||
|
localStorage.setItem('userPreferredLanguage', newLang);
|
||||||
|
setSelectedLanguage(newLang);
|
||||||
|
toast({ title: t('dashboard.profile.language_settings_title'), description: t('dashboard.profile.language_updated_toast') });
|
||||||
|
// No need to manually set isLanguageLoading to false if the page reloads/re-renders due to locale change
|
||||||
|
// However, if changeLocale doesn't cause an immediate unmount, manage loading state appropriately.
|
||||||
|
// For simplicity, we assume changeLocale will lead to a re-render where currentLocale updates.
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 max-w-3xl mx-auto">
|
<div className="space-y-8 max-w-3xl mx-auto">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -155,6 +177,32 @@ export default function ProfilePage() {
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl font-headline">{t('dashboard.profile.language_settings_title')}</CardTitle>
|
||||||
|
<CardDescription>{t('dashboard.profile.language_settings_description')}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="language-select">{t('dashboard.profile.display_language_label')}</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedLanguage}
|
||||||
|
onValueChange={(value) => handleLanguageChange(value as 'en' | 'zh-TW')}
|
||||||
|
disabled={isLanguageLoading}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="language-select">
|
||||||
|
<SelectValue placeholder={t('lang.select_language')} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="en">{t('lang.english')}</SelectItem>
|
||||||
|
<SelectItem value="zh-TW">{t('lang.traditional_chinese')}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,23 +15,41 @@ import {
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ThemeToggleButton } from '@/components/ui/theme-toggle';
|
import { ThemeToggleButton } from '@/components/ui/theme-toggle';
|
||||||
import LanguageSwitcher from './LanguageSwitcher';
|
import LanguageSwitcher from './LanguageSwitcher';
|
||||||
import { useI18n, useCurrentLocale } from '@/locales/client';
|
import { useI18n, useCurrentLocale, useChangeLocale } from '@/locales/client';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const locale = useCurrentLocale();
|
const locale = useCurrentLocale();
|
||||||
|
const changeLocale = useChangeLocale();
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
const authStatus = localStorage.getItem('isToyShareAuthenticated');
|
const authStatus = localStorage.getItem('isToyShareAuthenticated');
|
||||||
setIsAuthenticated(authStatus === 'true');
|
setIsAuthenticated(authStatus === 'true');
|
||||||
}, [pathname]); // Listen to pathname changes to re-check auth if needed after navigation
|
}, []); // Run only once on mount
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMounted) { // Ensure this runs only on the client after mount
|
||||||
|
const preferredLang = localStorage.getItem('userPreferredLanguage') as 'en' | 'zh-TW' | null;
|
||||||
|
if (preferredLang && preferredLang !== locale) {
|
||||||
|
// Check if the current pathname without locale prefix exists for the preferredLang
|
||||||
|
// This avoids redirect loops if a page doesn't exist in the preferredLang
|
||||||
|
const currentPathWithoutLocale = pathname.startsWith(`/${locale}`) ? pathname.substring(`/${locale}`.length) : pathname;
|
||||||
|
// It's hard to verify if `currentPathWithoutLocale` is valid for `preferredLang` without a full route list
|
||||||
|
// For simplicity, we'll directly change locale. If a 404 occurs, user can switch back.
|
||||||
|
changeLocale(preferredLang);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isMounted, locale, pathname, changeLocale]);
|
||||||
|
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('isToyShareAuthenticated');
|
localStorage.removeItem('isToyShareAuthenticated');
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
// No need to router.push here, links will use current locale
|
// No need to router.push here, links will use current locale, or redirect logic will apply
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to remove locale prefix for path comparison
|
// Helper to remove locale prefix for path comparison
|
||||||
|
|
@ -78,7 +96,7 @@ export default function Header() {
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleLogout}>
|
<DropdownMenuItem onClick={handleLogout}>
|
||||||
<LogIn className="mr-2 h-4 w-4" /> {/* Using LogIn icon for logout action for consistency */}
|
<LogIn className="mr-2 h-4 w-4" />
|
||||||
{t('header.logout')}
|
{t('header.logout')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
@ -102,7 +120,6 @@ export default function Header() {
|
||||||
</nav>
|
</nav>
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<ThemeToggleButton />
|
<ThemeToggleButton />
|
||||||
{/* Mobile menu trigger could be added here if needed */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -92,8 +92,12 @@ export default {
|
||||||
'dashboard.profile.confirm_new_password_label': 'Confirm New Password',
|
'dashboard.profile.confirm_new_password_label': 'Confirm New Password',
|
||||||
'dashboard.profile.update_password_button': 'Update Password',
|
'dashboard.profile.update_password_button': 'Update Password',
|
||||||
'dashboard.profile.updating_password_button': 'Updating Password...',
|
'dashboard.profile.updating_password_button': 'Updating Password...',
|
||||||
|
'dashboard.profile.language_settings_title': 'Language Settings',
|
||||||
|
'dashboard.profile.language_settings_description': 'Choose your preferred display language for the application.',
|
||||||
|
'dashboard.profile.display_language_label': 'Display Language',
|
||||||
|
'dashboard.profile.language_updated_toast': 'Language updated successfully.',
|
||||||
'lang.english': 'English',
|
'lang.english': 'English',
|
||||||
'lang.traditional_chinese': '繁體中文',
|
'lang.traditional_chinese': 'Traditional Chinese',
|
||||||
'lang.select_language': 'Select Language',
|
'lang.select_language': 'Select Language',
|
||||||
'general.back_to_my_toys': 'Back to My Toys',
|
'general.back_to_my_toys': 'Back to My Toys',
|
||||||
'general.toy_not_found': 'Toy Not Found',
|
'general.toy_not_found': 'Toy Not Found',
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,10 @@ export default {
|
||||||
'dashboard.profile.confirm_new_password_label': '確認新密碼',
|
'dashboard.profile.confirm_new_password_label': '確認新密碼',
|
||||||
'dashboard.profile.update_password_button': '更新密碼',
|
'dashboard.profile.update_password_button': '更新密碼',
|
||||||
'dashboard.profile.updating_password_button': '更新密碼中...',
|
'dashboard.profile.updating_password_button': '更新密碼中...',
|
||||||
|
'dashboard.profile.language_settings_title': '語言設定',
|
||||||
|
'dashboard.profile.language_settings_description': '選擇您偏好的應用程式顯示語言。',
|
||||||
|
'dashboard.profile.display_language_label': '顯示語言',
|
||||||
|
'dashboard.profile.language_updated_toast': '語言已成功更新。',
|
||||||
'lang.english': 'English',
|
'lang.english': 'English',
|
||||||
'lang.traditional_chinese': '繁體中文',
|
'lang.traditional_chinese': '繁體中文',
|
||||||
'lang.select_language': '選擇語言',
|
'lang.select_language': '選擇語言',
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,8 @@ import { createI18nMiddleware } from 'next-international/middleware';
|
||||||
|
|
||||||
const I18nMiddleware = createI18nMiddleware({
|
const I18nMiddleware = createI18nMiddleware({
|
||||||
locales: ['en', 'zh-TW'],
|
locales: ['en', 'zh-TW'],
|
||||||
defaultLocale: 'en',
|
defaultLocale: 'zh-TW', // Changed default locale
|
||||||
urlMappingStrategy: 'rewrite', // Or 'redirect', depending on preference
|
urlMappingStrategy: 'rewrite',
|
||||||
// prefix: 'as-needed' // default is 'as-needed'
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function middleware(request: NextRequest) {
|
export function middleware(request: NextRequest) {
|
||||||
|
|
@ -13,6 +12,5 @@ export function middleware(request: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
// Matcher ignoring `/_next/` and `/api/`
|
|
||||||
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue