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
107 changes: 107 additions & 0 deletions src/app/[locale]/admin/_components/AdminPageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'

interface AdminPanelSkeletonProps {
className?: string
rowCount?: number
}

interface AdminAccordionSkeletonProps {
itemCount: number
showDescription?: boolean
}

export function AdminAccordionSkeleton({ itemCount, showDescription = false }: AdminAccordionSkeletonProps) {
return (
<div className="grid gap-4" role="status" aria-label="Loading admin content">
{Array.from({ length: itemCount }).map((_, index) => (
<div
key={index}
className="flex h-18 items-center justify-between gap-4 rounded-xl border bg-background px-4"
aria-hidden="true"
>
<div className="flex min-w-0 flex-1 items-center gap-3">
<Skeleton className={cn('shrink-0', showDescription ? 'size-9 rounded-lg' : 'size-4')} />
<div className="grid min-w-0 flex-1 gap-1.5">
<Skeleton className="h-4 w-36 max-w-full" />
{showDescription && <Skeleton className="h-3 w-80 max-w-full" />}
</div>
</div>
<Skeleton className="size-6 shrink-0" />
</div>
))}
</div>
)
}

export function AdminPanelSkeleton({ className, rowCount = 3 }: AdminPanelSkeletonProps) {
return (
<section className={cn('grid gap-5 rounded-lg border bg-background p-6', className)} aria-hidden="true">
<div className="flex items-start justify-between gap-4">
<div className="grid min-w-0 flex-1 gap-2">
<Skeleton className="h-5 w-36 max-w-full" />
<Skeleton className="h-3 w-80 max-w-full" />
</div>
<Skeleton className="size-9 shrink-0 rounded-md" />
</div>

<div className="grid gap-4">
{Array.from({ length: rowCount }).map((_, index) => (
<div key={index} className="grid gap-2">
<Skeleton className="h-3 w-28" />
<Skeleton className="h-9 w-full" />
</div>
))}
</div>
</section>
)
}

export function AdminSettingsSkeleton({ sectionCount = 3 }: { sectionCount?: number }) {
return (
<div className="grid gap-4" role="status" aria-label="Loading admin content">
{Array.from({ length: sectionCount }).map((_, index) => (
<AdminPanelSkeleton key={index} rowCount={index === 0 ? 2 : 3} />
))}
</div>
)
}

export function AdminCalendarSkeleton() {
return (
<div
className="grid min-h-[420px] grid-rows-[auto_1fr] gap-4 rounded-sm border bg-background p-4"
role="status"
aria-label="Loading calendar"
>
<div className="flex flex-wrap items-center justify-between gap-3" aria-hidden="true">
<Skeleton className="h-9 w-28" />
<Skeleton className="h-7 w-36" />
<div className="flex gap-2">
<Skeleton className="h-9 w-16" />
<Skeleton className="h-9 w-16" />
<Skeleton className="h-9 w-16" />
</div>
</div>
<div className="grid grid-cols-7 grid-rows-5 gap-px overflow-hidden rounded-sm bg-border" aria-hidden="true">
{Array.from({ length: 35 }).map((_, index) => (
<div key={index} className="bg-background p-2">
<Skeleton className="size-5 rounded-full" />
</div>
))}
</div>
</div>
)
}

