146 lines
6.0 KiB
TypeScript
146 lines
6.0 KiB
TypeScript
import {
|
||
CalendarCheck2,
|
||
CircleHelp,
|
||
Heart,
|
||
MessageCircle,
|
||
Trophy,
|
||
} from "lucide-react";
|
||
import Image from "next/image";
|
||
import { GamificationBadges } from "@/components/profile/GamificationBadges";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card";
|
||
import { Progress } from "@/components/ui/progress";
|
||
import { getGamificationBadgeImagePath, getRankBadgeId } from "@/lib/gamification-badges";
|
||
import { Link } from "@/lib/router";
|
||
import type { GamificationSummarySchema } from "@/lib/types";
|
||
import { formatNumberPersian } from "@/lib/utils";
|
||
|
||
function ScoreBreakdown({
|
||
icon: Icon,
|
||
label,
|
||
points,
|
||
}: {
|
||
icon: typeof CalendarCheck2;
|
||
label: string;
|
||
points: number;
|
||
}) {
|
||
return (
|
||
<div className="flex min-w-0 items-center justify-between gap-3 rounded-xl border border-border/70 bg-muted/20 px-4 py-3">
|
||
<div className="flex min-w-0 items-center gap-2 text-sm text-muted-foreground">
|
||
<Icon className="h-4 w-4 shrink-0 text-primary" aria-hidden="true" />
|
||
<span>{label}</span>
|
||
</div>
|
||
<span className="shrink-0 font-semibold text-foreground">{formatNumberPersian(points)}</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function GamificationCard({
|
||
summary,
|
||
isLoading,
|
||
isError,
|
||
onRetry,
|
||
}: {
|
||
summary?: GamificationSummarySchema;
|
||
isLoading: boolean;
|
||
isError: boolean;
|
||
onRetry: () => void;
|
||
}) {
|
||
if (isLoading) {
|
||
return (
|
||
<Card className="border-border/70 shadow-sm">
|
||
<CardContent className="space-y-4 p-6" aria-busy="true" aria-label="در حال دریافت رتبه و نشانها">
|
||
<div className="h-6 w-40 animate-pulse rounded bg-muted" />
|
||
<div className="h-10 w-full animate-pulse rounded bg-muted" />
|
||
<div className="grid gap-3 sm:grid-cols-2"><div className="h-12 animate-pulse rounded bg-muted" /><div className="h-12 animate-pulse rounded bg-muted" /></div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
if (isError || !summary) {
|
||
return (
|
||
<Card className="border-border/70 shadow-sm">
|
||
<CardContent className="flex flex-col items-start gap-3 p-6 text-right sm:flex-row sm:items-center sm:justify-between">
|
||
<div>
|
||
<p className="font-semibold">رتبه و نشانها بارگذاری نشد</p>
|
||
<p className="mt-1 text-sm text-muted-foreground">برای دریافت دوباره اطلاعات تلاش کنید.</p>
|
||
</div>
|
||
<Button type="button" variant="outline" onClick={onRetry}>تلاش دوباره</Button>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
const currentRankBadgeId = getRankBadgeId(summary.current_level.id);
|
||
const currentRankImage = getGamificationBadgeImagePath(currentRankBadgeId);
|
||
|
||
return (
|
||
<Card className="border-border/70 shadow-sm">
|
||
<CardHeader className="flex flex-row items-start justify-between gap-3 text-right">
|
||
<div className="space-y-1">
|
||
<CardTitle className="text-xl">رتبه و دستاوردهای من</CardTitle>
|
||
<CardDescription>سطح مادامالعمر بر اساس مشارکتهای معتبر در انجمن</CardDescription>
|
||
</div>
|
||
<Button asChild type="button" variant="outline" size="icon" className="h-10 w-10 shrink-0" aria-label="راهنمای رتبهبندی و نشانها">
|
||
<Link to="/rank-guide" target="_blank" rel="noopener noreferrer" title="بازکردن راهنما در زبانه جدید">
|
||
<CircleHelp className="h-5 w-5" aria-hidden="true" />
|
||
</Link>
|
||
</Button>
|
||
</CardHeader>
|
||
<CardContent className="space-y-5">
|
||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||
<div className="flex min-w-0 items-center gap-3">
|
||
{currentRankImage ? (
|
||
<Image src={currentRankImage} alt="" width={112} height={112} sizes="112px" className="h-24 w-24 shrink-0 object-contain sm:h-28 sm:w-28" />
|
||
) : null}
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">سطح فعلی</p>
|
||
<p className="mt-1 text-2xl font-bold">{summary.current_level.label}</p>
|
||
</div>
|
||
</div>
|
||
<div className="inline-flex items-center gap-2 rounded-full bg-primary/10 px-3 py-2 text-sm font-semibold text-primary">
|
||
<Trophy className="h-4 w-4" aria-hidden="true" />
|
||
{formatNumberPersian(summary.points_total)} امتیاز
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
{summary.next_level ? (
|
||
<div className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
||
<span className="text-muted-foreground">پیشرفت تا سطح {summary.next_level.label}</span>
|
||
<span className="font-medium">{formatNumberPersian(summary.points_to_next)} امتیاز تا سطح بعدی</span>
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-muted-foreground">بالاترین سطح را دریافت کردهاید.</p>
|
||
)}
|
||
<Progress
|
||
value={summary.progress_percent}
|
||
aria-label="پیشرفت تا سطح بعدی"
|
||
aria-valuetext={`${formatNumberPersian(summary.progress_percent)} درصد`}
|
||
className="h-2.5 bg-muted"
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid gap-3 sm:grid-cols-2">
|
||
<ScoreBreakdown icon={CalendarCheck2} label="رویدادها" points={summary.points_event} />
|
||
<ScoreBreakdown icon={MessageCircle} label="بلاگ" points={summary.points_blog} />
|
||
</div>
|
||
|
||
<section className="space-y-3 border-t border-border/70 pt-4">
|
||
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
||
<Heart className="h-4 w-4 text-primary" aria-hidden="true" />نشانهای کسبشده
|
||
</h3>
|
||
<GamificationBadges badges={summary.badges} excludeIds={[currentRankBadgeId]} />
|
||
</section>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|