Skip to content
8 changes: 5 additions & 3 deletions frontend/__tests__/unit/pages/About.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,9 +411,11 @@ describe('About Component', () => {
render(<About />)
})
await waitFor(() => {
// Look for the element with alt text "Loading indicator"
const spinner = screen.getAllByAltText('Loading indicator')
expect(spinner.length).toBeGreaterThan(0)
// Check for skeleton loading state by looking for skeleton containers
const skeletonContainers = document.querySelectorAll(
String.raw`.bg-gray-100.dark\:bg-gray-800`
)
expect(skeletonContainers.length).toBeGreaterThan(0)
})
})

Expand Down
4 changes: 2 additions & 2 deletions frontend/__tests__/unit/pages/Snapshots.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ describe('SnapshotsPage', () => {
render(<SnapshotsPage />)

await waitFor(() => {
const loadingSpinners = screen.getAllByAltText('Loading indicator')
expect(loadingSpinners.length).toBe(2)
const loadingSkeletons = screen.getAllByRole('status')
expect(loadingSkeletons.length).toBeGreaterThan(0)
})
})

Expand Down
48 changes: 27 additions & 21 deletions frontend/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ import {
import AnchorTitle from 'components/AnchorTitle'
import AnimatedCounter from 'components/AnimatedCounter'
import Leaders from 'components/Leaders'
import LoadingSpinner from 'components/LoadingSpinner'
import Markdown from 'components/MarkdownWrapper'
import SecondaryCard from 'components/SecondaryCard'
import AboutSkeleton from 'components/skeletons/AboutSkeleton'
import TopContributorsList from 'components/TopContributorsList'

const leaders = {
Expand All @@ -50,6 +50,26 @@ const leaders = {

const projectKey = 'nest'

const getMilestoneStatus = (progress: number): string => {
if (progress === 100) {
return 'Completed'
}
if (progress > 0) {
return 'In Progress'
}
return 'Not Started'
}

const getMilestoneIcon = (progress: number) => {
if (progress === 100) {
return faCircleCheck
}
if (progress > 0) {
return faUserGear
}
return faClock
}

const About = () => {
const {
data: projectMetadataResponse,
Expand Down Expand Up @@ -100,7 +120,7 @@ const About = () => {
const isLoading = projectMetadataLoading || topContributorsLoading || leadersLoading

if (isLoading) {
return <LoadingSpinner />
return <AboutSkeleton />
}

if (!projectMetadata || !topContributors) {
Expand Down Expand Up @@ -218,28 +238,14 @@ const About = () => {
</Link>
<Tooltip
closeDelay={100}
content={
milestone.progress === 100
? 'Completed'
: milestone.progress > 0
? 'In Progress'
: 'Not Started'
}
content={getMilestoneStatus(milestone.progress)}
id={`tooltip-state-${index}`}
delay={100}
placement="top"
showArrow
>
<span className="absolute top-0 right-0 text-xl text-gray-400">
<FontAwesomeIcon
icon={
milestone.progress === 100
? faCircleCheck
: milestone.progress > 0
? faUserGear
: faClock
}
/>
<FontAwesomeIcon icon={getMilestoneIcon(milestone.progress)} />
</span>
</Tooltip>
</div>
Expand All @@ -251,8 +257,8 @@ const About = () => {
</SecondaryCard>
)}
<SecondaryCard icon={faScroll} title={<AnchorTitle title="Our Story" />}>
{projectStory.map((text, index) => (
<div key={`story-${index}`} className="mb-4">
{projectStory.map((text) => (
<div key={`story-${text.substring(0, 50).replaceAll(' ', '-')}`} className="mb-4">
<div>
<Markdown content={text} />
</div>
Expand All @@ -268,7 +274,7 @@ const About = () => {
)}
<div
aria-hidden="true"
className="absolute top-[10px] left-0 h-3 w-3 rounded-full bg-gray-400"
className="absolute top-2.5 left-0 h-3 w-3 rounded-full bg-gray-400"
></div>
<div>
<h3 className="text-lg font-semibold text-blue-400">{milestone.title}</h3>
Expand Down
34 changes: 19 additions & 15 deletions frontend/src/app/snapshots/page.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The skeleton state is narrower than the final page layout, which causes a visible jump on reload.
Could you update this to match the Snapshot page layout?

Snapshots-.-OWASP-Nest.mp4

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import React, { useState, useEffect } from 'react'
import FontAwesomeIconWrapper from 'wrappers/FontAwesomeIconWrapper'
import { GetCommunitySnapshotsDocument } from 'types/__generated__/snapshotQueries.generated'
import type { Snapshot } from 'types/snapshot'
import LoadingSpinner from 'components/LoadingSpinner'
import SnapshotSkeleton from 'components/skeletons/SnapshotSkeleton'
import SnapshotCard from 'components/SnapshotCard'

const SnapshotsPage: React.FC = () => {
Expand Down Expand Up @@ -41,7 +41,7 @@ const SnapshotsPage: React.FC = () => {

const renderSnapshotCard = (snapshot: Snapshot) => {
const SubmitButton = {
label: 'View Details',
label: 'View Snapshot',
icon: <FontAwesomeIconWrapper icon="fa-solid fa-right-to-bracket" />,
onclick: () => handleButtonClick(snapshot),
}
Expand All @@ -57,22 +57,26 @@ const SnapshotsPage: React.FC = () => {
)
}

if (isLoading) {
return <LoadingSpinner />
}

return (
<div className="min-h-screen p-8 text-gray-600 dark:bg-[#212529] dark:text-gray-300">
<div className="text-text flex min-h-screen w-full flex-col items-center justify-normal p-5">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{!snapshots?.length ? (
<div className="col-span-full py-8 text-center">No Snapshots found</div>
) : (
snapshots.map((snapshot: Snapshot) => (
<div key={snapshot.key}>{renderSnapshotCard(snapshot)}</div>
))
)}
</div>
{isLoading ? (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{Array.from({ length: 12 }, (_, index) => (
<SnapshotSkeleton key={`snapshot-skeleton-${index}`} />
))}
</div>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{!snapshots?.length ? (
<div className="col-span-full py-8 text-center">No Snapshots found</div>
) : (
snapshots.map((snapshot: Snapshot) => (
<div key={snapshot.key}>{renderSnapshotCard(snapshot)}</div>
))
)}
</div>
)}
</div>
</div>
)
Expand Down
23 changes: 21 additions & 2 deletions frontend/src/components/SkeletonsBase.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { Skeleton } from '@heroui/skeleton'
import LoadingSpinner from 'components/LoadingSpinner'
import AboutSkeleton from 'components/skeletons/AboutSkeleton'
import CardSkeleton from 'components/skeletons/Card'
import SnapshotSkeleton from 'components/skeletons/SnapshotSkeleton'
import UserCardSkeleton from 'components/skeletons/UserCard'

function userCardRender() {
const cardCount = 12
return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{Array.from({ length: cardCount }).map((_, index) => (
<UserCardSkeleton key={index} />
{Array.from({ length: cardCount }, (_, index) => (
<UserCardSkeleton key={`user-skeleton-${index}`} />
))}
</div>
)
}

function snapshotCardRender() {
const cardCount = 12
return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{Array.from({ length: cardCount }, (_, index) => (
<SnapshotSkeleton key={`snapshot-skeleton-${index}`} />
))}
</div>
)
Expand Down Expand Up @@ -49,6 +62,12 @@ const SkeletonBase = ({
break
case 'users':
return userCardRender()
case 'organizations':
return userCardRender()
case 'snapshots':
return snapshotCardRender()
case 'about':
return <AboutSkeleton />
default:
return <LoadingSpinner imageUrl={loadingImageUrl} />
}
Expand Down
156 changes: 156 additions & 0 deletions frontend/src/components/skeletons/AboutSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Skeleton } from '@heroui/skeleton'

const AboutSkeleton = () => {
return (
<div className="min-h-screen p-8 text-gray-600 dark:bg-[#212529] dark:text-gray-300">
<div className="mx-auto max-w-6xl">
{/* Title Skeleton */}
<Skeleton className="mt-4 mb-6 h-10 w-32" />

{/* Our Mission and Who It's For Grid */}
<div className="grid gap-0 md:grid-cols-2 md:gap-6">
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-40" />
<Skeleton className="mb-2 h-4 w-full" />
<Skeleton className="mb-2 h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-40" />
<Skeleton className="mb-2 h-4 w-full" />
<Skeleton className="mb-2 h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
</div>

{/* Key Features Section */}
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-40" />
<div className="grid gap-4 md:grid-cols-2">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="rounded-lg bg-gray-200 p-4 dark:bg-gray-700">
<Skeleton className="mb-2 h-5 w-3/4" />
<Skeleton className="mb-2 h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
))}
</div>
</div>

{/* Leaders Section */}
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-32" />
<div className="grid gap-4 md:grid-cols-3">
{[1, 2, 3].map((i) => (
<div key={i} className="flex flex-col items-center">
<Skeleton className="mb-3 h-24 w-24 rounded-full" />
<Skeleton className="mb-2 h-5 w-32" />
<Skeleton className="h-4 w-24" />
</div>
))}
</div>
</div>

{/* Top Contributors Section */}
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-48" />
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-4">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((i) => (
<div key={i} className="flex flex-col items-center">
<Skeleton className="mb-2 h-16 w-16 rounded-full" />
<Skeleton className="mb-1 h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
))}
</div>
</div>

{/* Technologies Section */}
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-52" />
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<div key={i}>
<Skeleton className="mb-3 h-5 w-32" />
<div className="flex flex-col gap-3">
{[1, 2, 3, 4].map((j) => (
<div key={j} className="flex items-center gap-2">
<Skeleton className="h-6 w-6 rounded" />
<Skeleton className="h-4 w-24" />
</div>
))}
</div>
</div>
))}
</div>
</div>

{/* Get Involved Section */}
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-40" />
<Skeleton className="mb-2 h-4 w-full" />
<div className="mb-6 space-y-2">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-4 w-full" />
))}
</div>
<Skeleton className="h-4 w-2/3" />
</div>

{/* Roadmap Section */}
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-32" />
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div key={i} className="rounded-lg bg-gray-200 p-6 dark:bg-gray-700">
<Skeleton className="mb-2 h-6 w-2/3" />
<Skeleton className="mb-2 h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
))}
</div>
</div>

{/* Our Story Section */}
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-32" />
{[1, 2, 3].map((i) => (
<div key={i} className="mb-4">
<Skeleton className="mb-2 h-4 w-full" />
<Skeleton className="mb-2 h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
))}
</div>

{/* Project Timeline Section */}
<div className="mb-6 rounded-lg bg-gray-100 p-6 dark:bg-gray-800">
<Skeleton className="mb-4 h-6 w-48" />
<div className="space-y-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="relative pl-10">
<Skeleton className="absolute top-[10px] left-0 h-3 w-3 rounded-full" />
<Skeleton className="mb-1 h-5 w-48" />
<Skeleton className="mb-2 h-4 w-24" />
<Skeleton className="mb-1 h-4 w-full" />
<Skeleton className="h-4 w-2/3" />
</div>
))}
</div>
</div>

{/* Stats Grid */}
<div className="grid gap-0 md:grid-cols-4 md:gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="mb-6 rounded-lg bg-gray-100 p-6 text-center dark:bg-gray-800">
<Skeleton className="mx-auto mb-2 h-8 w-20" />
<Skeleton className="mx-auto h-4 w-24" />
</div>
))}
</div>
</div>
</div>
)
}

export default AboutSkeleton
Loading