"use client"; import type * as Types from "@/lib/types"; import type { ComponentType, ReactNode } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Navigate, Link, useNavigate } from "@/lib/router"; import { Helmet } from "@/lib/helmet"; import { useAuth } from "@/contexts/AuthContext"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useToast } from "@/hooks/use-toast"; import { api } from "@/lib/api"; import { BadgeCheck, Bookmark, CalendarClock, Camera, CheckCircle2, Clock3, Heart, Loader2, LogOut, Mail, MessageSquareText, PencilLine, Reply, Shield, Trash2, UserRound, XCircle, } from "lucide-react"; import { formatJalali, formatNumberPersian, resolveErrorMessage, toPersianDigits, } from "@/lib/utils"; import Markdown from "@/components/Markdown"; import { useQuery } from "@tanstack/react-query"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; type ActivityCardProps = { icon: ComponentType<{ className?: string }>; title: string; count: number | string; description: string; }; function ActivityPlaceholderCard({ icon: Icon, title, count, description, }: ActivityCardProps) { return (

{title}

{description}

به‌زودی

{count}

); } function InfoRow({ label, value, }: { label: string; value: ReactNode; }) { return (
{value ?? "—"}
{label}
); } export default function Profile() { const { user, isAuthenticated, loading } = useAuth(); const navigate = useNavigate(); const { toast } = useToast(); const fileInputRef = useRef(null); const { data: myRegs, isLoading: regsLoading, isError: regsError, } = useQuery({ queryKey: ["my-registrations"], queryFn: () => api.getMyRegistrations(), enabled: isAuthenticated, }); const { data: majors, isLoading: majorsLoading } = useQuery({ queryKey: ["majors"], queryFn: () => api.getMajors(), staleTime: 7 * 24 * 60 * 60 * 1000, }); const { data: universities, isLoading: universitiesLoading } = useQuery({ queryKey: ["universities"], queryFn: () => api.getUniversities(), staleTime: 7 * 24 * 60 * 60 * 1000, }); const [me, setMe] = useState(user ?? null); const [fetching, setFetching] = useState(false); const [editing, setEditing] = useState(false); const [uploading, setUploading] = useState(false); const [formData, setFormData] = useState({ first_name: "", last_name: "", bio: "", year_of_study: null, major: null, university: null, student_id: "", }); const isAdminUser = Boolean(me?.is_staff || me?.is_superuser); const confirmedRegistrations = useMemo( () => myRegs?.filter((reg) => reg.status === "confirmed" || reg.status === "attended") ?? [], [myRegs], ); const pendingRegistrations = useMemo( () => myRegs?.filter((reg) => reg.status === "pending") ?? [], [myRegs], ); const canceledRegistrations = useMemo( () => myRegs?.filter((reg) => reg.status === "cancelled") ?? [], [myRegs], ); const siteUrl = "https://east-guilan-ce.ir"; const siteName = "انجمن علمی کامپیوتر شرق دانشگاه گیلان"; const canonicalUrl = `${siteUrl}/profile`; const toAbsoluteSiteUrl = useCallback((url?: string | null) => { if (!url) return undefined; if (url.startsWith("http")) return url; const normalizedSite = siteUrl.endsWith("/") ? siteUrl.slice(0, -1) : siteUrl; const normalizedPath = url.startsWith("/") ? url.slice(1) : url; return `${normalizedSite}/${normalizedPath}`; }, [siteUrl]); const majorLabel = useMemo(() => { if (!me?.major) return "—"; const found = majors?.find((item) => item.code === me.major || item.label === me.major); return found?.label ?? me.major; }, [majors, me?.major]); const universityLabel = useMemo(() => { if (!me?.university) return "—"; const found = universities?.find( (item) => item.code === me.university || item.label === me.university, ); return found?.label ?? me.university; }, [me?.university, universities]); const statusLabels: Record = { confirmed: "تأیید شده", cancelled: "لغو شده", pending: "در انتظار", attended: "حضور یافت", }; const { pageTitle, pageDescription, ogImage } = useMemo(() => { const nameParts = [me?.first_name, me?.last_name].filter(Boolean) as string[]; const displayName = nameParts.join(" ").trim(); const identifier = displayName || me?.username || me?.email || "عضو"; return { pageTitle: `پروفایل ${identifier} | ${siteName}`, pageDescription: `داشبورد حساب ${identifier} در انجمن علمی کامپیوتر شرق گیلان؛ مدیریت اطلاعات شخصی، وضعیت حساب و فعالیت‌های رویدادی و بلاگ.`, ogImage: toAbsoluteSiteUrl(me?.profile_picture_preview_url || me?.profile_picture) ?? `${siteUrl}/favicon.ico`, }; }, [ me?.email, me?.first_name, me?.last_name, me?.profile_picture, me?.profile_picture_preview_url, me?.username, siteName, siteUrl, toAbsoluteSiteUrl, ]); const avatarFallback = useMemo( () => (me?.first_name?.[0] || me?.last_name?.[0] || me?.username?.[0] || me?.email?.[0] || "?").toUpperCase(), [me?.email, me?.first_name, me?.last_name, me?.username], ); const syncProfileToForm = useCallback((profile: Types.UserProfileSchema) => { setFormData({ first_name: profile.first_name ?? "", last_name: profile.last_name ?? "", bio: profile.bio ?? "", year_of_study: typeof profile.year_of_study === "number" ? profile.year_of_study : null, major: profile.major ?? null, university: profile.university ?? null, student_id: profile.student_id ?? "", }); }, []); const loadProfile = useCallback(async () => { try { setFetching(true); const profile = await api.getProfile(); setMe(profile); syncProfileToForm(profile); } catch (error: unknown) { toast({ title: "خطا در دریافت پروفایل", description: resolveErrorMessage(error, "مشکلی پیش آمد"), variant: "destructive", }); } finally { setFetching(false); } }, [syncProfileToForm, toast]); useEffect(() => { if (user) { setMe(user); syncProfileToForm(user); } }, [syncProfileToForm, user]); useEffect(() => { if (isAuthenticated) { loadProfile(); } }, [isAuthenticated, loadProfile]); useEffect(() => { if (!majors || !me?.major) return; const found = majors.find((item) => item.code === me.major || item.label === me.major); if (found && formData.major !== found.code) { setFormData((prev) => ({ ...prev, major: found.code })); } }, [formData.major, majors, me?.major]); useEffect(() => { if (!universities || !me?.university) return; const found = universities.find( (item) => item.code === me.university || item.label === me.university, ); if (found && formData.university !== found.code) { setFormData((prev) => ({ ...prev, university: found.code })); } }, [formData.university, me?.university, universities]); const onPickFile = () => fileInputRef.current?.click(); const onUpload = async (file: File) => { try { setUploading(true); await api.uploadProfilePicture(file); await loadProfile(); toast({ title: "تصویر پروفایل به‌روزرسانی شد", variant: "success" }); } catch (error: unknown) { toast({ title: "خطا در آپلود تصویر", description: resolveErrorMessage(error, "مشکلی پیش آمد"), variant: "destructive", }); } finally { setUploading(false); } }; const onDeletePicture = async () => { try { await api.deleteProfilePicture(); await loadProfile(); toast({ title: "تصویر پروفایل حذف شد", variant: "success" }); } catch (error: unknown) { toast({ title: "خطا در حذف تصویر", description: resolveErrorMessage(error, "مشکلی پیش آمد"), variant: "destructive", }); } }; const onFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { onUpload(file); } event.currentTarget.value = ""; }; const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); try { const payload: Types.UserUpdateSchema = { first_name: formData.first_name ?? "", last_name: formData.last_name ?? "", bio: formData.bio ?? "", year_of_study: formData.year_of_study === undefined || formData.year_of_study === null ? null : Number(formData.year_of_study), major: formData.major || null, university: formData.university || null, student_id: formData.student_id || null, }; const updated = await api.updateProfile(payload); setMe(updated); syncProfileToForm(updated); setEditing(false); toast({ title: "پروفایل به‌روزرسانی شد", variant: "success" }); } catch (error: unknown) { toast({ title: "خطا در ذخیره پروفایل", description: resolveErrorMessage(error, "مشکلی پیش آمد"), variant: "destructive", }); } }; const renderRegistrationRow = (registration: Types.MyEventRegistrationSchema) => { const eventData = registration.event as Types.EventListItemSchema & { start_date?: string }; const rawDate = eventData.start_date ?? eventData.start_time; return (
مشاهده رویداد

{registration.event.title}

{statusLabels[registration.status] ?? registration.status} {rawDate ? • {formatJalali(rawDate)} : null}
); }; const renderRegistrationGroup = ({ title, description, items, icon: Icon, badgeVariant, }: { title: string; description: string; items: Types.MyEventRegistrationSchema[]; icon: React.ComponentType<{ className?: string }>; badgeVariant: "default" | "secondary" | "destructive"; }) => (
{formatNumberPersian(items.length)} {title}
{description}
{items.length > 0 ? ( items.map(renderRegistrationRow) ) : (
موردی در این بخش ثبت نشده است.
)}
); if (!loading && !isAuthenticated) { return ( <> {pageTitle} ); } const helmet = ( {pageTitle} ); return ( <> {helmet}
{(loading || fetching) && !me ? (
در حال بارگذاری پروفایل...
) : editing ? (
ویرایش پروفایل اطلاعات شخصی و دانشگاهی خود را با دقت به‌روزرسانی کنید.

{me?.username}

{me?.email}

{avatarFallback} {uploading ? (
) : null}
setFormData((prev) => ({ ...prev, first_name: event.target.value })) } />
setFormData((prev) => ({ ...prev, last_name: event.target.value })) } />
setFormData((prev) => ({ ...prev, year_of_study: event.target.value === "" ? null : Number(event.target.value), })) } />
setFormData((prev) => ({ ...prev, student_id: event.target.value })) } />
{universitiesLoading ? (
) : ( )}
{majorsLoading ? (
) : ( )}