Files
guilan-ace-frontend/src/views/PublicProfile.tsx

149 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useQuery } from "@tanstack/react-query";
import Image from "next/image";
import { CalendarDays, EyeOff, UserRound } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { PublicProfileBadges } from "@/components/profile/PublicProfileBadges";
import { Helmet } from "@/lib/helmet";
import { Link, useParams } from "@/lib/router";
import { api } from "@/lib/api";
import { formatJalali, toPersianDigits } from "@/lib/utils";
import type { PublicProfileEventSchema } from "@/lib/types";
function EventCard({ event }: { event: PublicProfileEventSchema }) {
return (
<Link to={`/events/${event.slug}`} className="group flex min-h-28 flex-col gap-3 rounded-2xl border border-border/70 bg-background p-3 text-right transition-colors hover:bg-muted/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:flex-row sm:gap-4">
{event.image_url ? <Image src={event.image_url} alt="" width={640} height={360} unoptimized className="aspect-video w-full shrink-0 rounded-xl object-cover sm:w-32" /> : (
<span className="flex aspect-video w-full shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary sm:w-32"><CalendarDays className="h-6 w-6" aria-hidden="true" /></span>
)}
<span className="flex min-w-0 flex-1 flex-col gap-2 py-1">
<span className="font-semibold text-foreground group-hover:text-primary">{event.title}</span>
<span className="text-xs text-muted-foreground">{formatJalali(event.start_time, false)}</span>
</span>
</Link>
);
}
export default function PublicProfile() {
const { username: rawUsername } = useParams<{ username: string }>();
const username = Array.isArray(rawUsername) ? rawUsername[0] : rawUsername;
const profileQuery = useQuery({
queryKey: ["public-profile", username],
queryFn: () => api.getPublicProfile(username),
enabled: Boolean(username),
retry: false,
});
const profile = profileQuery.data;
if (profileQuery.isLoading) {
return <main className="flex min-h-[70vh] items-center justify-center text-muted-foreground" dir="rtl">در حال بارگذاری پروفایل عمومی...</main>;
}
if (profileQuery.isError || !profile) {
return (
<main className="container mx-auto max-w-4xl px-4 py-16" dir="rtl">
<Card className="border-border/70 text-center shadow-sm"><CardContent className="p-10">
<UserRound className="mx-auto h-8 w-8 text-muted-foreground" aria-hidden="true" />
<h1 className="mt-4 text-xl font-bold">این پروفایل در دسترس نیست</h1>
<p className="mt-2 text-sm text-muted-foreground">نام کاربری را بررسی کنید یا بعداً دوباره تلاش کنید.</p>
</CardContent></Card>
</main>
);
}
const fallback = `${profile.first_name?.[0] ?? ""}${profile.last_name?.[0] ?? ""}` || profile.username[0];
const maskedDetails = [
{ label: "شماره موبایل", value: profile.mobile, isEmail: false },
...(profile.email ? [{ label: "ایمیل", value: profile.email, isEmail: true }] : []),
{ label: "دانشگاه", value: profile.university, isEmail: false },
{ label: "رشته", value: profile.major, isEmail: false },
{ label: "شماره دانشجویی", value: profile.student_id, isEmail: false },
{ label: "سال ورود", value: profile.year_of_study || null, isEmail: false },
];
const pageDescription = profile.bio || `پروفایل عمومی ${profile.full_name} و رویدادهای او در انجمن علمی مهندسی کامپیوتر.`;
return (
<>
<Helmet>
<title>{`${profile.full_name} | پروفایل عمومی`}</title>
<meta name="description" content={pageDescription} />
<meta name="robots" content="index, follow" />
</Helmet>
<main className="min-h-screen bg-muted/15 px-4 py-8 md:py-12" dir="rtl">
<div className="container mx-auto max-w-6xl space-y-5">
<div className="grid gap-5 lg:grid-cols-12 lg:items-start">
<aside className="space-y-5 lg:col-span-4">
<Card className="border-border/70 shadow-sm">
<CardContent className="p-6 text-right">
<div className="flex items-center gap-4">
<Avatar className="h-20 w-20 border-2 border-background shadow-sm">
<AvatarImage src={profile.profile_picture || undefined} alt={profile.full_name} />
<AvatarFallback className="text-xl">{fallback}</AvatarFallback>
</Avatar>
<div className="min-w-0">
<h1 className="truncate text-xl font-bold">{profile.full_name}</h1>
<p className="mt-1 text-sm text-muted-foreground">@{profile.username}</p>
</div>
</div>
{profile.bio ? <p className="mt-5 whitespace-pre-wrap text-sm leading-7 text-muted-foreground">{profile.bio}</p> : null}
{!profile.details_visible ? (
<div className="relative isolate mt-5 overflow-hidden rounded-xl border border-border/70 bg-muted/20 p-4 sm:p-5">
<div aria-hidden="true" className="pointer-events-none select-none space-y-2.5 opacity-55 blur-[4px]">
{maskedDetails.map(({ label }) => (
<div key={label} className="flex items-center justify-between gap-4 border-b border-border/60 py-2.5 last:border-0 text-sm">
<span className="shrink-0 text-muted-foreground">{label}</span>
<span className="font-medium">••••••••</span>
</div>
))}
</div>
<div className="absolute inset-0 flex items-center justify-center bg-background/55 p-4 text-center">
<div className="max-w-xs">
<span className="mx-auto mb-2 flex h-11 w-11 items-center justify-center rounded-full bg-primary/10 text-primary">
<EyeOff className="h-5 w-5" aria-hidden="true" />
</span>
<p className="font-semibold">جزئیات این پروفایل خصوصی است</p>
<p className="mt-1 text-sm leading-6 text-muted-foreground">نمایش اطلاعات تکمیلی به درخواست صاحب حساب غیرفعال شده است.</p>
</div>
</div>
</div>
) : (
<div className="mt-5 space-y-1">
{maskedDetails.map(({ label, value, isEmail }) => (
<div key={label} className="flex items-center justify-between gap-4 border-b border-border/60 py-3 last:border-0 text-sm">
<span className="shrink-0 text-muted-foreground">{label}</span>
<span dir={isEmail ? "ltr" : undefined} className={`min-w-0 text-left font-medium ${isEmail ? "break-all" : ""}`}>
{value ? (isEmail ? String(value) : toPersianDigits(String(value))) : "ثبت نشده"}
</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card className="border-border/70 shadow-sm">
<CardContent className="pt-6">
<PublicProfileBadges badges={profile.badges} />
</CardContent>
</Card>
</aside>
<div className="space-y-5 lg:col-span-8">
<Card className="border-border/70 shadow-sm">
<CardHeader className="text-right">
<CardTitle className="flex items-center gap-2"><CalendarDays className="h-5 w-5 text-primary" aria-hidden="true" />رویدادهای انجمن</CardTitle>
<CardDescription>رویدادهایی که {profile.first_name || profile.username} در آن‌ها شرکت کرده</CardDescription>
</CardHeader>
<CardContent className="h-[70vh] space-y-3 overflow-y-auto overscroll-contain">
{profile.events.length ? profile.events.map((event) => <EventCard key={`${event.slug}-${event.start_time}`} event={event} />) : <p className="rounded-xl bg-muted/30 p-5 text-center text-sm text-muted-foreground">هنوز رویدادی برای نمایش ثبت نشده است.</p>}
</CardContent>
</Card>
</div>
</div>
</div>
</main>
</>
);
}