Skip to content

Commit 446e52e

Browse files
maximcodingclaude
andcommitted
refactor(shared): promote SectionHeader and useShimmer to shared layer
- Extract useShimmer() → src/shared/hooks/useShimmer.ts Pure Animated.Value pulse hook, no feature coupling - Extract SectionHeader → src/shared/components/ui/SectionHeader.tsx Label row + optional offline/synced status pill; reusable by any list screen - HomeScreen imports both from shared; removes ~70 lines of inline code Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 666333c commit 446e52e

3 files changed

Lines changed: 100 additions & 86 deletions

File tree

src/features/home/screens/HomeScreen.tsx

Lines changed: 3 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { useNavigation } from '@react-navigation/native'
44
import type { NativeStackNavigationProp } from '@react-navigation/native-stack'
55
import { FlashList } from '@shopify/flash-list'
6-
import React, { memo, useCallback, useEffect, useRef } from 'react'
6+
import React, { memo, useCallback } from 'react'
77
import { Animated, Platform, Pressable, ScrollView, View } from 'react-native'
88
import { useFeedQuery } from '@/features/home/hooks/useFeedQuery'
99
import type { FeedItem } from '@/features/home/types'
@@ -12,38 +12,17 @@ import type { RootStackParamList } from '@/navigation/root-param-list'
1212
import { ROUTES } from '@/navigation/routes'
1313
import { ScreenHeader } from '@/shared/components/ui/ScreenHeader'
1414
import { ScreenWrapper } from '@/shared/components/ui/ScreenWrapper'
15+
import { SectionHeader } from '@/shared/components/ui/SectionHeader'
1516
import { Text } from '@/shared/components/ui/Text'
1617
import { useOnlineStatus } from '@/shared/hooks/useOnlineStatus'
18+
import { useShimmer } from '@/shared/hooks/useShimmer'
1719
import { useTheme } from '@/shared/theme/useTheme'
1820

1921
type HomeNavProp = NativeStackNavigationProp<RootStackParamList>
2022

2123
const TAB_BAR_CLEARANCE = 88
2224

