Skip to content

Commit 88ad3af

Browse files
refactor: admin skeletons (kuestcom#1331)
Upstream: f2ff8c7
1 parent 0b8f4cb commit 88ad3af

11 files changed

Lines changed: 177 additions & 22 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Skeleton } from '@/components/ui/skeleton'
2+
import { cn } from '@/lib/utils'
3+
4+
interface AdminPanelSkeletonProps {
5+
className?: string
6+
rowCount?: number
7+
}
8+
9+
interface AdminAccordionSkeletonProps {
10+
itemCount: number
11+
showDescription?: boolean
12+
}
13+
14+
export function AdminAccordionSkeleton({ itemCount, showDescription = false }: AdminAccordionSkeletonProps) {
15+
return (
16+
<div className="grid gap-4" role="status" aria-label="Loading admin content">
17+
{Array.from({ length: itemCount }).map((_, index) => (
18+
<div
19+
key={index}
20+
className="flex h-18 items-center justify-between gap-4 rounded-xl border bg-background px-4"
21+
aria-hidden="true"
22+
>
23+
<div className="flex min-w-0 flex-1 items-center gap-3">
24+
<Skeleton className={cn('shrink-0', showDescription ? 'size-9 rounded-lg' : 'size-4')} />
25+
<div className="grid min-w-0 flex-1 gap-1.5">
26+
<Skeleton className="h-4 w-36 max-w-full" />
27+
{showDescription && <Skeleton className="h-3 w-80 max-w-full" />}
28+
</div>
29+
</div>
30+
<Skeleton className="size-6 shrink-0" />
31+
</div>
32+
))}
33+
</div>
34+
)
35+
}
36+
37+
export function AdminPanelSkeleton({ className, rowCount = 3 }: AdminPanelSkeletonProps) {
38+
return (
39+
<section className={cn('grid gap-5 rounded-lg border bg-background p-6', className)} aria-hidden="true">
40+
<div className="flex items-start justify-between gap-4">
41+
<div className="grid min-w-0 flex-1 gap-2">
42+
<Skeleton className="h-5 w-36 max-w-full" />
43+
<Skeleton className="h-3 w-80 max-w-full" />
44+
</div>
45+
<Skeleton className="size-9 shrink-0 rounded-md" />
46+
</div>
47+
48+
<div className="grid gap-4">
49+
{Array.from({ length: rowCount }).map((_, index) => (
50+
<div key={index} className="grid gap-2">
51+
<Skeleton className="h-3 w-28" />
52+
<Skeleton className="h-9 w-full" />
53+
</div>
54+
))}
55+
</div>
56+
</section>
57+
)
58+
}
59+
60+
export function AdminSettingsSkeleton({ sectionCount = 3 }: { sectionCount?: number }) {
61+
return (
62+
<div className="grid gap-4" role="status" aria-label="Loading admin content">
63+
{Array.from({ length: sectionCount }).map((_, index) => (
64+
<AdminPanelSkeleton key={index} rowCount={index === 0 ? 2 : 3} />
65+
))}
66+
</div>
67+
)
68+
}
69+
70+
export function AdminCalendarSkeleton() {
71+
return (
72+
<div
73+
className="grid min-h-[420px] grid-rows-[auto_1fr] gap-4 rounded-sm border bg-background p-4"
74+
role="status"
75+
aria-label="Loading calendar"
76+
>
77+
<div className="flex flex-wrap items-center justify-between gap-3" aria-hidden="true">
78+
<Skeleton className="h-9 w-28" />
79+
<Skeleton className="h-7 w-36" />
80+
<div className="flex gap-2">
81+
<Skeleton className="h-9 w-16" />
82+
<Skeleton className="h-9 w-16" />
83+
<Skeleton className="h-9 w-16" />
84+
</div>
85+
</div>
86+
<div className="grid grid-cols-7 grid-rows-5 gap-px overflow-hidden rounded-sm bg-border" aria-hidden="true">
87+
{Array.from({ length: 35 }).map((_, index) => (
88+
<div key={index} className="bg-background p-2">
89+
<Skeleton className="size-5 rounded-full" />
90+
</div>
91+
))}
92+
</div>
93+
</div>
94+
)
95+
}
96+
97+
export default function AdminPageSkeleton() {
98+
return (
99+
<section className="grid min-w-0 gap-4">
100+
<div className="grid gap-2" aria-hidden="true">
101+
<Skeleton className="h-8 w-44" />
102+
<Skeleton className="h-4 w-96 max-w-full" />
103+
</div>
104+
<AdminSettingsSkeleton />
105+
</section>
106+
)
107+
}

