// teknav-auth.jsx — Login / Signup page import { useState, useEffect } from 'react'; import { useAuth, useNav, useToast } from './teknav-ui.jsx'; import { ApiError } from './src/lib/api.js'; import { api } from './src/lib/api.js'; import TeknavCAP from './src/lib/TeknavCAP.jsx'; // ── Hooks ───────────────────────────────────────────────────────────────────── function useTimeGreeting() { const h = new Date().getHours(); if (h < 5) return 'شب بخیر'; if (h < 12) return 'صبح بخیر'; if (h < 17) return 'روز بخیر'; if (h < 21) return 'عصر بخیر'; return 'شب بخیر'; } function useCountUp(target, delayMs = 0) { const [count, setCount] = useState(0); useEffect(() => { const t = setTimeout(() => { const steps = 36; let step = 0; const iv = setInterval(() => { step++; const ease = 1 - Math.pow(1 - step / steps, 3); setCount(Math.floor(ease * target)); if (step >= steps) { setCount(target); clearInterval(iv); } }, 1600 / steps); return () => clearInterval(iv); }, delayMs); return () => clearTimeout(t); }, [target, delayMs]); return count; } // ── Page-scoped CSS ─────────────────────────────────────────────────────────── function AuthStyles() { return ( ); } // ── Icons ───────────────────────────────────────────────────────────────────── function EyeIcon({ open }) { return open ? ( ) : ( ); } function GoogleIcon() { return ( ); } function GithubIcon() { return ( ); } // ── Brand Panel ─────────────────────────────────────────────────────────────── const TICKER_TOPICS = [ 'هوش مصنوعی و آینده توسعه نرم‌افزار', 'امنیت سایبری در دوران پس از کوانتوم', 'پردازش ابری بومی‌سازی‌شده در ایران', 'متن‌باز در اکوسیستم فارسی‌زبان', 'زیرساخت‌های توزیع‌شده مقیاس‌پذیر', 'یادگیری عمیق با مجموعه‌داده‌های فارسی', 'معماری میکروسرویس در محیط تولید', 'مدیریت داده در مقیاس پتابایت', ]; const BRAND_WORDS = [ { text: 'هوش مصنوعی', s: 12, x: 6, y: 12, spd: 3.2 }, { text: 'ابر', s: 10, x: 72, y: 22, spd: 4.1 }, { text: 'امنیت', s: 11, x: 18, y: 74, spd: 3.6 }, { text: 'متن‌باز', s: 10, x: 62, y: 84, spd: 4.5 }, { text: 'پردازش', s: 11, x: 4, y: 48, spd: 3.9 }, { text: 'شبکه', s: 10, x: 76, y: 58, spd: 3.3 }, { text: 'داده', s: 12, x: 38, y: 8, spd: 4.8 }, { text: 'زیرساخت', s: 10, x: 48, y: 91, spd: 3.7 }, ]; function BrandTicker() { const doubled = [...TICKER_TOPICS, ...TICKER_TOPICS]; return (
{doubled.map((item, i) => (
◉ {item}
))}
); } function BrandPanel() { const [mouse, setMouse] = useState({ x: 0, y: 0 }); const articles = useCountUp(23, 700); const readers = useCountUp(8400, 1000); const series = useCountUp(6, 1300); useEffect(() => { const h = (e) => setMouse({ x: e.clientX / window.innerWidth - 0.5, y: e.clientY / window.innerHeight - 0.5, }); window.addEventListener('mousemove', h, { passive: true }); return () => window.removeEventListener('mousemove', h); }, []); return (
{/* Giant watermark letter */}
ت
{/* Dot grid */}
{/* Parallax glow — follows cursor */}
{/* Terracotta accent glow */}
{/* Floating word bubbles — subtle parallax */}
{BRAND_WORDS.map((w, i) => (
{w.text}
))}
{/* ── Content ── */}
{/* Orbital logo */}
{/* Outer dashed ring */}
{/* Middle ring + orbiting dot */}
{/* Inner ring + dot */}
{/* Logo square */}
ت
{/* Wordmark */}
تکناو
TEKNAV · IR
{/* Animated divider */}
{/* Tagline */}

آخرین دانش فناوری،{' '} اول به فارسی

{/* Live stats — count-up */}
{[ { n: articles, label: 'مقاله', delay: '0s', suffix: '+' }, { n: readers, label: 'خواننده', delay: '0.18s', suffix: '' }, { n: series, label: 'سری', delay: '0.34s', suffix: '' }, ].map(({ n, label, delay, suffix }) => (
{n.toLocaleString('fa-IR')}{suffix}
{label}
))}
{/* Divider + ticker */}
موضوعات داغ
{/* Online pulse indicator */}
در حال حاضر آنلاین
); } // ── AuthInput ───────────────────────────────────────────────────────────────── function AuthInput({ label, hint, error, suffix, inputProps = {} }) { const { style: inputStyle, ...rest } = inputProps; return (
{label && ( )}
{suffix && (
{suffix}
)}
{hint && !error &&
{hint}
} {error &&
{error}
}
); } // ── Password strength ───────────────────────────────────────────────────────── function PasswordStrength({ pw }) { const checks = [ { label: '۸+ کاراکتر', ok: pw.length >= 8 }, { label: 'حرف کوچک', ok: /[a-z]/.test(pw) }, { label: 'حرف بزرگ', ok: /[A-Z]/.test(pw) }, { label: 'عدد', ok: /[0-9]/.test(pw) }, { label: 'نماد', ok: /[^a-zA-Z0-9]/.test(pw) }, ]; const score = checks.filter(c => c.ok).length; const barColor = score <= 2 ? '#C94C4C' : score === 3 ? '#D08A22' : '#2F8F6B'; return (
{[1,2,3,4,5].map(i => (
))}
{checks.map(({ label, ok }) => ( {ok ? '✓' : '·'} {label} ))}
); } // ── Forgot password modal ───────────────────────────────────────────────────── function ForgotPasswordModal({ onClose }) { const toast = useToast(); const [email, setEmail] = useState(''); const [sent, setSent] = useState(false); const [loading, setLoading] = useState(false); const send = async (e) => { e.preventDefault(); if (!email.trim()) { toast('ایمیل را وارد کنید', 'error'); return; } setLoading(true); try { await api.post('/api/auth/forgot-password', { email: email.trim().toLowerCase() }); setSent(true); } catch { toast('خطا در ارسال ایمیل. لطفاً دوباره تلاش کنید', 'error'); } finally { setLoading(false); } }; return (

