"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 (
);
}
function InfoRow({
label,
value,
}: {
label: string;
value: ReactNode;
}) {
return (
);
}
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 ? (
ویرایش پروفایل
اطلاعات شخصی و دانشگاهی خود را با دقت بهروزرسانی کنید.
setEditing(false)}>
انصراف
تغییر تصویر
{me?.username}
{me?.email}
{avatarFallback}
{uploading ? (
) : null}
) : (
<>
{me?.is_mobile_verified ? "موبایل تأیید شده" : "نیازمند تأیید موبایل"}
{isAdminUser ? دسترسی مدیریتی : null}
عضویت از {formatJalali(me?.date_joined, false)}
{me?.first_name || "عضو"} {me?.last_name || ""}
{me?.email}
{me?.bio ? (
) : (
اینجا مرکز مدیریت حساب شماست؛ از ثبتنامهای رویدادی تا بخشهایی که بعداً
برای فعالیتهای بلاگ تکمیل خواهند شد.
)}
setEditing(true)}>
ویرایش پروفایل
تغییر تصویر
{me?.profile_picture ? (
حذف تصویر
) : null}
{avatarFallback}
{uploading ? (
) : null}
{me?.username}
{me?.student_id ? `شماره دانشجویی ${toPersianDigits(me.student_id)}` : "حساب کاربری فعال"}
{formatNumberPersian(confirmedRegistrations.length)}
رویداد تأیید شده
{formatNumberPersian(pendingRegistrations.length)}
در انتظار
اطلاعات شخصی و حساب
تفکیک روشن برای اطلاعات فردی، دانشگاهی و وضعیت دسترسی حساب.
جزئیات فردی
وضعیت حساب
{me?.is_mobile_verified ? (
) : (
)}
{me?.is_mobile_verified ? "تأیید شده" : "در انتظار"}
}
/>
اقدامهای سریع
میانبرهایی که از این پس جای خروج و مدیریت پنل را میگیرند.
setEditing(true)}>
ویرایش اطلاعات و بیو
بازیابی یا تغییر رمز با موبایل
{isAdminUser ? (
ورود به داشبورد مدیریت
) : null}
navigate("/logout")}
>
خروج از حساب
{formatNumberPersian(confirmedRegistrations.length)}
رویدادهای تأیید شده
{formatNumberPersian(pendingRegistrations.length)}
ثبتنامهای در انتظار
{formatNumberPersian(canceledRegistrations.length)}
رویدادهای لغو شده
فعالیتهای رویدادی
ثبتنامهای شما با تفکیک وضعیت، بدون نیاز به جستوجو در کل سایت.
{renderRegistrationGroup({
title: "تأیید شده",
description: "رویدادهایی که ثبتنام آنها تکمیل یا حضور شما ثبت شده است.",
items: confirmedRegistrations,
icon: CheckCircle2,
badgeVariant: "default",
})}
{renderRegistrationGroup({
title: "در انتظار",
description: "ثبتنامهایی که هنوز نهایی یا تأیید نشدهاند.",
items: pendingRegistrations,
icon: CalendarClock,
badgeVariant: "secondary",
})}
{renderRegistrationGroup({
title: "لغو شده",
description: "رویدادهایی که ثبتنام آنها لغو شده یا ادامه پیدا نکرده است.",
items: canceledRegistrations,
icon: XCircle,
badgeVariant: "destructive",
})}
{regsLoading ? (
در حال بارگذاری فعالیتهای رویدادی...
) : null}
{regsError ? (
خطا در دریافت ثبتنامهای رویدادی
) : null}
فعالیتهای بلاگ
این ساختار برای نسخههای بعدی آماده شده تا بدون بازطراحی دوباره، لایکها،
پستهای ذخیرهشده، نظرها و پاسخها را نشان دهد.
>
)}
>
);
}