// 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 ? {user.name} : (
{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 (