expotesting2 4.1.0

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 (77) hide show
  1. package/README.md +289 -0
  2. package/apps/expo-app/app.json +60 -0
  3. package/apps/expo-app/babel.config.js +7 -0
  4. package/apps/expo-app/index.js +6 -0
  5. package/apps/expo-app/package.json +46 -0
  6. package/apps/expo-app/src/App.jsx +37 -0
  7. package/apps/expo-app/src/navigation/RootNavigator.jsx +82 -0
  8. package/apps/expo-app/src/navigation/types.js +5 -0
  9. package/apps/expo-app/src/screens/HomeScreen.jsx +178 -0
  10. package/package.json +24 -0
  11. package/packages/animations/package.json +20 -0
  12. package/packages/animations/src/components/FadeView.jsx +42 -0
  13. package/packages/animations/src/components/ScaleView.jsx +28 -0
  14. package/packages/animations/src/components/SlideView.jsx +32 -0
  15. package/packages/animations/src/hooks/useFade.js +50 -0
  16. package/packages/animations/src/hooks/useScale.js +59 -0
  17. package/packages/animations/src/hooks/useSlide.js +53 -0
  18. package/packages/animations/src/index.js +21 -0
  19. package/packages/animations/src/reanimated.js +83 -0
  20. package/packages/core/package.json +22 -0
  21. package/packages/core/src/components/Button.jsx +92 -0
  22. package/packages/core/src/components/Card.jsx +47 -0
  23. package/packages/core/src/components/Container.jsx +61 -0
  24. package/packages/core/src/components/Input.jsx +83 -0
  25. package/packages/core/src/components/List.jsx +80 -0
  26. package/packages/core/src/components/index.js +9 -0
  27. package/packages/core/src/hooks/index.js +5 -0
  28. package/packages/core/src/hooks/useAsync.js +60 -0
  29. package/packages/core/src/hooks/useCounter.js +36 -0
  30. package/packages/core/src/hooks/useToggle.js +18 -0
  31. package/packages/core/src/index.js +5 -0
  32. package/packages/core/src/theme/index.js +67 -0
  33. package/packages/core/src/utils/helpers.js +93 -0
  34. package/packages/core/src/utils/index.js +10 -0
  35. package/packages/device/package.json +24 -0
  36. package/packages/device/src/hooks/useCamera.js +45 -0
  37. package/packages/device/src/hooks/useGallery.js +70 -0
  38. package/packages/device/src/hooks/useLocation.js +99 -0
  39. package/packages/device/src/index.js +5 -0
  40. package/packages/examples/package.json +36 -0
  41. package/packages/examples/src/experiments/animations-device/AnimationsDeviceScreen.jsx +291 -0
  42. package/packages/examples/src/experiments/basic-app/BasicAppScreen.jsx +162 -0
  43. package/packages/examples/src/experiments/components-props-state/ComponentsStateScreen.jsx +280 -0
  44. package/packages/examples/src/experiments/navigation/NavigationScreen.jsx +202 -0
  45. package/packages/examples/src/experiments/network-storage/NetworkStorageScreen.jsx +367 -0
  46. package/packages/examples/src/experiments/state-management/StateManagementScreen.jsx +255 -0
  47. package/packages/examples/src/index.js +76 -0
  48. package/packages/navigation/package.json +20 -0
  49. package/packages/navigation/src/DrawerNavigator.jsx +35 -0
  50. package/packages/navigation/src/StackNavigator.jsx +51 -0
  51. package/packages/navigation/src/TabNavigator.jsx +44 -0
  52. package/packages/navigation/src/createAppNavigator.jsx +48 -0
  53. package/packages/navigation/src/index.js +8 -0
  54. package/packages/navigation/src/types.js +18 -0
  55. package/packages/network/package.json +19 -0
  56. package/packages/network/src/apiClient.js +90 -0
  57. package/packages/network/src/fetchHelpers.js +97 -0
  58. package/packages/network/src/hooks/useFetch.js +56 -0
  59. package/packages/network/src/index.js +3 -0
  60. package/packages/network/src/types.js +4 -0
  61. package/packages/state/package.json +22 -0
  62. package/packages/state/src/context/AuthContext.jsx +94 -0
  63. package/packages/state/src/context/ThemeContext.jsx +79 -0
  64. package/packages/state/src/context/index.js +3 -0
  65. package/packages/state/src/index.js +5 -0
  66. package/packages/state/src/redux/hooks.js +12 -0
  67. package/packages/state/src/redux/index.js +7 -0
  68. package/packages/state/src/redux/slices/counterSlice.js +39 -0
  69. package/packages/state/src/redux/slices/postsSlice.js +92 -0
  70. package/packages/state/src/redux/store.js +32 -0
  71. package/packages/storage/package.json +24 -0
  72. package/packages/storage/src/asyncStorage.js +82 -0
  73. package/packages/storage/src/index.js +2 -0
  74. package/packages/storage/src/sqlite/database.js +65 -0
  75. package/packages/storage/src/sqlite/index.js +3 -0
  76. package/packages/storage/src/sqlite/operations.js +112 -0
  77. package/packages/storage/src/sqlite/useSQLite.js +45 -0
