feat: add profile rank badges and guide
This commit is contained in:
145
src/components/profile/GamificationCard.tsx
Normal file
145
src/components/profile/GamificationCard.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user