diff --git a/src/components/profile/GamificationCard.tsx b/src/components/profile/GamificationCard.tsx index 062b688..2bf93fe 100644 --- a/src/components/profile/GamificationCard.tsx +++ b/src/components/profile/GamificationCard.tsx @@ -1,13 +1,15 @@ +import { useState, type CSSProperties } from "react"; import { CalendarCheck2, + ChevronLeft, + ChevronRight, 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 { ActivityBadgePaths } from "@/components/profile/ActivityBadgePaths"; import { Card, CardContent, @@ -16,7 +18,10 @@ import { CardTitle, } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; -import { getGamificationBadgeImagePath, getRankBadgeId } from "@/lib/gamification-badges"; +import { + getGamificationBadgeImagePath, + rankLevels, +} from "@/lib/gamification-badges"; import { Link } from "@/lib/router"; import type { GamificationSummarySchema } from "@/lib/types"; import { formatNumberPersian } from "@/lib/utils"; @@ -52,6 +57,8 @@ export function GamificationCard({ isError: boolean; onRetry: () => void; }) { + const [selectedRankIndex, setSelectedRankIndex] = useState(null); + if (isLoading) { return ( @@ -78,15 +85,30 @@ export function GamificationCard({ ); } - const currentRankBadgeId = getRankBadgeId(summary.current_level.id); - const currentRankImage = getGamificationBadgeImagePath(currentRankBadgeId); + const currentRankIndex = Math.max(rankLevels.findIndex((level) => level.id === summary.current_level.id), 0); + const nextRankIndex = summary.next_level + ? rankLevels.findIndex((level) => level.id === summary.next_level?.id) + : -1; + const defaultRankIndex = nextRankIndex >= 0 ? nextRankIndex : currentRankIndex; + const visibleRankIndex = Math.min(selectedRankIndex ?? defaultRankIndex, rankLevels.length - 1); + const visibleRank = rankLevels[visibleRankIndex]; + const visibleRankImage = getGamificationBadgeImagePath(visibleRank.badgeId); + const visibleRankPointTarget = visibleRank.minimumPoints; + const visibleRankPoints = Math.min(Math.max(summary.points_total, 0), visibleRankPointTarget); + const nextLevelPointTarget = summary.next_level?.minimum_points ?? 0; + const nextLevelProgressPercent = nextLevelPointTarget > 0 + ? Math.min(Math.max((summary.points_total / nextLevelPointTarget) * 100, 0), 100) + : 100; + const earnedBadgeIds = new Set(summary.badges.map((badge) => badge.id)); + const visibleRankIsEarned = earnedBadgeIds.has(visibleRank.badgeId); + const visibleRankIsNextLevel = summary.next_level?.id === visibleRank.id; return (
- رتبه و دستاوردهای من - سطح مادام‌العمر بر اساس مشارکت‌های معتبر در انجمن + دستاوردهای من + وضعیت نشان‌ها و جوایز شما بر اساس فعالیت‌ها و مشارکت‌ها در انجمن
-
-
- {currentRankImage ? ( - - ) : null} -
-

سطح فعلی

-

{summary.current_level.label}

+
+
+
+ +
+
+
+

+ {visibleRank.label} +

+

+ {formatNumberPersian(visibleRankPoints).replace(/,/g, "")} از {formatNumberPersian(visibleRankPointTarget).replace(/,/g, "")} امتیاز +

+
+ {visibleRankImage ? ( + visibleRankIsNextLevel && !visibleRankIsEarned ? ( +
+ + +
+ ) : ( + + ) + ) : null} +
+
+
-
-
-
- {summary.next_level ? ( -
- پیشرفت تا سطح {summary.next_level.label} - {formatNumberPersian(summary.points_to_next)} امتیاز تا سطح بعدی -
- ) : ( -

بالاترین سطح را دریافت کرده‌اید.

- )} - -
-
-
-

-

- -
+
+ +
); diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx index f9f131e..e8ee5b1 100644 --- a/src/components/ui/progress.tsx +++ b/src/components/ui/progress.tsx @@ -6,15 +6,16 @@ import { cn } from "@/lib/utils"; const Progress = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, value, ...props }, ref) => ( +>(({ className, value, dir, ...props }, ref) => ( )); diff --git a/src/index.css b/src/index.css index 345813d..85909c4 100644 --- a/src/index.css +++ b/src/index.css @@ -620,3 +620,23 @@ html.dark .event-date-range-modal { animation: none; } } + +@keyframes rank-badge-fill-up { + from { + clip-path: inset(100% 0 0); + } + to { + clip-path: inset(var(--rank-badge-reveal-top) 0 0); + } +} + +.rank-badge-color-fill { + clip-path: inset(var(--rank-badge-reveal-top) 0 0); + animation: rank-badge-fill-up 850ms cubic-bezier(0.22, 0.61, 0.36, 1) both; +} + +@media (prefers-reduced-motion: reduce) { + .rank-badge-color-fill { + animation: none; + } +}