stable ui

This commit is contained in:
2026-06-11 19:45:03 +03:30
parent b2a79df338
commit 3ce26d4d1a
21 changed files with 6368 additions and 114 deletions
+258
View File
@@ -0,0 +1,258 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Header from '@/components/Header';
import { motion, AnimatePresence } from 'framer-motion';
import { Fingerprint, Lock, User, Save, Camera } from 'lucide-react';
import { startRegistration } from '@simplewebauthn/browser';
export default function SettingsPage() {
const [user, setUser] = useState(null);
const [name, setName] = useState('');
const [password, setPassword] = useState('');
const [avatarBase64, setAvatarBase64] = useState(null);
const [biometricEnabled, setBiometricEnabled] = useState(false);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [isError, setIsError] = useState(false);
const fileInputRef = useRef(null);
useEffect(() => {
const userData = localStorage.getItem('user');
if (userData) {
const parsedUser = JSON.parse(userData);
setUser(parsedUser);
setName(parsedUser.name || '');
}
const bio = localStorage.getItem('biometric_enabled');
if (bio === 'true') setBiometricEnabled(true);
}, []);
const showToast = (msg, error = false) => {
setMessage(msg);
setIsError(error);
setTimeout(() => setMessage(''), 4000);
};
const handleBiometricToggle = async () => {
if (biometricEnabled) {
setBiometricEnabled(false);
localStorage.setItem('biometric_enabled', 'false');
showToast('ورود با اثر انگشت غیرفعال شد');
return;
}
try {
showToast('در حال آماده‌سازی سنسور...', false);
const res = await fetch('/api/auth/webauthn/register/generate-options', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
});
const options = await res.json();
if (options.error) throw new Error(options.error);
// Trigger native biometric prompt
const attResp = await startRegistration(options);
const verificationRes = await fetch('/api/auth/webauthn/register/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify(attResp),
});
const verification = await verificationRes.json();
if (verification.verified) {
setBiometricEnabled(true);
localStorage.setItem('biometric_enabled', 'true');
showToast('اثر انگشت با موفقیت فعال شد');
} else {
throw new Error('Verification failed');
}
} catch (error) {
console.error(error);
showToast('خطا در فعال‌سازی یا لغو توسط کاربر', true);
}
};
const handleFileChange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => setAvatarBase64(reader.result);
reader.readAsDataURL(file);
}
};
const handleSave = async () => {
setLoading(true);
try {
const res = await fetch('/api/user/settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
name,
password: password ? password : undefined,
avatarUrl: avatarBase64 ? avatarBase64 : undefined
})
});
const data = await res.json();
if (res.ok) {
localStorage.setItem('user', JSON.stringify(data.user));
setUser(data.user);
setPassword('');
showToast('تغییرات شما با موفقیت ذخیره شد.');
} else {
showToast(data.error || 'خطا در ذخیره تغییرات', true);
}
} catch (err) {
showToast('خطا در ارتباط با سرور', true);
} finally {
setLoading(false);
}
};
const container = {
hidden: { opacity: 0 },
show: { opacity: 1, transition: { staggerChildren: 0.1 } }
};
const item = { hidden: { opacity: 0, y: 10 }, show: { opacity: 1, y: 0 } };
return (
<div className="w-full min-h-screen bg-[#F8FAFC] flex flex-col pb-20 relative">
<Header title="تنظیمات حساب" showBack={true} />
{/* Global Toast Notification */}
<AnimatePresence>
{message && (
<motion.div
initial={{ opacity: 0, y: 50, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.9 }}
className={`fixed bottom-8 left-1/2 -translate-x-1/2 z-[100] px-5 py-3 rounded-full shadow-[0_10px_40px_rgb(0,0,0,0.1)] backdrop-blur-xl border text-[11px] font-extrabold whitespace-nowrap flex items-center justify-center ${isError ? 'bg-red-500/95 border-red-400 text-white shadow-red-500/30' : 'bg-gray-800/95 border-gray-700 text-white shadow-gray-900/30'}`}
>
{message}
</motion.div>
)}
</AnimatePresence>
<motion.div variants={container} initial="hidden" animate="show" className="px-6 pt-2 flex flex-col gap-8">
<motion.section variants={item} className="flex flex-col gap-5 items-center">
<div className="relative group mt-2">
<div className="w-28 h-28 rounded-full shadow-[0_10px_40px_rgb(0,0,0,0.08)] overflow-hidden border-[4px] border-white bg-gradient-to-tr from-indigo-50 to-purple-50">
<img src={avatarBase64 || user?.avatarUrl || "/images/default_avatar.png"} alt="avatar" className="w-full h-full object-cover" />
</div>
<button
onClick={() => fileInputRef.current?.click()}
className="absolute bottom-0 right-0 w-10 h-10 bg-white border border-gray-100 text-indigo-500 rounded-full shadow-lg flex items-center justify-center hover:scale-105 active:scale-95 transition-all"
>
<Camera size={18} strokeWidth={2.5} />
</button>
<input type="file" ref={fileInputRef} onChange={handleFileChange} accept="image/*" className="hidden" />
</div>
<div className="text-center">
<h3 className="text-lg font-black text-gray-800">{user?.name || 'کاربر'}</h3>
<p className="text-xs text-gray-400 font-medium mt-1 uppercase tracking-wider">{user?.role === 'ADMIN' ? 'مدیریت کل' : 'کاربر عادی'}</p>
</div>
</motion.section>
<motion.section variants={item} className="flex flex-col gap-4">
<h2 className="text-[11px] font-extrabold text-gray-400 mr-2 uppercase tracking-widest">مشخصات کاربری</h2>
<div className="bg-white/90 backdrop-blur-3xl border border-gray-100/50 rounded-[28px] shadow-[0_8px_30px_rgb(0,0,0,0.04)] p-2">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-4 bg-gray-50/50 hover:bg-gray-50 p-4 rounded-[20px] transition-colors">
<div className="w-10 h-10 bg-white text-indigo-500 rounded-[14px] flex items-center justify-center shadow-sm">
<User size={18} strokeWidth={2.5} />
</div>
<div className="flex flex-col flex-1">
<label className="text-[10px] font-bold text-gray-400 mb-0.5">نام و نام خانوادگی</label>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
className="bg-transparent border-none text-sm font-bold text-gray-800 outline-none w-full"
placeholder="نام شما"
/>
</div>
</div>
<div className="flex items-center gap-4 bg-gray-50/50 hover:bg-gray-50 p-4 rounded-[20px] transition-colors">
<div className="w-10 h-10 bg-white text-rose-500 rounded-[14px] flex items-center justify-center shadow-sm">
<Lock size={18} strokeWidth={2.5} />
</div>
<div className="flex flex-col flex-1">
<label className="text-[10px] font-bold text-gray-400 mb-0.5">رمز عبور جدید (اختیاری)</label>
<input
type="password"
dir="ltr"
value={password}
onChange={e => setPassword(e.target.value)}
className="bg-transparent border-none text-sm font-bold text-gray-800 outline-none w-full placeholder-gray-300"
placeholder="••••••••"
/>
</div>
</div>
</div>
</div>
</motion.section>
<motion.section variants={item} className="flex flex-col gap-4">
<h2 className="text-[11px] font-extrabold text-gray-400 mr-2 uppercase tracking-widest">امنیت</h2>
<div className="bg-white/90 backdrop-blur-3xl border border-gray-100/50 rounded-[28px] shadow-[0_8px_30px_rgb(0,0,0,0.04)] p-2">
<div className="flex items-center justify-between p-4 hover:bg-gray-50 rounded-[20px] transition-colors">
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-[14px] flex items-center justify-center transition-colors shadow-sm ${biometricEnabled ? 'bg-green-500 text-white shadow-green-500/20' : 'bg-gray-100 text-gray-400'}`}>
<Fingerprint size={18} strokeWidth={2.5} />
</div>
<div className="flex flex-col">
<span className="text-sm font-bold text-gray-800">ورود با اثر انگشت</span>
<span className="text-[10px] text-gray-400 font-medium mt-0.5">Face ID / Touch ID</span>
</div>
</div>
<button
onClick={handleBiometricToggle}
className={`relative flex items-center w-12 h-6 rounded-full px-1 transition-colors duration-300 ease-in-out shrink-0 ${biometricEnabled ? 'bg-green-500' : 'bg-gray-200'}`}
>
<motion.div
className="w-4 h-4 bg-white rounded-full shadow-sm"
animate={{ x: biometricEnabled ? -24 : 0 }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
</button>
</div>
</div>
</motion.section>
<motion.div variants={item} className="mt-4">
<button
onClick={handleSave}
disabled={loading}
className="w-full py-4 bg-gradient-to-r from-gray-900 to-gray-800 hover:from-black hover:to-gray-900 text-white text-sm font-extrabold rounded-[20px] transition-all shadow-xl shadow-gray-900/20 disabled:opacity-70 flex items-center justify-center gap-2"
>
{loading ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
) : (
<>
<Save size={18} strokeWidth={2.5} />
ذخیره اطلاعات
</>
)}
</button>
</motion.div>
</motion.div>
</div>
);
}