Files
guilan-ace-frontend/src/components/profile/GamificationCard.tsx

146 lines
6.0 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.
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>
);
}