feat(frontend): add skeleton loading state for dashboard
Replace simple spinner with skeleton loading placeholders that match the dashboard layout structure, providing better visual feedback during data loading.
This commit is contained in:
115
frontend/src/components/common/Skeleton.jsx
Normal file
115
frontend/src/components/common/Skeleton.jsx
Normal file
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Skeleton loading components for dashboard
|
||||
*/
|
||||
|
||||
// Base skeleton with animation
|
||||
const SkeletonBase = ({ className = '' }) => (
|
||||
<div className={`animate-pulse bg-gray-200 rounded ${className}`} />
|
||||
);
|
||||
|
||||
// Skeleton for event cards
|
||||
export const EventCardSkeleton = () => (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<SkeletonBase className="h-6 w-48" />
|
||||
<SkeletonBase className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<SkeletonBase className="h-4 w-4" />
|
||||
<SkeletonBase className="h-4 w-32" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SkeletonBase className="h-4 w-4" />
|
||||
<SkeletonBase className="h-4 w-40" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SkeletonBase className="h-4 w-4" />
|
||||
<SkeletonBase className="h-4 w-28" />
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonBase className="h-10 w-full" />
|
||||
</div>
|
||||
);
|
||||
|
||||
// Skeleton for match cards
|
||||
export const MatchCardSkeleton = () => (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<SkeletonBase className="h-12 w-12 rounded-full" />
|
||||
<div className="flex-1">
|
||||
<SkeletonBase className="h-5 w-36 mb-1" />
|
||||
<SkeletonBase className="h-4 w-24 mb-2" />
|
||||
<SkeletonBase className="h-4 w-32 mb-3" />
|
||||
<div className="flex gap-4">
|
||||
<SkeletonBase className="h-4 w-24" />
|
||||
<SkeletonBase className="h-4 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonBase className="h-10 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Skeleton for request cards
|
||||
export const RequestCardSkeleton = () => (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<SkeletonBase className="h-10 w-10 rounded-full" />
|
||||
<div className="flex-1">
|
||||
<SkeletonBase className="h-5 w-32 mb-1" />
|
||||
<SkeletonBase className="h-4 w-24 mb-1" />
|
||||
<SkeletonBase className="h-4 w-28" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<SkeletonBase className="h-8 w-8 rounded-full" />
|
||||
<SkeletonBase className="h-8 w-8 rounded-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Dashboard skeleton layout
|
||||
export const DashboardSkeleton = () => (
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header skeleton */}
|
||||
<div className="mb-8">
|
||||
<SkeletonBase className="h-9 w-48 mb-2" />
|
||||
<SkeletonBase className="h-5 w-64" />
|
||||
</div>
|
||||
|
||||
{/* Events section */}
|
||||
<section className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<SkeletonBase className="h-7 w-36" />
|
||||
<SkeletonBase className="h-5 w-24" />
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<EventCardSkeleton />
|
||||
<EventCardSkeleton />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Matches section */}
|
||||
<section className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<SkeletonBase className="h-7 w-40" />
|
||||
<SkeletonBase className="h-5 w-20" />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<MatchCardSkeleton />
|
||||
<MatchCardSkeleton />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Requests section */}
|
||||
<section className="mb-8">
|
||||
<SkeletonBase className="h-7 w-44 mb-4" />
|
||||
<div className="space-y-3">
|
||||
<RequestCardSkeleton />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default DashboardSkeleton;
|
||||
@@ -6,6 +6,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { dashboardAPI, matchesAPI } from '../services/api';
|
||||
import { connectSocket, disconnectSocket, getSocket } from '../services/socket';
|
||||
import HeatBadges from '../components/heats/HeatBadges';
|
||||
import { DashboardSkeleton } from '../components/common/Skeleton';
|
||||
import {
|
||||
Calendar,
|
||||
MapPin,
|
||||
@@ -139,12 +140,7 @@ const DashboardPage = () => {
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout>
|
||||
<div className="max-w-5xl mx-auto flex items-center justify-center min-h-[400px]">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Loader2 className="w-12 h-12 animate-spin text-primary-600" />
|
||||
<p className="text-gray-600">Loading dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardSkeleton />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,12 +59,14 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should show loading spinner while fetching data', async () => {
|
||||
it('should show skeleton loading state while fetching data', async () => {
|
||||
dashboardAPI.getData.mockImplementation(() => new Promise(() => {}));
|
||||
|
||||
renderWithRouter(<DashboardPage />);
|
||||
|
||||
expect(screen.getByText('Loading dashboard...')).toBeInTheDocument();
|
||||
// Skeleton uses animate-pulse class for loading animation
|
||||
const skeletons = document.querySelectorAll('.animate-pulse');
|
||||
expect(skeletons.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -410,14 +412,13 @@ describe('DashboardPage', () => {
|
||||
|
||||
renderWithRouter(<DashboardPage />);
|
||||
|
||||
// Wait for content to load
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('John Lead')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Find the accept button by title attribute
|
||||
const acceptButton = document.querySelector('button[title="Accept"]');
|
||||
expect(acceptButton).toBeTruthy();
|
||||
|
||||
// Click accept using fireEvent which is synchronous
|
||||
const acceptButton = screen.getByRole('button', { name: /accept/i });
|
||||
fireEvent.click(acceptButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
Reference in New Issue
Block a user