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) => (
+
+ ))}
+
)}