Skip to content
6 changes: 3 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,9 @@
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('.bg-gray-100.dark\\:bg-gray-800')

Check warning on line 415 in frontend/__tests__/unit/pages/About.test.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

`String.raw` should be used to avoid escaping `\`.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZrTsa0om_AGdvKrIhXm&open=AZrTsa0om_AGdvKrIhXm&pullRequest=2757
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
4 changes: 2 additions & 2 deletions frontend/src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
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 Down Expand Up @@ -100,7 +100,7 @@
const isLoading = projectMetadataLoading || topContributorsLoading || leadersLoading

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

if (!projectMetadata || !topContributors) {
Expand Down Expand Up @@ -221,9 +221,9 @@
content={
milestone.progress === 100
? 'Completed'
: milestone.progress > 0
? 'In Progress'
: 'Not Started'

Check warning on line 226 in frontend/src/app/about/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZrTsayem_AGdvKrIhXi&open=AZrTsayem_AGdvKrIhXi&pullRequest=2757
}
id={`tooltip-state-${index}`}
delay={100}
Expand All @@ -235,9 +235,9 @@
icon={
milestone.progress === 100
? faCircleCheck
: milestone.progress > 0
? faUserGear
: faClock

Check warning on line 240 in frontend/src/app/about/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZrTsayem_AGdvKrIhXj&open=AZrTsayem_AGdvKrIhXj&pullRequest=2757
}
/>
</span>
Expand All @@ -252,7 +252,7 @@
)}
<SecondaryCard icon={faScroll} title={<AnchorTitle title="Our Story" />}>
{projectStory.map((text, index) => (
<div key={`story-${index}`} className="mb-4">

Check warning on line 255 in frontend/src/app/about/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZrTsayem_AGdvKrIhXk&open=AZrTsayem_AGdvKrIhXk&pullRequest=2757
<div>
<Markdown content={text} />
</div>
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 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 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 @@
)
}

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 }).map((_, index) => (
<SnapshotSkeleton key={index} />

Check warning on line 66 in frontend/src/app/snapshots/page.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZrTsa0Qm_AGdvKrIhXl&open=AZrTsa0Qm_AGdvKrIhXl&pullRequest=2757
))}
</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
22 changes: 20 additions & 2 deletions frontend/src/components/SkeletonsBase.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
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} />
<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 }).map((_, index) => (
<SnapshotSkeleton key={`snapshot-skeleton-${index}`} />

Check warning on line 23 in frontend/src/components/SkeletonsBase.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not use Array index in keys

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZrTsasVm_AGdvKrIhXg&open=AZrTsasVm_AGdvKrIhXg&pullRequest=2757
))}
</div>
)
Expand Down Expand Up @@ -49,6 +61,12 @@
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
39 changes: 39 additions & 0 deletions frontend/src/components/skeletons/SnapshotSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Skeleton } from '@heroui/skeleton'
import type React from 'react'

interface SnapshotSkeletonProps {
showTitle?: boolean
showDateRange?: boolean
showViewButton?: boolean
}

const SnapshotSkeleton: React.FC<SnapshotSkeletonProps> = ({
showTitle = true,
showDateRange = true,
showViewButton = true,
}) => {
return (
<div
role="status"
className="group flex h-40 w-full flex-col items-center rounded-lg bg-white p-6 text-left shadow-lg dark:bg-gray-800 dark:shadow-gray-900/30"
>

Check warning on line 19 in frontend/src/components/skeletons/SnapshotSkeleton.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use <output> instead of the "status" role to ensure accessibility across all devices.

See more on https://sonarcloud.io/project/issues?id=OWASP_Nest&issues=AZrTsawkm_AGdvKrIhXh&open=AZrTsawkm_AGdvKrIhXh&pullRequest=2757
<div className="text-center">{showTitle && <Skeleton className="h-7 w-56 rounded-md" />}</div>

{showDateRange && (
<div className="mt-2 flex items-center gap-2">
<Skeleton className="h-4 w-4 rounded" />
<Skeleton className="h-5 w-48 rounded-md" />
</div>
)}

{showViewButton && (
<div className="mt-auto flex items-center gap-2">
<Skeleton className="h-5 w-28 rounded-md" />
<Skeleton className="h-4 w-4 rounded" />
</div>
)}
</div>
)
}

export default SnapshotSkeleton