Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/features/landing/components/Hero.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('Hero component layout shift prevention', () => {
display: { activeProjects: '—', contributors: '—', grantsDistributed: '—' },
isLoading: true,
error: null,
refetch: vi.fn(),
})
renderHero()
// Image placeholder should be present
Expand All @@ -46,6 +47,7 @@ describe('Hero component layout shift prevention', () => {
display: { activeProjects: '10', contributors: '200', grantsDistributed: '5000' },
isLoading: false,
error: null,
refetch: vi.fn(),
})
renderHero()
expect(screen.queryByTestId('stat-skeleton')).not.toBeInTheDocument()
Expand Down
80 changes: 73 additions & 7 deletions src/features/landing/pages/LandingPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,24 @@ vi.mock('../../../shared/contexts/AuthContext', () => ({
useAuth: () => ({ isAuthenticated: false, logout: vi.fn() }),
}))

let mockUseLandingStats: {
display: { activeProjects: string; contributors: string; grantsDistributed: string }
isLoading: boolean
error: string | null
refetch: ReturnType<typeof vi.fn>
} = {
display: {
activeProjects: '1,234',
contributors: '5,678',
grantsDistributed: '$2.1M',
},
isLoading: false,
error: null,
refetch: vi.fn(),
}

vi.mock('../../../shared/hooks/useLandingStats', () => ({
useLandingStats: () => ({
display: {
activeProjects: '1,234',
contributors: '5,678',
grantsDistributed: '$2.1M',
},
}),
useLandingStats: () => mockUseLandingStats,
}))

vi.mock('../../../shared/utils/logger', () => ({
Expand Down Expand Up @@ -171,3 +181,59 @@ describe('ImageWithFallback security', () => {
expect(images.length).toBe(3)
})
})

describe('WhyChooseUs stats', () => {
beforeEach(() => {
mockUseLandingStats = {
display: {
activeProjects: '1,234',
contributors: '5,678',
grantsDistributed: '$2.1M',
},
isLoading: false,
error: null,
refetch: vi.fn(),
}
})

it('shows formatted stat values on success', () => {
renderWithRouter(<LandingPage />)
expect(screen.getAllByText('5,678').length).toBeGreaterThanOrEqual(1)
expect(screen.getAllByText('1,234').length).toBeGreaterThanOrEqual(1)
expect(screen.getAllByText('$2.1M').length).toBeGreaterThanOrEqual(1)
})

it('shows skeleton loaders while loading', () => {
mockUseLandingStats = {
...mockUseLandingStats,
isLoading: true,
}
renderWithRouter(<LandingPage />)
const skeletons = screen.getAllByTestId('skeleton-loader')
// At least 2 skeleton loaders for the stat values (Active Users, Projects Funded)
expect(skeletons.length).toBeGreaterThanOrEqual(2)
})

it('shows retry buttons on error', () => {
mockUseLandingStats = {
...mockUseLandingStats,
error: 'Failed to load stats',
}
renderWithRouter(<LandingPage />)
const retryBtns = screen.getAllByTestId(/^retry-/)
expect(retryBtns.length).toBeGreaterThanOrEqual(2)
})

it('calls refetch when retry button is clicked', () => {
const refetchMock = vi.fn()
mockUseLandingStats = {
...mockUseLandingStats,
error: 'Failed to load stats',
refetch: refetchMock,
}
renderWithRouter(<LandingPage />)
const retryBtn = screen.getByTestId('retry-contributors')
retryBtn.click()
expect(refetchMock).toHaveBeenCalledTimes(1)
})
})
28 changes: 26 additions & 2 deletions src/features/landing/pages/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ import {
CheckCircle,
Star,
Quote,
DollarSign,
RefreshCw,
} from 'lucide-react'
import { useTheme } from '../../../shared/contexts/ThemeContext'
import { useLandingStats } from '../../../shared/hooks/useLandingStats'
import { SkeletonLoader } from '../../../shared/components/SkeletonLoader'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { logger } from '../../../shared/utils/logger'
Expand Down Expand Up @@ -258,7 +261,7 @@ function HowItWorks() {

function WhyChooseUs() {
const { theme } = useTheme()
const { display } = useLandingStats()
const { display, isLoading, error, refetch } = useLandingStats()

const benefits = [
'Verified and vetted projects from trusted organizations',
Expand Down Expand Up @@ -348,11 +351,19 @@ function WhyChooseUs() {
icon: Users,
label: 'Active Users',
value: display.contributors,
statKey: 'contributors' as const,
},
{
icon: Award,
label: 'Projects Funded',
value: display.activeProjects,
statKey: 'activeProjects' as const,
},
{
icon: DollarSign,
label: 'Grants Distributed',
value: display.grantsDistributed,
statKey: 'grantsDistributed' as const,
},
].map((item, index) => (
<div
Expand All @@ -376,7 +387,20 @@ function WhyChooseUs() {
theme === 'dark' ? 'text-[#e8dfd0]' : 'text-[#2d2820]'
}`}
>
{item.value}
{item.statKey && isLoading ? (
<SkeletonLoader width="60px" height="24px" />
) : item.statKey && error ? (
<button
onClick={refetch}
className="text-xs text-[#c9983a] hover:underline flex items-center gap-1"
data-testid={`retry-${item.statKey}`}
>
<RefreshCw className="w-3 h-3" />
Retry
</button>
) : (
item.value
)}
</span>
</div>
))}
Expand Down
19 changes: 16 additions & 3 deletions src/shared/hooks/useLandingStats.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';

import { getLandingStats, type LandingStats } from '../api/client';
import { useTranslation } from '../i18n';
Expand Down Expand Up @@ -40,12 +40,25 @@ export function useLandingStats() {
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);

const fetchStats = useCallback(async () => {
setIsLoading(true);
try {
const s = await getLandingStats();
setStats(s);
setError(null);
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load stats');
} finally {
setIsLoading(false);
}
}, []);

useEffect(() => {
let isMounted = true;

(async () => {
setIsLoading(true);
try {
setIsLoading(true);
const s = await getLandingStats();
if (!isMounted) return;
setStats(s);
Expand Down Expand Up @@ -80,7 +93,7 @@ export function useLandingStats() {
};
}, [stats, locale]);

return { stats, display, isLoading, error };
return { stats, display, isLoading, error, refetch: fetchStats };
}


Loading