بازیابی رمز عبور

{sent ? 'اگر این ایمیل در سیستم ثبت شده باشد، لینک بازنشانی برای شما ارسال خواهد شد.' : 'آدرس ایمیل ثبت‌شده خود را وارد کنید.'}

{sent ? ( ) : (
setEmail(e.target.value), placeholder: 'you@example.com', autoComplete: 'email', style: { direction: 'ltr', textAlign: 'left' }, }} />
)}
); } // ── Login form ──────────────────────────────────────────────────────────────── function LoginForm({ onSuccess }) { const { login } = useAuth(); const toast = useToast(); const [identifier, setIdentifier] = useState(''); const [password, setPassword] = useState(''); const [captcha, setCaptcha] = useState({ captchaId: '', userSolution: '' }); const [loading, setLoading] = useState(false); const [showPass, setShowPass] = useState(false); const [showForgot, setShowForgot] = useState(false); const handleSubmit = async (e) => { e.preventDefault(); if (!identifier || !password) { toast('نام کاربری/ایمیل و رمز عبور الزامی است', 'error'); return; } if (!captcha.captchaId || !captcha.userSolution.trim()) { toast('کد امنیتی را وارد کنید', 'error'); return; } setLoading(true); try { const user = await login(identifier, password, captcha.captchaId, captcha.userSolution); if (user) onSuccess(user); else toast('ایمیل یا رمز عبور نادرست است', 'error'); } catch (err) { if (err instanceof ApiError) { if (err.status === 429) toast('تعداد تلاش‌های ناموفق از حد مجاز گذشت. چند دقیقه صبر کنید', 'error'); else if (err.status === 401) toast('نام کاربری، ایمیل یا رمز عبور نادرست است', 'error'); else if (err.status === 403) toast('این حساب غیرفعال است', 'error'); else if (err.status === 400) toast('کد امنیتی نامعتبر است — کد جدید دریافت کنید', 'error'); else toast('خطا در اتصال به سرور', 'error'); } else toast('خطای غیرمنتظره', 'error'); } finally { setLoading(false); } }; return (
setIdentifier(e.target.value), placeholder: '@username یا you@example.com', autoComplete: 'username', style: { direction: 'ltr', textAlign: 'left' }, }} />
setPassword(e.target.value), placeholder: '••••••••', autoComplete: 'current-password', style: { direction: 'ltr', textAlign: 'left' }, }} suffix={ } />
{showForgot && setShowForgot(false)} />} ); } // ── Signup form ─────────────────────────────────────────────────────────────── function SignupForm({ onSuccess }) { const { signup } = useAuth(); const toast = useToast(); const [name, setName] = useState(''); const [username, setUsername] = useState(''); const [identifier, setIdentifier] = useState(''); const [identifierType, setIdentifierType] = useState('email'); const [password, setPassword] = useState(''); const [confirmPw, setConfirmPw] = useState(''); const [captcha, setCaptcha] = useState({ captchaId: '', userSolution: '' }); const [loading, setLoading] = useState(false); const [showPass, setShowPass] = useState(false); const isPhone = identifierType === 'phone'; const pwMatch = confirmPw && password === confirmPw; const pwMismatch = confirmPw && password !== confirmPw; const handleSubmit = async (e) => { e.preventDefault(); if (!name.trim()) { toast('نام الزامی است', 'error'); return; } if (!/^[a-z0-9_][a-z0-9_.]{1,28}[a-z0-9_]$/.test(username.trim().toLowerCase().replace(/^@/, ''))) { toast('نام کاربری معتبر وارد کنید', 'error'); return; } if (!identifier.trim()) { toast(isPhone ? 'شماره موبایل الزامی است' : 'ایمیل الزامی است', 'error'); return; } if (password.length < 8) { toast('رمز عبور باید حداقل ۸ کاراکتر باشد', 'error'); return; } if (password !== confirmPw) { toast('رمزهای عبور تطابق ندارند', 'error'); return; } if (!captcha.captchaId || !captcha.userSolution.trim()) { toast('کد امنیتی را وارد کنید', 'error'); return; } setLoading(true); try { const payload = { name: name.trim(), username: username.trim().toLowerCase().replace(/^@/, ''), password, captchaId: captcha.captchaId, userSolution: captcha.userSolution, ...(isPhone ? { phone: identifier.trim() } : { email: identifier.trim().toLowerCase() }), }; const user = await signup(payload); if (user) onSuccess(user); } catch (err) { if (err instanceof ApiError) { if (err.status === 409) { const msg = err.body?.error; toast(msg === 'username_taken' ? 'این نام کاربری قبلاً گرفته شده است' : isPhone ? 'این شماره قبلاً ثبت شده است' : 'این ایمیل قبلاً ثبت شده است', 'error'); } else if (err.status === 429) { toast('تعداد ثبت‌نام از این آی‌پی زیاد است. کمی صبر کنید', 'error'); } else if (err.status === 400) { toast(err.body?.error === 'invalid_captcha' ? 'کد امنیتی نامعتبر است' : 'اطلاعات وارد‌شده معتبر نیست', 'error'); } else toast('خطا در اتصال به سرور', 'error'); } else toast('خطای غیرمنتظره', 'error'); } finally { setLoading(false); } }; return (
setName(e.target.value), placeholder: 'مثال: علی رضایی', autoComplete: 'name', }} /> setUsername(e.target.value.toLowerCase().replace(/^@/, '')), placeholder: 'ali_rezaei', autoComplete: 'username', style: { direction: 'ltr', textAlign: 'left' }, }} />
{[{ key: 'email', label: 'ایمیل' }, { key: 'phone', label: 'موبایل' }].map(t => ( ))}
setIdentifier(e.target.value), placeholder: isPhone ? '09123456789' : 'you@example.com', autoComplete: isPhone ? 'tel' : 'email', style: { direction: 'ltr', textAlign: 'left' }, }} />
setPassword(e.target.value), placeholder: '••••••••', autoComplete: 'new-password', style: { direction: 'ltr', textAlign: 'left' }, }} suffix={ } /> {password && }
) : null} inputProps={{ type: 'password', value: confirmPw, onChange: e => setConfirmPw(e.target.value), placeholder: '••••••••', autoComplete: 'new-password', style: { direction: 'ltr', textAlign: 'left' }, }} />

