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,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment 01 — Basic App
|
|
3
|
+
* Module 1: React Native Core Components
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates:
|
|
6
|
+
* ✓ View, Text, Image — core RN components
|
|
7
|
+
* ✓ StyleSheet + Flexbox layouts
|
|
8
|
+
* ✓ JSX fundamentals
|
|
9
|
+
* ✓ ScrollView
|
|
10
|
+
* ✓ Basic component composition
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import {
|
|
15
|
+
Image,
|
|
16
|
+
ScrollView,
|
|
17
|
+
StyleSheet,
|
|
18
|
+
Text,
|
|
19
|
+
View } from 'react-native';
|
|
20
|
+
|
|
21
|
+
// ── 1. Functional component with typed props ─────────────────────────────────
|
|
22
|
+
|
|
23
|
+
function ProfileCard({ name, role, avatarUrl }){
|
|
24
|
+
return (
|
|
25
|
+
<View style={styles.card}>
|
|
26
|
+
<Image
|
|
27
|
+
source={{ uri: avatarUrl }}
|
|
28
|
+
style={styles.avatar}
|
|
29
|
+
accessibilityLabel={`${name}'s avatar`}
|
|
30
|
+
/>
|
|
31
|
+
<View style={styles.cardText}>
|
|
32
|
+
<Text style={styles.name}>{name}</Text>
|
|
33
|
+
<Text style={styles.role}>{role}</Text>
|
|
34
|
+
</View>
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── 2. Simple stateless component ────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
function Section({ title, children }){
|
|
42
|
+
return (
|
|
43
|
+
<View style={styles.section}>
|
|
44
|
+
<Text style={styles.sectionTitle}>{title}</Text>
|
|
45
|
+
{children}
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── 3. Main experiment screen ─────────────────────────────────────────────────
|
|
51
|
+
export function BasicAppScreen(){
|
|
52
|
+
return (
|
|
53
|
+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
54
|
+
{/* Header */}
|
|
55
|
+
<View style={styles.header}>
|
|
56
|
+
<Text style={styles.heading}>Experiment 01</Text>
|
|
57
|
+
<Text style={styles.subheading}>Basic App — Core Components</Text>
|
|
58
|
+
</View>
|
|
59
|
+
|
|
60
|
+
{/* Section: Core Components */}
|
|
61
|
+
<Section title="Core Components">
|
|
62
|
+
<Text style={styles.bodyText}>
|
|
63
|
+
React Native provides <Text style={styles.code}>View</Text>,{' '}
|
|
64
|
+
<Text style={styles.code}>Text</Text>, and{' '}
|
|
65
|
+
<Text style={styles.code}>Image</Text> as foundational building blocks.
|
|
66
|
+
Everything in the UI is composed from these primitives.
|
|
67
|
+
</Text>
|
|
68
|
+
</Section>
|
|
69
|
+
|
|
70
|
+
{/* Section: Flexbox */}
|
|
71
|
+
<Section title="Flexbox Layout">
|
|
72
|
+
<View style={styles.flexRow}>
|
|
73
|
+
{['flex-start', 'center', 'flex-end'].map((align) => (
|
|
74
|
+
<View key={align} style={styles.flexBox}>
|
|
75
|
+
<Text style={styles.flexLabel}>{align}</Text>
|
|
76
|
+
</View>
|
|
77
|
+
))}
|
|
78
|
+
</View>
|
|
79
|
+
<View style={styles.flexColumn}>
|
|
80
|
+
{[1, 2, 3].map((n) => (
|
|
81
|
+
<View key={n} style={[styles.flexBox, { flex: n }]}>
|
|
82
|
+
<Text style={styles.flexLabel}>flex: {n}</Text>
|
|
83
|
+
</View>
|
|
84
|
+
))}
|
|
85
|
+
</View>
|
|
86
|
+
</Section>
|
|
87
|
+
|
|
88
|
+
{/* Section: ProfileCard component */}
|
|
89
|
+
<Section title="Component Composition">
|
|
90
|
+
<ProfileCard
|
|
91
|
+
name="Alice Johnson"
|
|
92
|
+
role="Software Engineer"
|
|
93
|
+
avatarUrl="https://api.dicebear.com/7.x/personas/svg?seed=alice"
|
|
94
|
+
/>
|
|
95
|
+
<ProfileCard
|
|
96
|
+
name="Bob Smith"
|
|
97
|
+
role="Product Designer"
|
|
98
|
+
avatarUrl="https://api.dicebear.com/7.x/personas/svg?seed=bob"
|
|
99
|
+
/>
|
|
100
|
+
</Section>
|
|
101
|
+
|
|
102
|
+
{/* Section: Text variants */}
|
|
103
|
+
<Section title="Text Styles">
|
|
104
|
+
<Text style={styles.h1}>Heading 1</Text>
|
|
105
|
+
<Text style={styles.h2}>Heading 2</Text>
|
|
106
|
+
<Text style={styles.bodyText}>Body text — readable and accessible.</Text>
|
|
107
|
+
<Text style={styles.caption}>Caption — smaller supporting text</Text>
|
|
108
|
+
<Text style={[styles.bodyText, styles.bold]}>Bold text</Text>
|
|
109
|
+
<Text style={[styles.bodyText, styles.italic]}>Italic text</Text>
|
|
110
|
+
</Section>
|
|
111
|
+
</ScrollView>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── 4. Styles — demonstrating StyleSheet.create patterns ─────────────────────
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
container: { flex: 1, backgroundColor: '#fff' },
|
|
118
|
+
content: { paddingBottom: 40 },
|
|
119
|
+
header: {
|
|
120
|
+
backgroundColor: '#6200EE',
|
|
121
|
+
padding: 24,
|
|
122
|
+
paddingTop: 40 },
|
|
123
|
+
heading: { fontSize: 28, fontWeight: '700', color: '#fff' },
|
|
124
|
+
subheading: { fontSize: 16, color: 'rgba(255,255,255,0.8)', marginTop: 4 },
|
|
125
|
+
section: {
|
|
126
|
+
padding: 16,
|
|
127
|
+
borderBottomWidth: 1,
|
|
128
|
+
borderBottomColor: '#e0e0e0' },
|
|
129
|
+
sectionTitle: {
|
|
130
|
+
fontSize: 18,
|
|
131
|
+
fontWeight: '700',
|
|
132
|
+
color: '#333',
|
|
133
|
+
marginBottom: 12 },
|
|
134
|
+
bodyText: { fontSize: 15, color: '#555', lineHeight: 22 },
|
|
135
|
+
caption: { fontSize: 12, color: '#888', marginTop: 4 },
|
|
136
|
+
h1: { fontSize: 28, fontWeight: '700', color: '#212121', marginBottom: 4 },
|
|
137
|
+
h2: { fontSize: 22, fontWeight: '600', color: '#212121', marginBottom: 4 },
|
|
138
|
+
bold: { fontWeight: '700' },
|
|
139
|
+
italic: { fontStyle: 'italic' },
|
|
140
|
+
code: { fontFamily: 'monospace', backgroundColor: '#f5f5f5', color: '#6200EE' },
|
|
141
|
+
// Flexbox demo
|
|
142
|
+
flexRow: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 8 },
|
|
143
|
+
flexColumn: { flexDirection: 'row', height: 60, gap: 4, marginTop: 8 },
|
|
144
|
+
flexBox: {
|
|
145
|
+
backgroundColor: '#e8d5ff',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
justifyContent: 'center',
|
|
148
|
+
borderRadius: 6,
|
|
149
|
+
padding: 8 },
|
|
150
|
+
flexLabel: { fontSize: 10, color: '#6200EE', textAlign: 'center' },
|
|
151
|
+
// ProfileCard
|
|
152
|
+
card: {
|
|
153
|
+
flexDirection: 'row',
|
|
154
|
+
alignItems: 'center',
|
|
155
|
+
padding: 12,
|
|
156
|
+
backgroundColor: '#f9f9f9',
|
|
157
|
+
borderRadius: 10,
|
|
158
|
+
marginBottom: 8 },
|
|
159
|
+
avatar: { width: 52, height: 52, borderRadius: 26, backgroundColor: '#ddd' },
|
|
160
|
+
cardText: { marginLeft: 12 },
|
|
161
|
+
name: { fontSize: 16, fontWeight: '600', color: '#212121' },
|
|
162
|
+
role: { fontSize: 13, color: '#757575', marginTop: 2 } });
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment 02 — Components, Props & State
|
|
3
|
+
* Module 1: React Basics
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates:
|
|
6
|
+
* ✓ useState + useReducer
|
|
7
|
+
* ✓ Event handling (Pressable, TextInput)
|
|
8
|
+
* ✓ Props drilling vs lifted state
|
|
9
|
+
* ✓ Component lifecycle via useEffect
|
|
10
|
+
* ✓ Controlled inputs
|
|
11
|
+
* ✓ Conditional rendering
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useCallback, useEffect, useReducer, useState } from 'react';
|
|
15
|
+
import {
|
|
16
|
+
Pressable,
|
|
17
|
+
ScrollView,
|
|
18
|
+
StyleSheet,
|
|
19
|
+
Text,
|
|
20
|
+
TextInput,
|
|
21
|
+
View } from 'react-native';
|
|
22
|
+
|
|
23
|
+
// ── 1. useReducer — canonical todo pattern ────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function todoReducer(state, action){
|
|
26
|
+
switch (action.type) {
|
|
27
|
+
case 'ADD':
|
|
28
|
+
return [...state, { id: Date.now(), text: action.text, done: false }];
|
|
29
|
+
case 'TOGGLE':
|
|
30
|
+
return state.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
|
|
31
|
+
case 'REMOVE':
|
|
32
|
+
return state.filter((t) => t.id !== action.id);
|
|
33
|
+
default:
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── 2. Controlled TextInput component ────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
function AddTodoInput({ onAdd }){
|
|
41
|
+
const [text, setText] = useState('');
|
|
42
|
+
|
|
43
|
+
const handleSubmit = useCallback(() => {
|
|
44
|
+
const trimmed = text.trim();
|
|
45
|
+
if (!trimmed) return;
|
|
46
|
+
onAdd(trimmed);
|
|
47
|
+
setText('');
|
|
48
|
+
}, [text, onAdd]);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<View style={styles.inputRow}>
|
|
52
|
+
<TextInput
|
|
53
|
+
style={styles.textInput}
|
|
54
|
+
value={text}
|
|
55
|
+
onChangeText={setText}
|
|
56
|
+
placeholder="Add a task…"
|
|
57
|
+
placeholderTextColor="#aaa"
|
|
58
|
+
onSubmitEditing={handleSubmit}
|
|
59
|
+
returnKeyType="done"
|
|
60
|
+
accessibilityLabel="New task input"
|
|
61
|
+
/>
|
|
62
|
+
<Pressable
|
|
63
|
+
onPress={handleSubmit}
|
|
64
|
+
style={({ pressed }) => [styles.addBtn, pressed && { opacity: 0.7 }]}
|
|
65
|
+
accessibilityRole="button"
|
|
66
|
+
accessibilityLabel="Add task"
|
|
67
|
+
>
|
|
68
|
+
<Text style={styles.addBtnText}>Add</Text>
|
|
69
|
+
</Pressable>
|
|
70
|
+
</View>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── 3. TodoItem — demonstrates receiving + using props ────────────────────────
|
|
75
|
+
|
|
76
|
+
function TodoItem({ todo, onToggle, onRemove }){
|
|
77
|
+
return (
|
|
78
|
+
<View style={styles.todoRow}>
|
|
79
|
+
<Pressable
|
|
80
|
+
onPress={() => onToggle(todo.id)}
|
|
81
|
+
style={styles.todoMain}
|
|
82
|
+
accessibilityRole="checkbox"
|
|
83
|
+
accessibilityState={{ checked: todo.done }}
|
|
84
|
+
>
|
|
85
|
+
<View style={[styles.checkbox, todo.done && styles.checkboxDone]}>
|
|
86
|
+
{todo.done && <Text style={styles.checkmark}>✓</Text>}
|
|
87
|
+
</View>
|
|
88
|
+
<Text style={[styles.todoText, todo.done && styles.todoTextDone]}>
|
|
89
|
+
{todo.text}
|
|
90
|
+
</Text>
|
|
91
|
+
</Pressable>
|
|
92
|
+
<Pressable
|
|
93
|
+
onPress={() => onRemove(todo.id)}
|
|
94
|
+
style={styles.removeBtn}
|
|
95
|
+
accessibilityRole="button"
|
|
96
|
+
accessibilityLabel={`Remove ${todo.text}`}
|
|
97
|
+
>
|
|
98
|
+
<Text style={styles.removeBtnText}>✕</Text>
|
|
99
|
+
</Pressable>
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── 4. Counter — pure useState + lifted state example ────────────────────────
|
|
105
|
+
|
|
106
|
+
function Counter({ count, onIncrement, onDecrement, onReset }){
|
|
107
|
+
return (
|
|
108
|
+
<View style={styles.counter}>
|
|
109
|
+
<Pressable onPress={onDecrement} style={styles.counterBtn}>
|
|
110
|
+
<Text style={styles.counterBtnText}>−</Text>
|
|
111
|
+
</Pressable>
|
|
112
|
+
<View style={styles.countDisplay}>
|
|
113
|
+
<Text style={styles.countValue}>{count}</Text>
|
|
114
|
+
<Text style={styles.countLabel}>count</Text>
|
|
115
|
+
</View>
|
|
116
|
+
<Pressable onPress={onIncrement} style={styles.counterBtn}>
|
|
117
|
+
<Text style={styles.counterBtnText}>+</Text>
|
|
118
|
+
</Pressable>
|
|
119
|
+
<Pressable onPress={onReset} style={[styles.counterBtn, styles.resetBtn]}>
|
|
120
|
+
<Text style={styles.counterBtnText}>↺</Text>
|
|
121
|
+
</Pressable>
|
|
122
|
+
</View>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── 5. Main experiment screen ─────────────────────────────────────────────────
|
|
127
|
+
export function ComponentsStateScreen(){
|
|
128
|
+
const [count, setCount] = useState(0);
|
|
129
|
+
const [todos, dispatch] = useReducer(todoReducer, [
|
|
130
|
+
{ id: 1, text: 'Learn React Native', done: true },
|
|
131
|
+
{ id: 2, text: 'Build a component', done: false },
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
// useEffect — demonstrates lifecycle: runs after each render when count changes
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
// In a real app, you might sync to analytics, local storage, etc.
|
|
137
|
+
// console.log('Count changed to', count);
|
|
138
|
+
}, [count]);
|
|
139
|
+
|
|
140
|
+
const doneTodos = todos.filter((t) => t.done).length;
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
144
|
+
{/* Header */}
|
|
145
|
+
<View style={styles.header}>
|
|
146
|
+
<Text style={styles.heading}>Experiment 02</Text>
|
|
147
|
+
<Text style={styles.subheading}>Components · Props · State</Text>
|
|
148
|
+
</View>
|
|
149
|
+
|
|
150
|
+
{/* useState Counter */}
|
|
151
|
+
<View style={styles.section}>
|
|
152
|
+
<Text style={styles.sectionTitle}>useState — Counter</Text>
|
|
153
|
+
<Text style={styles.note}>
|
|
154
|
+
State is local to this component and lifted to props for Counter.
|
|
155
|
+
</Text>
|
|
156
|
+
<Counter
|
|
157
|
+
count={count}
|
|
158
|
+
onIncrement={() => setCount((c) => c + 1)}
|
|
159
|
+
onDecrement={() => setCount((c) => c - 1)}
|
|
160
|
+
onReset={() => setCount(0)}
|
|
161
|
+
/>
|
|
162
|
+
</View>
|
|
163
|
+
|
|
164
|
+
{/* useReducer TodoList */}
|
|
165
|
+
<View style={styles.section}>
|
|
166
|
+
<Text style={styles.sectionTitle}>
|
|
167
|
+
useReducer — Todo List ({doneTodos}/{todos.length} done)
|
|
168
|
+
</Text>
|
|
169
|
+
<Text style={styles.note}>
|
|
170
|
+
useReducer manages complex state with explicit action types.
|
|
171
|
+
</Text>
|
|
172
|
+
<AddTodoInput onAdd={(text) => dispatch({ type: 'ADD', text })} />
|
|
173
|
+
{todos.length === 0 ? (
|
|
174
|
+
<Text style={styles.emptyMsg}>No tasks. Add one above!</Text>
|
|
175
|
+
) : (
|
|
176
|
+
todos.map((todo) => (
|
|
177
|
+
<TodoItem
|
|
178
|
+
key={todo.id}
|
|
179
|
+
todo={todo}
|
|
180
|
+
onToggle={(id) => dispatch({ type: 'TOGGLE', id })}
|
|
181
|
+
onRemove={(id) => dispatch({ type: 'REMOVE', id })}
|
|
182
|
+
/>
|
|
183
|
+
))
|
|
184
|
+
)}
|
|
185
|
+
</View>
|
|
186
|
+
|
|
187
|
+
{/* Conditional rendering demo */}
|
|
188
|
+
<View style={styles.section}>
|
|
189
|
+
<Text style={styles.sectionTitle}>Conditional Rendering</Text>
|
|
190
|
+
{count > 0 ? (
|
|
191
|
+
<Text style={[styles.note, { color: '#388E3C' }]}>
|
|
192
|
+
✓ Count is positive: {count}
|
|
193
|
+
</Text>
|
|
194
|
+
) : count < 0 ? (
|
|
195
|
+
<Text style={[styles.note, { color: '#D32F2F' }]}>
|
|
196
|
+
✗ Count is negative: {count}
|
|
197
|
+
</Text>
|
|
198
|
+
) : (
|
|
199
|
+
<Text style={styles.note}>Count is zero. Use the counter above.</Text>
|
|
200
|
+
)}
|
|
201
|
+
</View>
|
|
202
|
+
</ScrollView>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Styles ────────────────────────────────────────────────────────────────────
|
|
207
|
+
const styles = StyleSheet.create({
|
|
208
|
+
container: { flex: 1, backgroundColor: '#fff' },
|
|
209
|
+
content: { paddingBottom: 40 },
|
|
210
|
+
header: { backgroundColor: '#6200EE', padding: 24, paddingTop: 40 },
|
|
211
|
+
heading: { fontSize: 28, fontWeight: '700', color: '#fff' },
|
|
212
|
+
subheading: { fontSize: 16, color: 'rgba(255,255,255,0.8)', marginTop: 4 },
|
|
213
|
+
section: {
|
|
214
|
+
padding: 16,
|
|
215
|
+
borderBottomWidth: 1,
|
|
216
|
+
borderBottomColor: '#e0e0e0' },
|
|
217
|
+
sectionTitle: { fontSize: 18, fontWeight: '700', color: '#333', marginBottom: 8 },
|
|
218
|
+
note: { fontSize: 13, color: '#666', lineHeight: 20, marginBottom: 12 },
|
|
219
|
+
emptyMsg: { color: '#aaa', textAlign: 'center', paddingVertical: 16 },
|
|
220
|
+
// Counter
|
|
221
|
+
counter: {
|
|
222
|
+
flexDirection: 'row',
|
|
223
|
+
alignItems: 'center',
|
|
224
|
+
justifyContent: 'center',
|
|
225
|
+
gap: 12,
|
|
226
|
+
paddingVertical: 8 },
|
|
227
|
+
counterBtn: {
|
|
228
|
+
width: 44,
|
|
229
|
+
height: 44,
|
|
230
|
+
borderRadius: 22,
|
|
231
|
+
backgroundColor: '#f0e6ff',
|
|
232
|
+
alignItems: 'center',
|
|
233
|
+
justifyContent: 'center' },
|
|
234
|
+
counterBtnText: { fontSize: 22, color: '#6200EE', fontWeight: '700' },
|
|
235
|
+
resetBtn: { backgroundColor: '#e8f5e9' },
|
|
236
|
+
countDisplay: { alignItems: 'center', minWidth: 64 },
|
|
237
|
+
countValue: { fontSize: 36, fontWeight: '800', color: '#6200EE' },
|
|
238
|
+
countLabel: { fontSize: 11, color: '#888', textTransform: 'uppercase' },
|
|
239
|
+
// Input
|
|
240
|
+
inputRow: { flexDirection: 'row', gap: 8, marginBottom: 12 },
|
|
241
|
+
textInput: {
|
|
242
|
+
flex: 1,
|
|
243
|
+
height: 44,
|
|
244
|
+
borderWidth: 1.5,
|
|
245
|
+
borderColor: '#ccc',
|
|
246
|
+
borderRadius: 8,
|
|
247
|
+
paddingHorizontal: 12,
|
|
248
|
+
fontSize: 15,
|
|
249
|
+
color: '#212121',
|
|
250
|
+
backgroundColor: '#fafafa' },
|
|
251
|
+
addBtn: {
|
|
252
|
+
paddingHorizontal: 20,
|
|
253
|
+
height: 44,
|
|
254
|
+
backgroundColor: '#6200EE',
|
|
255
|
+
borderRadius: 8,
|
|
256
|
+
alignItems: 'center',
|
|
257
|
+
justifyContent: 'center' },
|
|
258
|
+
addBtnText: { color: '#fff', fontWeight: '700', fontSize: 14 },
|
|
259
|
+
// Todo
|
|
260
|
+
todoRow: {
|
|
261
|
+
flexDirection: 'row',
|
|
262
|
+
alignItems: 'center',
|
|
263
|
+
paddingVertical: 10,
|
|
264
|
+
borderBottomWidth: 1,
|
|
265
|
+
borderBottomColor: '#f0f0f0' },
|
|
266
|
+
todoMain: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 12 },
|
|
267
|
+
checkbox: {
|
|
268
|
+
width: 22,
|
|
269
|
+
height: 22,
|
|
270
|
+
borderRadius: 4,
|
|
271
|
+
borderWidth: 2,
|
|
272
|
+
borderColor: '#ccc',
|
|
273
|
+
alignItems: 'center',
|
|
274
|
+
justifyContent: 'center' },
|
|
275
|
+
checkboxDone: { backgroundColor: '#6200EE', borderColor: '#6200EE' },
|
|
276
|
+
checkmark: { color: '#fff', fontSize: 13, fontWeight: '700' },
|
|
277
|
+
todoText: { fontSize: 15, color: '#212121' },
|
|
278
|
+
todoTextDone: { textDecorationLine: 'line-through', color: '#aaa' },
|
|
279
|
+
removeBtn: { padding: 8 },
|
|
280
|
+
removeBtnText: { color: '#D32F2F', fontSize: 16 } });
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experiment 03 — Navigation
|
|
3
|
+
* Module 1: Navigation (Stack · Tabs · Drawer · Params)
|
|
4
|
+
*
|
|
5
|
+
* Demonstrates:
|
|
6
|
+
* ✓ Stack navigation with typed params
|
|
7
|
+
* ✓ Bottom tab navigation
|
|
8
|
+
* ✓ Drawer navigation
|
|
9
|
+
* ✓ Navigating with params
|
|
10
|
+
* ✓ useNavigation + useRoute hooks
|
|
11
|
+
* ✓ NavigationContainer setup
|
|
12
|
+
*
|
|
13
|
+
* NOTE: This file exports composable screen components.
|
|
14
|
+
* The full navigator wiring is done in apps/expo-app.
|
|
15
|
+
* Each sub-screen can be run standalone for unit testing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import {
|
|
20
|
+
Pressable,
|
|
21
|
+
ScrollView,
|
|
22
|
+
StyleSheet,
|
|
23
|
+
Text,
|
|
24
|
+
View } from 'react-native';
|
|
25
|
+
|
|
26
|
+
// ── Param list type ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
// ── NavHomeScreen ─────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const SAMPLE_ITEMS = [
|
|
31
|
+
{ id: 1, title: 'Purple Widget', color: '#6200EE' },
|
|
32
|
+
{ id: 2, title: 'Teal Widget', color: '#03DAC6' },
|
|
33
|
+
{ id: 3, title: 'Red Widget', color: '#B00020' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export function NavHomeScreen({ navigation }){
|
|
37
|
+
return (
|
|
38
|
+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
39
|
+
<View style={styles.header}>
|
|
40
|
+
<Text style={styles.heading}>Experiment 03</Text>
|
|
41
|
+
<Text style={styles.subheading}>Navigation Patterns</Text>
|
|
42
|
+
</View>
|
|
43
|
+
|
|
44
|
+
<View style={styles.section}>
|
|
45
|
+
<Text style={styles.sectionTitle}>Stack with Params</Text>
|
|
46
|
+
<Text style={styles.note}>
|
|
47
|
+
Tap an item to push a Details screen, passing typed route params.
|
|
48
|
+
</Text>
|
|
49
|
+
{SAMPLE_ITEMS.map((item) => (
|
|
50
|
+
<Pressable
|
|
51
|
+
key={item.id}
|
|
52
|
+
style={({ pressed }) => [styles.itemRow, pressed && { opacity: 0.7 }]}
|
|
53
|
+
onPress={() =>
|
|
54
|
+
navigation.navigate('NavDetails', {
|
|
55
|
+
itemId: item.id,
|
|
56
|
+
title: item.title,
|
|
57
|
+
color: item.color })
|
|
58
|
+
}
|
|
59
|
+
accessibilityRole="button"
|
|
60
|
+
>
|
|
61
|
+
<View style={[styles.colorDot, { backgroundColor: item.color }]} />
|
|
62
|
+
<Text style={styles.itemText}>{item.title}</Text>
|
|
63
|
+
<Text style={styles.chevron}>›</Text>
|
|
64
|
+
</Pressable>
|
|
65
|
+
))}
|
|
66
|
+
</View>
|
|
67
|
+
|
|
68
|
+
<View style={styles.section}>
|
|
69
|
+
<Text style={styles.sectionTitle}>Concepts Covered</Text>
|
|
70
|
+
{[
|
|
71
|
+
'Stack Navigator — push/pop screens',
|
|
72
|
+
'Bottom Tabs — switchable views',
|
|
73
|
+
'Drawer — side panel navigation',
|
|
74
|
+
'Typed params — navigate<Screen>(params)',
|
|
75
|
+
'useNavigation + useRoute hooks',
|
|
76
|
+
].map((concept) => (
|
|
77
|
+
<Text key={concept} style={styles.bullet}>
|
|
78
|
+
• {concept}
|
|
79
|
+
</Text>
|
|
80
|
+
))}
|
|
81
|
+
</View>
|
|
82
|
+
</ScrollView>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── NavDetailsScreen ──────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export function NavDetailsScreen({ route, navigation }){
|
|
89
|
+
const { itemId, title, color } = route.params;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
93
|
+
<View style={[styles.header, { backgroundColor: color }]}>
|
|
94
|
+
<Text style={styles.heading}>{title}</Text>
|
|
95
|
+
<Text style={styles.subheading}>Item ID: {itemId}</Text>
|
|
96
|
+
</View>
|
|
97
|
+
|
|
98
|
+
<View style={styles.section}>
|
|
99
|
+
<Text style={styles.sectionTitle}>Route Params Received</Text>
|
|
100
|
+
<View style={styles.paramsBox}>
|
|
101
|
+
<ParamRow label="itemId" value={String(itemId)} />
|
|
102
|
+
<ParamRow label="title" value={title} />
|
|
103
|
+
<ParamRow label="color" value={color} />
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
<Text style={styles.note}>
|
|
107
|
+
Params are passed via {' '}
|
|
108
|
+
<Text style={styles.code}>navigation.navigate('NavDetails', {'{ itemId, title, color }'})</Text>
|
|
109
|
+
{' '} and read via {' '}
|
|
110
|
+
<Text style={styles.code}>route.params</Text>.
|
|
111
|
+
</Text>
|
|
112
|
+
|
|
113
|
+
<Pressable
|
|
114
|
+
style={[styles.backBtn, { backgroundColor: '#03DAC6', marginBottom: 8 }]}
|
|
115
|
+
onPress={() => navigation.navigate('NavProfile', { userId: String(itemId) })}
|
|
116
|
+
>
|
|
117
|
+
<Text style={styles.backBtnText}>View Profile →</Text>
|
|
118
|
+
</Pressable>
|
|
119
|
+
<Pressable
|
|
120
|
+
style={[styles.backBtn, { backgroundColor: color }]}
|
|
121
|
+
onPress={() => navigation.goBack()}
|
|
122
|
+
>
|
|
123
|
+
<Text style={styles.backBtnText}>← Go Back</Text>
|
|
124
|
+
</Pressable>
|
|
125
|
+
</View>
|
|
126
|
+
</ScrollView>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function ParamRow({ label, value }){
|
|
131
|
+
return (
|
|
132
|
+
<View style={styles.paramRow}>
|
|
133
|
+
<Text style={styles.paramLabel}>{label}</Text>
|
|
134
|
+
<Text style={styles.paramValue}>{value}</Text>
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── NavProfileScreen — demonstrates useNavigation/useRoute ───────────────────
|
|
140
|
+
|
|
141
|
+
export function NavProfileScreen({ route }){
|
|
142
|
+
const { userId } = route.params;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
146
|
+
<View style={[styles.header, { backgroundColor: '#03DAC6' }]}>
|
|
147
|
+
<Text style={styles.heading}>Profile</Text>
|
|
148
|
+
<Text style={styles.subheading}>userId: {userId}</Text>
|
|
149
|
+
</View>
|
|
150
|
+
<View style={styles.section}>
|
|
151
|
+
<Text style={styles.note}>
|
|
152
|
+
This screen illustrates how to receive and display route params. In a
|
|
153
|
+
real app you would fetch user data using the userId from an API.
|
|
154
|
+
</Text>
|
|
155
|
+
</View>
|
|
156
|
+
</ScrollView>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Styles ────────────────────────────────────────────────────────────────────
|
|
161
|
+
const styles = StyleSheet.create({
|
|
162
|
+
container: { flex: 1, backgroundColor: '#fff' },
|
|
163
|
+
content: { paddingBottom: 40 },
|
|
164
|
+
header: { backgroundColor: '#6200EE', padding: 24, paddingTop: 40 },
|
|
165
|
+
heading: { fontSize: 28, fontWeight: '700', color: '#fff' },
|
|
166
|
+
subheading: { fontSize: 16, color: 'rgba(255,255,255,0.8)', marginTop: 4 },
|
|
167
|
+
section: {
|
|
168
|
+
padding: 16,
|
|
169
|
+
borderBottomWidth: 1,
|
|
170
|
+
borderBottomColor: '#e0e0e0' },
|
|
171
|
+
sectionTitle: { fontSize: 18, fontWeight: '700', color: '#333', marginBottom: 8 },
|
|
172
|
+
note: { fontSize: 13, color: '#666', lineHeight: 20, marginBottom: 12 },
|
|
173
|
+
bullet: { fontSize: 14, color: '#444', lineHeight: 24, paddingLeft: 4 },
|
|
174
|
+
itemRow: {
|
|
175
|
+
flexDirection: 'row',
|
|
176
|
+
alignItems: 'center',
|
|
177
|
+
paddingVertical: 14,
|
|
178
|
+
borderBottomWidth: 1,
|
|
179
|
+
borderBottomColor: '#f0f0f0',
|
|
180
|
+
gap: 12 },
|
|
181
|
+
colorDot: { width: 14, height: 14, borderRadius: 7 },
|
|
182
|
+
itemText: { flex: 1, fontSize: 15, color: '#212121' },
|
|
183
|
+
chevron: { fontSize: 22, color: '#bbb', fontWeight: '300' },
|
|
184
|
+
paramsBox: {
|
|
185
|
+
backgroundColor: '#f8f0ff',
|
|
186
|
+
borderRadius: 8,
|
|
187
|
+
padding: 12,
|
|
188
|
+
marginBottom: 12 },
|
|
189
|
+
paramRow: {
|
|
190
|
+
flexDirection: 'row',
|
|
191
|
+
paddingVertical: 4,
|
|
192
|
+
borderBottomWidth: 1,
|
|
193
|
+
borderBottomColor: '#e8d5ff' },
|
|
194
|
+
paramLabel: { width: 80, fontFamily: 'monospace', fontSize: 13, color: '#555' },
|
|
195
|
+
paramValue: { flex: 1, fontFamily: 'monospace', fontSize: 13, color: '#6200EE', fontWeight: '600' },
|
|
196
|
+
code: { fontFamily: 'monospace', backgroundColor: '#f0f0f0', fontSize: 12, color: '#333' },
|
|
197
|
+
backBtn: {
|
|
198
|
+
borderRadius: 8,
|
|
199
|
+
padding: 14,
|
|
200
|
+
alignItems: 'center',
|
|
201
|
+
marginTop: 8 },
|
|
202
|
+
backBtnText: { color: '#fff', fontWeight: '700', fontSize: 15 } });
|