diff --git a/frontend/src/components/bounty/BountyGrid.tsx b/frontend/src/components/bounty/BountyGrid.tsx index 7709ab94c..9e1359fb2 100644 --- a/frontend/src/components/bounty/BountyGrid.tsx +++ b/frontend/src/components/bounty/BountyGrid.tsx @@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'; import { motion } from 'framer-motion'; import { ChevronDown, Loader2, Plus } from 'lucide-react'; import { BountyCard } from './BountyCard'; +import { BountyCardSkeleton } from '../ui/Skeleton'; import { useInfiniteBounties } from '../../hooks/useBounties'; import { staggerContainer, staggerItem } from '../../lib/animations'; @@ -74,12 +75,7 @@ export function BountyGrid() { {isLoading && (
{Array.from({ length: 6 }).map((_, i) => ( -
-
-
+ ))}
)} diff --git a/frontend/src/components/profile/ProfileDashboard.tsx b/frontend/src/components/profile/ProfileDashboard.tsx index 2c509d7aa..4f57d5f29 100644 --- a/frontend/src/components/profile/ProfileDashboard.tsx +++ b/frontend/src/components/profile/ProfileDashboard.tsx @@ -4,6 +4,7 @@ import { Clock, GitPullRequest, DollarSign, Settings } from 'lucide-react'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts'; import { useAuth } from '../../hooks/useAuth'; import { useBounties } from '../../hooks/useBounties'; +import { ProfileSkeleton } from '../ui/Skeleton'; import { timeAgo, formatCurrency } from '../../lib/utils'; import { fadeIn, staggerContainer, staggerItem } from '../../lib/animations'; import type { Bounty } from '../../types/bounty'; @@ -35,7 +36,7 @@ function BountyStatusBadge({ status }: { status: string }) { function MyBountiesTab({ bounties, loading }: { bounties: Bounty[]; loading: boolean }) { if (loading) { - return
Loading...
; + return ; } if (!bounties.length) { return ( diff --git a/frontend/src/components/ui/Skeleton.tsx b/frontend/src/components/ui/Skeleton.tsx new file mode 100644 index 000000000..7121b47fa --- /dev/null +++ b/frontend/src/components/ui/Skeleton.tsx @@ -0,0 +1,127 @@ +import React from 'react'; + +interface SkeletonProps { + className?: string; +} + +/** Base skeleton block with shimmer animation */ +export function Skeleton({ className = '' }: SkeletonProps) { + return ( +
+ ); +} + +/** Bounty card skeleton — matches BountyCard layout */ +export function BountyCardSkeleton() { + return ( +
+ {/* Row 1: Repo + Tier */} +
+ + +
+ {/* Row 2: Title */} +
+ + +
+ {/* Row 3: Skills */} +
+ + +
+ {/* Separator */} +
+ {/* Row 4: Reward + Meta */} +
+ +
+ + +
+
+
+ ); +} + +/** Leaderboard row skeleton */ +export function LeaderboardRowSkeleton() { + return ( +
+ + +
+ + +
+
+ + +
+
+ ); +} + +/** Podium skeleton for top 3 */ +export function PodiumSkeleton() { + return ( +
+ {/* 2nd place */} +
+ + +
+ {/* 1st place */} +
+ + +
+ {/* 3rd place */} +
+ + +
+
+ ); +} + +/** Profile page skeleton */ +export function ProfileSkeleton() { + return ( +
+ {/* Profile header */} +
+ +
+ + +
+
+ {/* Stats cards */} +
+ {[1, 2, 3].map((i) => ( +
+ + +
+ ))} +
+ {/* Recent activity */} +
+ + {[1, 2, 3, 4].map((i) => ( +
+ +
+ + +
+ +
+ ))} +
+
+ ); +} diff --git a/frontend/src/pages/LeaderboardPage.tsx b/frontend/src/pages/LeaderboardPage.tsx index f79caff97..dbc4e875b 100644 --- a/frontend/src/pages/LeaderboardPage.tsx +++ b/frontend/src/pages/LeaderboardPage.tsx @@ -4,6 +4,7 @@ import { PageLayout } from '../components/layout/PageLayout'; import { PodiumCards } from '../components/leaderboard/PodiumCards'; import { LeaderboardTable } from '../components/leaderboard/LeaderboardTable'; import { useLeaderboard } from '../hooks/useLeaderboard'; +import { PodiumSkeleton, LeaderboardRowSkeleton } from '../components/ui/Skeleton'; import type { TimePeriod } from '../types/leaderboard'; import { fadeIn } from '../lib/animations'; @@ -47,8 +48,13 @@ export function LeaderboardPage() { {/* Loading */} {isLoading && ( -
-
+
+ +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
)}