feat: 重新設計個人檔案頁面並整合設定功能
- 建立全新分頁式個人檔案頁面(👤個人資料 ⚙️學習設定 🎯英語程度) - 整合原有 settings 功能到 profile 頁面的分頁中 - 重新設計導航列:移除設定連結,個人檔案放在右上角用戶區域 - 改進響應式設計:桌面和手機版都有清晰的個人檔案入口 - 簡化 settings 頁面為重新導向頁面,統一用戶體驗 - 修復前端條件判斷邏輯,改善空狀態畫面顯示 新設計更簡潔易用,符合標準 UI 模式。 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4a7c3aec92
commit
4c7696f80b
|
|
@ -0,0 +1,700 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Navigation } from '@/components/shared/Navigation'
|
||||||
|
|
||||||
|
interface UserProfile {
|
||||||
|
id: string
|
||||||
|
email: string
|
||||||
|
displayName: string | null
|
||||||
|
avatarUrl: string | null
|
||||||
|
subscriptionType: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserSettings {
|
||||||
|
dailyGoal: number
|
||||||
|
reminderTime: string
|
||||||
|
reminderEnabled: boolean
|
||||||
|
difficultyPreference: string
|
||||||
|
autoPlayAudio: boolean
|
||||||
|
showPronunciation: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LanguageLevel {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
examples: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type TabType = 'profile' | 'settings' | 'level'
|
||||||
|
|
||||||
|
export default function ProfilePage() {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>('profile')
|
||||||
|
const [profile, setProfile] = useState<UserProfile | null>(null)
|
||||||
|
const [settings, setSettings] = useState<UserSettings | null>(null)
|
||||||
|
const [userLevel, setUserLevel] = useState('A2')
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
|
||||||
|
// 編輯表單狀態
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
displayName: '',
|
||||||
|
dailyGoal: 20,
|
||||||
|
reminderEnabled: true,
|
||||||
|
difficultyPreference: 'balanced',
|
||||||
|
autoPlayAudio: true,
|
||||||
|
showPronunciation: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 英語程度定義
|
||||||
|
const levels: LanguageLevel[] = [
|
||||||
|
{
|
||||||
|
value: 'A1',
|
||||||
|
label: 'A1 - 初學者',
|
||||||
|
description: '能理解基本詞彙和簡單句子',
|
||||||
|
examples: ['hello', 'good', 'house', 'eat', 'happy']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'A2',
|
||||||
|
label: 'A2 - 基礎',
|
||||||
|
description: '能處理日常對話和常見主題',
|
||||||
|
examples: ['important', 'difficult', 'interesting', 'beautiful', 'understand']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'B1',
|
||||||
|
label: 'B1 - 中級',
|
||||||
|
description: '能理解清楚標準語言的要點',
|
||||||
|
examples: ['analyze', 'opportunity', 'environment', 'responsibility', 'development']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'B2',
|
||||||
|
label: 'B2 - 中高級',
|
||||||
|
description: '能理解複雜文本的主要內容',
|
||||||
|
examples: ['sophisticated', 'implications', 'comprehensive', 'substantial', 'methodology']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'C1',
|
||||||
|
label: 'C1 - 高級',
|
||||||
|
description: '能流利表達,理解含蓄意思',
|
||||||
|
examples: ['meticulous', 'predominantly', 'intricate', 'corroborate', 'paradigm']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'C2',
|
||||||
|
label: 'C2 - 精通',
|
||||||
|
description: '接近母語水平',
|
||||||
|
examples: ['ubiquitous', 'ephemeral', 'perspicacious', 'multifarious', 'idiosyncratic']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'profile' as TabType, label: '個人資料', icon: '👤' },
|
||||||
|
{ id: 'settings' as TabType, label: '學習設定', icon: '⚙️' },
|
||||||
|
{ id: 'level' as TabType, label: '英語程度', icon: '🎯' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 載入資料
|
||||||
|
useEffect(() => {
|
||||||
|
loadData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token')
|
||||||
|
if (!token) {
|
||||||
|
setError('請先登入')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('🚀 載入個人檔案資料...')
|
||||||
|
|
||||||
|
// 並行載入所有資料
|
||||||
|
const [profileResponse, settingsResponse] = await Promise.all([
|
||||||
|
fetch('http://localhost:5000/api/auth/profile', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
fetch('http://localhost:5000/api/auth/settings', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
if (!profileResponse.ok || !settingsResponse.ok) {
|
||||||
|
if (profileResponse.status === 401 || settingsResponse.status === 401) {
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
setError('登入已過期,請重新登入')
|
||||||
|
} else {
|
||||||
|
setError('載入個人資料失敗')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [profileData, settingsData] = await Promise.all([
|
||||||
|
profileResponse.json(),
|
||||||
|
settingsResponse.json()
|
||||||
|
])
|
||||||
|
|
||||||
|
if (profileData.success && settingsData.success) {
|
||||||
|
setProfile(profileData.data)
|
||||||
|
setSettings(settingsData.data)
|
||||||
|
|
||||||
|
// 初始化編輯表單
|
||||||
|
setEditForm({
|
||||||
|
displayName: profileData.data.displayName || '',
|
||||||
|
dailyGoal: settingsData.data.dailyGoal || 20,
|
||||||
|
reminderEnabled: settingsData.data.reminderEnabled ?? true,
|
||||||
|
difficultyPreference: settingsData.data.difficultyPreference || 'balanced',
|
||||||
|
autoPlayAudio: settingsData.data.autoPlayAudio ?? true,
|
||||||
|
showPronunciation: settingsData.data.showPronunciation ?? true
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('✅ 個人檔案資料載入成功')
|
||||||
|
} else {
|
||||||
|
setError('載入資料失敗')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 載入英語程度
|
||||||
|
const savedLevel = localStorage.getItem('userEnglishLevel')
|
||||||
|
if (savedLevel) {
|
||||||
|
setUserLevel(savedLevel)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('載入個人資料錯誤:', error)
|
||||||
|
setError(error instanceof Error ? error.message : '載入失敗')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveProfile = async () => {
|
||||||
|
if (!profile) return
|
||||||
|
|
||||||
|
setIsSaving(true)
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token')
|
||||||
|
if (!token) {
|
||||||
|
setError('請先登入')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:5000/api/auth/profile', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ displayName: editForm.displayName })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await loadData() // 重新載入資料
|
||||||
|
console.log('✅ 個人資料更新成功')
|
||||||
|
} else {
|
||||||
|
setError('儲存失敗')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError('儲存失敗')
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveSettings = async () => {
|
||||||
|
setIsSaving(true)
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('auth_token')
|
||||||
|
if (!token) {
|
||||||
|
setError('請先登入')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('http://localhost:5000/api/auth/settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
dailyGoal: editForm.dailyGoal,
|
||||||
|
reminderEnabled: editForm.reminderEnabled,
|
||||||
|
difficultyPreference: editForm.difficultyPreference,
|
||||||
|
autoPlayAudio: editForm.autoPlayAudio,
|
||||||
|
showPronunciation: editForm.showPronunciation
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
await loadData() // 重新載入資料
|
||||||
|
console.log('✅ 學習設定更新成功')
|
||||||
|
} else {
|
||||||
|
setError('儲存失敗')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError('儲存失敗')
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveLevel = () => {
|
||||||
|
localStorage.setItem('userEnglishLevel', userLevel)
|
||||||
|
console.log('✅ 英語程度已保存:', userLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
localStorage.removeItem('userEnglishLevel')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 載入狀態
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
<Navigation />
|
||||||
|
<div className="py-8">
|
||||||
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-700">載入個人資料中...</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 錯誤狀態
|
||||||
|
if (error) {
|
||||||
|
const isAuthError = error.includes('登入') || error.includes('認證')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
<Navigation />
|
||||||
|
<div className="py-8">
|
||||||
|
<div className="max-w-4xl mx-auto px-4">
|
||||||
|
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||||
|
<div className={`text-4xl mb-4 ${isAuthError ? 'text-yellow-500' : 'text-red-500'}`}>
|
||||||
|
{isAuthError ? '🔒' : '⚠️'}
|
||||||
|
</div>
|
||||||
|
<h2 className={`text-xl font-semibold mb-2 ${isAuthError ? 'text-yellow-700' : 'text-red-700'}`}>
|
||||||
|
{isAuthError ? '需要重新登入' : '載入失敗'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 mb-6">{error}</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
{isAuthError ? (
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/login'}
|
||||||
|
className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-semibold"
|
||||||
|
>
|
||||||
|
前往登入
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={loadData}
|
||||||
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
重新載入
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/'}
|
||||||
|
className="px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
回到首頁
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profile || !settings) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||||
|
<Navigation />
|
||||||
|
|
||||||
|
<div className="py-8">
|
||||||
|
<div className="max-w-6xl mx-auto px-4">
|
||||||
|
{/* 頁面標題 */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">個人檔案</h1>
|
||||||
|
<p className="text-gray-600">管理您的帳戶資訊和學習偏好設定</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分頁導航 */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="border-b border-gray-200 bg-white rounded-t-xl">
|
||||||
|
<nav className="flex space-x-8 px-6 py-4">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`flex items-center space-x-2 py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? 'border-blue-500 text-blue-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{tab.icon}</span>
|
||||||
|
<span>{tab.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分頁內容 */}
|
||||||
|
<div className="bg-white rounded-xl rounded-t-none shadow-lg">
|
||||||
|
{/* 個人資料分頁 */}
|
||||||
|
{activeTab === 'profile' && (
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
{/* 用戶資訊卡片 */}
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<div className="w-24 h-24 bg-gradient-to-br from-blue-500 to-purple-600 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
{profile.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={profile.avatarUrl}
|
||||||
|
alt="用戶頭像"
|
||||||
|
className="w-full h-full rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="text-3xl text-white">
|
||||||
|
{(profile.displayName || profile.email)[0].toUpperCase()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-1">
|
||||||
|
{profile.displayName || '用戶'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-500 mb-3">{profile.email}</p>
|
||||||
|
|
||||||
|
<span className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
|
||||||
|
profile.subscriptionType === 'premium'
|
||||||
|
? 'bg-yellow-100 text-yellow-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{profile.subscriptionType === 'premium' ? '🌟 Premium' : '🆓 免費版'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 編輯資料 */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
顯示名稱
|
||||||
|
</label>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.displayName}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, displayName: e.target.value }))}
|
||||||
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
placeholder="請輸入顯示名稱"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveProfile}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSaving ? '儲存中...' : '儲存'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 帳戶資訊 */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-3">帳戶資訊</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-600">用戶 ID</span>
|
||||||
|
<p className="font-mono text-gray-800 text-xs break-all">{profile.id}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-600">加入時間</span>
|
||||||
|
<p className="font-medium text-gray-800">
|
||||||
|
{new Date(profile.createdAt).toLocaleDateString('zh-TW')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 登出 */}
|
||||||
|
<div className="border-t pt-6">
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full px-4 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium"
|
||||||
|
>
|
||||||
|
登出帳戶
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 學習設定分頁 */}
|
||||||
|
{activeTab === 'settings' && (
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 每日目標 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||||
|
每日學習目標
|
||||||
|
</label>
|
||||||
|
<div className="bg-gray-50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className="text-2xl font-bold text-blue-600">{editForm.dailyGoal}</span>
|
||||||
|
<span className="text-sm text-gray-600">張詞卡/天</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={editForm.dailyGoal}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, dailyGoal: Number(e.target.value) }))}
|
||||||
|
className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
||||||
|
<span>1</span>
|
||||||
|
<span>50</span>
|
||||||
|
<span>100</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 學習偏好 */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-3">
|
||||||
|
學習難度偏好
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
{[
|
||||||
|
{ value: 'conservative', label: '保守', desc: '較簡單', color: 'green' },
|
||||||
|
{ value: 'balanced', label: '均衡', desc: '適中', color: 'blue' },
|
||||||
|
{ value: 'aggressive', label: '積極', desc: '較難', color: 'purple' }
|
||||||
|
].map(option => (
|
||||||
|
<label
|
||||||
|
key={option.value}
|
||||||
|
className={`p-3 border-2 rounded-lg cursor-pointer transition-all ${
|
||||||
|
editForm.difficultyPreference === option.value
|
||||||
|
? `border-${option.color}-500 bg-${option.color}-50`
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="difficulty"
|
||||||
|
value={option.value}
|
||||||
|
checked={editForm.difficultyPreference === option.value}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, difficultyPreference: e.target.value }))}
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="font-semibold text-gray-900">{option.label}</div>
|
||||||
|
<div className="text-sm text-gray-600">{option.desc}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 音頻和顯示設定 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-medium text-gray-900">音頻和顯示設定</h3>
|
||||||
|
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
key: 'reminderEnabled' as keyof typeof editForm,
|
||||||
|
label: '複習提醒',
|
||||||
|
desc: '接收學習提醒通知'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'autoPlayAudio' as keyof typeof editForm,
|
||||||
|
label: '自動播放音頻',
|
||||||
|
desc: '顯示詞卡時自動播放發音'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'showPronunciation' as keyof typeof editForm,
|
||||||
|
label: '顯示發音標示',
|
||||||
|
desc: '在詞卡上顯示音標'
|
||||||
|
}
|
||||||
|
].map(setting => (
|
||||||
|
<div key={setting.key} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-gray-900">{setting.label}</span>
|
||||||
|
<p className="text-sm text-gray-600">{setting.desc}</p>
|
||||||
|
</div>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editForm[setting.key] as boolean}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, [setting.key]: e.target.checked }))}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 儲存按鈕 */}
|
||||||
|
<div className="pt-4">
|
||||||
|
<button
|
||||||
|
onClick={handleSaveSettings}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="w-full px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isSaving ? '儲存中...' : '儲存學習設定'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 英語程度分頁 */}
|
||||||
|
{activeTab === 'level' && (
|
||||||
|
<div className="p-8">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 mb-2">🎯 英語程度設定</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
選擇您的英語程度,系統將為您提供個人化的詞彙學習建議
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 程度選擇 */}
|
||||||
|
<div className="space-y-4 mb-8">
|
||||||
|
{levels.map(level => (
|
||||||
|
<label
|
||||||
|
key={level.value}
|
||||||
|
className={`block p-6 border-2 rounded-xl cursor-pointer transition-all hover:shadow-md ${
|
||||||
|
userLevel === level.value
|
||||||
|
? 'border-blue-500 bg-blue-50 shadow-md'
|
||||||
|
: 'border-gray-200 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="level"
|
||||||
|
value={level.value}
|
||||||
|
checked={userLevel === level.value}
|
||||||
|
onChange={(e) => setUserLevel(e.target.value)}
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-bold text-xl text-gray-800 mb-2">
|
||||||
|
{level.label}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-600 mb-3">
|
||||||
|
{level.description}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{level.examples.map(example => (
|
||||||
|
<span
|
||||||
|
key={example}
|
||||||
|
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm"
|
||||||
|
>
|
||||||
|
{example}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{userLevel === level.value && (
|
||||||
|
<div className="ml-4">
|
||||||
|
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
|
||||||
|
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 學習效果預覽 */}
|
||||||
|
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-xl mb-6">
|
||||||
|
<h3 className="font-bold text-lg text-blue-800 mb-3">
|
||||||
|
💡 您的個人化學習效果
|
||||||
|
</h3>
|
||||||
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-blue-700 mb-2">重點學習範圍</h4>
|
||||||
|
<p className="text-blue-600">
|
||||||
|
系統將為您標記比目前程度({userLevel})高1-2級的重點詞彙
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold text-blue-700 mb-2">學習建議</h4>
|
||||||
|
<p className="text-blue-600 text-sm">
|
||||||
|
專注學習具有挑戰性但不會太困難的詞彙,提升學習效率
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 儲存按鈕 */}
|
||||||
|
<button
|
||||||
|
onClick={handleSaveLevel}
|
||||||
|
className="w-full py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-lg"
|
||||||
|
>
|
||||||
|
保存英語程度設定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 快速操作區 */}
|
||||||
|
<div className="mt-8 bg-white rounded-xl shadow-lg p-6">
|
||||||
|
<h3 className="font-semibold text-gray-900 mb-4">快速操作</h3>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
{[
|
||||||
|
{ href: '/review', icon: '📚', label: '開始複習', desc: '複習詞卡' },
|
||||||
|
{ href: '/generate', icon: '➕', label: '新增詞卡', desc: '建立內容' },
|
||||||
|
{ href: '/flashcards', icon: '📋', label: '管理詞卡', desc: '編輯詞卡' },
|
||||||
|
{ href: '/stats', icon: '📊', label: '學習統計', desc: '查看進度' }
|
||||||
|
].map(action => (
|
||||||
|
<button
|
||||||
|
key={action.href}
|
||||||
|
onClick={() => window.location.href = action.href}
|
||||||
|
className="p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors text-center"
|
||||||
|
>
|
||||||
|
<div className="text-2xl mb-1">{action.icon}</div>
|
||||||
|
<div className="font-medium text-gray-900 text-sm">{action.label}</div>
|
||||||
|
<div className="text-xs text-gray-500">{action.desc}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,209 +1,18 @@
|
||||||
'use client'
|
'use client'
|
||||||
import { useState, useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
interface LanguageLevel {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
examples: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [userLevel, setUserLevel] = useState('A2');
|
// 自動重新導向到個人檔案頁面
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const levels: LanguageLevel[] = [
|
|
||||||
{
|
|
||||||
value: 'A1',
|
|
||||||
label: 'A1 - 初學者',
|
|
||||||
description: '能理解基本詞彙和簡單句子',
|
|
||||||
examples: ['hello', 'good', 'house', 'eat', 'happy']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'A2',
|
|
||||||
label: 'A2 - 基礎',
|
|
||||||
description: '能處理日常對話和常見主題',
|
|
||||||
examples: ['important', 'difficult', 'interesting', 'beautiful', 'understand']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'B1',
|
|
||||||
label: 'B1 - 中級',
|
|
||||||
description: '能理解清楚標準語言的要點',
|
|
||||||
examples: ['analyze', 'opportunity', 'environment', 'responsibility', 'development']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'B2',
|
|
||||||
label: 'B2 - 中高級',
|
|
||||||
description: '能理解複雜文本的主要內容',
|
|
||||||
examples: ['sophisticated', 'implications', 'comprehensive', 'substantial', 'methodology']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'C1',
|
|
||||||
label: 'C1 - 高級',
|
|
||||||
description: '能流利表達,理解含蓄意思',
|
|
||||||
examples: ['meticulous', 'predominantly', 'intricate', 'corroborate', 'paradigm']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'C2',
|
|
||||||
label: 'C2 - 精通',
|
|
||||||
description: '接近母語水平',
|
|
||||||
examples: ['ubiquitous', 'ephemeral', 'perspicacious', 'multifarious', 'idiosyncratic']
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// 載入用戶已設定的程度
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedLevel = localStorage.getItem('userEnglishLevel');
|
window.location.href = '/profile'
|
||||||
if (savedLevel) {
|
}, [])
|
||||||
setUserLevel(savedLevel);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const saveUserLevel = async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
// 保存到本地存儲
|
|
||||||
localStorage.setItem('userEnglishLevel', userLevel);
|
|
||||||
|
|
||||||
// TODO: 如果用戶已登入,也保存到伺服器
|
|
||||||
// const token = localStorage.getItem('authToken');
|
|
||||||
// if (token) {
|
|
||||||
// await fetch('/api/user/update-level', {
|
|
||||||
// method: 'POST',
|
|
||||||
// headers: {
|
|
||||||
// 'Content-Type': 'application/json',
|
|
||||||
// 'Authorization': `Bearer ${token}`
|
|
||||||
// },
|
|
||||||
// body: JSON.stringify({ englishLevel: userLevel })
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
alert('✅ 程度設定已保存!系統將為您提供個人化的詞彙標記。');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving user level:', error);
|
|
||||||
alert('❌ 保存失敗,請稍後再試');
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHighValueRange = (level: string) => {
|
|
||||||
const ranges = {
|
|
||||||
'A1': 'A2-B1',
|
|
||||||
'A2': 'B1-B2',
|
|
||||||
'B1': 'B2-C1',
|
|
||||||
'B2': 'C1-C2',
|
|
||||||
'C1': 'C2',
|
|
||||||
'C2': 'C2'
|
|
||||||
};
|
|
||||||
return ranges[level as keyof typeof ranges] || 'B1-B2';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
|
||||||
<div className="mb-8">
|
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
|
||||||
<h1 className="text-3xl font-bold mb-4">🎯 英語程度設定</h1>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">正在跳轉到個人檔案頁面...</p>
|
||||||
設定您的英語程度,系統將為您提供個人化的詞彙學習建議。
|
|
||||||
設定後,我們會重點標記比您目前程度高1-2級的詞彙。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="grid gap-4 mb-8">
|
)
|
||||||
{levels.map(level => (
|
|
||||||
<label
|
|
||||||
key={level.value}
|
|
||||||
className={`
|
|
||||||
p-6 border-2 rounded-xl cursor-pointer transition-all hover:shadow-md
|
|
||||||
${userLevel === level.value
|
|
||||||
? 'border-blue-500 bg-blue-50 shadow-md'
|
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="level"
|
|
||||||
value={level.value}
|
|
||||||
checked={userLevel === level.value}
|
|
||||||
onChange={(e) => setUserLevel(e.target.value)}
|
|
||||||
className="sr-only"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-bold text-xl text-gray-800 mb-2">
|
|
||||||
{level.label}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-600 mb-3">
|
|
||||||
{level.description}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{level.examples.map(example => (
|
|
||||||
<span
|
|
||||||
key={example}
|
|
||||||
className="px-3 py-1 bg-gray-100 text-gray-700 rounded-full text-sm"
|
|
||||||
>
|
|
||||||
{example}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{userLevel === level.value && (
|
|
||||||
<div className="ml-4">
|
|
||||||
<div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
|
|
||||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 個人化效果預覽 */}
|
|
||||||
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 p-6 rounded-xl mb-8">
|
|
||||||
<h3 className="font-bold text-lg text-blue-800 mb-3">
|
|
||||||
💡 您的個人化學習效果預覽
|
|
||||||
</h3>
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-blue-700 mb-2">重點學習範圍</h4>
|
|
||||||
<p className="text-blue-600">
|
|
||||||
系統將為您標記 <span className="font-bold">{getHighValueRange(userLevel)}</span> 等級的重點學習詞彙
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-blue-700 mb-2">學習建議</h4>
|
|
||||||
<p className="text-blue-600 text-sm">
|
|
||||||
專注於比您目前程度({userLevel})高1-2級的詞彙,既有挑戰性又不會太困難
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={saveUserLevel}
|
|
||||||
disabled={isLoading}
|
|
||||||
className={`
|
|
||||||
w-full py-4 rounded-xl font-semibold text-lg transition-all
|
|
||||||
${isLoading
|
|
||||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
|
||||||
: 'bg-blue-500 text-white hover:bg-blue-600 shadow-lg hover:shadow-xl'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{isLoading ? '⏳ 保存中...' : '✅ 保存程度設定'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<p className="text-sm text-gray-500">
|
|
||||||
💡 提示: 您隨時可以回到這裡調整程度設定
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
@ -19,8 +19,7 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat
|
||||||
{ href: '/dashboard', label: '儀表板' },
|
{ href: '/dashboard', label: '儀表板' },
|
||||||
{ href: '/flashcards', label: '詞卡' },
|
{ href: '/flashcards', label: '詞卡' },
|
||||||
{ href: '/review', label: '複習' },
|
{ href: '/review', label: '複習' },
|
||||||
{ href: '/generate', label: 'AI 生成' },
|
{ href: '/generate', label: 'AI 生成' }
|
||||||
{ href: '/settings', label: '⚙️ 設定' }
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -82,14 +81,19 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 用戶資訊 - 只在桌面版顯示 */}
|
{/* 用戶資訊 - 只在桌面版顯示 */}
|
||||||
<div className="hidden md:flex items-center space-x-2">
|
<div className="hidden md:flex items-center space-x-3">
|
||||||
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
className="flex items-center space-x-2 hover:bg-gray-100 rounded-lg px-2 py-1 transition-colors"
|
||||||
|
>
|
||||||
<div className="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-white font-semibold">
|
<div className="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-white font-semibold">
|
||||||
{user?.username?.[0]?.toUpperCase() || 'U'}
|
{user?.username?.[0]?.toUpperCase() || 'U'}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">{user?.displayName || user?.username}</span>
|
<span className="text-sm font-medium text-gray-700">{user?.displayName || user?.username}</span>
|
||||||
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="ml-4 text-sm text-gray-600 hover:text-gray-900"
|
className="text-sm text-gray-600 hover:text-gray-900 px-2 py-1 hover:bg-gray-100 rounded"
|
||||||
>
|
>
|
||||||
登出
|
登出
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -120,12 +124,16 @@ export function Navigation({ showExitLearning = false, onExitLearning }: Navigat
|
||||||
|
|
||||||
{/* 手機版用戶區域 */}
|
{/* 手機版用戶區域 */}
|
||||||
<div className="pt-4 border-t border-gray-200 mt-4">
|
<div className="pt-4 border-t border-gray-200 mt-4">
|
||||||
<div className="flex items-center space-x-3 px-3 py-2">
|
<Link
|
||||||
|
href="/profile"
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
className="flex items-center space-x-3 px-3 py-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
<div className="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-white font-semibold">
|
<div className="w-8 h-8 bg-primary rounded-full flex items-center justify-center text-white font-semibold">
|
||||||
{user?.username?.[0]?.toUpperCase() || 'U'}
|
{user?.username?.[0]?.toUpperCase() || 'U'}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-sm font-medium">{user?.displayName || user?.username}</span>
|
<span className="text-sm font-medium">👤 個人檔案</span>
|
||||||
</div>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logout()
|
logout()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue