diff --git a/frontend/__tests__/src/pages/Chapters.test.tsx b/frontend/__tests__/src/pages/Chapters.test.tsx index 820a4135f..64189123d 100644 --- a/frontend/__tests__/src/pages/Chapters.test.tsx +++ b/frontend/__tests__/src/pages/Chapters.test.tsx @@ -35,11 +35,11 @@ describe('ChaptersPage Component', () => { jest.clearAllMocks() }) - test('renders loading spinner initially', async () => { + test('renders skeleton initially', async () => { render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + const skeletonLoaders = screen.getAllByRole('status') + expect(skeletonLoaders.length).toBeGreaterThan(0) }) }) @@ -86,9 +86,9 @@ describe('ChaptersPage Component', () => { }) render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') + const skeletonLoaders = screen.getAllByRole('status') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + expect(skeletonLoaders.length).toBeGreaterThan(0) expect(screen.queryByText('Next Page')).not.toBeInTheDocument() }) await waitFor(() => { @@ -97,7 +97,7 @@ describe('ChaptersPage Component', () => { expect(screen.getByText('Next Page')).toBeInTheDocument() }) - expect(screen.queryByAltText('Loading indicator')).not.toBeInTheDocument() + expect(screen.queryByTestId('status')).not.toBeInTheDocument() }) test('opens window on View Details button click', async () => { const navigateMock = jest.fn() diff --git a/frontend/__tests__/src/pages/CommitteeDetails.test.tsx b/frontend/__tests__/src/pages/CommitteeDetails.test.tsx index 9df0dd72b..7edf1898d 100644 --- a/frontend/__tests__/src/pages/CommitteeDetails.test.tsx +++ b/frontend/__tests__/src/pages/CommitteeDetails.test.tsx @@ -35,11 +35,11 @@ describe('Committees Component', () => { jest.clearAllMocks() }) - test('renders loading spinner initially', async () => { + test('renders skeleton initially', async () => { render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + const skeletonLoaders = screen.getAllByRole('status') + expect(skeletonLoaders.length).toBeGreaterThan(0) }) }) diff --git a/frontend/__tests__/src/pages/Committees.test.tsx b/frontend/__tests__/src/pages/Committees.test.tsx index b7d7ff9d6..0c50adec9 100644 --- a/frontend/__tests__/src/pages/Committees.test.tsx +++ b/frontend/__tests__/src/pages/Committees.test.tsx @@ -36,11 +36,11 @@ describe('Committees Component', () => { jest.clearAllMocks() }) - test('renders loading spinner initially', async () => { + test('renders skeleton initially', async () => { render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + const skeletonLoaders = screen.getAllByRole('status') + expect(skeletonLoaders.length).toBeGreaterThan(0) }) }) @@ -53,11 +53,8 @@ describe('Committees Component', () => { render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) - - expect(screen.queryByText('Next Page')).not.toBeInTheDocument() + expect(screen.queryByRole('status')).not.toBeInTheDocument() }) await waitFor(() => { @@ -65,7 +62,7 @@ describe('Committees Component', () => { expect(screen.getByText('Committee 1')).toBeInTheDocument() expect(screen.getByText('Next Page')).toBeInTheDocument() }) - expect(screen.queryByAltText('Loading indicator')).not.toBeInTheDocument() + expect(screen.queryByRole('status')).not.toBeInTheDocument() }) test('renders committee data correctly', async () => { diff --git a/frontend/__tests__/src/pages/Contribute.test.tsx b/frontend/__tests__/src/pages/Contribute.test.tsx index 0612de8f7..699584374 100644 --- a/frontend/__tests__/src/pages/Contribute.test.tsx +++ b/frontend/__tests__/src/pages/Contribute.test.tsx @@ -27,11 +27,11 @@ describe('Contribute Component', () => { jest.clearAllMocks() }) - test('renders loading spinner initially', async () => { + test('renders skeleton initially', async () => { render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + const skeletonLoaders = screen.getAllByRole('status') + expect(skeletonLoaders.length).toBeGreaterThan(0) }) }) diff --git a/frontend/__tests__/src/pages/Projects.test.tsx b/frontend/__tests__/src/pages/Projects.test.tsx index 5f3104341..ca7e0b682 100644 --- a/frontend/__tests__/src/pages/Projects.test.tsx +++ b/frontend/__tests__/src/pages/Projects.test.tsx @@ -35,11 +35,11 @@ describe('ProjectPage Component', () => { jest.clearAllMocks() }) - test('renders loading spinner initially', async () => { + test('renders skeleton initially', async () => { render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + const skeletonLoaders = screen.getAllByRole('status') + expect(skeletonLoaders.length).toBeGreaterThan(0) }) }) @@ -52,9 +52,9 @@ describe('ProjectPage Component', () => { render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') + const skeletonLoaders = screen.getAllByRole('status') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + expect(skeletonLoaders.length).toBeGreaterThan(0) expect(screen.queryByText('Next Page')).not.toBeInTheDocument() }) await waitFor(() => { @@ -63,7 +63,7 @@ describe('ProjectPage Component', () => { expect(screen.getByText('Next Page')).toBeInTheDocument() }) - expect(screen.queryByAltText('Loading indicator')).not.toBeInTheDocument() + expect(screen.queryByTestId('status')).not.toBeInTheDocument() }) test('renders project data correctly', async () => { diff --git a/frontend/__tests__/src/pages/Users.test.tsx b/frontend/__tests__/src/pages/Users.test.tsx index f12617140..edbdceea8 100644 --- a/frontend/__tests__/src/pages/Users.test.tsx +++ b/frontend/__tests__/src/pages/Users.test.tsx @@ -39,11 +39,11 @@ describe('UsersPage Component', () => { jest.clearAllMocks() }) - test('renders loading spinner initially', async () => { + test('renders skeleton initially', async () => { render() - const loadingSpinner = screen.getAllByAltText('Loading indicator') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + const skeletonLoaders = screen.getAllByRole('status') + expect(skeletonLoaders.length).toBeGreaterThan(0) }) }) @@ -52,9 +52,9 @@ describe('UsersPage Component', () => { render() // Check loading state - const loadingSpinner = screen.getAllByAltText('Loading indicator') + const skeletonLoaders = screen.getAllByRole('status') await waitFor(() => { - expect(loadingSpinner.length).toBeGreaterThan(0) + expect(skeletonLoaders.length).toBeGreaterThan(0) expect(screen.queryByText('Next Page')).not.toBeInTheDocument() }) @@ -65,7 +65,7 @@ describe('UsersPage Component', () => { expect(screen.getByText('Next Page')).toBeInTheDocument() }) - expect(screen.queryByAltText('Loading indicator')).not.toBeInTheDocument() + expect(screen.queryByTestId('status')).not.toBeInTheDocument() }) test('renders user cards correctly', async () => { diff --git a/frontend/src/components/SearchPageLayout.tsx b/frontend/src/components/SearchPageLayout.tsx index dd56a377d..1985b8468 100644 --- a/frontend/src/components/SearchPageLayout.tsx +++ b/frontend/src/components/SearchPageLayout.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' -import LoadingSpinner from 'components/LoadingSpinner' import Pagination from 'components/Pagination' import SearchBar from 'components/Search' +import SkeletonBase from 'components/SkeletonsBase' interface SearchPageLayoutProps { isLoaded: boolean @@ -52,9 +52,7 @@ const SearchPageLayout = ({ /> {!isSearchBarReady || !isLoaded ? ( -
- -
+ ) : ( <>
diff --git a/frontend/src/components/SkeletonsBase.tsx b/frontend/src/components/SkeletonsBase.tsx new file mode 100644 index 000000000..2a0fcd9e7 --- /dev/null +++ b/frontend/src/components/SkeletonsBase.tsx @@ -0,0 +1,53 @@ +import LoadingSpinner from 'components/LoadingSpinner' +import CardSkeleton from 'components/skeletons/Card' +import UserCardSkeleton from 'components/skeletons/UserCard' +import { Skeleton } from 'components/ui/Skeleton' + +function userCardRender() { + const cardCount = 12 + return ( +
+ {Array.from({ length: cardCount }).map((_, index) => ( + + ))} +
+ ) +} + +const SkeletonBase = ({ indexName, loadingImageUrl }) => { + let Component + switch (indexName) { + case 'chapters': + Component = () => + break + case 'issues': + Component = () => ( + + ) + break + case 'projects': + Component = () => + break + case 'committees': + Component = () => + break + case 'users': + return userCardRender() + default: + return + } + return ( +
+ {indexName == 'chapters' ? ( + + ) : ( + + )} + + + +
+ ) +} + +export default SkeletonBase diff --git a/frontend/src/components/skeletons/Card.tsx b/frontend/src/components/skeletons/Card.tsx new file mode 100644 index 000000000..59b175df4 --- /dev/null +++ b/frontend/src/components/skeletons/Card.tsx @@ -0,0 +1,82 @@ +import { Box, Flex } from '@chakra-ui/react' +import type React from 'react' +import { CardSkeletonProps } from 'types/skeleton' +import { Skeleton, SkeletonCircle, SkeletonText } from 'components/ui/Skeleton' + +const CardSkeleton: React.FC = ({ + showLevel = true, + showIcons = 4, + showProjectName = true, + showSummary = true, + showLink = true, + showContributors = true, + showSocial = true, + showActionButton = true, +}) => { + const NUM_CONTRIBUTORS = 8 + + return ( +
+ + + {/* Header Section */} + + + {showLevel && } + + {showProjectName && } + + + + {showIcons && ( + + {Array.from({ length: showIcons }).map((_, i) => ( + + ))} + + + )} + + + {/* Link Section */} + {showLink && } + + {/* Description Section */} + {showSummary && } + + {/* Footer Section */} + +
+ {showContributors && ( + + {[...Array(NUM_CONTRIBUTORS)].map((_, i) => ( + + ))} + + )} + {showSocial && ( + + + + + + + + + )} +
+ + + {showActionButton && } + +
+
+
+
+ ) +} + +export default CardSkeleton diff --git a/frontend/src/components/skeletons/UserCard.tsx b/frontend/src/components/skeletons/UserCard.tsx new file mode 100644 index 000000000..fe9ef4f2c --- /dev/null +++ b/frontend/src/components/skeletons/UserCard.tsx @@ -0,0 +1,37 @@ +import { Box, Flex } from '@chakra-ui/react' +import type React from 'react' +import { UserCardSkeletonProps } from 'types/skeleton' +import { Skeleton, SkeletonCircle } from 'components/ui/Skeleton' + +const UserCardSkeleton: React.FC = ({ + showAvatar = true, + showName = true, + showViewProfile = true, +}) => { + return ( + + + {showAvatar && ( + + + + )} + + + {showName && } + + + + {showViewProfile && ( + + + + )} + + ) +} + +export default UserCardSkeleton diff --git a/frontend/src/components/ui/Skeleton.tsx b/frontend/src/components/ui/Skeleton.tsx new file mode 100644 index 000000000..76b729ef5 --- /dev/null +++ b/frontend/src/components/ui/Skeleton.tsx @@ -0,0 +1,37 @@ +import type { SkeletonProps as ChakraSkeletonProps, CircleProps } from '@chakra-ui/react' +import { Skeleton as ChakraSkeleton, Circle, Stack } from '@chakra-ui/react' +import * as React from 'react' + +export interface SkeletonCircleProps extends ChakraSkeletonProps { + size?: CircleProps['size'] +} + +export const SkeletonCircle = React.forwardRef( + function SkeletonCircle(props, ref) { + const { size, ...rest } = props + return ( + + + + ) + } +) + +export interface SkeletonTextProps extends ChakraSkeletonProps { + noOfLines?: number +} + +export const SkeletonText = React.forwardRef( + function SkeletonText(props, ref) { + const { noOfLines = 3, gap, ...rest } = props + return ( + + {Array.from({ length: noOfLines }).map((_, index) => ( + + ))} + + ) + } +) + +export const Skeleton = ChakraSkeleton diff --git a/frontend/src/pages/CommitteeDetails.tsx b/frontend/src/pages/CommitteeDetails.tsx index c3eab91c2..b19596b14 100644 --- a/frontend/src/pages/CommitteeDetails.tsx +++ b/frontend/src/pages/CommitteeDetails.tsx @@ -5,7 +5,7 @@ import { getFilteredIcons, handleSocialUrls } from 'utils/utility' import { ErrorDisplay } from 'wrappers/ErrorWrapper' import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper' import Card from 'components/Card' -import LoadingSpinner from 'components/LoadingSpinner' +import CardSkeleton from 'components/skeletons/Card' const CommitteeDetailsPage = () => { const { committeeKey } = useParams() @@ -26,8 +26,10 @@ const CommitteeDetailsPage = () => { }, [committeeKey]) if (isLoading) return ( -
- +
+
+ +
) diff --git a/frontend/src/types/skeleton.ts b/frontend/src/types/skeleton.ts new file mode 100644 index 000000000..f4556d7de --- /dev/null +++ b/frontend/src/types/skeleton.ts @@ -0,0 +1,16 @@ +export interface CardSkeletonProps { + showLevel?: boolean + showIcons?: number + showProjectName?: boolean + showSummary?: boolean + showLink?: boolean + showContributors?: boolean + showSocial?: boolean + showActionButton?: boolean +} + +export interface UserCardSkeletonProps { + showAvatar?: boolean + showName?: boolean + showViewProfile?: boolean +}