Skip to content
Open
Show file tree
Hide file tree
Changes from 10 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
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
}
Comment on lines +62 to +70
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Undefined icon reference will cause runtime error.

Line 67 references faUserGear, but this icon is not imported. The imports at line 12 show faUsersGear (plural, not singular).

Apply this diff to fix the reference:

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

Alternatively, if you intended to use the singular faUserGear icon, add it to the imports:

 import {
   faCircleCheck,
   faClock,
   faMapSigns,
   faScroll,
   faUsers,
   faTools,
   faBullseye,
   faUser,
+  faUserGear,
   faUsersGear,
 } from '@fortawesome/free-solid-svg-icons'

Note: Semantically, neither faUserGear nor faUsersGear clearly conveys "in progress" status. Consider using a more intuitive icon for this state.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getMilestoneIcon = (progress: number) => {
if (progress === 100) {
return faCircleCheck
}
if (progress > 0) {
return faUserGear
}
return faClock
}
const getMilestoneIcon = (progress: number) => {
if (progress === 100) {
return faCircleCheck
}
if (progress > 0) {
return faUsersGear
}
return faClock
}
🤖 Prompt for AI Agents
In frontend/src/app/about/page.tsx around lines 62 to 70, the code references an
undefined icon identifier faUserGear which causes a runtime error; replace the
reference with the imported faUsersGear (or alternatively add faUserGear to the
import list if you intended the singular icon) so the returned icon matches an
actual import, and optionally evaluate/replace with a more semantically
appropriate "in progress" icon if desired.


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