2325
// ─── Shimmer skeleton ─────────────────────────────────────────────────────────
24-
function useShimmer() {
25-
const anim = useRef(new Animated.Value(0.4)).current
26-
useEffect(() => {
27-
const loop = Animated.loop(
28-
Animated.sequence([
29-
Animated.timing(anim, {
30-
toValue: 1,
31-
duration: 750,
32-
useNativeDriver: true,
33-
}),
34-
Animated.timing(anim, {
35-
toValue: 0.4,
36-
duration: 750,
37-
useNativeDriver: true,
38-
}),
39-
]),
40-
)
41-
loop.start()
42-
return () => loop.stop()
43-
}, [anim])
44-
return anim
45-
}
46-
4726
function SkeletonCard({ shimmer }: { shimmer: Animated.Value }) {
4827
const { theme } = useTheme()
4928
const c = theme.colors
@@ -220,68 +199,6 @@ function GreetingSection({
220199
)
221200
}
222201

223-
// ─── Section header with optional sync status ─────────────────────────────────
224-
function SectionHeader({
225-
label,
226-
sublabel,
227-
sublabelIsOffline,
228-
}: {
229-
label: string
230-
sublabel?: string | null
231-
sublabelIsOffline?: boolean
232-
}) {
233-
const { theme } = useTheme()
234-
const c = theme.colors
235-
const sp = theme.spacing
236-
const r = theme.radius
237-
const ty = theme.typography
238-
return (
239-
<View
240-
style={{
241-
flexDirection: 'row',
242-
alignItems: 'center',
243-
paddingHorizontal: sp.lg,
244-
paddingBottom: sp.xs,
245-
gap: sp.xs,
246-
}}
247-
>
248-
<Text style={[ty.caps, { color: c.textTertiary, flex: 1 }]}>{label}</Text>
249-
{sublabel ? (
250-
<View
251-
style={{
252-
flexDirection: 'row',
253-
alignItems: 'center',
254-
backgroundColor: sublabelIsOffline
255-
? c.warning + '22'
256-
: c.success + '22',
257-
borderRadius: r.pill,
258-
paddingHorizontal: sp.xs,
259-
paddingVertical: 2,
260-
gap: 4,
261-
}}
262-
>
263-
<View
264-
style={{
265-
width: 6,
266-
height: 6,
267-
borderRadius: 3,
268-
backgroundColor: sublabelIsOffline ? c.warning : c.success,
269-
}}
270-
/>
271-
<Text
272-
style={[
273-
ty.labelSmall,
274-
{ color: sublabelIsOffline ? c.warning : c.success },
275-
]}
276-
>
277-
{sublabel}
278-
</Text>
279-
</View>
280-
) : null}
281-
</View>
282-
)
283-
}
284-
285202
// ─── News story card ──────────────────────────────────────────────────────────
286203
const StoryCard = memo(function StoryCard({ item }: { item: FeedItem }) {
287204
const { theme } = useTheme()
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// src/shared/components/ui/SectionHeader.tsx
2+
3+
import React from 'react'
4+
import { View } from 'react-native'
5+
import { Text } from '@/shared/components/ui/Text'
6+
import { useTheme } from '@/shared/theme/useTheme'
7+
8+
interface Props {
9+
label: string
10+
/** Optional badge shown on the right. */
11+
sublabel?: string | null
12+
/** When true the badge renders in warning colour; otherwise success. */
13+
sublabelIsOffline?: boolean
14+
}
15+
16+
export function SectionHeader({ label, sublabel, sublabelIsOffline }: Props) {
17+
const { theme } = useTheme()
18+
const c = theme.colors
19+
const sp = theme.spacing
20+
const r = theme.radius
21+
const ty = theme.typography
22+
23+
return (
24+
<View
25+
style={{
26+
flexDirection: 'row',
27+
alignItems: 'center',
28+
paddingHorizontal: sp.lg,
29+
paddingBottom: sp.xs,
30+
gap: sp.xs,
31+
}}
32+
>
33+
<Text style={[ty.caps, { color: c.textTertiary, flex: 1 }]}>{label}</Text>
34+
{sublabel ? (
35+
<View
36+
style={{
37+
flexDirection: 'row',
38+
alignItems: 'center',
39+
backgroundColor: sublabelIsOffline
40+
? c.warning + '22'
41+
: c.success + '22',
42+
borderRadius: r.pill,
43+
paddingHorizontal: sp.xs,
44+
paddingVertical: 2,
45+
gap: 4,
46+
}}
47+
>
48+
<View
49+
style={{
50+
width: 6,
51+
height: 6,
52+
borderRadius: 3,
53+
backgroundColor: sublabelIsOffline ? c.warning : c.success,
54+
}}
55+
/>
56+
<Text
57+
style={[
58+
ty.labelSmall,
59+
{ color: sublabelIsOffline ? c.warning : c.success },
60+
]}
61+
>
62+
{sublabel}
63+
</Text>
64+
</View>
65+
) : null}
66+
</View>
67+
)
68+
}

src/shared/hooks/useShimmer.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useEffect, useRef } from 'react'
2+
import { Animated } from 'react-native'
3+
4+
/**
5+
* Returns an Animated.Value that pulses between 0.4 and 1 opacity —
6+
* use it to drive shimmer/skeleton placeholder animations.
7+
*/
8+
export function useShimmer(): Animated.Value {
9+
const anim = useRef(new Animated.Value(0.4)).current
10+
useEffect(() => {
11+
const loop = Animated.loop(
12+
Animated.sequence([
13+
Animated.timing(anim, {
14+
toValue: 1,
15+
duration: 750,
16+
useNativeDriver: true,
17+
}),
18+
Animated.timing(anim, {
19+
toValue: 0.4,
20+
duration: 750,
21+
useNativeDriver: true,
22+
}),
23+
]),
24+
)
25+
loop.start()
26+
return () => loop.stop()
27+
}, [anim])
28+
return anim
29+
}

0 commit comments

Comments
 (0)