با ثبت‌نام،{' '} شرایط استفاده {' '}و{' '} سیاست حریم خصوصی {' '}تکناو را می‌پذیرید.

); } // ── OAuth buttons ───────────────────────────────────────────────────────────── function OAuthButtons({ oauthError }) { const { startOAuth } = useAuth(); const [providers, setProviders] = useState([]); useEffect(() => { api.get('/api/auth/oauth/providers') .then(d => setProviders(d?.providers ?? [])) .catch(() => {}); }, []); if (providers.length === 0 && !oauthError) return null; return (
{oauthError && (
{oauthError === 'account_suspended' ? 'این حساب معلق شده است' : 'ورود با این روش ممکن نبود. دوباره تلاش کنید'}
)} {providers.length > 0 && ( <>
{providers.includes('google') && ( )} {providers.includes('github') && ( )}
یا با ایمیل
)}
); } // ── Main LoginPage ──────────────────────────────────────────────────────────── function LoginPage() { const { navigate } = useNav(); const toast = useToast(); const { user, refreshMe } = useAuth(); const [tab, setTab] = useState('login'); const greeting = useTimeGreeting(); const hashParams = new URLSearchParams(window.location.hash.split('?')[1] || ''); const oauthError = hashParams.get('error') || null; const emailVerifyToken = new URLSearchParams(window.location.search).get('verifyEmail'); useEffect(() => { if (!emailVerifyToken || !user) return; api.post('/api/auth/email/verify/confirm', { token: emailVerifyToken }) .then(async () => { const freshUser = await refreshMe?.(); toast('ایمیل شما تایید شد'); window.history.replaceState(null, '', window.location.pathname + window.location.hash); navigate((freshUser?.role ?? user.role) === 'reader' ? '/' : '/admin'); }) .catch(() => toast('تایید ایمیل ناموفق بود یا لینک منقضی شده است', 'error')); }, [emailVerifyToken, user]); useEffect(() => { if (user && !emailVerifyToken) navigate(user.role === 'reader' ? '/' : '/admin'); }, [user, emailVerifyToken]); const handleSuccess = (u) => { toast(`خوش آمدید، ${u.name}!`); navigate(u.role === 'reader' ? '/' : '/admin'); }; return ( <>
{/* ── Form column (right in RTL) ──────────────────────────────── */}
{/* Radial vignette overlay */}
{/* Mobile logo */}
ت
تکناو
{/* Heading with vertical accent bar + time-based greeting */}