export default function AdminPageSkeleton() {
return (
<section className="grid min-w-0 gap-4">
<div className="grid gap-2" aria-hidden="true">
<Skeleton className="h-8 w-44" />
<Skeleton className="h-4 w-96 max-w-full" />
</div>
<AdminSettingsSkeleton />
</section>
)
}
11 changes: 6 additions & 5 deletions src/app/[locale]/admin/affiliate/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { setRequestLocale } from 'next-intl/server'
import { io } from 'next/cache'
import { Suspense } from 'react'
import { AdminPanelSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
import AdminAffiliateContentClient from '@/app/[locale]/admin/affiliate/_components/AdminAffiliateContentClient'
import AdminAffiliateOverview from '@/app/[locale]/admin/affiliate/_components/AdminAffiliateOverview'
import { getAffiliateFeeSettings, getAffiliateFeeSettingsUpdatedAt } from '@/lib/affiliate-fee-settings'
Expand Down Expand Up @@ -44,13 +45,13 @@ function formatIsoUtcFromTimestamp(timestamp: number) {

function AdminAffiliateFallback() {
return (
<>
<div className="grid gap-6" role="status" aria-label="Loading affiliate settings">
<section className="grid gap-6 lg:grid-cols-[1.2fr_1fr]">
<div className="min-h-96 rounded-lg border bg-background" />
<div className="min-h-64 rounded-lg border bg-background" />
<AdminPanelSkeleton className="min-h-96" rowCount={4} />
<AdminPanelSkeleton className="min-h-64" rowCount={2} />
</section>
<div className="min-h-80 rounded-lg border bg-background" />
</>
<AdminPanelSkeleton className="min-h-80" rowCount={3} />
</div>
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useExtracted } from 'next-intl'
import dynamic from 'next/dynamic'
import { useEffect, useReducer, useRef, useState } from 'react'
import { toast } from 'sonner'
import { AdminCalendarSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
import EventIconImage from '@/components/EventIconImage'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
Expand Down Expand Up @@ -37,14 +38,7 @@ import { cn } from '@/lib/utils'
const COPY_EVENT_FALLBACK_ICON_CLASS_NAME = 'flex size-14 items-center justify-center rounded-lg border text-muted-foreground'
const AdminCreateEventCalendarView = dynamic(() => import('./AdminCreateEventCalendarView'), {
ssr: false,
loading: () => (
<div className="
flex min-h-[420px] items-center justify-center rounded-sm border border-dashed text-sm text-muted-foreground
"
>
Loading calendar...
</div>
),
loading: () => <AdminCalendarSkeleton />,
})
const AdminProposersDialog = dynamic(() => import('./AdminProposersDialog'), {
ssr: false,
Expand Down
3 changes: 2 additions & 1 deletion src/app/[locale]/admin/events/calendar/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ArrowLeftIcon } from 'lucide-react'
import { setRequestLocale } from 'next-intl/server'
import { Suspense } from 'react'
import { AdminPanelSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
import AdminCreateEventForm from '@/app/[locale]/admin/events/calendar/_components/AdminCreateEventForm'
import { Button } from '@/components/ui/button'
import { Link } from '@/i18n/navigation'
Expand Down Expand Up @@ -141,7 +142,7 @@ export default async function AdminCreateEventNewPage({
</Button>
</div>

<div className="min-h-40 rounded-xl border bg-background" />
<AdminPanelSkeleton className="min-h-40" rowCount={2} />
</>
)}
>
Expand Down
3 changes: 2 additions & 1 deletion src/app/[locale]/admin/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { SupportedLocale } from '@/i18n/locales'
import type { AdminEventAttentionFilter } from '@/lib/db/queries/admin-event-attention'
import { getExtracted, setRequestLocale } from 'next-intl/server'
import { Suspense } from 'react'
import { DataTableSkeleton } from '@/app/[locale]/admin/_components/DataTableSkeleton'
import AdminEventsTable from '@/app/[locale]/admin/events/_components/AdminEventsTable'
import { isAdminEventAttentionFilter } from '@/lib/db/queries/admin-event-attention'
import { TagRepository } from '@/lib/db/queries/tag'
Expand Down Expand Up @@ -59,7 +60,7 @@ export default async function AdminEventsPage({ params, searchParams }: PageProp
</p>
</div>
<div className="min-w-0">
<Suspense fallback={<div className="min-h-64 rounded-xl border bg-background" />}>
<Suspense fallback={<DataTableSkeleton columnCount={6} rowCount={8} />}>
<AdminEventsContent locale={locale as SupportedLocale} searchParams={searchParams} />
</Suspense>
</div>
Expand Down
3 changes: 2 additions & 1 deletion src/app/[locale]/admin/general/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getExtracted, setRequestLocale } from 'next-intl/server'
import { io } from 'next/cache'
import { Suspense } from 'react'
import AdminGeneralSettingsForm from '@/app/[locale]/admin/(general)/_components/AdminGeneralSettingsForm'
import { AdminAccordionSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
import { parseMarketContextSettings } from '@/lib/ai/market-context-config'
import { MARKET_CONTEXT_VARIABLES } from '@/lib/ai/market-context-template'
import { HomeFeaturedEventsRepository } from '@/lib/db/queries/home-featured-events'
Expand All @@ -20,7 +21,7 @@ interface AdminGeneralSettingsPageProps {
}

function AdminGeneralSettingsFallback() {
return <div className="min-h-96 rounded-lg border bg-background" />
return <AdminAccordionSkeleton itemCount={7} />
}

async function AdminGeneralSettingsContent({ locale }: { locale: string }) {
Expand Down
3 changes: 2 additions & 1 deletion src/app/[locale]/admin/integrations/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getExtracted, setRequestLocale } from 'next-intl/server'
import { io } from 'next/cache'
import { Suspense } from 'react'
import { AdminAccordionSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
import AdminIntegrationsForm from '@/app/[locale]/admin/integrations/_components/AdminIntegrationsForm'
import { parseMarketContextSettings } from '@/lib/ai/market-context-config'
import { fetchOpenRouterModels } from '@/lib/ai/openrouter'
Expand All @@ -11,7 +12,7 @@ import { parseSumsubSettings, sanitizeSumsubSettings } from '@/lib/sumsub/settin
import { getThemeSiteSettingsFormState } from '@/lib/theme-settings'

function AdminIntegrationsFallback() {
return <div className="min-h-96 rounded-lg border bg-background" />
return <AdminAccordionSkeleton itemCount={8} showDescription />
}

async function AdminIntegrationsContent({ locale }: { locale: string }) {
Expand Down
27 changes: 24 additions & 3 deletions src/app/[locale]/admin/locales/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
'use cache'

import { getExtracted, setRequestLocale } from 'next-intl/server'
import { connection } from 'next/server'
import { Suspense } from 'react'
import AdminPageSkeleton from '@/app/[locale]/admin/_components/AdminPageSkeleton'
import AdminLocalesSettingsForm from '@/app/[locale]/admin/locales/_components/AdminLocalesSettingsForm'
import { getAutomaticTranslationsEnabledFromSettings, getEnabledLocalesFromSettings } from '@/i18n/locale-settings'
import { SUPPORTED_LOCALES } from '@/i18n/locales'
import { parseOpenRouterProviderSettings } from '@/lib/ai/market-context-config'
import { SettingsRepository } from '@/lib/db/queries/settings'

export default async function AdminLocalesSettingsPage({ params }: PageProps<'/[locale]/admin/locales'>) {
async function DynamicMarker() {
await connection()
return null
}

async function AdminLocalesSettingsContent({ params }: PageProps<'/[locale]/admin/locales'>) {
'use cache'

const { locale } = await params
setRequestLocale(locale)
const t = await getExtracted()
Expand Down Expand Up @@ -36,3 +44,16 @@ export default async function AdminLocalesSettingsPage({ params }: PageProps<'/[
</section>
)
}

export default function AdminLocalesSettingsPage(props: PageProps<'/[locale]/admin/locales'>) {
return (
<>
<Suspense fallback={<AdminPageSkeleton />}>
<AdminLocalesSettingsContent {...props} />
</Suspense>
<Suspense>
<DynamicMarker />
</Suspense>
</>
)
}
3 changes: 2 additions & 1 deletion src/app/[locale]/admin/theme/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { AdminThemeSiteSettingsInitialState } from '@/app/[locale]/admin/th
import { getExtracted, setRequestLocale } from 'next-intl/server'
import { io } from 'next/cache'
import { Suspense } from 'react'
import { AdminSettingsSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
import AdminThemeSettingsForm from '@/app/[locale]/admin/theme/_components/AdminThemeSettingsForm'
import { SettingsRepository } from '@/lib/db/queries/settings'
import { getPublicAssetUrl } from '@/lib/storage'
Expand All @@ -10,7 +11,7 @@ import { getThemeSettingsFormState, getThemeSiteSettingsFormState } from '@/lib/
import { DEFAULT_THEME_SITE_PWA_ICON_192_URL, DEFAULT_THEME_SITE_PWA_ICON_512_URL } from '@/lib/theme-site-identity'

function AdminThemeSettingsFallback() {
return <div className="min-h-96 rounded-lg border bg-background" />
return <AdminSettingsSkeleton sectionCount={3} />
}

async function AdminThemeSettingsContent() {
Expand Down
3 changes: 2 additions & 1 deletion src/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export async function generateViewport(): Promise<Viewport> {
}

export async function generateMetadata(): Promise<Metadata> {
await deferPublicShellPrerenderIfNeeded()
'use cache'
cacheTag(cacheTags.settings)

const runtimeTheme = await loadRuntimeThemeState()
const site = runtimeTheme.site
Expand Down
26 changes: 26 additions & 0 deletions tests/unit/admin-page-skeleton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { render, screen } from '@testing-library/react'
import AdminPageSkeleton, {
AdminCalendarSkeleton,
AdminPanelSkeleton,
} from '@/app/[locale]/admin/_components/AdminPageSkeleton'

describe('admin loading skeletons', () => {
it('renders populated placeholders for route navigation', () => {
const { container } = render(<AdminPageSkeleton />)

expect(screen.getByRole('status', { name: 'Loading admin content' })).toBeInTheDocument()
expect(container.querySelectorAll('[data-slot="skeleton"]').length).toBeGreaterThan(20)
})

it('renders populated panel and calendar placeholders', () => {
const { container } = render(
<>
<AdminPanelSkeleton rowCount={2} />
<AdminCalendarSkeleton />
</>,
)

expect(screen.getByRole('status', { name: 'Loading calendar' })).toBeInTheDocument()
expect(container.querySelectorAll('[data-slot="skeleton"]').length).toBeGreaterThan(40)
})
})