Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ describe('OrganizationDetailsPage', () => {
test('renders loading state', async () => {
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: null,
loading: true,
error: null,
})

render(<OrganizationDetailsPage />)

const loadingSpinner = screen.getAllByAltText('Loading indicator')
// Use semantic role query instead of CSS selectors for better stability
await waitFor(() => {
expect(loadingSpinner.length).toBeGreaterThan(0)
expect(screen.getByTestId('org-loading-skeleton')).toBeInTheDocument()
})
})

Expand Down Expand Up @@ -201,6 +202,7 @@ describe('OrganizationDetailsPage', () => {
})
})
})

test('does not render sponsor block', async () => {
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: mockOrganizationDetailsData,
Expand Down
10 changes: 6 additions & 4 deletions frontend/__tests__/unit/pages/UserDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,15 @@ describe('UserDetailsPage', () => {
test('renders loading state', async () => {
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: null,
loading: true,
error: null,
})

render(<UserDetailsPage />)
const loadingSpinner = screen.getAllByAltText('Loading indicator')

// Use semantic role query instead of CSS selectors for better stability
await waitFor(() => {
expect(loadingSpinner.length).toBeGreaterThan(0)
expect(screen.getByTestId('user-loading-skeleton')).toBeInTheDocument()
})
})

Expand All @@ -137,10 +139,9 @@ describe('UserDetailsPage', () => {
render(<UserDetailsPage />)

await waitFor(() => {
expect(screen.queryByAltText('Loading indicator')).not.toBeInTheDocument()
expect(screen.getByText('Test User')).toBeInTheDocument()
})

expect(screen.getByText('Test User')).toBeInTheDocument()
expect(screen.getByText('Statistics')).toBeInTheDocument()
expect(screen.getByText('Test Company')).toBeInTheDocument()
expect(screen.getByText('Test Location')).toBeInTheDocument()
Expand Down Expand Up @@ -540,6 +541,7 @@ describe('UserDetailsPage', () => {
expect(bioContainer).toHaveClass('lg:text-left')
})
})

test('does not render sponsor block', async () => {
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: mockUserDetailsData,
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/app/members/[memberKey]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { formatDate } from 'utils/dateFormatter'
import { drawContributions, fetchHeatmapData, HeatmapData } from 'utils/helpers/githubHeatmap'
import Badges from 'components/Badges'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
import MemberDetailsPageSkeleton from 'components/skeletons/MemberDetailsPageSkeleton'

const UserDetailsPage: React.FC = () => {
const { memberKey } = useParams<{ memberKey: string }>()
Expand Down Expand Up @@ -100,7 +100,11 @@ const UserDetailsPage: React.FC = () => {
})

if (isLoading) {
return <LoadingSpinner />
return (
<div data-testid="user-loading-skeleton">
<MemberDetailsPageSkeleton />
</div>
)
}

if (!isLoading && user == null) {
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/app/organizations/[organizationKey]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { handleAppError, ErrorDisplay } from 'app/global-error'
import { GetOrganizationDataDocument } from 'types/__generated__/organizationQueries.generated'
import { formatDate } from 'utils/dateFormatter'
import DetailsCard from 'components/CardDetailsPage'
import LoadingSpinner from 'components/LoadingSpinner'
import OrganizationDetailsPageSkeleton from 'components/skeletons/OrganizationDetailsPageSkeleton'
const OrganizationDetailsPage = () => {
const { organizationKey } = useParams<{ organizationKey: string }>()
const [organization, setOrganization] = useState(null)
Expand Down Expand Up @@ -47,7 +47,11 @@ const OrganizationDetailsPage = () => {
}, [graphQLData, graphQLRequestError, organizationKey])

if (isLoading) {
return <LoadingSpinner />
return (
<div data-testid="org-loading-skeleton">
<OrganizationDetailsPageSkeleton />
</div>
)
}

if (!isLoading && !graphQLData?.organization) {
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/components/SkeletonsBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { Skeleton } from '@heroui/skeleton'
import LoadingSpinner from 'components/LoadingSpinner'
import AboutSkeleton from 'components/skeletons/AboutSkeleton'
import CardSkeleton from 'components/skeletons/Card'
import MemberDetailsPageSkeleton from 'components/skeletons/MemberDetailsPageSkeleton'
import OrganizationDetailsPageSkeleton from 'components/skeletons/OrganizationDetailsPageSkeleton'
import SnapshotSkeleton from 'components/skeletons/SnapshotSkeleton'
import UserCardSkeleton from 'components/skeletons/UserCard'

function userCardRender() {
const cardCount = 12
return (
Expand Down Expand Up @@ -68,6 +69,11 @@ const SkeletonBase = ({
return snapshotCardRender()
case 'about':
return <AboutSkeleton />
case 'member-details':
case 'members':
return <MemberDetailsPageSkeleton />
case 'organizations-details':
return <OrganizationDetailsPageSkeleton />
default:
return <LoadingSpinner imageUrl={loadingImageUrl} />
}
Expand Down
163 changes: 163 additions & 0 deletions frontend/src/components/skeletons/MemberDetailsPageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { Skeleton } from '@heroui/skeleton'
import React from 'react'
import {
CardSection,
PageWrapper,
SectionHeader,
TitleSection,
TwoColumnSection,
} from 'components/skeletons/sharedSkeletons'

const MemberDetailsPageSkeleton: React.FC = () => {
return (
<PageWrapper>
<TitleSection skeletonClassName="h-10 w-64" />

{/* User Summary Card - bg-gray-100 like SecondaryCard */}
<div className="mt-6 mb-8 rounded-lg bg-gray-100 p-6 shadow-md dark:bg-gray-800">
<div className="mt-4 flex flex-col items-center lg:flex-row">
{/* Avatar */}
<Skeleton
className="mb-4 h-[200px] w-[200px] shrink-0 rounded-full lg:mr-6 lg:mb-0"
aria-hidden="true"
/>

{/* User Info and Heatmap */}
<div className="w-full text-center lg:text-left">
<div className="pl-0 lg:pl-4">
{/* Username with badge */}
<div className="mb-1 flex items-center justify-center gap-3 text-center text-sm lg:justify-start lg:text-left">
<Skeleton className="h-6 w-40" aria-hidden="true" />
</div>
{/* Bio */}
<Skeleton className="mb-1 h-4 w-full max-w-xl" aria-hidden="true" />
</div>

{/* Heatmap - Only on desktop */}
<div className="mt-4 hidden w-full lg:block">
<div className="overflow-hidden rounded-lg bg-white dark:bg-gray-800">
<Skeleton className="h-32 w-full" aria-hidden="true" />
</div>
</div>
</div>
</div>
</div>

{/* Main Grid - User Details and Statistics */}
<div className="grid grid-cols-1 gap-6 md:grid-cols-7">
{/* User Details Card */}
<CardSection minHeight="min-h-[210px]" className="gap-2" colSpan="md:col-span-5">
<SectionHeader titleSkeletonWidth="w-36" />
<div>
{[1, 2, 3, 4].map((i) => (
<div key={`detail-${i}`} className="pb-1">
<div className="flex flex-row gap-1">
<Skeleton className="h-5 w-20" aria-hidden="true" />
<Skeleton className="h-5 w-32" aria-hidden="true" />
</div>
</div>
))}
</div>
</CardSection>

{/* Statistics Card */}
<CardSection className="gap-2" colSpan="md:col-span-2">
<SectionHeader titleSkeletonWidth="w-24" />
<div>
{[1, 2, 3, 4].map((i) => (
<div key={`stat-${i}`}>
<div className="pb-1">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4" aria-hidden="true" />
<Skeleton className="h-4 w-16" aria-hidden="true" />
<Skeleton className="h-4 w-20" aria-hidden="true" />
</div>
</div>
</div>
))}
</div>
</CardSection>
</div>

{/* Two Column Layout - Issues and Milestones */}
<TwoColumnSection
sections={[
{
keyPrefix: 'issue',
titleWidth: 'w-32',
itemTitleWidth: 'w-4/5',
minHeight: 'min-h-[600px]',
},
{
keyPrefix: 'milestone',
titleWidth: 'w-40',
itemTitleWidth: 'w-4/5',
minHeight: 'min-h-[600px]',
},
]}
/>

{/* Two Column Layout - Pull Requests and Releases */}
<TwoColumnSection
sections={[
{
keyPrefix: 'pr',
titleWidth: 'w-48',
itemTitleWidth: 'w-3/4',
minHeight: 'min-h-[600px]',
},
{
keyPrefix: 'release',
titleWidth: 'w-48',
itemTitleWidth: 'w-3/4',
minHeight: 'min-h-[600px]',
},
]}
/>

{/* Repositories Section */}
<CardSection>
<SectionHeader titleSkeletonWidth="w-32" />
<div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<div
key={`repo-${i}`}
className="flex h-48 w-full flex-col gap-3 rounded-lg border-1 border-gray-200 p-4 shadow-xs ease-in-out hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
>
<Skeleton className="h-5 w-3/4" aria-hidden="true" />
<div className="flex flex-col gap-2 text-sm">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4" aria-hidden="true" />
<Skeleton className="h-4 w-10" aria-hidden="true" />
<Skeleton className="h-4 w-10" aria-hidden="true" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4" aria-hidden="true" />
<Skeleton className="h-4 w-10" aria-hidden="true" />
<Skeleton className="h-4 w-12" aria-hidden="true" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4" aria-hidden="true" />
<Skeleton className="h-4 w-16" aria-hidden="true" />
<Skeleton className="h-4 w-20" aria-hidden="true" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4" aria-hidden="true" />
<Skeleton className="h-4 w-10" aria-hidden="true" />
<Skeleton className="h-4 w-10" aria-hidden="true" />
</div>
</div>
</div>
))}
</div>
<div className="mt-4 flex justify-start">
<Skeleton className="h-10 w-24 rounded-md" aria-hidden="true" />
</div>
</div>
</CardSection>
</PageWrapper>
)
}

export default MemberDetailsPageSkeleton
Loading