@multiplayer-app/session-recorder-react-native 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +226 -0
  3. package/babel.config.js +13 -0
  4. package/dist/config/masking.d.ts +30 -0
  5. package/dist/config/masking.js +1 -0
  6. package/dist/config/masking.js.map +1 -0
  7. package/dist/expo.d.ts +11 -0
  8. package/dist/expo.js +1 -0
  9. package/dist/expo.js.map +1 -0
  10. package/dist/index.d.ts +11 -0
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/otel/helpers.d.ts +3 -0
  14. package/dist/otel/helpers.js +1 -0
  15. package/dist/otel/helpers.js.map +1 -0
  16. package/dist/otel/index.d.ts +40 -0
  17. package/dist/otel/index.js +1 -0
  18. package/dist/otel/index.js.map +1 -0
  19. package/dist/otel/instrumentations/gestureInstrumentation.d.ts +15 -0
  20. package/dist/otel/instrumentations/gestureInstrumentation.js +1 -0
  21. package/dist/otel/instrumentations/gestureInstrumentation.js.map +1 -0
  22. package/dist/otel/instrumentations/index.d.ts +5 -0
  23. package/dist/otel/instrumentations/index.js +1 -0
  24. package/dist/otel/instrumentations/index.js.map +1 -0
  25. package/dist/otel/instrumentations/reactNativeInstrumentation.d.ts +8 -0
  26. package/dist/otel/instrumentations/reactNativeInstrumentation.js +1 -0
  27. package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +1 -0
  28. package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +12 -0
  29. package/dist/otel/instrumentations/reactNavigationInstrumentation.js +1 -0
  30. package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +1 -0
  31. package/dist/recorder/gestureRecorder.d.ts +42 -0
  32. package/dist/recorder/gestureRecorder.js +1 -0
  33. package/dist/recorder/gestureRecorder.js.map +1 -0
  34. package/dist/recorder/index.d.ts +16 -0
  35. package/dist/recorder/index.js +1 -0
  36. package/dist/recorder/index.js.map +1 -0
  37. package/dist/recorder/navigationTracker.d.ts +43 -0
  38. package/dist/recorder/navigationTracker.js +1 -0
  39. package/dist/recorder/navigationTracker.js.map +1 -0
  40. package/dist/recorder/screenRecorder.d.ts +46 -0
  41. package/dist/recorder/screenRecorder.js +1 -0
  42. package/dist/recorder/screenRecorder.js.map +1 -0
  43. package/dist/services/api.service.d.ts +20 -0
  44. package/dist/services/api.service.js +1 -0
  45. package/dist/services/api.service.js.map +1 -0
  46. package/dist/services/storage.service.d.ts +23 -0
  47. package/dist/services/storage.service.js +1 -0
  48. package/dist/services/storage.service.js.map +1 -0
  49. package/dist/sessionRecorder.d.ts +54 -0
  50. package/dist/sessionRecorder.js +1 -0
  51. package/dist/sessionRecorder.js.map +1 -0
  52. package/dist/types/index.d.ts +81 -0
  53. package/dist/types/index.js +1 -0
  54. package/dist/types/index.js.map +1 -0
  55. package/dist/utils/platform.d.ts +9 -0
  56. package/dist/utils/platform.js +1 -0
  57. package/dist/utils/platform.js.map +1 -0
  58. package/dist/version.d.ts +1 -0
  59. package/dist/version.js +1 -0
  60. package/dist/version.js.map +1 -0
  61. package/examples/sample-expo-app/README.md +142 -0
  62. package/examples/sample-expo-app/app/(tabs)/_layout.tsx +60 -0
  63. package/examples/sample-expo-app/app/(tabs)/explore.tsx +110 -0
  64. package/examples/sample-expo-app/app/(tabs)/index.tsx +125 -0
  65. package/examples/sample-expo-app/app/(tabs)/posts.tsx +96 -0
  66. package/examples/sample-expo-app/app/(tabs)/users.tsx +131 -0
  67. package/examples/sample-expo-app/app/+not-found.tsx +32 -0
  68. package/examples/sample-expo-app/app/_layout.tsx +53 -0
  69. package/examples/sample-expo-app/app/post/[id].tsx +199 -0
  70. package/examples/sample-expo-app/app/user/[id].tsx +270 -0
  71. package/examples/sample-expo-app/app.json +42 -0
  72. package/examples/sample-expo-app/assets/fonts/SpaceMono-Regular.ttf +0 -0
  73. package/examples/sample-expo-app/assets/images/adaptive-icon.png +0 -0
  74. package/examples/sample-expo-app/assets/images/favicon.png +0 -0
  75. package/examples/sample-expo-app/assets/images/icon.png +0 -0
  76. package/examples/sample-expo-app/assets/images/partial-react-logo.png +0 -0
  77. package/examples/sample-expo-app/assets/images/react-logo.png +0 -0
  78. package/examples/sample-expo-app/assets/images/react-logo@2x.png +0 -0
  79. package/examples/sample-expo-app/assets/images/react-logo@3x.png +0 -0
  80. package/examples/sample-expo-app/assets/images/splash-icon.png +0 -0
  81. package/examples/sample-expo-app/components/Collapsible.tsx +45 -0
  82. package/examples/sample-expo-app/components/ErrorView.tsx +52 -0
  83. package/examples/sample-expo-app/components/ExternalLink.tsx +24 -0
  84. package/examples/sample-expo-app/components/HapticTab.tsx +18 -0
  85. package/examples/sample-expo-app/components/HelloWave.tsx +40 -0
  86. package/examples/sample-expo-app/components/LoadingSpinner.tsx +34 -0
  87. package/examples/sample-expo-app/components/ParallaxScrollView.tsx +82 -0
  88. package/examples/sample-expo-app/components/ThemedText.tsx +60 -0
  89. package/examples/sample-expo-app/components/ThemedView.tsx +14 -0
  90. package/examples/sample-expo-app/components/ui/IconSymbol.ios.tsx +32 -0
  91. package/examples/sample-expo-app/components/ui/IconSymbol.tsx +41 -0
  92. package/examples/sample-expo-app/components/ui/TabBarBackground.ios.tsx +19 -0
  93. package/examples/sample-expo-app/components/ui/TabBarBackground.tsx +6 -0
  94. package/examples/sample-expo-app/constants/Colors.ts +26 -0
  95. package/examples/sample-expo-app/eslint.config.js +10 -0
  96. package/examples/sample-expo-app/hooks/useApi.ts +41 -0
  97. package/examples/sample-expo-app/hooks/useColorScheme.ts +1 -0
  98. package/examples/sample-expo-app/hooks/useColorScheme.web.ts +21 -0
  99. package/examples/sample-expo-app/hooks/useThemeColor.ts +21 -0
  100. package/examples/sample-expo-app/metro.config.js +26 -0
  101. package/examples/sample-expo-app/package-lock.json +26296 -0
  102. package/examples/sample-expo-app/package.json +59 -0
  103. package/examples/sample-expo-app/scripts/reset-project.js +112 -0
  104. package/examples/sample-expo-app/services/api.ts +98 -0
  105. package/examples/sample-expo-app/tsconfig.json +17 -0
  106. package/examples/sample-expo-app/utils/navigation.ts +19 -0
  107. package/package.json +98 -0
  108. package/src/config/masking.ts +78 -0
  109. package/src/expo.ts +41 -0
  110. package/src/index.ts +20 -0
  111. package/src/otel/helpers.ts +21 -0
  112. package/src/otel/index.ts +348 -0
  113. package/src/otel/instrumentations/gestureInstrumentation.ts +141 -0
  114. package/src/otel/instrumentations/index.ts +86 -0
  115. package/src/otel/instrumentations/reactNativeInstrumentation.ts +164 -0
  116. package/src/otel/instrumentations/reactNavigationInstrumentation.ts +114 -0
  117. package/src/recorder/gestureRecorder.ts +429 -0
  118. package/src/recorder/index.ts +71 -0
  119. package/src/recorder/navigationTracker.ts +447 -0
  120. package/src/recorder/screenRecorder.ts +411 -0
  121. package/src/services/api.service.ts +78 -0
  122. package/src/services/storage.service.ts +130 -0
  123. package/src/sessionRecorder.ts +367 -0
  124. package/src/types/expo.d.ts +23 -0
  125. package/src/types/index.ts +88 -0
  126. package/src/utils/platform.ts +75 -0
  127. package/src/version.ts +1 -0
  128. package/tsconfig.json +24 -0
