diff --git a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
index 3881dd3d55..7b96134ef4 100644
--- a/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/OrganizationDetails.test.tsx
@@ -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()
- 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()
})
})
@@ -201,6 +202,7 @@ describe('OrganizationDetailsPage', () => {
})
})
})
+
test('does not render sponsor block', async () => {
;(useQuery as unknown as jest.Mock).mockReturnValue({
data: mockOrganizationDetailsData,
diff --git a/frontend/__tests__/unit/pages/UserDetails.test.tsx b/frontend/__tests__/unit/pages/UserDetails.test.tsx
index 3af7b6d26a..0181f2b598 100644
--- a/frontend/__tests__/unit/pages/UserDetails.test.tsx
+++ b/frontend/__tests__/unit/pages/UserDetails.test.tsx
@@ -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()
- 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()
})
})
@@ -137,10 +139,9 @@ describe('UserDetailsPage', () => {
render()
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()
@@ -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,
diff --git a/frontend/src/app/members/[memberKey]/page.tsx b/frontend/src/app/members/[memberKey]/page.tsx
index e4a56e148d..62fb71362a 100644
--- a/frontend/src/app/members/[memberKey]/page.tsx
+++ b/frontend/src/app/members/[memberKey]/page.tsx
@@ -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 }>()
@@ -100,7 +100,11 @@ const UserDetailsPage: React.FC = () => {
})
if (isLoading) {
- return
+ return (
+
+
+
+ )
}
if (!isLoading && user == null) {
diff --git a/frontend/src/app/organizations/[organizationKey]/page.tsx b/frontend/src/app/organizations/[organizationKey]/page.tsx
index 1541f91d95..052606b050 100644
--- a/frontend/src/app/organizations/[organizationKey]/page.tsx
+++ b/frontend/src/app/organizations/[organizationKey]/page.tsx
@@ -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)
@@ -47,7 +47,11 @@ const OrganizationDetailsPage = () => {
}, [graphQLData, graphQLRequestError, organizationKey])
if (isLoading) {
- return
+ return (
+
+
+
+ )
}
if (!isLoading && !graphQLData?.organization) {
diff --git a/frontend/src/components/SkeletonsBase.tsx b/frontend/src/components/SkeletonsBase.tsx
index 7d9fb64395..96577819bd 100644
--- a/frontend/src/components/SkeletonsBase.tsx
+++ b/frontend/src/components/SkeletonsBase.tsx
@@ -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 (
@@ -68,6 +69,11 @@ const SkeletonBase = ({
return snapshotCardRender()
case 'about':
return
+ case 'member-details':
+ case 'members':
+ return
+ case 'organizations-details':
+ return
default:
return
}
diff --git a/frontend/src/components/skeletons/MemberDetailsPageSkeleton.tsx b/frontend/src/components/skeletons/MemberDetailsPageSkeleton.tsx
new file mode 100644
index 0000000000..387b295924
--- /dev/null
+++ b/frontend/src/components/skeletons/MemberDetailsPageSkeleton.tsx
@@ -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 (
+
+
+
+ {/* User Summary Card - bg-gray-100 like SecondaryCard */}
+
+
+ {/* Avatar */}
+
+
+ {/* User Info and Heatmap */}
+
+
+ {/* Username with badge */}
+
+
+
+ {/* Bio */}
+
+
+
+ {/* Heatmap - Only on desktop */}
+
+
+
+
+
+ {/* Main Grid - User Details and Statistics */}
+
+ {/* User Details Card */}
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+
+ {/* Statistics Card */}
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
+ ))}
+
+
+
+
+ {/* Two Column Layout - Issues and Milestones */}
+
+
+ {/* Two Column Layout - Pull Requests and Releases */}
+
+
+ {/* Repositories Section */}
+
+
+
+
+ {[1, 2, 3, 4].map((i) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
+
+export default MemberDetailsPageSkeleton
diff --git a/frontend/src/components/skeletons/OrganizationDetailsPageSkeleton.tsx b/frontend/src/components/skeletons/OrganizationDetailsPageSkeleton.tsx
new file mode 100644
index 0000000000..77fdd8ff62
--- /dev/null
+++ b/frontend/src/components/skeletons/OrganizationDetailsPageSkeleton.tsx
@@ -0,0 +1,121 @@
+import { Skeleton } from '@heroui/skeleton'
+import {
+ CardSection,
+ PageWrapper,
+ SectionHeader,
+ TitleSection,
+ TwoColumnSection,
+} from 'components/skeletons/sharedSkeletons'
+
+const OrganizationDetailsPageSkeleton = () => {
+ return (
+
+
+
+ {/* Description - empty for organization */}
+
+
+ {/* Main Grid - Organization Details and Statistics */}
+
+ {/* Organization Details - Takes 5 columns */}
+
+
+
+ {Array.from({ length: 4 }, (_, i) => (
+
+
+
+ ))}
+
+
+
+ {/* Statistics - Takes 2 columns */}
+
+
+
+ {Array.from({ length: 5 }, (_, i) => (
+
+
+
+ ))}
+
+
+
+
+ {/* Top Contributors */}
+
+
+
+ {Array.from({ length: 12 }, (_, i) => (
+
+ ))}
+
+
+
+ {/* Two Column Layout - Recent Issues and Milestones */}
+
+
+ {/* Two Column Layout - Pull Requests and Releases */}
+
+
+ {/* Repositories */}
+
+
+
+ {Array.from({ length: 4 }, (_, i) => (
+
+ ))}
+
+
+
+
+
+
+ )
+}
+
+export default OrganizationDetailsPageSkeleton
diff --git a/frontend/src/components/skeletons/sharedSkeletons.tsx b/frontend/src/components/skeletons/sharedSkeletons.tsx
new file mode 100644
index 0000000000..72e2d2fb6b
--- /dev/null
+++ b/frontend/src/components/skeletons/sharedSkeletons.tsx
@@ -0,0 +1,129 @@
+import { Skeleton } from '@heroui/skeleton'
+import type { ReactNode } from 'react'
+
+export const ItemCardSkeleton = ({ titleWidth }: { titleWidth: string }) => (
+
+)
+
+export const SectionSkeleton = ({
+ titleWidth,
+ itemCount,
+ itemKeyPrefix,
+ titleSkeletonWidth,
+ minHeight,
+}: {
+ titleWidth: string
+ itemCount: number
+ itemKeyPrefix: string
+ titleSkeletonWidth: string
+ minHeight?: string
+}) => (
+
+
+
+
+
+
+ {Array.from({ length: itemCount }, (_, i) => (
+
+ ))}
+
+
+)
+
+export const PageWrapper = ({
+ children,
+ ariaBusy = true,
+}: {
+ children: ReactNode
+ ariaBusy?: boolean
+}) => (
+
+)
+
+export const TitleSection = ({
+ skeletonClassName = 'h-10 w-64 rounded',
+}: {
+ skeletonClassName?: string
+}) => (
+
+)
+
+export const TwoColumnSection = ({
+ sections,
+}: {
+ sections: Array<{
+ keyPrefix: string
+ titleWidth: string
+ itemTitleWidth: string
+ minHeight?: string
+ }>
+}) => (
+
+ {sections.map(({ keyPrefix, titleWidth, itemTitleWidth, minHeight }) => (
+
+ ))}
+
+)
+
+export const SectionHeader = ({
+ titleSkeletonWidth,
+ rounded = false,
+}: {
+ titleSkeletonWidth: string
+ rounded?: boolean
+}) => (
+
+
+
+
+)
+
+export const CardSection = ({
+ children,
+ className = '',
+ minHeight,
+ colSpan,
+}: {
+ children: ReactNode
+ className?: string
+ minHeight?: string
+ colSpan?: string
+}) => (
+
+ {children}
+
+)