Files
eryao/web/src/components/SettingsPage.tsx
T
zl-q 2b8984edbc fix(web): validate question input before divination and fix logout flow
- Add validation for question text in ManualDivinationPage and AutoDivinationPage
- Fix logout flow to call backend API before clearing local auth
- Show error message when question is empty before starting interpretation
2026-05-11 20:07:36 +08:00

224 lines
11 KiB
TypeScript

import { useState } from 'react';
import { logout, getAuth, clearAuth, redirectToLogin } from '../lib/auth';
import { usePoints, useProfile } from '../lib/resources';
interface Props {
locale: string;
dashboard: { brandName: string; navHome: string; navStore: string; navDivination: string; navManual: string; navAuto: string; navHistory: string; navLanguage: string; navSettings: string; logout: string };
settings: { title: string; profileTitle: string; emailLabel: string; nameLabel: string; joinedLabel: string; pointsTitle: string; pointsBalance: string; accountTitle: string; changeName: string; changeAvatar: string; changeLanguage: string; legalTitle: string; privacy: string; terms: string; logout: string; logoutConfirm: string };
}
export default function SettingsPage({ locale, settings: s }: Props) {
const profileState = useProfile();
const pointsState = usePoints();
const profile = profileState.data ?? null;
const points = pointsState.data ?? null;
const loading = profileState.loading || pointsState.loading;
const [logoutLoading, setLogoutLoading] = useState(false);
const handleLogout = async () => {
if (logoutLoading) return;
if (!confirm(s.logoutConfirm)) return;
setLogoutLoading(true);
try {
// Call backend logout first (needs auth)
await logout();
} catch {
// Ignore logout API errors
}
// Clear local auth and redirect
clearAuth();
redirectToLogin();
};
const authEmail = getAuth()?.user?.email;
const displayName = loading ? '' : (profile?.display_name || authEmail?.split('@')[0] || '');
const email = loading ? '' : (authEmail || '');
const bio = profile?.bio || '';
return (
<div className="flex flex-col gap-6 min-h-full">
{/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="flex flex-col gap-1.5">
<h1 className="text-slate-900 text-2xl font-bold">{s.title}</h1>
<p className="text-slate-500 text-sm">
{locale === 'en' ? 'Manage account, preferences, and policies' : '管理账号资料、偏好与协议信息'}
</p>
</div>
<div className="flex items-center gap-2 px-3.5 py-2.5 bg-white rounded-full border border-slate-200">
<span className="material-symbols-rounded text-violet-600 text-lg">verified_user</span>
<span className="text-slate-700 text-sm font-medium">
{locale === 'en' ? 'Account OK' : '账号正常'}
</span>
</div>
</div>
{/* Main Content */}
<div className="flex flex-col xl:flex-row gap-6 flex-1 min-h-0">
{/* Left Column */}
<div className="w-full xl:w-[360px] flex flex-col gap-4 shrink-0">
{/* Profile Summary Card */}
<div className="bg-white rounded-2xl p-[18px] border border-slate-200 flex flex-col gap-3">
<div className="flex items-start gap-3">
{/* Avatar */}
{profile?.avatar_url ? (
<img src={profile.avatar_url} alt={displayName} className="w-14 h-14 rounded-[28px] object-cover" />
) : (
<div className="w-14 h-14 rounded-[28px] bg-violet-50 flex items-center justify-center">
<span className="text-violet-600 text-xl font-bold">{displayName ? displayName[0].toUpperCase() : '?'}</span>
</div>
)}
{/* Name & Email */}
<div className="flex-1 min-w-0">
<p className="text-slate-900 text-lg font-bold truncate">{loading ? '...' : (displayName || '-')}</p>
<p className="text-slate-500 text-xs truncate">{loading ? '...' : (email || '-')}</p>
</div>
{/* Edit Profile Button */}
<a
href={`/${locale}/profile`}
className="w-8 h-8 rounded-2xl bg-slate-50 border border-slate-200 flex items-center justify-center hover:bg-slate-100 transition-colors shrink-0"
title={s.changeName}
>
<span className="material-symbols-rounded text-violet-600 text-[17px]">edit</span>
</a>
</div>
{/* Bio */}
{bio && (
<p className="text-slate-500 text-[13px] leading-relaxed">{bio}</p>
)}
</div>
{/* Points Card */}
<a
href={`/${locale}/store`}
className="bg-white rounded-2xl p-5 border border-slate-200 flex items-center gap-3.5 hover:shadow-sm transition-shadow"
>
{/* Points Icon */}
<div className="w-11 h-11 rounded-xl bg-violet-50 flex items-center justify-center shrink-0">
<span className="material-symbols-rounded text-violet-600 text-[26px]">paid</span>
</div>
{/* Points Info */}
<div className="flex-1 min-w-0">
<p className="text-slate-400 text-[13px]">{s.pointsTitle}</p>
<p className="text-slate-900 text-xl font-bold">
{loading ? '...' : points?.balance ?? 0}
<span className="text-sm font-normal text-slate-400 ml-1">{s.pointsBalance}</span>
</p>
</div>
{/* Arrow */}
<span className="material-symbols-rounded text-violet-600 text-[22px] shrink-0">chevron_right</span>
</a>
</div>
{/* Right Column */}
<div className="flex-1 flex flex-col gap-4">
{/* Account Settings Panel */}
<div className="bg-white rounded-2xl p-6 border border-slate-200 flex flex-col gap-4">
<h3 className="text-slate-900 text-lg font-bold">{s.accountTitle}</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3.5">
{/* General Settings */}
<a
href={`/${locale}/settings/general`}
className="bg-slate-50 rounded-xl p-4 flex flex-col gap-2.5 hover:bg-slate-100 transition-colors cursor-pointer"
>
<span className="material-symbols-rounded text-violet-600 text-xl">tune</span>
<div>
<p className="text-slate-900 text-[15px] font-bold">
{locale === 'en' ? 'General' : '通用设置'}
</p>
<p className="text-slate-400 text-xs">
{locale === 'en' ? 'Language, notifications' : '语言、通知'}
</p>
</div>
</a>
{/* Feedback */}
<a
href={`/${locale}/settings/feedback`}
className="bg-slate-50 rounded-xl p-4 flex flex-col gap-2.5 hover:bg-slate-100 transition-colors cursor-pointer"
>
<span className="material-symbols-rounded text-violet-600 text-xl">feedback</span>
<div>
<p className="text-slate-900 text-[15px] font-bold">
{locale === 'en' ? 'Feedback' : '意见反馈'}
</p>
<p className="text-slate-400 text-xs">
{locale === 'en' ? 'Submit suggestions' : '提交问题与建议'}
</p>
</div>
</a>
{/* Account Data */}
<a
href={`/${locale}/profile`}
className="bg-slate-50 rounded-xl p-4 flex flex-col gap-2.5 hover:bg-slate-100 transition-colors cursor-pointer"
>
<span className="material-symbols-rounded text-violet-600 text-xl">person</span>
<div>
<p className="text-slate-900 text-[15px] font-bold">
{locale === 'en' ? 'Account Data' : '账号数据'}
</p>
<p className="text-slate-400 text-xs">
{locale === 'en' ? 'Profile information' : '账号信息'}
</p>
</div>
</a>
</div>
</div>
{/* Legal Panel */}
<div className="bg-white rounded-2xl p-6 border border-slate-200 flex flex-col gap-4">
<h3 className="text-slate-900 text-lg font-bold">{s.legalTitle}</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3.5">
{/* About */}
<a href={`/${locale}/about`} className="bg-slate-50 rounded-xl p-4 flex flex-col gap-2.5 hover:bg-slate-100 transition-colors">
<span className="material-symbols-rounded text-violet-600 text-xl">info</span>
<div>
<p className="text-slate-900 text-[15px] font-bold">
{locale === 'en' ? 'About' : '关于我们'}
</p>
<p className="text-slate-400 text-xs">
{locale === 'en' ? 'Product vision' : '产品理念与定位'}
</p>
</div>
</a>
{/* Privacy */}
<a href={`/${locale}/privacy`} className="bg-slate-50 rounded-xl p-4 flex flex-col gap-2.5 hover:bg-slate-100 transition-colors">
<span className="material-symbols-rounded text-violet-600 text-xl">security</span>
<div>
<p className="text-slate-900 text-[15px] font-bold">{s.privacy}</p>
<p className="text-slate-400 text-xs">
{locale === 'en' ? 'Privacy policy' : '隐私保护说明'}
</p>
</div>
</a>
{/* Terms */}
<a href={`/${locale}/terms`} className="bg-slate-50 rounded-xl p-4 flex flex-col gap-2.5 hover:bg-slate-100 transition-colors">
<span className="material-symbols-rounded text-violet-600 text-xl">description</span>
<div>
<p className="text-slate-900 text-[15px] font-bold">{s.terms}</p>
<p className="text-slate-400 text-xs">
{locale === 'en' ? 'User agreement' : '用户服务协议'}
</p>
</div>
</a>
</div>
</div>
{/* Logout Button */}
<button
onClick={handleLogout}
disabled={logoutLoading}
className={`bg-white rounded-2xl px-5 py-3.5 border border-red-200 flex items-center justify-between hover:bg-red-50 transition-colors ${logoutLoading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<span className="text-red-500 text-sm font-medium">
{logoutLoading ? (locale === 'en' ? 'Logging out...' : '退出中...') : s.logout}
</span>
<span className="material-symbols-rounded text-red-400 text-lg">logout</span>
</button>
</div>
</div>
</div>
);
}