@@ -0,0 +1,131 @@
1
+ import React from 'react'
2
+ import { FlatList, StyleSheet, TouchableOpacity, RefreshControl } from 'react-native'
3
+ import { router } from 'expo-router'
4
+ import { ThemedView } from '@/components/ThemedView'
5
+ import { ThemedText } from '@/components/ThemedText'
6
+ import { LoadingSpinner } from '@/components/LoadingSpinner'
7
+ import { ErrorView } from '@/components/ErrorView'
8
+ import { IconSymbol } from '@/components/ui/IconSymbol'
9
+ import { useApi } from '@/hooks/useApi'
10
+ import { apiService, User } from '@/services/api'
11
+ import { Colors } from '@/constants/Colors'
12
+ import { useColorScheme } from '@/hooks/useColorScheme'
13
+
14
+ export default function UsersScreen() {
15
+ const { data: users, loading, error, refetch } = useApi(() => apiService.getUsers())
16
+ const colorScheme = useColorScheme()
17
+
18
+ const handleUserPress = (user: User) => {
19
+ router.push(`/user/${user.id}`)
20
+ }
21
+
22
+ const renderUser = ({ item }: { item: User }) => (
23
+ <TouchableOpacity style={styles.userCard} onPress={() => handleUserPress(item)} activeOpacity={0.7}>
24
+ <ThemedView style={styles.userAvatar}>
25
+ <IconSymbol name='person.fill' size={24} color={Colors[colorScheme ?? 'light'].tint} />
26
+ </ThemedView>
27
+
28
+ <ThemedView style={styles.userInfo}>
29
+ <ThemedText type='subtitle' style={styles.userName}>
30
+ {item.name}
31
+ </ThemedText>
32
+ <ThemedText style={styles.userUsername}>@{item.username}</ThemedText>
33
+ <ThemedText style={styles.userEmail} numberOfLines={1}>
34
+ {item.email}
35
+ </ThemedText>
36
+ <ThemedText style={styles.userCompany}>{item.company.name}</ThemedText>
37
+ </ThemedView>
38
+
39
+ <IconSymbol name='chevron.right' size={16} color={Colors[colorScheme ?? 'light'].tint} style={styles.chevron} />
40
+ </TouchableOpacity>
41
+ )
42
+
43
+ if (loading) {
44
+ return <LoadingSpinner message='Loading users...' />
45
+ }
46
+
47
+ if (error) {
48
+ return <ErrorView message={error} onRetry={refetch} />
49
+ }
50
+
51
+ return (
52
+ <ThemedView style={styles.container}>
53
+ <ThemedView style={styles.header}>
54
+ <ThemedText type='title'>Users</ThemedText>
55
+ <ThemedText style={styles.subtitle}>Users from JSONPlaceholder API</ThemedText>
56
+ </ThemedView>
57
+
58
+ <FlatList
59
+ data={users}
60
+ renderItem={renderUser}
61
+ keyExtractor={(item) => item.id.toString()}
62
+ contentContainerStyle={styles.listContainer}
63
+ refreshControl={<RefreshControl refreshing={loading} onRefresh={refetch} />}
64
+ showsVerticalScrollIndicator={false}
65
+ />
66
+ </ThemedView>
67
+ )
68
+ }
69
+
70
+ const styles = StyleSheet.create({
71
+ container: {
72
+ flex: 1
73
+ },
74
+ header: {
75
+ padding: 20,
76
+ paddingTop: 60,
77
+ paddingBottom: 10
78
+ },
79
+ subtitle: {
80
+ marginTop: 4,
81
+ opacity: 0.7
82
+ },
83
+ listContainer: {
84
+ padding: 20,
85
+ paddingTop: 10
86
+ },
87
+ userCard: {
88
+ flexDirection: 'row',
89
+ alignItems: 'center',
90
+ padding: 16,
91
+ marginBottom: 12,
92
+ borderRadius: 12,
93
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
94
+ borderWidth: 1,
95
+ borderColor: 'rgba(255, 255, 255, 0.2)'
96
+ },
97
+ userAvatar: {
98
+ width: 48,
99
+ height: 48,
100
+ borderRadius: 24,
101
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
102
+ justifyContent: 'center',
103
+ alignItems: 'center',
104
+ marginRight: 12
105
+ },
106
+ userInfo: {
107
+ flex: 1
108
+ },
109
+ userName: {
110
+ marginBottom: 2,
111
+ fontWeight: '600'
112
+ },
113
+ userUsername: {
114
+ fontSize: 14,
115
+ opacity: 0.7,
116
+ marginBottom: 4
117
+ },
118
+ userEmail: {
119
+ fontSize: 12,
120
+ opacity: 0.6,
121
+ marginBottom: 2
122
+ },
123
+ userCompany: {
124
+ fontSize: 12,
125
+ opacity: 0.8,
126
+ fontStyle: 'italic'
127
+ },
128
+ chevron: {
129
+ opacity: 0.5
130
+ }
131
+ })
@@ -0,0 +1,32 @@
1
+ import { Link, Stack } from 'expo-router';
2
+ import { StyleSheet } from 'react-native';
3
+
4
+ import { ThemedText } from '@/components/ThemedText';
5
+ import { ThemedView } from '@/components/ThemedView';
6
+
7
+ export default function NotFoundScreen() {
8
+ return (
9
+ <>
10
+ <Stack.Screen options={{ title: 'Oops!' }} />
11
+ <ThemedView style={styles.container}>
12
+ <ThemedText type="title">This screen does not exist.</ThemedText>
13
+ <Link href="/" style={styles.link}>
14
+ <ThemedText type="link">Go to home screen!</ThemedText>
15
+ </Link>
16
+ </ThemedView>
17
+ </>
18
+ );
19
+ }
20
+
21
+ const styles = StyleSheet.create({
22
+ container: {
23
+ flex: 1,
24
+ alignItems: 'center',
25
+ justifyContent: 'center',
26
+ padding: 20,
27
+ },
28
+ link: {
29
+ marginTop: 15,
30
+ paddingVertical: 15,
31
+ },
32
+ });
@@ -0,0 +1,53 @@
1
+ import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
2
+ import { useFonts } from 'expo-font'
3
+ import { Stack } from 'expo-router'
4
+ import { StatusBar } from 'expo-status-bar'
5
+ import 'react-native-reanimated'
6
+
7
+ import { useColorScheme } from '@/hooks/useColorScheme'
8
+ import SessionRecorder from '@multiplayer-app/session-recorder-react-native'
9
+
10
+ SessionRecorder.init({
11
+ version: '0.0.1',
12
+ application: 'multiplayer-web-app',
13
+ environment: 'development',
14
+ apiKey:
15
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnRlZ3JhdGlvbiI6IjY4NGZlMDljMjA0NmYwYjM0ZjU5ZDNjYyIsIndvcmtzcGFjZSI6IjY4NGMzYmYwYjQ2MGUzMmY3YWJmZjRlMSIsInByb2plY3QiOiI2ODRjM2M0MmI0NjBlMzJmN2FiZmY1YzgiLCJ0eXBlIjoiT1RFTCIsImlhdCI6MTc1MDA2NTMwOH0.F15dW5RUHtq4-e2FUZD_vK0FJ5USs8SRFbnPYO_0XVk',
16
+ apiBaseUrl: 'http://localhost',
17
+ exporterEndpoint: 'http://localhost/v1/traces',
18
+ showWidget: true,
19
+ ignoreUrls: [
20
+ /posthog\.com.*/,
21
+ /https:\/\/bam\.nr-data\.net\/.*/,
22
+ /https:\/\/cdn\.jsdelivr\.net\/.*/,
23
+ /https:\/\/pixel\.source\.app\/.*/
24
+ ]
25
+ // propagateTraceHeaderCorsUrls: new RegExp(
26
+ // `${process.env.REACT_APP_API_BASE_URL}\.*`,
27
+ // "i"
28
+ // ),
29
+ })
30
+
31
+ export default function RootLayout() {
32
+ const colorScheme = useColorScheme()
33
+ const [loaded] = useFonts({
34
+ SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf')
35
+ })
36
+
37
+ if (!loaded) {
38
+ // Async font loading only occurs in development.
39
+ return null
40
+ }
41
+
42
+ return (
43
+ <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
44
+ <Stack>
45
+ <Stack.Screen name='(tabs)' options={{ headerShown: false }} />
46
+ <Stack.Screen name='post/[id]' options={{ headerShown: false }} />
47
+ <Stack.Screen name='user/[id]' options={{ headerShown: false }} />
48
+ <Stack.Screen name='+not-found' />
49
+ </Stack>
50
+ <StatusBar style='auto' />
51
+ </ThemeProvider>
52
+ )
53
+ }
@@ -0,0 +1,199 @@
1
+ import React from 'react'
2
+ import { ScrollView, StyleSheet, TouchableOpacity, FlatList } from 'react-native'
3
+ import { useLocalSearchParams, router } from 'expo-router'
4
+ import { ThemedView } from '@/components/ThemedView'
5
+ import { ThemedText } from '@/components/ThemedText'
6
+ import { LoadingSpinner } from '@/components/LoadingSpinner'
7
+ import { ErrorView } from '@/components/ErrorView'
8
+ import { IconSymbol } from '@/components/ui/IconSymbol'
9
+ import { useApi } from '@/hooks/useApi'
10
+ import { apiService, Comment } from '@/services/api'
11
+ import { Colors } from '@/constants/Colors'
12
+ import { useColorScheme } from '@/hooks/useColorScheme'
13
+
14
+ export default function PostDetailScreen() {
15
+ const { id } = useLocalSearchParams<{ id: string }>()
16
+ const postId = parseInt(id!)
17
+ const colorScheme = useColorScheme()
18
+
19
+ const {
20
+ data: post,
21
+ loading: postLoading,
22
+ error: postError,
23
+ refetch: refetchPost
24
+ } = useApi(() => apiService.getPost(postId), [postId])
25
+
26
+ const {
27
+ data: comments,
28
+ loading: commentsLoading,
29
+ error: commentsError,
30
+ refetch: refetchComments
31
+ } = useApi(() => apiService.getPostComments(postId), [postId])
32
+
33
+ const handleUserPress = (userId: number) => {
34
+ router.push(`/user/${userId}`)
35
+ }
36
+
37
+ const renderComment = ({ item }: { item: Comment }) => (
38
+ <ThemedView style={styles.commentCard}>
39
+ <ThemedView style={styles.commentHeader}>
40
+ <ThemedText type='defaultSemiBold' style={styles.commentName}>
41
+ {item.name}
42
+ </ThemedText>
43
+ <ThemedText style={styles.commentEmail}>{item.email}</ThemedText>
44
+ </ThemedView>
45
+ <ThemedText style={styles.commentBody}>{item.body}</ThemedText>
46
+ </ThemedView>
47
+ )
48
+
49
+ if (postLoading) {
50
+ return <LoadingSpinner message='Loading post...' />
51
+ }
52
+
53
+ if (postError) {
54
+ return <ErrorView message={postError} onRetry={refetchPost} />
55
+ }
56
+
57
+ if (!post) {
58
+ return <ErrorView message='Post not found' />
59
+ }
60
+
61
+ return (
62
+ <ThemedView style={styles.container}>
63
+ {/* Header */}
64
+ <ThemedView style={styles.header}>
65
+ <TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
66
+ <IconSymbol name='chevron.left' size={24} color={Colors[colorScheme ?? 'light'].tint} />
67
+ </TouchableOpacity>
68
+ <ThemedText type='title' style={styles.headerTitle}>
69
+ Post Details
70
+ </ThemedText>
71
+ </ThemedView>
72
+
73
+ <ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
74
+ {/* Post Content */}
75
+ <ThemedView style={styles.postCard}>
76
+ <ThemedText type='title' style={styles.postTitle}>
77
+ {post.title}
78
+ </ThemedText>
79
+ <ThemedText style={styles.postBody}>{post.body}</ThemedText>
80
+
81
+ <TouchableOpacity style={styles.userButton} onPress={() => handleUserPress(post.userId)}>
82
+ <IconSymbol name='person.fill' size={16} color={Colors[colorScheme ?? 'light'].tint} />
83
+ <ThemedText style={styles.userButtonText}>View User {post.userId}</ThemedText>
84
+ <IconSymbol name='chevron.right' size={16} color={Colors[colorScheme ?? 'light'].tint} />
85
+ </TouchableOpacity>
86
+ </ThemedView>
87
+
88
+ {/* Comments Section */}
89
+ <ThemedView style={styles.commentsSection}>
90
+ <ThemedText type='subtitle' style={styles.commentsTitle}>
91
+ Comments ({comments?.length || 0})
92
+ </ThemedText>
93
+
94
+ {commentsLoading ? (
95
+ <LoadingSpinner message='Loading comments...' />
96
+ ) : commentsError ? (
97
+ <ErrorView message={commentsError} onRetry={refetchComments} />
98
+ ) : comments && comments.length > 0 ? (
99
+ <FlatList
100
+ data={comments}
101
+ renderItem={renderComment}
102
+ keyExtractor={(item) => item.id.toString()}
103
+ scrollEnabled={false}
104
+ showsVerticalScrollIndicator={false}
105
+ />
106
+ ) : (
107
+ <ThemedText style={styles.noComments}>No comments yet</ThemedText>
108
+ )}
109
+ </ThemedView>
110
+ </ScrollView>
111
+ </ThemedView>
112
+ )
113
+ }
114
+
115
+ const styles = StyleSheet.create({
116
+ container: {
117
+ flex: 1
118
+ },
119
+ header: {
120
+ flexDirection: 'row',
121
+ alignItems: 'center',
122
+ padding: 20,
123
+ paddingTop: 60,
124
+ paddingBottom: 10
125
+ },
126
+ backButton: {
127
+ marginRight: 16
128
+ },
129
+ headerTitle: {
130
+ flex: 1
131
+ },
132
+ content: {
133
+ flex: 1,
134
+ padding: 20
135
+ },
136
+ postCard: {
137
+ padding: 20,
138
+ marginBottom: 20,
139
+ borderRadius: 12,
140
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
141
+ borderWidth: 1,
142
+ borderColor: 'rgba(255, 255, 255, 0.2)'
143
+ },
144
+ postTitle: {
145
+ marginBottom: 16,
146
+ lineHeight: 28
147
+ },
148
+ postBody: {
149
+ marginBottom: 20,
150
+ lineHeight: 22
151
+ },
152
+ userButton: {
153
+ flexDirection: 'row',
154
+ alignItems: 'center',
155
+ padding: 12,
156
+ borderRadius: 8,
157
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
158
+ borderWidth: 1,
159
+ borderColor: 'rgba(255, 255, 255, 0.2)'
160
+ },
161
+ userButtonText: {
162
+ flex: 1,
163
+ marginLeft: 8,
164
+ marginRight: 8
165
+ },
166
+ commentsSection: {
167
+ marginBottom: 20
168
+ },
169
+ commentsTitle: {
170
+ marginBottom: 16
171
+ },
172
+ commentCard: {
173
+ padding: 16,
174
+ marginBottom: 12,
175
+ borderRadius: 8,
176
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
177
+ borderWidth: 1,
178
+ borderColor: 'rgba(255, 255, 255, 0.1)'
179
+ },
180
+ commentHeader: {
181
+ marginBottom: 8
182
+ },
183
+ commentName: {
184
+ marginBottom: 2
185
+ },
186
+ commentEmail: {
187
+ fontSize: 12,
188
+ opacity: 0.6
189
+ },
190
+ commentBody: {
191
+ lineHeight: 18
192
+ },
193
+ noComments: {
194
+ textAlign: 'center',
195
+ opacity: 0.6,
196
+ fontStyle: 'italic',
197
+ padding: 20
198
+ }
199
+ })
@@ -0,0 +1,270 @@
1
+ import React from 'react'
2
+ import { ScrollView, StyleSheet, TouchableOpacity, FlatList } from 'react-native'
3
+ import { useLocalSearchParams, router } from 'expo-router'
4
+ import { ThemedView } from '@/components/ThemedView'
5
+ import { ThemedText } from '@/components/ThemedText'
6
+ import { LoadingSpinner } from '@/components/LoadingSpinner'
7
+ import { ErrorView } from '@/components/ErrorView'
8
+ import { IconSymbol } from '@/components/ui/IconSymbol'
9
+ import { useApi } from '@/hooks/useApi'
10
+ import { apiService, Post } from '@/services/api'
11
+ import { Colors } from '@/constants/Colors'
12
+ import { useColorScheme } from '@/hooks/useColorScheme'
13
+
14
+ export default function UserDetailScreen() {
15
+ const { id } = useLocalSearchParams<{ id: string }>()
16
+ const userId = parseInt(id!)
17
+ const colorScheme = useColorScheme()
18
+
19
+ const {
20
+ data: user,
21
+ loading: userLoading,
22
+ error: userError,
23
+ refetch: refetchUser
24
+ } = useApi(() => apiService.getUser(userId), [userId])
25
+
26
+ const {
27
+ data: posts,
28
+ loading: postsLoading,
29
+ error: postsError,
30
+ refetch: refetchPosts
31
+ } = useApi(() => apiService.getUserPosts(userId), [userId])
32
+
33
+ const handlePostPress = (post: Post) => {
34
+ router.push(`/post/${post.id}`)
35
+ }
36
+
37
+ const renderPost = ({ item }: { item: Post }) => (
38
+ <TouchableOpacity style={styles.postCard} onPress={() => handlePostPress(item)} activeOpacity={0.7}>
39
+ <ThemedText type='subtitle' style={styles.postTitle}>
40
+ {item.title}
41
+ </ThemedText>
42
+ <ThemedText style={styles.postBody} numberOfLines={2}>
43
+ {item.body}
44
+ </ThemedText>
45
+ <ThemedText style={styles.postMeta}>Post ID: {item.id}</ThemedText>
46
+ </TouchableOpacity>
47
+ )
48
+
49
+ if (userLoading) {
50
+ return <LoadingSpinner message='Loading user...' />
51
+ }
52
+
53
+ if (userError) {
54
+ return <ErrorView message={userError} onRetry={refetchUser} />
55
+ }
56
+
57
+ if (!user) {
58
+ return <ErrorView message='User not found' />
59
+ }
60
+
61
+ return (
62
+ <ThemedView style={styles.container}>
63
+ {/* Header */}
64
+ <ThemedView style={styles.header}>
65
+ <TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
66
+ <IconSymbol name='chevron.left' size={24} color={Colors[colorScheme ?? 'light'].tint} />
67
+ </TouchableOpacity>
68
+ <ThemedText type='title' style={styles.headerTitle}>
69
+ User Profile
70
+ </ThemedText>
71
+ </ThemedView>
72
+
73
+ <ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
74
+ {/* User Info Card */}
75
+ <ThemedView style={styles.userCard}>
76
+ <ThemedView style={styles.userAvatar}>
77
+ <IconSymbol name='person.fill' size={32} color={Colors[colorScheme ?? 'light'].tint} />
78
+ </ThemedView>
79
+
80
+ <ThemedText type='title' style={styles.userName}>
81
+ {user.name}
82
+ </ThemedText>
83
+ <ThemedText style={styles.userUsername}>@{user.username}</ThemedText>
84
+
85
+ <ThemedView style={styles.userDetails}>
86
+ <ThemedView style={styles.detailRow}>
87
+ <IconSymbol name='envelope.fill' size={16} color={Colors[colorScheme ?? 'light'].tint} />
88
+ <ThemedText style={styles.detailText}>{user.email}</ThemedText>
89
+ </ThemedView>
90
+
91
+ <ThemedView style={styles.detailRow}>
92
+ <IconSymbol name='phone.fill' size={16} color={Colors[colorScheme ?? 'light'].tint} />
93
+ <ThemedText style={styles.detailText}>{user.phone}</ThemedText>
94
+ </ThemedView>
95
+
96
+ <ThemedView style={styles.detailRow}>
97
+ <IconSymbol name='globe' size={16} color={Colors[colorScheme ?? 'light'].tint} />
98
+ <ThemedText style={styles.detailText}>{user.website}</ThemedText>
99
+ </ThemedView>
100
+ </ThemedView>
101
+
102
+ <ThemedView style={styles.companySection}>
103
+ <ThemedText type='subtitle' style={styles.sectionTitle}>
104
+ Company
105
+ </ThemedText>
106
+ <ThemedText style={styles.companyName}>{user.company.name}</ThemedText>
107
+ <ThemedText style={styles.companyCatchPhrase}>&ldquo;{user.company.catchPhrase}&rdquo;</ThemedText>
108
+ </ThemedView>
109
+
110
+ <ThemedView style={styles.addressSection}>
111
+ <ThemedText type='subtitle' style={styles.sectionTitle}>
112
+ Address
113
+ </ThemedText>
114
+ <ThemedText style={styles.addressText}>
115
+ {user.address.street}, {user.address.suite}
116
+ </ThemedText>
117
+ <ThemedText style={styles.addressText}>
118
+ {user.address.city}, {user.address.zipcode}
119
+ </ThemedText>
120
+ </ThemedView>
121
+ </ThemedView>
122
+
123
+ {/* Posts Section */}
124
+ <ThemedView style={styles.postsSection}>
125
+ <ThemedText type='subtitle' style={styles.postsTitle}>
126
+ Posts ({posts?.length || 0})
127
+ </ThemedText>
128
+
129
+ {postsLoading ? (
130
+ <LoadingSpinner message='Loading posts...' />
131
+ ) : postsError ? (
132
+ <ErrorView message={postsError} onRetry={refetchPosts} />
133
+ ) : posts && posts.length > 0 ? (
134
+ <FlatList
135
+ data={posts}
136
+ renderItem={renderPost}
137
+ keyExtractor={(item) => item.id.toString()}
138
+ scrollEnabled={false}
139
+ showsVerticalScrollIndicator={false}
140
+ />
141
+ ) : (
142
+ <ThemedText style={styles.noPosts}>No posts yet</ThemedText>
143
+ )}
144
+ </ThemedView>
145
+ </ScrollView>
146
+ </ThemedView>
147
+ )
148
+ }
149
+
150
+ const styles = StyleSheet.create({
151
+ container: {
152
+ flex: 1
153
+ },
154
+ header: {
155
+ flexDirection: 'row',
156
+ alignItems: 'center',
157
+ padding: 20,
158
+ paddingTop: 60,
159
+ paddingBottom: 10
160
+ },
161
+ backButton: {
162
+ marginRight: 16
163
+ },
164
+ headerTitle: {
165
+ flex: 1
166
+ },
167
+ content: {
168
+ flex: 1,
169
+ padding: 20
170
+ },
171
+ userCard: {
172
+ padding: 20,
173
+ marginBottom: 20,
174
+ borderRadius: 12,
175
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
176
+ borderWidth: 1,
177
+ borderColor: 'rgba(255, 255, 255, 0.2)',
178
+ alignItems: 'center'
179
+ },
180
+ userAvatar: {
181
+ width: 80,
182
+ height: 80,
183
+ borderRadius: 40,
184
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
185
+ justifyContent: 'center',
186
+ alignItems: 'center',
187
+ marginBottom: 16
188
+ },
189
+ userName: {
190
+ marginBottom: 4,
191
+ textAlign: 'center'
192
+ },
193
+ userUsername: {
194
+ fontSize: 16,
195
+ opacity: 0.7,
196
+ marginBottom: 20
197
+ },
198
+ userDetails: {
199
+ width: '100%',
200
+ marginBottom: 20
201
+ },
202
+ detailRow: {
203
+ flexDirection: 'row',
204
+ alignItems: 'center',
205
+ marginBottom: 8
206
+ },
207
+ detailText: {
208
+ marginLeft: 8,
209
+ flex: 1
210
+ },
211
+ companySection: {
212
+ width: '100%',
213
+ marginBottom: 16,
214
+ padding: 16,
215
+ borderRadius: 8,
216
+ backgroundColor: 'rgba(255, 255, 255, 0.05)'
217
+ },
218
+ sectionTitle: {
219
+ marginBottom: 8
220
+ },
221
+ companyName: {
222
+ marginBottom: 4,
223
+ fontWeight: '600'
224
+ },
225
+ companyCatchPhrase: {
226
+ fontSize: 14,
227
+ opacity: 0.8,
228
+ fontStyle: 'italic'
229
+ },
230
+ addressSection: {
231
+ width: '100%',
232
+ padding: 16,
233
+ borderRadius: 8,
234
+ backgroundColor: 'rgba(255, 255, 255, 0.05)'
235
+ },
236
+ addressText: {
237
+ marginBottom: 2
238
+ },
239
+ postsSection: {
240
+ marginBottom: 20
241
+ },
242
+ postsTitle: {
243
+ marginBottom: 16
244
+ },
245
+ postCard: {
246
+ padding: 16,
247
+ marginBottom: 12,
248
+ borderRadius: 8,
249
+ backgroundColor: 'rgba(255, 255, 255, 0.05)',
250
+ borderWidth: 1,
251
+ borderColor: 'rgba(255, 255, 255, 0.1)'
252
+ },
253
+ postTitle: {
254
+ marginBottom: 8
255
+ },
256
+ postBody: {
257
+ marginBottom: 8,
258
+ lineHeight: 18
259
+ },
260
+ postMeta: {
261
+ fontSize: 12,
262
+ opacity: 0.6
263
+ },
264
+ noPosts: {
265
+ textAlign: 'center',
266
+ opacity: 0.6,
267
+ fontStyle: 'italic',
268
+ padding: 20
269
+ }
270
+ })