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
+}