diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1c78aeb02..7835630af 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,6 +1,7 @@
import React, { Suspense } from 'react';
-import { Routes, Route } from 'react-router-dom';
+import { Routes, Route, useLocation } from 'react-router-dom';
import { AuthGuard } from './components/auth/AuthGuard';
+import { BountyGridSkeleton, LeaderboardSkeleton, ProfileSkeleton, Skeleton } from './components/ui/Skeleton';
// Lazy load pages
const HomePage = React.lazy(() => import('./pages/HomePage').then((m) => ({ default: m.HomePage })));
@@ -14,11 +15,49 @@ const BountiesPage = React.lazy(() => import('./pages/BountiesPage').then((m) =>
const NotFoundPage = React.lazy(() => import('./pages/NotFoundPage').then((m) => ({ default: m.NotFoundPage })));
function PageLoader() {
- return (
-
- );
+ const { pathname } = useLocation();
+
+ const fallback = (() => {
+ if (pathname.startsWith('/leaderboard')) {
+ return (
+
+
+
+
+ );
+ }
+ if (pathname.startsWith('/profile')) {
+ return ;
+ }
+ if (pathname.startsWith('/bounties') && !pathname.includes('/create')) {
+ return (
+
+
+
+
+ );
+ }
+ if (pathname === '/' || pathname.startsWith('/how-it-works')) {
+ return (
+
+ );
+ }
+ return (
+
+ );
+ })();
+
+ return fallback;
}
export default function App() {
diff --git a/frontend/src/components/ui/Skeleton.tsx b/frontend/src/components/ui/Skeleton.tsx
new file mode 100644
index 000000000..b7bd290ea
--- /dev/null
+++ b/frontend/src/components/ui/Skeleton.tsx
@@ -0,0 +1,120 @@
+import React from "react";
+
+/**
+ * Base shimmer skeleton block.
+ * Used as a building block for page-specific skeleton layouts.
+ */
+interface SkeletonProps {
+ className?: string;
+}
+
+export function Skeleton({ className = "" }: SkeletonProps) {
+ return (
+
+ );
+}
+
+/**
+ * BountyCardSkeleton — matches the shape of BountyCard layout
+ */
+export function BountyCardSkeleton() {
+ return (
+
+ {/* Row 1: Repo + Tier */}
+
+ {/* Title */}
+
+
+ {/* Language dots */}
+
+
+
+
+
+ {/* Separator */}
+
+ {/* Reward + Meta */}
+
+
+ );
+}
+
+/**
+ * Grid of bounty card skeletons
+ */
+export function BountyGridSkeleton({ count = 6 }: { count?: number }) {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => (
+
+ ))}
+
+ );
+}
+
+/**
+ * LeaderboardTableSkeleton — matches LeaderboardTable layout
+ */
+export function LeaderboardSkeleton({ rows = 5 }: { rows?: number }) {
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+ {/* Rows */}
+ {Array.from({ length: rows }).map((_, i) => (
+
+
+
+
+
+
+
+ ))}
+
+ );
+}
+
+/**
+ * ProfileSkeleton — matches ProfileDashboard layout
+ */
+export function ProfileSkeleton() {
+ return (
+
+ {/* Avatar + Name */}
+
+ {/* Stats row */}
+
+
+
+
+
+ {/* Activity section */}
+
+
+ );
+}