@highbeek/create-rnstarterkit 1.0.2-beta.10 → 1.0.2-beta.12
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/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/bin/create-rnstarterkit.js +6 -0
- package/dist/src/generators/appGenerator.js +1251 -116
- package/dist/templates/cli-base/App.tsx +199 -21
- package/dist/templates/cli-base/assets/images/icon.png +0 -0
- package/dist/templates/cli-base/assets/images/partial-react-logo.png +0 -0
- package/dist/templates/cli-base/assets/images/react-logo.png +0 -0
- package/dist/templates/cli-base/jest.config.js +4 -0
- package/dist/templates/cli-base/package.json +5 -3
- package/dist/templates/cli-base/tsconfig.json +1 -0
- package/dist/templates/expo-base/app/_layout.tsx +6 -3
- package/dist/templates/expo-base/app/index.tsx +164 -5
- package/dist/templates/expo-base/package.json +5 -2
- package/dist/templates/optional/auth-context/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-context/navigation/ProtectedStack.tsx +33 -5
- package/dist/templates/optional/auth-context/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/LoginScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/SettingsScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-redux/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-redux/navigation/ProtectedStack.tsx +39 -2
- package/dist/templates/optional/auth-redux/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-redux/screens/LoginScreen.tsx +4 -3
- package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +7 -10
- package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +4 -3
- package/dist/templates/optional/auth-redux/screens/SettingsScreen.tsx +6 -9
- package/dist/templates/optional/auth-redux/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/auth-zustand/components/layout/ScreenLayout.tsx +46 -0
- package/dist/templates/optional/auth-zustand/navigation/ProtectedStack.tsx +34 -6
- package/dist/templates/optional/auth-zustand/screens/HomeScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/LoginScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/SettingsScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/WelcomeScreen.tsx +174 -0
- package/dist/templates/optional/ci/.github/workflows/ci.yml +32 -0
- package/dist/templates/optional/error-boundary/components/ErrorBoundary.tsx +83 -0
- package/dist/templates/optional/formik/components/formik/FormikInput.tsx +45 -0
- package/dist/templates/optional/formik/components/formik/LoginForm.tsx +60 -0
- package/dist/templates/optional/formik/schemas/auth.schema.ts +17 -0
- package/dist/templates/optional/mmkv/utils/storage.ts +17 -0
- package/dist/templates/optional/react-hook-form/components/rhf/LoginForm.tsx +63 -0
- package/dist/templates/optional/react-hook-form/components/rhf/RHFInput.tsx +50 -0
- package/dist/templates/optional/react-hook-form/schemas/auth.schema.ts +29 -0
- package/dist/templates/optional/react-query/hooks/useAppMutation.ts +16 -0
- package/dist/templates/optional/react-query/hooks/useAppQuery.ts +12 -0
- package/dist/templates/optional/react-query/services/queryClient.ts +14 -0
- package/dist/templates/optional/redux/store/hooks.ts +6 -0
- package/dist/templates/optional/redux/store/store.ts +11 -0
- package/dist/templates/optional/swr/hooks/useSWRFetch.ts +14 -0
- package/dist/templates/optional/swr/providers/SWRProvider.tsx +21 -0
- package/dist/templates/optional/tsconfig.json +17 -0
- package/dist/templates/optional/zustand/store/appStore.ts +13 -0
- package/package.json +1 -1
- package/dist/templates/expo-base/components/ui/collapsible.tsx +0 -45
- package/dist/templates/expo-base/components/ui/icon-symbol.ios.tsx +0 -32
- package/dist/templates/expo-base/components/ui/icon-symbol.tsx +0 -41
|
@@ -1,44 +1,222 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
Image,
|
|
5
|
+
ImageSourcePropType,
|
|
6
|
+
Pressable,
|
|
7
|
+
StatusBar,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Text,
|
|
10
|
+
useWindowDimensions,
|
|
11
|
+
View,
|
|
12
|
+
ViewToken,
|
|
13
|
+
} from "react-native";
|
|
10
14
|
import {
|
|
11
15
|
SafeAreaProvider,
|
|
12
|
-
|
|
13
|
-
} from
|
|
16
|
+
SafeAreaView,
|
|
17
|
+
} from "react-native-safe-area-context";
|
|
18
|
+
|
|
19
|
+
type Slide = {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
image: ImageSourcePropType;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const slides: Slide[] = [
|
|
27
|
+
{
|
|
28
|
+
id: "1",
|
|
29
|
+
title: "Welcome to RN Starter Kit",
|
|
30
|
+
description: "Start with a clean React Native CLI project structure.",
|
|
31
|
+
image: require("./assets/images/react-logo.png"),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "2",
|
|
35
|
+
title: "Ready for Scale",
|
|
36
|
+
description: "Build faster with sensible architecture you can extend.",
|
|
37
|
+
image: require("./assets/images/partial-react-logo.png"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "3",
|
|
41
|
+
title: "Make It Yours",
|
|
42
|
+
description: "Use this onboarding as a placeholder for your product design.",
|
|
43
|
+
image: require("./assets/images/icon.png"),
|
|
44
|
+
},
|
|
45
|
+
];
|
|
14
46
|
|
|
15
47
|
function App() {
|
|
16
|
-
const
|
|
48
|
+
const [isOnboarded, setIsOnboarded] = useState(false);
|
|
17
49
|
|
|
18
50
|
return (
|
|
19
51
|
<SafeAreaProvider>
|
|
20
|
-
<StatusBar barStyle=
|
|
21
|
-
|
|
52
|
+
<StatusBar barStyle="dark-content" />
|
|
53
|
+
{isOnboarded ? (
|
|
54
|
+
<HomeScreen />
|
|
55
|
+
) : (
|
|
56
|
+
<WelcomeScreen onFinish={() => setIsOnboarded(true)} />
|
|
57
|
+
)}
|
|
22
58
|
</SafeAreaProvider>
|
|
23
59
|
);
|
|
24
60
|
}
|
|
25
61
|
|
|
26
|
-
function
|
|
27
|
-
const
|
|
62
|
+
function WelcomeScreen({ onFinish }: { onFinish: () => void }) {
|
|
63
|
+
const { width } = useWindowDimensions();
|
|
64
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
65
|
+
const listRef = useRef<FlatList<Slide>>(null);
|
|
66
|
+
|
|
67
|
+
const onViewableItemsChanged = useRef(
|
|
68
|
+
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
|
69
|
+
if (viewableItems[0]?.index != null) {
|
|
70
|
+
setActiveIndex(viewableItems[0].index);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
).current;
|
|
74
|
+
|
|
75
|
+
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }).current;
|
|
76
|
+
|
|
77
|
+
const handleNext = () => {
|
|
78
|
+
const nextIndex = activeIndex + 1;
|
|
79
|
+
if (nextIndex < slides.length) {
|
|
80
|
+
listRef.current?.scrollToIndex({ index: nextIndex, animated: true });
|
|
81
|
+
} else {
|
|
82
|
+
onFinish();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const isLastSlide = activeIndex === slides.length - 1;
|
|
28
87
|
|
|
29
88
|
return (
|
|
30
|
-
<
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
89
|
+
<SafeAreaView style={styles.safeArea}>
|
|
90
|
+
<FlatList
|
|
91
|
+
ref={listRef}
|
|
92
|
+
data={slides}
|
|
93
|
+
horizontal
|
|
94
|
+
pagingEnabled
|
|
95
|
+
bounces={false}
|
|
96
|
+
keyExtractor={(item) => item.id}
|
|
97
|
+
showsHorizontalScrollIndicator={false}
|
|
98
|
+
renderItem={({ item }) => (
|
|
99
|
+
<View style={[styles.slide, { width }]}>
|
|
100
|
+
<Image source={item.image} style={styles.image} resizeMode="contain" />
|
|
101
|
+
<Text style={styles.title}>{item.title}</Text>
|
|
102
|
+
<Text style={styles.description}>{item.description}</Text>
|
|
103
|
+
</View>
|
|
104
|
+
)}
|
|
105
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
106
|
+
viewabilityConfig={viewabilityConfig}
|
|
34
107
|
/>
|
|
35
|
-
|
|
108
|
+
|
|
109
|
+
<View style={styles.footer}>
|
|
110
|
+
<View style={styles.dotsRow}>
|
|
111
|
+
{slides.map((slide, index) => (
|
|
112
|
+
<View
|
|
113
|
+
key={slide.id}
|
|
114
|
+
style={[styles.dot, index === activeIndex && styles.dotActive]}
|
|
115
|
+
/>
|
|
116
|
+
))}
|
|
117
|
+
</View>
|
|
118
|
+
|
|
119
|
+
<Pressable style={styles.button} onPress={handleNext}>
|
|
120
|
+
<Text style={styles.buttonLabel}>{isLastSlide ? "Get Started" : "Next"}</Text>
|
|
121
|
+
</Pressable>
|
|
122
|
+
</View>
|
|
123
|
+
</SafeAreaView>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function HomeScreen() {
|
|
128
|
+
return (
|
|
129
|
+
<SafeAreaView style={homeStyles.container}>
|
|
130
|
+
<Text style={homeStyles.title}>Home</Text>
|
|
131
|
+
<Text style={homeStyles.subtitle}>
|
|
132
|
+
Replace this screen with your app content.
|
|
133
|
+
</Text>
|
|
134
|
+
</SafeAreaView>
|
|
36
135
|
);
|
|
37
136
|
}
|
|
38
137
|
|
|
39
138
|
const styles = StyleSheet.create({
|
|
139
|
+
safeArea: {
|
|
140
|
+
flex: 1,
|
|
141
|
+
backgroundColor: "#F7F7F9",
|
|
142
|
+
},
|
|
143
|
+
slide: {
|
|
144
|
+
flex: 1,
|
|
145
|
+
paddingHorizontal: 24,
|
|
146
|
+
justifyContent: "center",
|
|
147
|
+
alignItems: "center",
|
|
148
|
+
},
|
|
149
|
+
image: {
|
|
150
|
+
width: 220,
|
|
151
|
+
height: 220,
|
|
152
|
+
marginBottom: 28,
|
|
153
|
+
},
|
|
154
|
+
title: {
|
|
155
|
+
fontSize: 28,
|
|
156
|
+
fontWeight: "700",
|
|
157
|
+
color: "#111827",
|
|
158
|
+
textAlign: "center",
|
|
159
|
+
marginBottom: 12,
|
|
160
|
+
},
|
|
161
|
+
description: {
|
|
162
|
+
fontSize: 16,
|
|
163
|
+
lineHeight: 24,
|
|
164
|
+
color: "#4B5563",
|
|
165
|
+
textAlign: "center",
|
|
166
|
+
maxWidth: 320,
|
|
167
|
+
},
|
|
168
|
+
footer: {
|
|
169
|
+
paddingHorizontal: 24,
|
|
170
|
+
paddingBottom: 28,
|
|
171
|
+
gap: 20,
|
|
172
|
+
},
|
|
173
|
+
dotsRow: {
|
|
174
|
+
flexDirection: "row",
|
|
175
|
+
justifyContent: "center",
|
|
176
|
+
gap: 8,
|
|
177
|
+
},
|
|
178
|
+
dot: {
|
|
179
|
+
width: 8,
|
|
180
|
+
height: 8,
|
|
181
|
+
borderRadius: 999,
|
|
182
|
+
backgroundColor: "#D1D5DB",
|
|
183
|
+
},
|
|
184
|
+
dotActive: {
|
|
185
|
+
width: 24,
|
|
186
|
+
backgroundColor: "#111827",
|
|
187
|
+
},
|
|
188
|
+
button: {
|
|
189
|
+
height: 52,
|
|
190
|
+
borderRadius: 12,
|
|
191
|
+
backgroundColor: "#111827",
|
|
192
|
+
alignItems: "center",
|
|
193
|
+
justifyContent: "center",
|
|
194
|
+
},
|
|
195
|
+
buttonLabel: {
|
|
196
|
+
color: "#FFFFFF",
|
|
197
|
+
fontSize: 16,
|
|
198
|
+
fontWeight: "600",
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const homeStyles = StyleSheet.create({
|
|
40
203
|
container: {
|
|
41
204
|
flex: 1,
|
|
205
|
+
backgroundColor: "#F7F7F9",
|
|
206
|
+
alignItems: "center",
|
|
207
|
+
justifyContent: "center",
|
|
208
|
+
padding: 24,
|
|
209
|
+
},
|
|
210
|
+
title: {
|
|
211
|
+
fontSize: 28,
|
|
212
|
+
fontWeight: "700",
|
|
213
|
+
color: "#111827",
|
|
214
|
+
marginBottom: 8,
|
|
215
|
+
},
|
|
216
|
+
subtitle: {
|
|
217
|
+
fontSize: 16,
|
|
218
|
+
color: "#4B5563",
|
|
219
|
+
textAlign: "center",
|
|
42
220
|
},
|
|
43
221
|
});
|
|
44
222
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
"scripts": {
|
|
6
6
|
"android": "react-native run-android",
|
|
7
7
|
"ios": "react-native run-ios",
|
|
8
|
-
"lint": "eslint .",
|
|
9
8
|
"start": "react-native start",
|
|
9
|
+
"lint": "eslint . --max-warnings 0",
|
|
10
|
+
"format": "prettier --write .",
|
|
11
|
+
"type-check": "tsc --noEmit",
|
|
10
12
|
"test": "jest"
|
|
11
13
|
},
|
|
12
14
|
"dependencies": {
|
|
13
15
|
"react": "19.2.3",
|
|
14
16
|
"react-native": "0.84.0",
|
|
15
|
-
"
|
|
16
|
-
"react-native-
|
|
17
|
+
"react-native-safe-area-context": "^5.5.2",
|
|
18
|
+
"react-native-reanimated": "^3.17.4"
|
|
17
19
|
},
|
|
18
20
|
"devDependencies": {
|
|
19
21
|
"@babel/core": "^7.25.2",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { Stack } from "expo-router";
|
|
2
|
+
import { SafeAreaProvider } from "react-native-safe-area-context";
|
|
2
3
|
|
|
3
4
|
export default function RootLayout() {
|
|
4
5
|
return (
|
|
5
|
-
<
|
|
6
|
-
<Stack
|
|
7
|
-
|
|
6
|
+
<SafeAreaProvider>
|
|
7
|
+
<Stack>
|
|
8
|
+
<Stack.Screen name="index" options={{ title: "Home" }} />
|
|
9
|
+
</Stack>
|
|
10
|
+
</SafeAreaProvider>
|
|
8
11
|
);
|
|
9
12
|
}
|
|
@@ -1,9 +1,168 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import React, { useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
FlatList,
|
|
4
|
+
Image,
|
|
5
|
+
ImageSourcePropType,
|
|
6
|
+
Pressable,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
useWindowDimensions,
|
|
10
|
+
View,
|
|
11
|
+
ViewToken,
|
|
12
|
+
} from "react-native";
|
|
13
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
14
|
+
|
|
15
|
+
type Slide = {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
image: ImageSourcePropType;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const slides: Slide[] = [
|
|
23
|
+
{
|
|
24
|
+
id: "1",
|
|
25
|
+
title: "Welcome to RN Starter Kit",
|
|
26
|
+
description: "Kickstart your app with Expo and a clean production-ready baseline.",
|
|
27
|
+
image: require("../assets/images/react-logo.png"),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "2",
|
|
31
|
+
title: "Built for Real Projects",
|
|
32
|
+
description: "State, auth, API layers, and folder structure are ready for scaling.",
|
|
33
|
+
image: require("../assets/images/partial-react-logo.png"),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "3",
|
|
37
|
+
title: "Customize and Ship",
|
|
38
|
+
description: "Replace this screen with your own brand and ship faster.",
|
|
39
|
+
image: require("../assets/images/icon.png"),
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export default function WelcomeScreen() {
|
|
44
|
+
const { width } = useWindowDimensions();
|
|
45
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
46
|
+
const listRef = useRef<FlatList<Slide>>(null);
|
|
47
|
+
|
|
48
|
+
const onViewableItemsChanged = useRef(
|
|
49
|
+
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
|
50
|
+
if (viewableItems[0]?.index != null) {
|
|
51
|
+
setActiveIndex(viewableItems[0].index);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
).current;
|
|
55
|
+
|
|
56
|
+
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }).current;
|
|
57
|
+
|
|
58
|
+
const goNext = () => {
|
|
59
|
+
const nextIndex = activeIndex + 1;
|
|
60
|
+
if (nextIndex < slides.length) {
|
|
61
|
+
listRef.current?.scrollToIndex({ index: nextIndex, animated: true });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const isLastSlide = activeIndex === slides.length - 1;
|
|
2
66
|
|
|
3
|
-
export default function HomeScreen() {
|
|
4
67
|
return (
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
|
|
68
|
+
<SafeAreaView style={styles.safeArea}>
|
|
69
|
+
<FlatList
|
|
70
|
+
ref={listRef}
|
|
71
|
+
data={slides}
|
|
72
|
+
horizontal
|
|
73
|
+
pagingEnabled
|
|
74
|
+
bounces={false}
|
|
75
|
+
keyExtractor={(item) => item.id}
|
|
76
|
+
showsHorizontalScrollIndicator={false}
|
|
77
|
+
renderItem={({ item }) => (
|
|
78
|
+
<View style={[styles.slide, { width }]}>
|
|
79
|
+
<Image source={item.image} style={styles.image} resizeMode="contain" />
|
|
80
|
+
<Text style={styles.title}>{item.title}</Text>
|
|
81
|
+
<Text style={styles.description}>{item.description}</Text>
|
|
82
|
+
</View>
|
|
83
|
+
)}
|
|
84
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
85
|
+
viewabilityConfig={viewabilityConfig}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<View style={styles.footer}>
|
|
89
|
+
<View style={styles.dotsRow}>
|
|
90
|
+
{slides.map((slide, index) => (
|
|
91
|
+
<View
|
|
92
|
+
key={slide.id}
|
|
93
|
+
style={[styles.dot, index === activeIndex && styles.dotActive]}
|
|
94
|
+
/>
|
|
95
|
+
))}
|
|
96
|
+
</View>
|
|
97
|
+
|
|
98
|
+
<Pressable style={styles.button} onPress={goNext}>
|
|
99
|
+
<Text style={styles.buttonLabel}>{isLastSlide ? "Get Started" : "Next"}</Text>
|
|
100
|
+
</Pressable>
|
|
101
|
+
</View>
|
|
102
|
+
</SafeAreaView>
|
|
8
103
|
);
|
|
9
104
|
}
|
|
105
|
+
|
|
106
|
+
const styles = StyleSheet.create({
|
|
107
|
+
safeArea: {
|
|
108
|
+
flex: 1,
|
|
109
|
+
backgroundColor: "#F7F7F9",
|
|
110
|
+
},
|
|
111
|
+
slide: {
|
|
112
|
+
flex: 1,
|
|
113
|
+
paddingHorizontal: 24,
|
|
114
|
+
justifyContent: "center",
|
|
115
|
+
alignItems: "center",
|
|
116
|
+
},
|
|
117
|
+
image: {
|
|
118
|
+
width: 220,
|
|
119
|
+
height: 220,
|
|
120
|
+
marginBottom: 28,
|
|
121
|
+
},
|
|
122
|
+
title: {
|
|
123
|
+
fontSize: 28,
|
|
124
|
+
fontWeight: "700",
|
|
125
|
+
color: "#111827",
|
|
126
|
+
textAlign: "center",
|
|
127
|
+
marginBottom: 12,
|
|
128
|
+
},
|
|
129
|
+
description: {
|
|
130
|
+
fontSize: 16,
|
|
131
|
+
lineHeight: 24,
|
|
132
|
+
color: "#4B5563",
|
|
133
|
+
textAlign: "center",
|
|
134
|
+
maxWidth: 320,
|
|
135
|
+
},
|
|
136
|
+
footer: {
|
|
137
|
+
paddingHorizontal: 24,
|
|
138
|
+
paddingBottom: 28,
|
|
139
|
+
gap: 20,
|
|
140
|
+
},
|
|
141
|
+
dotsRow: {
|
|
142
|
+
flexDirection: "row",
|
|
143
|
+
justifyContent: "center",
|
|
144
|
+
gap: 8,
|
|
145
|
+
},
|
|
146
|
+
dot: {
|
|
147
|
+
width: 8,
|
|
148
|
+
height: 8,
|
|
149
|
+
borderRadius: 999,
|
|
150
|
+
backgroundColor: "#D1D5DB",
|
|
151
|
+
},
|
|
152
|
+
dotActive: {
|
|
153
|
+
width: 24,
|
|
154
|
+
backgroundColor: "#111827",
|
|
155
|
+
},
|
|
156
|
+
button: {
|
|
157
|
+
height: 52,
|
|
158
|
+
borderRadius: 12,
|
|
159
|
+
backgroundColor: "#111827",
|
|
160
|
+
alignItems: "center",
|
|
161
|
+
justifyContent: "center",
|
|
162
|
+
},
|
|
163
|
+
buttonLabel: {
|
|
164
|
+
color: "#FFFFFF",
|
|
165
|
+
fontSize: 16,
|
|
166
|
+
fontWeight: "600",
|
|
167
|
+
},
|
|
168
|
+
});
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
"android": "expo start --android",
|
|
8
8
|
"ios": "expo start --ios",
|
|
9
9
|
"web": "expo start --web",
|
|
10
|
-
"lint": "expo lint"
|
|
10
|
+
"lint": "expo lint",
|
|
11
|
+
"format": "prettier --write .",
|
|
12
|
+
"type-check": "tsc --noEmit"
|
|
11
13
|
},
|
|
12
14
|
"dependencies": {
|
|
13
15
|
"@expo/vector-icons": "^15.0.3",
|
|
@@ -40,7 +42,8 @@
|
|
|
40
42
|
"@types/react": "~19.1.0",
|
|
41
43
|
"typescript": "~5.9.2",
|
|
42
44
|
"eslint": "^9.25.0",
|
|
43
|
-
"eslint-config-expo": "~10.0.0"
|
|
45
|
+
"eslint-config-expo": "~10.0.0",
|
|
46
|
+
"prettier": "^3.5.0"
|
|
44
47
|
},
|
|
45
48
|
"private": true
|
|
46
49
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { StyleProp, StyleSheet, ViewStyle } from "react-native";
|
|
3
|
+
import { Edge, SafeAreaView } from "react-native-safe-area-context";
|
|
4
|
+
|
|
5
|
+
type ScreenLayoutProps = {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
padded?: boolean;
|
|
8
|
+
centered?: boolean;
|
|
9
|
+
edges?: Edge[];
|
|
10
|
+
style?: StyleProp<ViewStyle>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function ScreenLayout({
|
|
14
|
+
children,
|
|
15
|
+
padded = true,
|
|
16
|
+
centered = false,
|
|
17
|
+
edges = ["top", "left", "right"],
|
|
18
|
+
style,
|
|
19
|
+
}: ScreenLayoutProps) {
|
|
20
|
+
return (
|
|
21
|
+
<SafeAreaView
|
|
22
|
+
edges={edges}
|
|
23
|
+
style={[
|
|
24
|
+
styles.container,
|
|
25
|
+
padded && styles.padded,
|
|
26
|
+
centered && styles.centered,
|
|
27
|
+
style,
|
|
28
|
+
]}
|
|
29
|
+
>
|
|
30
|
+
{children}
|
|
31
|
+
</SafeAreaView>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const styles = StyleSheet.create({
|
|
36
|
+
container: {
|
|
37
|
+
flex: 1,
|
|
38
|
+
},
|
|
39
|
+
padded: {
|
|
40
|
+
padding: 20,
|
|
41
|
+
},
|
|
42
|
+
centered: {
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
justifyContent: "center",
|
|
45
|
+
},
|
|
46
|
+
});
|
|
@@ -1,24 +1,52 @@
|
|
|
1
|
-
import React, { useContext } from "react";
|
|
2
|
-
import { ActivityIndicator
|
|
1
|
+
import React, { useContext, useEffect, useState } from "react";
|
|
2
|
+
import { ActivityIndicator } from "react-native";
|
|
3
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
3
4
|
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
5
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
4
6
|
import { AuthContext } from "../context/AuthContext";
|
|
5
7
|
import LoginScreen from "../screens/LoginScreen";
|
|
6
8
|
import RegisterScreen from "../screens/RegisterScreen";
|
|
9
|
+
import WelcomeScreen from "../screens/WelcomeScreen";
|
|
7
10
|
import BottomTabs from "./BottomTabs";
|
|
8
11
|
|
|
9
12
|
const Stack = createNativeStackNavigator();
|
|
13
|
+
const WELCOME_SEEN_KEY = "@rnskit_has_seen_welcome";
|
|
10
14
|
|
|
11
15
|
export default function ProtectedStack() {
|
|
12
16
|
const { token, isHydrated } = useContext(AuthContext);
|
|
17
|
+
const [showWelcome, setShowWelcome] = useState(false);
|
|
18
|
+
const [isCheckingWelcome, setIsCheckingWelcome] = useState(true);
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const readWelcomeState = async () => {
|
|
22
|
+
try {
|
|
23
|
+
const seen = await AsyncStorage.getItem(WELCOME_SEEN_KEY);
|
|
24
|
+
setShowWelcome(seen !== "true");
|
|
25
|
+
} finally {
|
|
26
|
+
setIsCheckingWelcome(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
void readWelcomeState();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
const handleWelcomeContinue = async () => {
|
|
34
|
+
await AsyncStorage.setItem(WELCOME_SEEN_KEY, "true");
|
|
35
|
+
setShowWelcome(false);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (!isHydrated || isCheckingWelcome) {
|
|
15
39
|
return (
|
|
16
|
-
<
|
|
40
|
+
<ScreenLayout centered padded={false}>
|
|
17
41
|
<ActivityIndicator />
|
|
18
|
-
</
|
|
42
|
+
</ScreenLayout>
|
|
19
43
|
);
|
|
20
44
|
}
|
|
21
45
|
|
|
46
|
+
if (showWelcome) {
|
|
47
|
+
return <WelcomeScreen onContinue={() => void handleWelcomeContinue()} />;
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
return (
|
|
23
51
|
<Stack.Navigator>
|
|
24
52
|
{token ? (
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import React, { useContext } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Text, Button } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
3
4
|
import { AuthContext } from "../context/AuthContext";
|
|
4
5
|
|
|
5
6
|
export default function HomeScreen() {
|
|
6
7
|
const { logout } = useContext(AuthContext);
|
|
7
8
|
|
|
8
9
|
return (
|
|
9
|
-
<
|
|
10
|
+
<ScreenLayout>
|
|
10
11
|
<Text>Welcome Home!</Text>
|
|
11
12
|
<Button title="Logout" onPress={() => void logout()} />
|
|
12
|
-
</
|
|
13
|
+
</ScreenLayout>
|
|
13
14
|
);
|
|
14
15
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useContext, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { TextInput, Button, Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
3
4
|
import { AuthContext } from "../context/AuthContext";
|
|
4
5
|
import { loginApi } from "../api";
|
|
5
6
|
|
|
@@ -20,7 +21,7 @@ export default function LoginScreen() {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
return (
|
|
23
|
-
<
|
|
24
|
+
<ScreenLayout>
|
|
24
25
|
<TextInput
|
|
25
26
|
placeholder="Email"
|
|
26
27
|
value={email}
|
|
@@ -57,6 +58,6 @@ export default function LoginScreen() {
|
|
|
57
58
|
/>
|
|
58
59
|
{!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
|
|
59
60
|
<Button title="Login" onPress={() => void handleLogin()} />
|
|
60
|
-
</
|
|
61
|
+
</ScreenLayout>
|
|
61
62
|
);
|
|
62
63
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
3
4
|
|
|
4
5
|
export default function ProfileScreen() {
|
|
5
6
|
return (
|
|
6
|
-
<
|
|
7
|
+
<ScreenLayout>
|
|
7
8
|
<Text>This is the Profile Screen</Text>
|
|
8
|
-
</
|
|
9
|
+
</ScreenLayout>
|
|
9
10
|
);
|
|
10
11
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useContext, useState } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { TextInput, Button, Text } from "react-native";
|
|
3
|
+
import ScreenLayout from "../components/layout/ScreenLayout";
|
|
3
4
|
import { AuthContext } from "../context/AuthContext";
|
|
4
5
|
import { registerApi } from "../api";
|
|
5
6
|
|
|
@@ -20,7 +21,7 @@ export default function RegisterScreen() {
|
|
|
20
21
|
};
|
|
21
22
|
|
|
22
23
|
return (
|
|
23
|
-
<
|
|
24
|
+
<ScreenLayout>
|
|
24
25
|
<TextInput
|
|
25
26
|
placeholder="Email"
|
|
26
27
|
value={email}
|
|
@@ -57,6 +58,6 @@ export default function RegisterScreen() {
|
|
|
57
58
|
/>
|
|
58
59
|
{!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
|
|
59
60
|
<Button title="Register" onPress={() => void handleRegister()} />
|
|
60
|
-
</
|
|
61
|
+
</ScreenLayout>
|
|
61
62
|
);
|
|
62
63
|
}
|