{tab === 'login' ? greeting : 'عضو تکناو شوید'}

{tab === 'login' ? 'با حساب کاربری خود وارد شوید' : 'در چند ثانیه حساب رایگان بسازید'}

{/* Pill tab switcher */}
{[{ key: 'login', label: 'ورود' }, { key: 'signup', label: 'ثبت‌نام' }].map(t => ( ))}
{/* OAuth */} {/* Form */}
{tab === 'login' ? : }
{/* Switch prompt */}
{tab === 'login' ? ( <>حساب ندارید؟{' '} ) : ( <>قبلاً حساب ساخته‌اید؟{' '} )}
TEKNAV · نشریه فناوری فارسی
{/* ── Brand column (left in RTL) ──────────────────────────────── */}
); } // ── Shared styles ───────────────────────────────────────────────────────────── const goldBtn = (loading) => ({ width: '100%', padding: '13px 0', borderRadius: 10, border: 'none', background: loading ? '#E8E2D8' : 'linear-gradient(135deg, #C8951C 0%, #D49A2A 60%, #C8951C 100%)', color: loading ? '#B0B8BA' : '#0F2A2E', fontSize: 14, fontWeight: 700, cursor: loading ? 'not-allowed' : 'pointer', fontFamily: 'Vazirmatn,sans-serif', boxShadow: loading ? 'none' : '0 4px 16px rgba(200,149,28,0.22)', transition: 'opacity 0.2s, transform 0.15s, box-shadow 0.2s', letterSpacing: '0.02em', marginTop: 4, }); const cancelBtn = { flex: 1, padding: '12px 0', border: '1.5px solid #E4DDD2', borderRadius: 10, background: 'transparent', color: '#7B8C8E', cursor: 'pointer', fontSize: 13, fontWeight: 600, fontFamily: 'Vazirmatn,sans-serif', }; const oauthBtnStyle = { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10, width: '100%', padding: '11px 0', borderRadius: 10, border: '1.5px solid #E4DDD2', background: '#fff', cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif', fontSize: 13, fontWeight: 600, color: '#1A2E30', transition: 'border-color 0.18s, background 0.18s', }; export { LoginPage };