src/app/[locale]/admin/affiliate/page.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { setRequestLocale } from 'next-intl/server'
22
import { io } from 'next/cache'
33
import { Suspense } from 'react'
4+
import { AdminPanelSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
45
import AdminAffiliateContentClient from '@/app/[locale]/admin/affiliate/_components/AdminAffiliateContentClient'
56
import AdminAffiliateOverview from '@/app/[locale]/admin/affiliate/_components/AdminAffiliateOverview'
67
import { getAffiliateFeeSettings, getAffiliateFeeSettingsUpdatedAt } from '@/lib/affiliate-fee-settings'
@@ -44,13 +45,13 @@ function formatIsoUtcFromTimestamp(timestamp: number) {
4445

4546
function AdminAffiliateFallback() {
4647
return (
47-
<>
48+
<div className="grid gap-6" role="status" aria-label="Loading affiliate settings">
4849
<section className="grid gap-6 lg:grid-cols-[1.2fr_1fr]">
49-
<div className="min-h-96 rounded-lg border bg-background" />
50-
<div className="min-h-64 rounded-lg border bg-background" />
50+
<AdminPanelSkeleton className="min-h-96" rowCount={4} />
51+
<AdminPanelSkeleton className="min-h-64" rowCount={2} />
5152
</section>
52-
<div className="min-h-80 rounded-lg border bg-background" />
53-
</>
53+
<AdminPanelSkeleton className="min-h-80" rowCount={3} />
54+
</div>
5455
)
5556
}
5657

src/app/[locale]/admin/events/calendar/_components/AdminCreateEventCalendar.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useExtracted } from 'next-intl'
88
import dynamic from 'next/dynamic'
99
import { useEffect, useReducer, useRef, useState } from 'react'
1010
import { toast } from 'sonner'
11+
import { AdminCalendarSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
1112
import EventIconImage from '@/components/EventIconImage'
1213
import { Button } from '@/components/ui/button'
1314
import { Card, CardContent } from '@/components/ui/card'
@@ -37,14 +38,7 @@ import { cn } from '@/lib/utils'
3738
const COPY_EVENT_FALLBACK_ICON_CLASS_NAME = 'flex size-14 items-center justify-center rounded-lg border text-muted-foreground'
3839
const AdminCreateEventCalendarView = dynamic(() => import('./AdminCreateEventCalendarView'), {
3940
ssr: false,
40-
loading: () => (
41-
<div className="
42-
flex min-h-[420px] items-center justify-center rounded-sm border border-dashed text-sm text-muted-foreground
43-
"
44-
>
45-
Loading calendar...
46-
</div>
47-
),
41+
loading: () => <AdminCalendarSkeleton />,
4842
})
4943
const AdminProposersDialog = dynamic(() => import('./AdminProposersDialog'), {
5044
ssr: false,

src/app/[locale]/admin/events/calendar/new/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ArrowLeftIcon } from 'lucide-react'
22
import { setRequestLocale } from 'next-intl/server'
33
import { Suspense } from 'react'
4+
import { AdminPanelSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
45
import AdminCreateEventForm from '@/app/[locale]/admin/events/calendar/_components/AdminCreateEventForm'
56
import { Button } from '@/components/ui/button'
67
import { Link } from '@/i18n/navigation'
@@ -141,7 +142,7 @@ export default async function AdminCreateEventNewPage({
141142
</Button>
142143
</div>
143144

144-
<div className="min-h-40 rounded-xl border bg-background" />
145+
<AdminPanelSkeleton className="min-h-40" rowCount={2} />
145146
</>
146147
)}
147148
>

src/app/[locale]/admin/events/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { SupportedLocale } from '@/i18n/locales'
22
import type { AdminEventAttentionFilter } from '@/lib/db/queries/admin-event-attention'
33
import { getExtracted, setRequestLocale } from 'next-intl/server'
44
import { Suspense } from 'react'
5+
import { DataTableSkeleton } from '@/app/[locale]/admin/_components/DataTableSkeleton'
56
import AdminEventsTable from '@/app/[locale]/admin/events/_components/AdminEventsTable'
67
import { isAdminEventAttentionFilter } from '@/lib/db/queries/admin-event-attention'
78
import { TagRepository } from '@/lib/db/queries/tag'
@@ -59,7 +60,7 @@ export default async function AdminEventsPage({ params, searchParams }: PageProp
5960
</p>
6061
</div>
6162
<div className="min-w-0">
62-
<Suspense fallback={<div className="min-h-64 rounded-xl border bg-background" />}>
63+
<Suspense fallback={<DataTableSkeleton columnCount={6} rowCount={8} />}>
6364
<AdminEventsContent locale={locale as SupportedLocale} searchParams={searchParams} />
6465
</Suspense>
6566
</div>

src/app/[locale]/admin/general/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getExtracted, setRequestLocale } from 'next-intl/server'
33
import { io } from 'next/cache'
44
import { Suspense } from 'react'
55
import AdminGeneralSettingsForm from '@/app/[locale]/admin/(general)/_components/AdminGeneralSettingsForm'
6+
import { AdminAccordionSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
67
import { parseMarketContextSettings } from '@/lib/ai/market-context-config'
78
import { MARKET_CONTEXT_VARIABLES } from '@/lib/ai/market-context-template'
89
import { HomeFeaturedEventsRepository } from '@/lib/db/queries/home-featured-events'
@@ -20,7 +21,7 @@ interface AdminGeneralSettingsPageProps {
2021
}
2122

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

2627
async function AdminGeneralSettingsContent({ locale }: { locale: string }) {

src/app/[locale]/admin/integrations/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getExtracted, setRequestLocale } from 'next-intl/server'
22
import { io } from 'next/cache'
33
import { Suspense } from 'react'
4+
import { AdminAccordionSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
45
import AdminIntegrationsForm from '@/app/[locale]/admin/integrations/_components/AdminIntegrationsForm'
56
import { parseMarketContextSettings } from '@/lib/ai/market-context-config'
67
import { fetchOpenRouterModels } from '@/lib/ai/openrouter'
@@ -11,7 +12,7 @@ import { parseSumsubSettings, sanitizeSumsubSettings } from '@/lib/sumsub/settin
1112
import { getThemeSiteSettingsFormState } from '@/lib/theme-settings'
1213

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

1718
async function AdminIntegrationsContent({ locale }: { locale: string }) {

src/app/[locale]/admin/locales/page.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1-
'use cache'
2-
31
import { getExtracted, setRequestLocale } from 'next-intl/server'
2+
import { connection } from 'next/server'
3+
import { Suspense } from 'react'
4+
import AdminPageSkeleton from '@/app/[locale]/admin/_components/AdminPageSkeleton'
45
import AdminLocalesSettingsForm from '@/app/[locale]/admin/locales/_components/AdminLocalesSettingsForm'
56
import { getAutomaticTranslationsEnabledFromSettings, getEnabledLocalesFromSettings } from '@/i18n/locale-settings'
67
import { SUPPORTED_LOCALES } from '@/i18n/locales'
78
import { parseOpenRouterProviderSettings } from '@/lib/ai/market-context-config'
89
import { SettingsRepository } from '@/lib/db/queries/settings'
910

10-
export default async function AdminLocalesSettingsPage({ params }: PageProps<'/[locale]/admin/locales'>) {
11+
async function DynamicMarker() {
12+
await connection()
13+
return null
14+
}
15+
16+
async function AdminLocalesSettingsContent({ params }: PageProps<'/[locale]/admin/locales'>) {
17+
'use cache'
18+
1119
const { locale } = await params
1220
setRequestLocale(locale)
1321
const t = await getExtracted()
@@ -36,3 +44,16 @@ export default async function AdminLocalesSettingsPage({ params }: PageProps<'/[
3644
</section>
3745
)
3846
}
47+
48+
export default function AdminLocalesSettingsPage(props: PageProps<'/[locale]/admin/locales'>) {
49+
return (
50+
<>
51+
<Suspense fallback={<AdminPageSkeleton />}>
52+
<AdminLocalesSettingsContent {...props} />
53+
</Suspense>
54+
<Suspense>
55+
<DynamicMarker />
56+
</Suspense>
57+
</>
58+
)
59+
}

src/app/[locale]/admin/theme/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { AdminThemeSiteSettingsInitialState } from '@/app/[locale]/admin/th
22
import { getExtracted, setRequestLocale } from 'next-intl/server'
33
import { io } from 'next/cache'
44
import { Suspense } from 'react'
5+
import { AdminSettingsSkeleton } from '@/app/[locale]/admin/_components/AdminPageSkeleton'
56
import AdminThemeSettingsForm from '@/app/[locale]/admin/theme/_components/AdminThemeSettingsForm'
67
import { SettingsRepository } from '@/lib/db/queries/settings'
78
import { getPublicAssetUrl } from '@/lib/storage'
@@ -10,7 +11,7 @@ import { getThemeSettingsFormState, getThemeSiteSettingsFormState } from '@/lib/
1011
import { DEFAULT_THEME_SITE_PWA_ICON_192_URL, DEFAULT_THEME_SITE_PWA_ICON_512_URL } from '@/lib/theme-site-identity'
1112

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

1617
async function AdminThemeSettingsContent() {

src/app/[locale]/layout.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export async function generateViewport(): Promise<Viewport> {
4646
}
4747

4848
export async function generateMetadata(): Promise<Metadata> {
49-
await deferPublicShellPrerenderIfNeeded()
49+
'use cache'
50+
cacheTag(cacheTags.settings)
5051

5152
const runtimeTheme = await loadRuntimeThemeState()
5253
const site = runtimeTheme.site

0 commit comments

Comments
 (0)