@@ -0,0 +1,367 @@
1
+ /**
2
+ * Experiment 05 — Networking & Storage
3
+ * Module 2: Fetch · Axios · AsyncStorage · SQLite
4
+ *
5
+ * Demonstrates:
6
+ * ✓ Fetch API with async/await
7
+ * ✓ Axios via @expotesting/network apiClient
8
+ * ✓ useFetch hook
9
+ * ✓ AsyncStorage CRUD (wrapped via @expotesting/storage)
10
+ * ✓ SQLite CRUD operations
11
+ * ✓ FlatList with real data
12
+ * ✓ Loading / error states
13
+ */
14
+
15
+ import React, { useCallback, useEffect, useState } from 'react';
16
+ import {
17
+ ActivityIndicator,
18
+ FlatList,
19
+ Pressable,
20
+ ScrollView,
21
+ StyleSheet,
22
+ Text,
23
+ TextInput,
24
+ View } from 'react-native';
25
+ import { apiClient } from '@expotesting/network';
26
+ import { appStorage, useSQLite } from '@expotesting/storage';
27
+
28
+ // ── Types ─────────────────────────────────────────────────────────────────────
29
+
30
+ // ── Sub-section: Fetch API demo ───────────────────────────────────────────────
31
+ function FetchSection(){
32
+ const [posts, setPosts] = useState([]);
33
+ const [loading, setLoading] = useState(false);
34
+ const [error, setError] = useState(null);
35
+
36
+ const fetchWithNative = useCallback(async () => {
37
+ setLoading(true);
38
+ setError(null);
39
+ try {
40
+ // Native Fetch API — async/await pattern
41
+ const response = await fetch(
42
+ 'https://jsonplaceholder.typicode.com/posts?_limit=5',
43
+ );
44
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
45
+ const data = (await response.json());
46
+ setPosts(data);
47
+ } catch (err) {
48
+ setError(err instanceof Error ? err.message : 'Fetch failed');
49
+ } finally {
50
+ setLoading(false);
51
+ }
52
+ }, []);
53
+
54
+ return (
55
+ <View style={styles.widget}>
56
+ <Text style={styles.widgetTitle}>Native fetch() + async/await</Text>
57
+ <Pressable
58
+ style={[styles.btn, loading && styles.btnDisabled]}
59
+ onPress={() => void fetchWithNative()}
60
+ disabled={loading}
61
+ >
62
+ <Text style={styles.btnText}>{loading ? 'Fetching…' : 'Fetch Posts'}</Text>
63
+ </Pressable>
64
+ {loading && <ActivityIndicator color="#6200EE" style={{ marginTop: 8 }} />}
65
+ {error && <Text style={styles.errorText}>{error}</Text>}
66
+ <FlatList
67
+ data={posts}
68
+ scrollEnabled={false}
69
+ keyExtractor={(item) => String(item.id)}
70
+ renderItem={({ item }) => (
71
+ <View style={styles.postRow}>
72
+ <Text style={styles.postId}>#{item.id}</Text>
73
+ <Text style={styles.postTitle} numberOfLines={1}>{item.title}</Text>
74
+ </View>
75
+ )}
76
+ />
77
+ </View>
78
+ );
79
+ }
80
+
81
+ // ── Sub-section: Axios client demo ────────────────────────────────────────────
82
+ function AxiosSection(){
83
+ const [post, setPost] = useState(null);
84
+ const [loading, setLoading] = useState(false);
85
+ const [error, setError] = useState(null);
86
+
87
+ const fetchWithAxios = useCallback(async () => {
88
+ setLoading(true);
89
+ setError(null);
90
+ try {
91
+ // Axios instance from @expotesting/network
92
+ const response = await apiClient.get<Post>('/posts/1');
93
+ setPost(response.data);
94
+ } catch (err) {
95
+ setError(err instanceof Error ? err.message : 'Axios error');
96
+ } finally {
97
+ setLoading(false);
98
+ }
99
+ }, []);
100
+
101
+ return (
102
+ <View style={styles.widget}>
103
+ <Text style={styles.widgetTitle}>Axios client — GET /posts/1</Text>
104
+ <Pressable
105
+ style={[styles.btn, loading && styles.btnDisabled]}
106
+ onPress={() => void fetchWithAxios()}
107
+ disabled={loading}
108
+ >
109
+ <Text style={styles.btnText}>{loading ? 'Loading…' : 'Fetch via Axios'}</Text>
110
+ </Pressable>
111
+ {loading && <ActivityIndicator color="#6200EE" style={{ marginTop: 8 }} />}
112
+ {error && <Text style={styles.errorText}>{error}</Text>}
113
+ {post && (
114
+ <View style={styles.detailCard}>
115
+ <Text style={styles.detailTitle}>{post.title}</Text>
116
+ <Text style={styles.detailBody}>{post.body}</Text>
117
+ </View>
118
+ )}
119
+ </View>
120
+ );
121
+ }
122
+
123
+ // ── Sub-section: AsyncStorage CRUD ───────────────────────────────────────────
124
+ const STORAGE_KEY = 'exp05:savedNote';
125
+
126
+ function AsyncStorageSection(){
127
+ const [note, setNote] = useState('');
128
+ const [saved, setSaved] = useState(null);
129
+ const [status, setStatus] = useState('idle');
130
+
131
+ // Load on mount
132
+ useEffect(() => {
133
+ void (async () => {
134
+ setStatus('loading');
135
+ const value = await appStorage.getItem<string>(STORAGE_KEY);
136
+ setSaved(value);
137
+ setStatus('idle');
138
+ })();
139
+ }, []);
140
+
141
+ const save = useCallback(async () => {
142
+ setStatus('saving');
143
+ await appStorage.setItem(STORAGE_KEY, note.trim());
144
+ setSaved(note.trim());
145
+ setNote('');
146
+ setStatus('idle');
147
+ }, [note]);
148
+
149
+ const clear = useCallback(async () => {
150
+ await appStorage.removeItem(STORAGE_KEY);
151
+ setSaved(null);
152
+ }, []);
153
+
154
+ return (
155
+ <View style={styles.widget}>
156
+ <Text style={styles.widgetTitle}>AsyncStorage</Text>
157
+ {status === 'loading' ? (
158
+ <ActivityIndicator color="#6200EE" />
159
+ ) : (
160
+ <>
161
+ <Text style={styles.noteSmall}>Persisted across app restarts.</Text>
162
+ {saved && (
163
+ <View style={styles.savedBox}>
164
+ <Text style={styles.savedLabel}>Saved value:</Text>
165
+ <Text style={styles.savedValue}>{saved}</Text>
166
+ </View>
167
+ )}
168
+ <TextInput
169
+ style={styles.input}
170
+ value={note}
171
+ onChangeText={setNote}
172
+ placeholder="Type something to save…"
173
+ placeholderTextColor="#aaa"
174
+ />
175
+ <View style={styles.row}>
176
+ <Pressable
177
+ style={[styles.btn, { flex: 1 }, status === 'saving' && styles.btnDisabled]}
178
+ onPress={() => void save()}
179
+ disabled={status === 'saving' || !note.trim()}
180
+ >
181
+ <Text style={styles.btnText}>{status === 'saving' ? 'Saving…' : 'Save'}</Text>
182
+ </Pressable>
183
+ {saved && (
184
+ <Pressable
185
+ style={[styles.btn, { flex: 1, backgroundColor: '#B00020', marginLeft: 8 }]}
186
+ onPress={() => void clear()}
187
+ >
188
+ <Text style={styles.btnText}>Clear</Text>
189
+ </Pressable>
190
+ )}
191
+ </View>
192
+ </>
193
+ )}
194
+ </View>
195
+ );
196
+ }
197
+
198
+ // ── Sub-section: SQLite CRUD ──────────────────────────────────────────────────
199
+ function SQLiteSection(){
200
+ const { db, ready, error: dbError } = useSQLite('exp05.db');
201
+ const [notes, setNotes] = useState([]);
202
+ const [input, setInput] = useState('');
203
+
204
+ // Initialise table and load
205
+ useEffect(() => {
206
+ if (!ready || !db) return;
207
+ void db
208
+ .runAsync(
209
+ `CREATE TABLE IF NOT EXISTS notes (
210
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
211
+ content TEXT NOT NULL,
212
+ createdAt TEXT NOT NULL
213
+ )`,
214
+ )
215
+ .then(() => loadNotes());
216
+ // eslint-disable-next-line react-hooks/exhaustive-deps
217
+ }, [ready, db]);
218
+
219
+ const loadNotes = useCallback(async () => {
220
+ if (!db) return;
221
+ const rows = await db.getAllAsync<Note>('SELECT * FROM notes ORDER BY id DESC LIMIT 10');
222
+ setNotes(rows);
223
+ }, [db]);
224
+
225
+ const addNote = useCallback(async () => {
226
+ if (!db || !input.trim()) return;
227
+ await db.runAsync('INSERT INTO notes (content, createdAt) VALUES (?, ?)', [
228
+ input.trim(),
229
+ new Date().toISOString(),
230
+ ]);
231
+ setInput('');
232
+ await loadNotes();
233
+ }, [db, input, loadNotes]);
234
+
235
+ const deleteNote = useCallback(async (id) => {
236
+ if (!db) return;
237
+ await db.runAsync('DELETE FROM notes WHERE id = ?', [id]);
238
+ await loadNotes();
239
+ }, [db, loadNotes]);
240
+
241
+ if (dbError) return <Text style={styles.errorText}>SQLite: {dbError}</Text>;
242
+ if (!ready) return <ActivityIndicator color="#6200EE" />;
243
+
244
+ return (
245
+ <View style={styles.widget}>
246
+ <Text style={styles.widgetTitle}>SQLite — Notes CRUD</Text>
247
+ <Text style={styles.noteSmall}>
248
+ CREATE TABLE · INSERT · SELECT · DELETE
249
+ </Text>
250
+ <View style={styles.row}>
251
+ <TextInput
252
+ style={[styles.input, { flex: 1 }]}
253
+ value={input}
254
+ onChangeText={setInput}
255
+ placeholder="New note…"
256
+ placeholderTextColor="#aaa"
257
+ onSubmitEditing={() => void addNote()}
258
+ returnKeyType="done"
259
+ />
260
+ <Pressable
261
+ style={[styles.btn, { marginLeft: 8 }]}
262
+ onPress={() => void addNote()}
263
+ disabled={!input.trim()}
264
+ >
265
+ <Text style={styles.btnText}>Add</Text>
266
+ </Pressable>
267
+ </View>
268
+ {notes.map((n) => (
269
+ <View key={n.id} style={styles.sqliteRow}>
270
+ <Text style={styles.sqliteId}>#{n.id}</Text>
271
+ <Text style={styles.sqliteContent}>{n.content}</Text>
272
+ <Pressable onPress={() => void deleteNote(n.id)} style={styles.deleteBtn}>
273
+ <Text style={styles.deleteBtnText}>✕</Text>
274
+ </Pressable>
275
+ </View>
276
+ ))}
277
+ {notes.length === 0 && (
278
+ <Text style={styles.emptyMsg}>No notes yet. Add one above.</Text>
279
+ )}
280
+ </View>
281
+ );
282
+ }
283
+
284
+ // ── Main screen ───────────────────────────────────────────────────────────────
285
+ export function NetworkStorageScreen(){
286
+ return (
287
+ <ScrollView style={styles.container} contentContainerStyle={styles.content}>
288
+ <View style={styles.header}>
289
+ <Text style={styles.heading}>Experiment 05</Text>
290
+ <Text style={styles.subheading}>Networking & Storage</Text>
291
+ </View>
292
+
293
+ <View style={styles.section}>
294
+ <Text style={styles.sectionTitle}>Networking</Text>
295
+ <FetchSection />
296
+ <AxiosSection />
297
+ </View>
298
+
299
+ <View style={styles.section}>
300
+ <Text style={styles.sectionTitle}>Storage</Text>
301
+ <AsyncStorageSection />
302
+ <SQLiteSection />
303
+ </View>
304
+ </ScrollView>
305
+ );
306
+ }
307
+
308
+ // ── Styles ────────────────────────────────────────────────────────────────────
309
+ const styles = StyleSheet.create({
310
+ container: { flex: 1, backgroundColor: '#fff' },
311
+ content: { paddingBottom: 40 },
312
+ header: { backgroundColor: '#6200EE', padding: 24, paddingTop: 40 },
313
+ heading: { fontSize: 28, fontWeight: '700', color: '#fff' },
314
+ subheading: { fontSize: 16, color: 'rgba(255,255,255,0.8)', marginTop: 4 },
315
+ section: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' },
316
+ sectionTitle: { fontSize: 18, fontWeight: '700', color: '#333', marginBottom: 12 },
317
+ widget: { backgroundColor: '#f9f9f9', borderRadius: 12, padding: 14, marginBottom: 12 },
318
+ widgetTitle: { fontSize: 15, fontWeight: '700', color: '#333', marginBottom: 8 },
319
+ noteSmall: { fontSize: 12, color: '#888', marginBottom: 8 },
320
+ btn: {
321
+ backgroundColor: '#6200EE',
322
+ paddingVertical: 10,
323
+ paddingHorizontal: 20,
324
+ borderRadius: 8,
325
+ alignItems: 'center',
326
+ justifyContent: 'center' },
327
+ btnDisabled: { opacity: 0.5 },
328
+ btnText: { color: '#fff', fontWeight: '700', fontSize: 14 },
329
+ errorText: { color: '#B00020', fontSize: 13, marginVertical: 4 },
330
+ postRow: {
331
+ flexDirection: 'row',
332
+ alignItems: 'center',
333
+ paddingVertical: 6,
334
+ borderBottomWidth: 1,
335
+ borderBottomColor: '#eee',
336
+ gap: 8 },
337
+ postId: { fontSize: 12, color: '#aaa', width: 28 },
338
+ postTitle: { flex: 1, fontSize: 13, color: '#333' },
339
+ detailCard: { backgroundColor: '#fff', borderRadius: 8, padding: 10, marginTop: 8, borderWidth: 1, borderColor: '#eee' },
340
+ detailTitle: { fontSize: 14, fontWeight: '600', color: '#333', marginBottom: 4 },
341
+ detailBody: { fontSize: 13, color: '#777', lineHeight: 18 },
342
+ savedBox: { backgroundColor: '#e8f5e9', borderRadius: 6, padding: 10, marginBottom: 8 },
343
+ savedLabel: { fontSize: 11, color: '#555', marginBottom: 2 },
344
+ savedValue: { fontSize: 14, fontWeight: '600', color: '#2E7D32' },
345
+ input: {
346
+ height: 44,
347
+ borderWidth: 1.5,
348
+ borderColor: '#ddd',
349
+ borderRadius: 8,
350
+ paddingHorizontal: 12,
351
+ fontSize: 14,
352
+ color: '#212121',
353
+ marginBottom: 8,
354
+ backgroundColor: '#fff' },
355
+ row: { flexDirection: 'row', gap: 8 },
356
+ sqliteRow: {
357
+ flexDirection: 'row',
358
+ alignItems: 'center',
359
+ paddingVertical: 8,
360
+ borderBottomWidth: 1,
361
+ borderBottomColor: '#eee',
362
+ gap: 8 },
363
+ sqliteId: { fontSize: 11, color: '#aaa', width: 28 },
364
+ sqliteContent: { flex: 1, fontSize: 13, color: '#333' },
365
+ deleteBtn: { padding: 4 },
366
+ deleteBtnText: { color: '#D32F2F', fontSize: 15 },
367
+ emptyMsg: { color: '#aaa', textAlign: 'center', paddingVertical: 12 } });
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Experiment 04 — State Management
3
+ * Module 2: useState · useReducer · Context API · Redux Toolkit
4
+ *
5
+ * Demonstrates:
6
+ * ✓ Context API (ThemeContext + AuthContext) from @expotesting/state
7
+ * ✓ Redux Toolkit counter slice (sync actions)
8
+ * ✓ Redux Toolkit posts slice (async thunk)
9
+ * ✓ useAppSelector + useAppDispatch typed hooks
10
+ * ✓ Provider wiring
11
+ */
12
+
13
+ import React, { useState } from 'react';
14
+ import { ActivityIndicator, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
15
+ import { Provider } from 'react-redux';
16
+
17
+ import {
18
+ store,
19
+ useAppDispatch,
20
+ useAppSelector,
21
+ increment,
22
+ decrement,
23
+ reset as resetCounter,
24
+ selectCount,
25
+ fetchPosts,
26
+ selectAllPosts,
27
+ selectPostsStatus,
28
+ selectPostsError,
29
+ ThemeProvider,
30
+ useTheme,
31
+ AuthProvider,
32
+ useAuth } from '@expotesting/state';
33
+
34
+ // ── Redux Counter widget ─────────────────────────────────────────────────────
35
+ function ReduxCounterWidget(){
36
+ const dispatch = useAppDispatch();
37
+ const count = useAppSelector(selectCount);
38
+
39
+ return (
40
+ <View style={styles.widget}>
41
+ <Text style={styles.widgetTitle}>Redux Counter</Text>
42
+ <Text style={styles.bigNumber}>{count}</Text>
43
+ <View style={styles.row}>
44
+ <Pressable style={styles.btn} onPress={() => dispatch(decrement())}>
45
+ <Text style={styles.btnText}>−</Text>
46
+ </Pressable>
47
+ <Pressable style={styles.btn} onPress={() => dispatch(increment())}>
48
+ <Text style={styles.btnText}>+</Text>
49
+ </Pressable>
50
+ <Pressable style={[styles.btn, styles.btnGhost]} onPress={() => dispatch(resetCounter())}>
51
+ <Text style={[styles.btnText, { color: '#6200EE' }]}>Reset</Text>
52
+ </Pressable>
53
+ </View>
54
+ <Text style={styles.noteSmall}>
55
+ Actions dispatched: increment · decrement · reset (counterSlice)
56
+ </Text>
57
+ </View>
58
+ );
59
+ }
60
+
61
+ // ── Redux async posts widget ──────────────────────────────────────────────────
62
+ function ReduxPostsWidget(){
63
+ const dispatch = useAppDispatch();
64
+ const posts = useAppSelector(selectAllPosts);
65
+ const status = useAppSelector(selectPostsStatus);
66
+ const error = useAppSelector(selectPostsError);
67
+
68
+ return (
69
+ <View style={styles.widget}>
70
+ <Text style={styles.widgetTitle}>Redux Async Thunk — Posts</Text>
71
+ <Pressable
72
+ style={[styles.btn, styles.btnFull, status === 'loading' && { opacity: 0.6 }]}
73
+ onPress={() => void dispatch(fetchPosts({ page: 1, limit: 5 }))}
74
+ disabled={status === 'loading'}
75
+ >
76
+ <Text style={styles.btnText}>
77
+ {status === 'loading' ? 'Loading…' : 'Fetch Posts (page 1)'}
78
+ </Text>
79
+ </Pressable>
80
+
81
+ {status === 'loading' && <ActivityIndicator color="#6200EE" style={{ marginTop: 8 }} />}
82
+ {error && <Text style={styles.errorText}>{error}</Text>}
83
+ {posts.slice(0, 3).map((p) => (
84
+ <View key={p.id} style={styles.postCard}>
85
+ <Text style={styles.postTitle} numberOfLines={1}>{p.title}</Text>
86
+ <Text style={styles.postBody} numberOfLines={2}>{p.body}</Text>
87
+ </View>
88
+ ))}
89
+ {posts.length > 3 && (
90
+ <Text style={styles.noteSmall}>…and {posts.length - 3} more</Text>
91
+ )}
92
+ </View>
93
+ );
94
+ }
95
+
96
+ // ── Context theme widget ───────────────────────────────────────────────────────
97
+ function ThemeWidget(){
98
+ const { colorScheme, isDark, toggleTheme, colors } = useTheme();
99
+
100
+ return (
101
+ <View style={[styles.widget, { backgroundColor: colors.surface }]}>
102
+ <Text style={[styles.widgetTitle, { color: colors.text }]}>Context API — Theme</Text>
103
+ <Text style={[styles.noteSmall, { color: colors.subtext }]}>
104
+ Current scheme: <Text style={{ fontWeight: '700' }}>{colorScheme}</Text>
105
+ </Text>
106
+ <Pressable
107
+ style={[styles.btn, styles.btnFull, { backgroundColor: colors.primary }]}
108
+ onPress={toggleTheme}
109
+ >
110
+ <Text style={styles.btnText}>
111
+ Switch to {isDark ? 'Light' : 'Dark'} Mode
112
+ </Text>
113
+ </Pressable>
114
+ <View style={[styles.colorSwatch, { backgroundColor: colors.background }]}>
115
+ <Text style={{ color: colors.text, fontSize: 12 }}>background: {colors.background}</Text>
116
+ </View>
117
+ <View style={[styles.colorSwatch, { backgroundColor: colors.primary }]}>
118
+ <Text style={{ color: '#fff', fontSize: 12 }}>primary: {colors.primary}</Text>
119
+ </View>
120
+ </View>
121
+ );
122
+ }
123
+
124
+ // ── Context auth widget ───────────────────────────────────────────────────────
125
+ function AuthWidget(){
126
+ const { user, isAuthenticated, loading, error, login, logout } = useAuth();
127
+ const [email, setEmail] = useState('alice@example.com');
128
+ const [password, setPassword] = useState('secret123');
129
+
130
+ if (isAuthenticated && user) {
131
+ return (
132
+ <View style={styles.widget}>
133
+ <Text style={styles.widgetTitle}>Context API — Auth (Logged In)</Text>
134
+ <Text style={styles.noteSmall}>Welcome, <Text style={{ fontWeight: '700' }}>{user.name}</Text></Text>
135
+ <Text style={styles.noteSmall}>{user.email}</Text>
136
+ <Pressable style={[styles.btn, styles.btnFull, { backgroundColor: '#B00020' }]} onPress={logout}>
137
+ <Text style={styles.btnText}>Logout</Text>
138
+ </Pressable>
139
+ </View>
140
+ );
141
+ }
142
+
143
+ return (
144
+ <View style={styles.widget}>
145
+ <Text style={styles.widgetTitle}>Context API — Auth</Text>
146
+ <TextInput
147
+ style={styles.input}
148
+ value={email}
149
+ onChangeText={setEmail}
150
+ placeholder="Email"
151
+ placeholderTextColor="#aaa"
152
+ keyboardType="email-address"
153
+ autoCapitalize="none"
154
+ />
155
+ <TextInput
156
+ style={styles.input}
157
+ value={password}
158
+ onChangeText={setPassword}
159
+ placeholder="Password (min 6 chars)"
160
+ placeholderTextColor="#aaa"
161
+ secureTextEntry
162
+ />
163
+ {error && <Text style={styles.errorText}>{error}</Text>}
164
+ <Pressable
165
+ style={[styles.btn, styles.btnFull, loading && { opacity: 0.6 }]}
166
+ onPress={() => void login(email, password)}
167
+ disabled={loading}
168
+ >
169
+ <Text style={styles.btnText}>{loading ? 'Logging in…' : 'Login'}</Text>
170
+ </Pressable>
171
+ </View>
172
+ );
173
+ }
174
+
175
+ // ── Root — wraps both Providers ───────────────────────────────────────────────
176
+ export function StateManagementScreen(){
177
+ return (
178
+ <Provider store={store}>
179
+ <ThemeProvider>
180
+ <AuthProvider>
181
+ <StateManagementInner />
182
+ </AuthProvider>
183
+ </ThemeProvider>
184
+ </Provider>
185
+ );
186
+ }
187
+
188
+ function StateManagementInner(){
189
+ const { colors } = useTheme();
190
+
191
+ return (
192
+ <ScrollView style={[styles.container, { backgroundColor: colors.background }]} contentContainerStyle={styles.content}>
193
+ <View style={styles.header}>
194
+ <Text style={styles.heading}>Experiment 04</Text>
195
+ <Text style={styles.subheading}>State Management</Text>
196
+ </View>
197
+
198
+ <View style={styles.section}>
199
+ <Text style={styles.sectionTitle}>Redux Toolkit</Text>
200
+ <ReduxCounterWidget />
201
+ <ReduxPostsWidget />
202
+ </View>
203
+
204
+ <View style={styles.section}>
205
+ <Text style={styles.sectionTitle}>Context API</Text>
206
+ <ThemeWidget />
207
+ <AuthWidget />
208
+ </View>
209
+ </ScrollView>
210
+ );
211
+ }
212
+
213
+ // ── Styles ────────────────────────────────────────────────────────────────────
214
+ const styles = StyleSheet.create({
215
+ container: { flex: 1, backgroundColor: '#fff' },
216
+ content: { paddingBottom: 40 },
217
+ header: { backgroundColor: '#6200EE', padding: 24, paddingTop: 40 },
218
+ heading: { fontSize: 28, fontWeight: '700', color: '#fff' },
219
+ subheading: { fontSize: 16, color: 'rgba(255,255,255,0.8)', marginTop: 4 },
220
+ section: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#e0e0e0' },
221
+ sectionTitle: { fontSize: 18, fontWeight: '700', color: '#333', marginBottom: 12 },
222
+ widget: { backgroundColor: '#f9f9f9', borderRadius: 12, padding: 16, marginBottom: 12 },
223
+ widgetTitle: { fontSize: 15, fontWeight: '700', color: '#333', marginBottom: 8 },
224
+ bigNumber: { fontSize: 56, fontWeight: '800', color: '#6200EE', textAlign: 'center', marginVertical: 4 },
225
+ row: { flexDirection: 'row', gap: 8, justifyContent: 'center', marginBottom: 8 },
226
+ btn: {
227
+ backgroundColor: '#6200EE',
228
+ paddingHorizontal: 20,
229
+ paddingVertical: 10,
230
+ borderRadius: 8,
231
+ alignItems: 'center',
232
+ justifyContent: 'center',
233
+ minWidth: 60 },
234
+ btnGhost: { backgroundColor: '#f0e6ff', borderWidth: 1, borderColor: '#6200EE' },
235
+ btnFull: { width: '100%', marginBottom: 8 },
236
+ btnText: { color: '#fff', fontWeight: '700', fontSize: 14 },
237
+ noteSmall: { fontSize: 12, color: '#888', textAlign: 'center', marginBottom: 4 },
238
+ errorText: { color: '#B00020', fontSize: 13, marginBottom: 8 },
239
+ postCard: { backgroundColor: '#fff', borderRadius: 8, padding: 10, marginTop: 6, borderWidth: 1, borderColor: '#eee' },
240
+ postTitle: { fontSize: 13, fontWeight: '600', color: '#333', marginBottom: 2 },
241
+ postBody: { fontSize: 12, color: '#777', lineHeight: 18 },
242
+ input: {
243
+ height: 44,
244
+ borderWidth: 1.5,
245
+ borderColor: '#ddd',
246
+ borderRadius: 8,
247
+ paddingHorizontal: 12,
248
+ fontSize: 14,
249
+ color: '#212121',
250
+ marginBottom: 8,
251
+ backgroundColor: '#fff' },
252
+ colorSwatch: {
253
+ borderRadius: 6,
254
+ padding: 8,
255
+ marginTop: 4 } });
@@ -0,0 +1,76 @@
1
+ // Experiment 01 — React Native Core Components
2
+ export { BasicAppScreen } from './experiments/basic-app/BasicAppScreen';
3
+
4
+ // Experiment 02 — Components, Props & State
5
+ export { ComponentsStateScreen } from './experiments/components-props-state/ComponentsStateScreen';
6
+
7
+ // Experiment 03 — Navigation
8
+ export {
9
+ NavHomeScreen,
10
+ NavDetailsScreen,
11
+ NavProfileScreen } from './experiments/navigation/NavigationScreen';
12
+
13
+ // Experiment 04 — State Management (Redux + Context)
14
+ export { StateManagementScreen } from './experiments/state-management/StateManagementScreen';
15
+
16
+ // Experiment 05 — Networking & Storage
17
+ export { NetworkStorageScreen } from './experiments/network-storage/NetworkStorageScreen';
18
+
19
+ // Experiment 06 — Animations & Device Features
20
+ export { AnimationsDeviceScreen } from './experiments/animations-device/AnimationsDeviceScreen';
21
+
22
+ /**
23
+ * EXPERIMENT_REGISTRY — ordered list for the demo app to auto-generate
24
+ * the experiment catalogue screen.
25
+ */
26
+ export const EXPERIMENT_REGISTRY = [
27
+ {
28
+ id: 'exp-01',
29
+ title: 'Basic App',
30
+ subtitle: 'Core Components · JSX · StyleSheet · Flexbox',
31
+ module: 1,
32
+ experiment: 1,
33
+ color: '#6200EE',
34
+ route: 'Exp01' },
35
+ {
36
+ id: 'exp-02',
37
+ title: 'Components, Props & State',
38
+ subtitle: 'useState · useReducer · Event Handling',
39
+ module: 1,
40
+ experiment: 2,
41
+ color: '#3700B3',
42
+ route: 'Exp02' },
43
+ {
44
+ id: 'exp-03',
45
+ title: 'Navigation',
46
+ subtitle: 'Stack · Tabs · Drawer · Typed Params',
47
+ module: 1,
48
+ experiment: 3,
49
+ color: '#018786',
50
+ route: 'Exp03' },
51
+ {
52
+ id: 'exp-04',
53
+ title: 'State Management',
54
+ subtitle: 'Context API · Redux Toolkit · Async Thunks',
55
+ module: 2,
56
+ experiment: 4,
57
+ color: '#B00020',
58
+ route: 'Exp04' },
59
+ {
60
+ id: 'exp-05',
61
+ title: 'Networking & Storage',
62
+ subtitle: 'Fetch · Axios · AsyncStorage · SQLite',
63
+ module: 2,
64
+ experiment: 5,
65
+ color: '#1565C0',
66
+ route: 'Exp05' },
67
+ {
68
+ id: 'exp-06',
69
+ title: 'Animations & Device',
70
+ subtitle: 'Animated API · Reanimated · Camera · GPS',
71
+ module: 3,
72
+ experiment: 6,
73
+ color: '#2E7D32',
74
+ route: 'Exp06' },
75
+ ];
76
+