diff --git a/app/teknav-profile.jsx b/app/teknav-profile.jsx
new file mode 100644
index 0000000..789a0ae
--- /dev/null
+++ b/app/teknav-profile.jsx
@@ -0,0 +1,1015 @@
+// User profile page — /profile/@username
+// Terracotta / warm-orange / unique-green palette; no teal.
+
+import { useState, useEffect, useRef } from 'react';
+import { useNav, useAuth, useToast, EmptyState, pillBtn, inputStyle } from './teknav-ui.jsx';
+import { api } from './src/lib/api.js';
+import { engagementApi, pushApi, notificationPreferencesApi } from './src/lib/engagement-api.js';
+import { TeknavData } from './teknav-data.js';
+import { iconNode } from './src/lib/icon.jsx';
+import eyeIcon from './src/assets/icons/eye.svg';
+import gradCapIcon from './src/assets/icons/graduation-cap.svg';
+import scrollIcon from './src/assets/icons/scroll.svg';
+import botIcon from './src/assets/icons/bot.svg';
+import chartIcon from './src/assets/icons/chart-column.svg';
+import lockIcon from './src/assets/icons/lock.svg';
+import laptopIcon from './src/assets/icons/laptop.svg';
+import flameIcon from './src/assets/icons/flame.svg';
+import zapIcon from './src/assets/icons/zap.svg';
+import libraryIcon from './src/assets/icons/library.svg';
+import sproutIcon from './src/assets/icons/sprout.svg';
+import medalIcon from './src/assets/icons/medal.svg';
+
+// ── Theme ────────────────────────────────────────────────────────────────────
+const P = {
+ terracotta: '#C45C3D',
+ orange: '#E07A3A',
+ green: '#3A7D5E',
+ greenLight: '#E8F4EF',
+ ink: '#263238',
+ muted: '#5F6B6D',
+ cream: '#FAF7F0',
+ panel: '#F4EFE6',
+ line: '#E4DDD2',
+ white: '#fff',
+};
+
+const ROLE_LABELS = {
+ admin: { label: 'ادمین', bg: 'rgba(196,92,61,0.10)', color: '#C45C3D', border: 'rgba(196,92,61,0.22)' },
+ editor: { label: 'سردبیر', bg: 'rgba(58,125,94,0.10)', color: '#2F8F6B', border: 'rgba(58,125,94,0.22)' },
+ writer: { label: 'نویسنده', bg: 'rgba(212,154,42,0.10)', color: '#A87A10', border: 'rgba(212,154,42,0.22)' },
+ reviewer: { label: 'بازبین', bg: 'rgba(100,120,200,0.10)', color: '#5566AA', border: 'rgba(100,120,200,0.22)' },
+ reader: { label: 'خواننده', bg: 'rgba(95,107,109,0.08)', color: '#5F6B6D', border: 'rgba(95,107,109,0.18)' },
+};
+
+function RolePill({ role }) {
+ const r = ROLE_LABELS[role] ?? ROLE_LABELS.reader;
+ return (
+ {r.label}
+ );
+}
+
+function findStaticAuthor(slug) {
+ return TeknavData.authors.find(author => author.username === slug || author.slug === slug);
+}
+
+function getPublishedAuthorArticles(author) {
+ return TeknavData.articles
+ .filter(article => (
+ (article.authorId === author.id || article.authorName === author.name || article.authorUsername === author.username)
+ && article.status === 'منتشرشده'
+ ))
+ .map(article => ({
+ id: article.id,
+ slug: article.slug,
+ title: article.title,
+ summary: article.summary,
+ publishedAt: article.dateEn ? `${article.dateEn}T09:00:00+03:30` : null,
+ views: article.views ?? 0,
+ reactions: article.reactions ?? 0,
+ }));
+}
+
+function buildStaticAuthorProfile(author) {
+ if (!author) return null;
+ const authorArticles = getPublishedAuthorArticles(author);
+ return {
+ id: author.id,
+ username: author.username,
+ name: author.name,
+ bio: author.bio,
+ avatarUrl: author.avatarUrl ?? null,
+ role: 'writer',
+ createdAt: author.joinedDate ? new Date(author.joinedDate).toISOString() : null,
+ author: {
+ articles: authorArticles,
+ articleCount: authorArticles.length,
+ verifiedExpert: author.verifiedExpert ?? false,
+ verificationNote: author.verificationNote ?? author.specialty,
+ },
+ _static: true,
+ expertise: author.expertise ?? [],
+ education: author.education ?? null,
+ alumniOf: author.alumniOf ?? null,
+ credential: author.credential ?? null,
+ social: author.social ?? {},
+ specialty: author.specialty,
+ };
+}
+
+function mergeProfileWithAuthor(profileData, author) {
+ if (!profileData || !author) return profileData;
+ return {
+ ...profileData,
+ expertise: profileData.expertise || author.expertise || [],
+ education: profileData.education || author.education || null,
+ alumniOf: profileData.alumniOf || author.alumniOf || null,
+ credential: profileData.credential || author.credential || null,
+ avatarUrl: profileData.avatarUrl || author.avatarUrl || null,
+ social: { ...author.social, ...profileData.social },
+ specialty: profileData.specialty || author.specialty,
+ author: {
+ ...profileData.author,
+ verifiedExpert: profileData.author?.verifiedExpert ?? author.verifiedExpert ?? false,
+ verificationNote: profileData.author?.verificationNote ?? author.verificationNote ?? author.specialty,
+ }
+ };
+}
+
+// ── Avatar with upload overlay ───────────────────────────────────────────────
+function AvatarUpload({ user, editable, onUploaded }) {
+ const inputRef = useRef(null);
+ const [busy, setBusy] = useState(false);
+ const [webp, setWebp] = useState(true);
+ const [preview, setPreview] = useState(user.avatarUrl ?? null);
+
+ useEffect(() => {
+ setPreview(user.avatarUrl ?? null);
+ }, [user.avatarUrl]);
+
+ const handleFile = async (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ if (file.size > 5 * 1024 * 1024) { alert('حجم تصویر نباید بیشتر از ۵ مگابایت باشد.'); return; }
+ setBusy(true);
+ try {
+ const dataUrl = await new Promise((res, rej) => {
+ const fr = new FileReader();
+ fr.onload = () => res(fr.result);
+ fr.onerror = rej;
+ fr.readAsDataURL(file);
+ });
+ const base64 = dataUrl.split(',')[1];
+ const data = await api.post('/api/auth/avatar', { file: base64, mimeType: file.type, webp });
+ setPreview(data.url);
+ onUploaded?.(data.url);
+ } catch {
+ alert('آپلود آواتار ناموفق بود. دوباره تلاش کنید.');
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const initials = user.name ? user.name.slice(0, 2) : '؟';
+ const avatarStyle = {
+ width: 110, height: 110, borderRadius: '50%',
+ border: `3px solid rgba(196,92,61,0.35)`,
+ boxShadow: '0 4px 20px rgba(196,92,61,0.15)',
+ objectFit: 'cover', display: 'block',
+ };
+
+ return (
+
+ {preview
+ ?

+ : (
+
+ {initials}
+
+ )
+ }
+ {editable && (
+ <>
+
+
+
+ >
+ )}
+
+ );
+}
+
+// ── Inline edit field ────────────────────────────────────────────────────────
+function EditField({ label, value, onChange, multiline, placeholder, maxLength }) {
+ const style = {
+ ...inputStyle,
+ fontSize: 14, padding: '8px 12px', marginTop: 4, resize: multiline ? 'vertical' : 'none',
+ minHeight: multiline ? 80 : undefined,
+ };
+ return (
+
+ );
+}
+
+// ── Article mini-card ────────────────────────────────────────────────────────
+function ArticleMiniCard({ article, onClick }) {
+ const date = article.publishedAt ? new Date(article.publishedAt).toLocaleDateString('fa-IR') : '';
+ return (
+ { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.07)'; }}
+ onMouseLeave={e => { e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = ''; }}
+ >
+
{article.title}
+ {article.summary &&
{article.summary.slice(0, 140)}{article.summary.length > 140 ? '…' : ''}
}
+
+ {date && {date}}
+ {article.views ?? 0}
+ ♥ {article.reactions ?? 0}
+
+
+ );
+}
+
+// ── Profile settings modal ───────────────────────────────────────────────────
+function ProfileSettingsModal({ user, account, onClose, onSaved }) {
+ const toast = useToast();
+ const { refreshMe } = useAuth();
+ const [form, setForm] = useState({ name: user.name ?? '', username: user.username ?? '', bio: user.bio ?? '' });
+ const [passwordForm, setPasswordForm] = useState({ currentPassword: '', password: '', confirm: '' });
+ const [phone, setPhone] = useState(account?.phone ?? '');
+ const [otp, setOtp] = useState('');
+ const [busy, setBusy] = useState(false);
+ const [verifyBusy, setVerifyBusy] = useState(false);
+ const [error, setError] = useState('');
+ const [pushEnabled, setPushEnabled] = useState(false);
+ const [pushBlocked, setPushBlocked] = useState(false);
+ const [pushBusy, setPushBusy] = useState(false);
+ const canEditUsername = user?.role !== 'reader';
+
+ useEffect(() => {
+ if (!('PushManager' in window) || !('serviceWorker' in navigator)) return;
+ if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
+ setPushBlocked(true);
+ return;
+ }
+ pushApi.isSubscribed().then(setPushEnabled).catch(() => {});
+ }, []);
+ const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
+ const setPasswordField = (k, v) => setPasswordForm(f => ({ ...f, [k]: v }));
+
+ const save = async () => {
+ setBusy(true); setError('');
+ try {
+ const username = form.username.trim().toLowerCase().replace(/^@/, '');
+ if (canEditUsername && !/^[a-z0-9_][a-z0-9_.]{1,28}[a-z0-9_]$/.test(username)) {
+ setError('نام کاربری باید ۳ تا ۳۰ کاراکتر لاتین، عدد، نقطه یا زیرخط باشد.');
+ return;
+ }
+ if (passwordForm.password || passwordForm.confirm || passwordForm.currentPassword) {
+ if (passwordForm.password.length < 8) {
+ setError('رمز عبور جدید باید حداقل ۸ کاراکتر باشد.');
+ return;
+ }
+ if (passwordForm.password !== passwordForm.confirm) {
+ setError('تکرار رمز عبور جدید تطابق ندارد.');
+ return;
+ }
+ }
+ const data = await api.put('/api/auth/profile', {
+ name: form.name || undefined,
+ ...(canEditUsername ? { username } : {}),
+ bio: form.bio || undefined,
+ ...(passwordForm.password ? { currentPassword: passwordForm.currentPassword, password: passwordForm.password } : {}),
+ });
+ onSaved?.(data.user);
+ await refreshMe?.();
+ onClose();
+ } catch (e) {
+ const code = e.body?.error;
+ setError(code === 'username_taken'
+ ? 'این نام کاربری قبلاً گرفته شده است.'
+ : code === 'current_password_required'
+ ? 'برای تغییر رمز عبور، رمز فعلی را وارد کنید.'
+ : code === 'current_password_invalid'
+ ? 'رمز فعلی درست نیست.'
+ : code === 'username_change_forbidden'
+ ? 'تغییر نام کاربری برای این حساب مجاز نیست.'
+ : 'ذخیره ناموفق بود.');
+ } finally {
+ setBusy(false);
+ }
+ };
+
+ const requestEmailVerification = async () => {
+ setVerifyBusy(true);
+ try {
+ const res = await api.post('/api/auth/email/verify/request', {});
+ toast(res?.alreadyVerified ? 'ایمیل قبلاً تایید شده است' : 'لینک تایید ایمیل ارسال شد');
+ await refreshMe?.();
+ } catch {
+ toast('ارسال لینک تایید ایمیل ناموفق بود', 'error');
+ } finally {
+ setVerifyBusy(false);
+ }
+ };
+
+ const sendOtp = async () => {
+ setVerifyBusy(true);
+ try {
+ await api.post('/api/auth/otp/send', { phone });
+ toast('کد تایید پیامک شد');
+ } catch (e) {
+ toast(e.body?.error === 'sms_not_configured' ? 'درگاه پیامک روی سرور تنظیم نشده است' : 'ارسال کد تایید ناموفق بود', 'error');
+ } finally {
+ setVerifyBusy(false);
+ }
+ };
+
+ const verifyOtp = async () => {
+ setVerifyBusy(true);
+ try {
+ await api.post('/api/auth/otp/verify', { phone, code: otp });
+ await refreshMe?.();
+ toast('شماره موبایل تایید شد');
+ } catch {
+ toast('کد تایید معتبر نیست', 'error');
+ } finally {
+ setVerifyBusy(false);
+ }
+ };
+
+ const toggleTwoFactor = async () => {
+ setVerifyBusy(true);
+ try {
+ const next = !account?.twoFactorEnabled;
+ const res = await api.put('/api/auth/2fa', { enabled: next });
+ await refreshMe?.();
+ onSaved?.(res.user);
+ toast(next ? 'ورود دومرحلهای فعال شد' : 'ورود دومرحلهای غیرفعال شد');
+ } catch (e) {
+ toast(e.body?.error === 'phone_required' ? 'برای فعالسازی، ابتدا موبایل را تایید کنید' : 'تغییر ورود دومرحلهای ناموفق بود', 'error');
+ } finally {
+ setVerifyBusy(false);
+ }
+ };
+
+ return (
+
+
+
+
ویرایش پروفایل
+
set('name', v)} placeholder="نام کامل شما" maxLength={80} />
+ {canEditUsername ? (
+ set('username', v.toLowerCase())} placeholder="coaxys" maxLength={30} />
+ ) : (
+
+ نام کاربری برای کاربران عادی قابل تغییر نیست.
+
+ )}
+ set('bio', v)} multiline placeholder="چند جمله درباره خودتان…" maxLength={280} />
+
+
+
تایید حساب
+
+
+ ایمیل: {account?.emailVerified ? 'تایید شده' : 'تایید نشده'}
+ {!account?.emailVerified && }
+
+
+ setPhone(e.target.value)} placeholder="شماره موبایل" style={inputStyle} dir="ltr" />
+
+
+
+ setOtp(e.target.value)} placeholder="کد تایید" style={inputStyle} dir="ltr" />
+
+
+
وضعیت موبایل: {account?.phoneVerified ? 'تایید شده' : 'تایید نشده'}
+
+ ورود دومرحلهای: {account?.twoFactorEnabled ? 'فعال' : 'غیرفعال'}
+
+
+
+
+ {'PushManager' in window && (
+
+
اعلانهای مرورگر
+ {pushBlocked ? (
+
مرورگر شما اجازه اعلان را مسدود کرده است. برای فعالسازی، از تنظیمات مرورگر اجازه دهید.
+ ) : (
+
+
+ اعلانهای مقالات جدید و نظرات: {pushEnabled ? 'فعال' : 'غیرفعال'}
+
+
+
+ )}
+
+ )}
+
+ {error && {error}
}
+
+
+
+
+
+
+ );
+}
+
+function NotificationPreferencesPanel() {
+ const toast = useToast();
+ const EVENT_LABELS = {
+ comment: 'نظر جدید روی مقاله',
+ comment_reply: 'پاسخ به نظر',
+ new_article: 'مقاله جدید از نویسندگان دنبالشده',
+ review_approved: 'تأیید مقاله',
+ review_revision: 'بازخورد ویراستار',
+ review_rejected: 'رد مقاله',
+ review_submitted: 'ارسال مقاله برای بررسی',
+ system: 'پیامهای سیستمی',
+ };
+ const CHANNEL_LABELS = { push: 'مرورگر', email: 'ایمیل', sms: 'پیامک' };
+ const CHANNELS = ['push', 'email', 'sms'];
+
+ const [prefs, setPrefs] = useState(null);
+ const [saving, setSaving] = useState(false);
+ const [open, setOpen] = useState(false);
+
+ useEffect(() => {
+ if (!open) return;
+ notificationPreferencesApi.get().then((data) => setPrefs(data.preferences)).catch(() => {});
+ }, [open]);
+
+ const toggle = async (eventType, channel) => {
+ if (!prefs) return;
+ const updated = prefs.map((p) =>
+ p.eventType === eventType && p.channel === channel ? { ...p, enabled: !p.enabled } : p,
+ );
+ setPrefs(updated);
+ setSaving(true);
+ try {
+ await notificationPreferencesApi.update(updated);
+ } catch {
+ toast('ذخیره تنظیمات ناموفق بود', 'error');
+ setPrefs(prefs); // revert
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const getEnabled = (eventType, channel) =>
+ prefs?.find((p) => p.eventType === eventType && p.channel === channel)?.enabled ?? true;
+
+ return (
+
+
+ {open && (
+
+ {prefs === null ? (
+
در حال بارگذاری…
+ ) : (
+
+
+
+ | رویداد |
+ {CHANNELS.map((ch) => (
+ {CHANNEL_LABELS[ch]} |
+ ))}
+
+
+
+ {Object.entries(EVENT_LABELS).map(([evt, label]) => (
+
+ | {label} |
+ {CHANNELS.map((ch) => (
+
+ |
+ ))}
+
+ ))}
+
+
+ )}
+
+ )}
+
+ );
+}
+
+// ── Inline SVG sparkline ─────────────────────────────────────────────────────
+function Sparkline({ data, color = '#C45C3D', width = 120, height = 36 }) {
+ if (!data || data.length < 2) return ;
+ const vals = data.map(d => d.views ?? 0);
+ const max = Math.max(...vals, 1);
+ const pts = vals.map((v, i) => {
+ const x = (i / (vals.length - 1)) * width;
+ const y = height - (v / max) * (height - 4) - 2;
+ return `${x.toFixed(1)},${y.toFixed(1)}`;
+ }).join(' ');
+ return (
+
+ );
+}
+
+// ── Writer analytics tab ──────────────────────────────────────────────────────
+function WriterAnalyticsTab() {
+ const [overview, setOverview] = useState(null);
+ const [articles, setArticles] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ let cancelled = false;
+ setLoading(true);
+ Promise.all([
+ engagementApi.writerAnalyticsOverview().catch(() => null),
+ engagementApi.writerAnalyticsArticles().catch(() => ({ items: [] })),
+ ]).then(([ov, arts]) => {
+ if (!cancelled) {
+ setOverview(ov);
+ setArticles(arts?.items ?? []);
+ setLoading(false);
+ }
+ });
+ return () => { cancelled = true; };
+ }, []);
+
+ if (loading) return در حال بارگذاری…
;
+
+ const totalViews = overview?.views ?? 0;
+ const totalReactions = overview?.reactions ?? 0;
+ const totalComments = overview?.comments ?? 0;
+ const viewsByDay = overview?.viewsByDay ?? [];
+
+ return (
+
+ {/* Summary cards */}
+
+ {[
+ { label: 'بازدید کل', value: totalViews.toLocaleString('fa-IR'), color: P.terracotta },
+ { label: 'واکنشها', value: totalReactions.toLocaleString('fa-IR'), color: P.orange },
+ { label: 'نظرات', value: totalComments.toLocaleString('fa-IR'), color: P.green },
+ ].map(({ label, value, color }) => (
+
+ ))}
+
+
+ {/* 30-day views sparkline */}
+ {viewsByDay.length > 1 && (
+
+
بازدید ۳۰ روز اخیر
+
+
+ {viewsByDay[0]?.date?.slice(5) ?? ''}
+ {viewsByDay[viewsByDay.length - 1]?.date?.slice(5) ?? ''}
+
+
+ )}
+
+ {/* Per-article table */}
+ {articles.length > 0 && (
+
+
عملکرد مقالهها
+
+
+
+
+ {['عنوان', 'بازدید', 'واکنش', 'نظر', 'ذخیره'].map(h => (
+ | {h} |
+ ))}
+
+
+
+ {articles.map((a, i) => (
+
+ | {a.title} |
+ {(a.views ?? 0).toLocaleString('fa-IR')} |
+ {(a.reactions ?? 0).toLocaleString('fa-IR')} |
+ {(a.comments ?? 0).toLocaleString('fa-IR')} |
+ {(a.saved ?? 0).toLocaleString('fa-IR')} |
+
+ ))}
+
+
+
+
+ )}
+
+ {articles.length === 0 && (
+
هنوز دادهای برای نمایش وجود ندارد.
+ )}
+
+ );
+}
+
+// ── Main profile page ─────────────────────────────────────────────────────────
+export function UserProfilePage({ username }) {
+ const { navigate } = useNav();
+ const { user: me } = useAuth();
+ const slug = username.replace(/^@/, '');
+ const [profile, setProfile] = useState(() => {
+ const author = findStaticAuthor(slug);
+ return buildStaticAuthorProfile(author);
+ });
+ const [loading, setLoading] = useState(() => !findStaticAuthor(slug));
+ const [showSettings, setShowSettings] = useState(false);
+ const [followers, setFollowers] = useState({ count: 0, following: false });
+ const [history, setHistory] = useState([]);
+ const [following, setFollowing] = useState([]);
+ const [activeTab, setActiveTab] = useState('articles');
+ const [badges, setBadges] = useState([]);
+
+ const isOwn = me && (me.username === slug || me.id === profile?.id);
+
+ useEffect(() => {
+ let cancelled = false;
+ const author = findStaticAuthor(slug);
+ const staticProfile = buildStaticAuthorProfile(author);
+ if (staticProfile) {
+ setProfile(staticProfile);
+ setLoading(false);
+ } else {
+ setProfile(null);
+ setLoading(true);
+ }
+ api.get(`/api/profile/${slug}`)
+ .then(d => {
+ if (cancelled) return;
+ const profileData = d.profile;
+ setProfile(profileData ? mergeProfileWithAuthor(profileData, author) : staticProfile);
+ })
+ .catch(() => {
+ if (cancelled) return;
+ if (!staticProfile) {
+ setProfile(null);
+ }
+ })
+ .finally(() => { if (!cancelled) setLoading(false); });
+ return () => { cancelled = true; };
+ }, [slug]);
+
+ useEffect(() => {
+ let cancelled = false;
+ if (!profile?.username || profile._static || !['writer', 'editor', 'admin'].includes(profile.role)) return;
+ engagementApi.writerFollowers(profile.username)
+ .then(res => { if (!cancelled) setFollowers({ count: res?.count ?? 0, following: !!res?.following }); })
+ .catch(() => {});
+ return () => { cancelled = true; };
+ }, [profile?.username, profile?.role]);
+
+ useEffect(() => {
+ let cancelled = false;
+ if (!isOwn) return;
+ Promise.all([
+ engagementApi.listHistory({ limit: 20 }).catch(() => ({ items: [] })),
+ engagementApi.following().catch(() => ({ items: [] })),
+ engagementApi.getBadges().catch(() => ({ badges: [] })),
+ ]).then(([historyRes, followingRes, badgesRes]) => {
+ if (!cancelled) {
+ setHistory(historyRes?.items ?? []);
+ setFollowing(followingRes?.items ?? []);
+ setBadges(badgesRes?.badges ?? []);
+ }
+ });
+ return () => { cancelled = true; };
+ }, [isOwn]);
+
+ if (loading) {
+ return (
+ در حال بارگذاری…
+ );
+ }
+
+ if (!profile) {
+ return ;
+ }
+
+ const articles = profile.author?.articles ?? [];
+ const articleCount = profile.author?.liveArticleCount ?? profile.author?.articleCount ?? articles.length;
+ const showArticles = ['writer', 'editor', 'admin'].includes(profile.role) && articles.length > 0;
+
+ const joinDate = profile.createdAt ? new Date(profile.createdAt).toLocaleDateString('fa-IR', { year: 'numeric', month: 'long' }) : '';
+ const canFollow = me && !isOwn && ['writer', 'editor', 'admin'].includes(profile.role);
+ const toggleFollow = async () => {
+ try {
+ const res = followers.following
+ ? await engagementApi.unfollowWriter(profile.username)
+ : await engagementApi.followWriter(profile.username);
+ setFollowers({ count: res.count ?? followers.count, following: !!res.following });
+ } catch {
+ /* follow state remains unchanged */
+ }
+ };
+
+ return (
+
+ {/* Hero banner */}
+
+ {/* Decorative shapes */}
+
+
+
+
+
+
setProfile(p => ({ ...p, avatarUrl: url }))}
+ />
+
+
+
+
+ {profile.name}
+ {profile.author?.verifiedExpert && متخصص تاییدشده}
+
+
+
+
@{profile.username}
+ {profile.author?.verifiedExpert && profile.author?.verificationNote && (
+
+
+ {profile.author.verificationNote}
+
+ )}
+ {profile.bio &&
{profile.bio}
}
+ {(profile.alumniOf || profile.credential) && (
+
+ {profile.alumniOf && (
+
+
+
دانشآموخته: {profile.alumniOf.name}
+ {profile.alumniOf.sameAs && (
+
تاییدیه نهاد
+ )}
+
+ )}
+ {profile.credential && (
+
+
+ درجه علمی: {profile.credential.name}
+
+ )}
+
+ )}
+ {profile.expertise?.length > 0 && (
+
+ {profile.expertise.map(tag => (
+ {tag}
+ ))}
+
+ )}
+
+ {joinDate &&
عضو از {joinDate}}
+ {showArticles &&
{articleCount} مقاله}
+ {['writer', 'editor', 'admin'].includes(profile.role) && !profile._static &&
{followers.count.toLocaleString('fa-IR')} دنبالکننده}
+ {profile.social?.twitter && profile.social.twitter !== '#' && (
+
𝕏
+ )}
+ {profile.social?.linkedin && profile.social.linkedin !== '#' && (
+
in
+ )}
+
+
+
+ {canFollow && (
+
+ )}
+
+ {isOwn && (
+
+ )}
+
+
+
+ {/* Stats bar (for writers/editors) */}
+ {showArticles && (
+
+
+ {[
+ { label: 'مقالات منتشرشده', value: articleCount },
+ { label: 'مجموع بازدید', value: articles.reduce((s, a) => s + (a.views ?? 0), 0).toLocaleString('fa-IR') },
+ { label: 'مجموع واکنش', value: articles.reduce((s, a) => s + (a.reactions ?? 0), 0).toLocaleString('fa-IR') },
+ ].map(({ label, value }) => (
+
+ ))}
+
+
+ )}
+
+ {/* Earned badges */}
+ {badges.length > 0 && (
+
+
+
نشانهای کسبشده
+
+ {badges.map((b) => {
+ const META = {
+ ai_reader: { icon: botIcon, label: 'خواننده AI' },
+ data_reader: { icon: chartIcon, label: 'خواننده داده' },
+ security_reader: { icon: lockIcon, label: 'خواننده امنیت' },
+ hardware_reader: { icon: laptopIcon, label: 'خواننده سختافزار' },
+ weekly_streak: { icon: flameIcon, label: 'رکورد هفتگی' },
+ monthly_streak: { icon: zapIcon, label: 'رکورد ماهانه' },
+ power_reader: { icon: libraryIcon, label: 'خواننده حرفهای' },
+ early_adopter: { icon: sproutIcon, label: 'اولین کاربران' },
+ };
+ const m = META[b.badgeType] || { icon: medalIcon, label: b.badgeType };
+ const earned = b.earnedAt ? new Date(b.earnedAt).toLocaleDateString('fa-IR', { year: 'numeric', month: 'long' }) : '';
+ return (
+
+ {iconNode(m.icon)}
+ {m.label}
+
+ );
+ })}
+
+
+
+ )}
+
+ {/* Article list */}
+
+ {isOwn && (
+
+
+
تاریخچه خواندن
+ {history.length === 0 ?
هنوز مقالهای در تاریخچه شما ثبت نشده است.
: history.slice(0, 5).map(item => (
+
+ ))}
+ {history.length > 0 &&
}
+
+
+
نویسندگان دنبالشده
+ {following.length === 0 ?
هنوز نویسندهای را دنبال نمیکنید.
: following.slice(0, 6).map(writer => (
+
+ ))}
+
+
+ )}
+ {showArticles ? (
+ <>
+ {isOwn && ['writer', 'editor', 'admin'].includes(profile.role) && (
+
+ {[{ id: 'articles', label: 'مقالات' }, { id: 'analytics', label: 'آمار و تحلیل' }].map(tab => (
+
+ ))}
+
+ )}
+ {activeTab === 'analytics' && isOwn && ['writer', 'editor', 'admin'].includes(profile.role) ? (
+
+ ) : (
+ <>
+
+
+ مقالات
+
+
+ {articles.map(article => (
+
navigate('/article/' + article.slug)}
+ />
+ ))}
+
+ >
+ )}
+ >
+ ) : (
+
+
✦
+
هنوز مقالهای منتشر نشده
+
مقالات این کاربر اینجا نمایش داده خواهند شد.
+
+ )}
+
+
+ {showSettings && (
+
setShowSettings(false)}
+ onSaved={updated => setProfile(p => ({ ...p, ...updated }))}
+ />
+ )}
+
+
+
+ );
+}