expotesting1 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.
- package/README.md +289 -0
- package/apps/expo-app/app.json +60 -0
- package/apps/expo-app/babel.config.js +7 -0
- package/apps/expo-app/index.js +6 -0
- package/apps/expo-app/package.json +46 -0
- package/apps/expo-app/src/App.jsx +37 -0
- package/apps/expo-app/src/navigation/RootNavigator.jsx +82 -0
- package/apps/expo-app/src/navigation/types.js +5 -0
- package/apps/expo-app/src/screens/HomeScreen.jsx +178 -0
- package/package.json +25 -0
- package/packages/animations/package.json +20 -0
- package/packages/animations/src/components/FadeView.jsx +42 -0
- package/packages/animations/src/components/ScaleView.jsx +28 -0
- package/packages/animations/src/components/SlideView.jsx +32 -0
- package/packages/animations/src/hooks/useFade.js +50 -0
- package/packages/animations/src/hooks/useScale.js +59 -0
- package/packages/animations/src/hooks/useSlide.js +53 -0
- package/packages/animations/src/index.js +21 -0
- package/packages/animations/src/reanimated.js +83 -0
- package/packages/core/package.json +22 -0
- package/packages/core/src/components/Button.jsx +92 -0
- package/packages/core/src/components/Card.jsx +47 -0
- package/packages/core/src/components/Container.jsx +61 -0
- package/packages/core/src/components/Input.jsx +83 -0
- package/packages/core/src/components/List.jsx +80 -0
- package/packages/core/src/components/index.js +9 -0
- package/packages/core/src/hooks/index.js +5 -0
- package/packages/core/src/hooks/useAsync.js +60 -0
- package/packages/core/src/hooks/useCounter.js +36 -0
- package/packages/core/src/hooks/useToggle.js +18 -0
- package/packages/core/src/index.js +5 -0
- package/packages/core/src/theme/index.js +67 -0
- package/packages/core/src/utils/helpers.js +93 -0
- package/packages/core/src/utils/index.js +10 -0
- package/packages/device/package.json +24 -0
- package/packages/device/src/hooks/useCamera.js +45 -0
- package/packages/device/src/hooks/useGallery.js +70 -0
- package/packages/device/src/hooks/useLocation.js +99 -0
- package/packages/device/src/index.js +5 -0
- package/packages/examples/package.json +36 -0
- package/packages/examples/src/experiments/animations-device/AnimationsDeviceScreen.jsx +291 -0
- package/packages/examples/src/experiments/basic-app/BasicAppScreen.jsx +162 -0
- package/packages/examples/src/experiments/components-props-state/ComponentsStateScreen.jsx +280 -0
- package/packages/examples/src/experiments/navigation/NavigationScreen.jsx +202 -0
- package/packages/examples/src/experiments/network-storage/NetworkStorageScreen.jsx +367 -0
- package/packages/examples/src/experiments/state-management/StateManagementScreen.jsx +255 -0
- package/packages/examples/src/index.js +76 -0
- package/packages/navigation/package.json +20 -0
- package/packages/navigation/src/DrawerNavigator.jsx +35 -0
- package/packages/navigation/src/StackNavigator.jsx +51 -0
- package/packages/navigation/src/TabNavigator.jsx +44 -0
- package/packages/navigation/src/createAppNavigator.jsx +48 -0
- package/packages/navigation/src/index.js +8 -0
- package/packages/navigation/src/types.js +18 -0
- package/packages/network/package.json +19 -0
- package/packages/network/src/apiClient.js +90 -0
- package/packages/network/src/fetchHelpers.js +97 -0
- package/packages/network/src/hooks/useFetch.js +56 -0
- package/packages/network/src/index.js +3 -0
- package/packages/network/src/types.js +4 -0
- package/packages/state/package.json +22 -0
- package/packages/state/src/context/AuthContext.jsx +94 -0
- package/packages/state/src/context/ThemeContext.jsx +79 -0
- package/packages/state/src/context/index.js +3 -0
- package/packages/state/src/index.js +5 -0
- package/packages/state/src/redux/hooks.js +12 -0
- package/packages/state/src/redux/index.js +7 -0
- package/packages/state/src/redux/slices/counterSlice.js +39 -0
- package/packages/state/src/redux/slices/postsSlice.js +92 -0
- package/packages/state/src/redux/store.js +32 -0
- package/packages/storage/package.json +24 -0
- package/packages/storage/src/asyncStorage.js +82 -0
- package/packages/storage/src/index.js +2 -0
- package/packages/storage/src/sqlite/database.js +65 -0
- package/packages/storage/src/sqlite/index.js +3 -0
- package/packages/storage/src/sqlite/operations.js +112 -0
- 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
|
+
|