Skip to content

Commit 0713394

Browse files
Cawlummclaude
andcommitted
feat(mobile): skeleton placeholder for Programs first load
Programs now shows a content-shaped skeleton on initial load — the real header + "New Program" action stay live, while the stat cards, search bar, and program rows render as placeholders that fill in when data arrives ("content is there but not yet"). Previously it rendered a dimmed/blank screen. Implemented as a thin ProgramsSkeleton wrapper over the shared ListScreenSkeleton, matching WorkoutsSkeleton. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ccq3rvfb9ArzS2DidghJm5
1 parent a49487e commit 0713394

2 files changed

Lines changed: 37 additions & 5 deletions

File tree

mobile/app/(tabs)/programs/index.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BookOpen, Plus } from 'lucide-react-native'
55
import type { Program } from '@lyftr/shared'
66
import { AppText, Card, EmptyState, IconButton, Label, PageHeader, Screen, SearchField } from '../../../src/components/ui'
77
import { ProgramCard } from '../../../src/components/programs/ProgramCard'
8+
import { ProgramsSkeleton } from '../../../src/components/programs/ProgramsSkeleton'
89
import { useServerInfiniteList } from '../../../src/hooks/useServerInfiniteList'
910
import { client } from '../../../src/lib/lyftr'
1011
import { useTheme } from '../../../src/theme/useTheme'
@@ -51,11 +52,15 @@ export default function Programs() {
5152
setPulling(false)
5253
}, [reload])
5354

54-
// Weight-style loading (no skeleton / center spinner): the screen renders
55-
// immediately and content is grayed out while a *full* (re)load is in flight —
56-
// the first fetch or a search/refresh. Pagination (loadMore) is excluded so
57-
// appending a page doesn't dim the whole list.
58-
const dim = initialLoading || refreshing
55+
// First load shows a content-shaped skeleton (header + stats + search + card rows as
56+
// placeholders) so the layout is there before the data fills in — same pattern as
57+
// the Workouts list.
58+
if (initialLoading) return <ProgramsSkeleton />
59+
60+
// Stale-while-revalidate: dim the loaded content while a search re-fetches (the
61+
// previous results stay on screen until the fresh page lands). Pagination (loadMore)
62+
// is excluded so appending a page doesn't dim the whole list.
63+
const dim = refreshing
5964

6065
const stats = [
6166
// 1:1 with web: summarize the *loaded* items, not a server-side stat.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { router } from 'expo-router'
2+
import { Plus } from 'lucide-react-native'
3+
import { IconButton, ListScreenSkeleton } from '../ui'
4+
5+
// Initial-load skeleton for the Programs list — a thin wrapper over the shared
6+
// ListScreenSkeleton (header · 2 stats · search · card rows), matching the Programs
7+
// layout so content fills into its own shape rather than popping after a blank/spinner.
8+
// The PageHeader + its "New Program" action are the REAL controls, live immediately;
9+
// only the data-shaped regions are placeholders. Mirrors WorkoutsSkeleton.
10+
export function ProgramsSkeleton() {
11+
return (
12+
<ListScreenSkeleton
13+
title="Programs"
14+
subtitle="Reusable workout templates"
15+
statCount={2}
16+
action={
17+
<IconButton
18+
icon={Plus}
19+
label="New Program"
20+
variant="solid"
21+
size="md"
22+
onPress={() => router.push('/programs/new')}
23+
/>
24+
}
25+
/>
26+
)
27+
}

0 commit comments

Comments
 (0)