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 */} + +
+ ); +}