feat(web): add settings sub-pages and connect to backend APIs
- Add GeneralSettingsPage for language, privacy, and notification settings - Add FeedbackPage for user feedback submission with image upload - Connect settings to backend PATCH /users/me/settings API - Implement language preference sync between frontend and backend - Update login flow to pass language preference and redirect based on user settings - Add Astro entry pages for /settings/general and /settings/feedback routes - Update sidebar navigation: language button links to general settings - Fix account data card to link to profile page - Remove "deletion" text from account data description
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { logout } from '../lib/auth';
|
||||
import { getUserProfile, getPointsBalance, type UserProfile, type PointsBalance } from '../lib/api';
|
||||
|
||||
interface Props {
|
||||
locale: string;
|
||||
@@ -7,6 +9,26 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function SettingsPage({ locale, settings: s }: Props) {
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [points, setPoints] = useState<PointsBalance | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Promise.all([
|
||||
getUserProfile(),
|
||||
getPointsBalance(),
|
||||
])
|
||||
.then(([profileData, pointsData]) => {
|
||||
setProfile(profileData);
|
||||
setPoints(pointsData);
|
||||
})
|
||||
.catch(() => {
|
||||
// ignore errors
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
if (confirm(s.logoutConfirm)) {
|
||||
logout().finally(() => {
|
||||
@@ -15,62 +37,183 @@ export default function SettingsPage({ locale, settings: s }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
const displayName = profile?.display_name || profile?.email?.split('@')[0] || 'User';
|
||||
const email = profile?.email || 'user@example.com';
|
||||
const bio = profile?.bio || '';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 min-h-full">
|
||||
<h1 className="text-slate-900 text-xl font-bold">{s.title}</h1>
|
||||
{/* 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>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-6 flex-1 min-h-0">
|
||||
{/* Left column */}
|
||||
<div className="w-full lg:w-[360px] flex flex-col gap-4 shrink-0">
|
||||
{/* Profile summary */}
|
||||
{/* 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">
|
||||
<h3 className="text-slate-900 text-sm font-bold">{s.profileTitle}</h3>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-violet-600 flex items-center justify-center text-white text-sm font-bold">U</div>
|
||||
<div>
|
||||
<p className="text-slate-900 text-sm font-medium">User</p>
|
||||
<p className="text-slate-400 text-xs">user@example.com</p>
|
||||
<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[0].toUpperCase()}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Name & Email */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-slate-900 text-lg font-bold truncate">{displayName}</p>
|
||||
<p className="text-slate-500 text-xs truncate">{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 */}
|
||||
<div className="bg-white rounded-2xl p-5 border border-slate-200 flex items-center gap-4">
|
||||
<span className="material-symbols-rounded text-violet-600 text-2xl">account_balance_wallet</span>
|
||||
<div>
|
||||
<p className="text-slate-400 text-xs">{s.pointsTitle}</p>
|
||||
<p className="text-slate-900 text-lg font-bold">210 <span className="text-sm font-normal text-slate-400">{s.pointsBalance}</span></p>
|
||||
|
||||
{/* 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>
|
||||
</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 */}
|
||||
{/* Right Column */}
|
||||
<div className="flex-1 flex flex-col gap-4">
|
||||
{/* Account settings */}
|
||||
{/* 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-base font-bold">{s.accountTitle}</h3>
|
||||
<a href={`/${locale}/profile`} className="flex items-center justify-between py-2 hover:bg-slate-50 rounded-lg px-1 transition-colors">
|
||||
<span className="text-slate-600 text-sm">{s.changeAvatar}</span>
|
||||
<span className="material-symbols-rounded text-slate-400 text-lg">chevron_right</span>
|
||||
</a>
|
||||
<a href={`/${locale}/profile`} className="flex items-center justify-between py-2 hover:bg-slate-50 rounded-lg px-1 transition-colors">
|
||||
<span className="text-slate-600 text-sm">{s.changeName}</span>
|
||||
<span className="material-symbols-rounded text-slate-400 text-lg">chevron_right</span>
|
||||
</a>
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-slate-600 text-sm">{s.changeLanguage}</span>
|
||||
<span className="text-slate-400 text-sm">简体中文</span>
|
||||
<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 */}
|
||||
{/* 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-base font-bold">{s.legalTitle}</h3>
|
||||
<a href={`/${locale}/privacy`} className="text-slate-600 text-sm hover:text-violet-600">{s.privacy}</a>
|
||||
<a href={`/${locale}/terms`} className="text-slate-600 text-sm hover:text-violet-600">{s.terms}</a>
|
||||
<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 onClick={handleLogout} 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">
|
||||
{/* Logout Button */}
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
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"
|
||||
>
|
||||
<span className="text-red-500 text-sm font-medium">{s.logout}</span>
|
||||
<span className="material-symbols-rounded text-red-400 text-lg">logout</span>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user