@highbeek/create-rnstarterkit 1.0.2-beta.7 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +270 -0
- package/dist/bin/create-rnstarterkit.js +205 -7
- package/dist/src/generators/appGenerator.js +1949 -127
- package/dist/src/generators/codeGenerator.js +289 -0
- 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/babel.config.js +1 -0
- package/dist/templates/cli-base/ios/BaseApp.xcodeproj/project.pbxproj +6 -0
- package/dist/templates/cli-base/ios/Podfile +5 -0
- package/dist/templates/cli-base/jest.config.js +4 -0
- package/dist/templates/cli-base/package.json +8 -5
- package/dist/templates/cli-base/tsconfig.json +1 -0
- package/dist/templates/expo-base/app/_layout.tsx +7 -3
- package/dist/templates/expo-base/app/home.tsx +37 -0
- package/dist/templates/expo-base/app/index.tsx +166 -5
- package/dist/templates/expo-base/app.json +1 -2
- 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 +41 -6
- package/dist/templates/optional/auth-context/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-context/screens/RegisterScreen.tsx +41 -6
- 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 +42 -7
- package/dist/templates/optional/auth-redux/screens/ProfileScreen.tsx +7 -10
- package/dist/templates/optional/auth-redux/screens/RegisterScreen.tsx +61 -11
- 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 +41 -6
- package/dist/templates/optional/auth-zustand/screens/ProfileScreen.tsx +4 -3
- package/dist/templates/optional/auth-zustand/screens/RegisterScreen.tsx +41 -6
- 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/i18n/src/i18n/hooks/useAppTranslation.ts +28 -0
- package/dist/templates/optional/i18n/src/i18n/i18n.ts +30 -0
- package/dist/templates/optional/i18n/src/i18n/locales/en.json +32 -0
- package/dist/templates/optional/i18n/src/i18n/locales/es.json +32 -0
- package/dist/templates/optional/maestro/.maestro/flows/01_welcome.yaml +5 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/01_welcome.yaml +5 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/02_login.yaml +13 -0
- package/dist/templates/optional/maestro-auth/.maestro/flows/03_logout.yaml +16 -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/sentry/src/utils/sentry.ts +24 -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 +40 -5
- 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,9 +1,170 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { router } from "expo-router";
|
|
2
|
+
import React, { useRef, useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
FlatList,
|
|
5
|
+
Image,
|
|
6
|
+
ImageSourcePropType,
|
|
7
|
+
Pressable,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Text,
|
|
10
|
+
useWindowDimensions,
|
|
11
|
+
View,
|
|
12
|
+
ViewToken,
|
|
13
|
+
} from "react-native";
|
|
14
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
15
|
+
|
|
16
|
+
type Slide = {
|
|
17
|
+
id: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
image: ImageSourcePropType;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const slides: Slide[] = [
|
|
24
|
+
{
|
|
25
|
+
id: "1",
|
|
26
|
+
title: "Welcome to RN Starter Kit",
|
|
27
|
+
description: "Kickstart your app with Expo and a clean production-ready baseline.",
|
|
28
|
+
image: require("../assets/images/react-logo.png"),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "2",
|
|
32
|
+
title: "Built for Real Projects",
|
|
33
|
+
description: "State, auth, API layers, and folder structure are ready for scaling.",
|
|
34
|
+
image: require("../assets/images/partial-react-logo.png"),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "3",
|
|
38
|
+
title: "Customize and Ship",
|
|
39
|
+
description: "Replace this screen with your own brand and ship faster.",
|
|
40
|
+
image: require("../assets/images/icon.png"),
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export default function WelcomeScreen() {
|
|
45
|
+
const { width } = useWindowDimensions();
|
|
46
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
47
|
+
const listRef = useRef<FlatList<Slide>>(null);
|
|
48
|
+
|
|
49
|
+
const onViewableItemsChanged = useRef(
|
|
50
|
+
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
|
51
|
+
if (viewableItems[0]?.index != null) {
|
|
52
|
+
setActiveIndex(viewableItems[0].index);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
).current;
|
|
56
|
+
|
|
57
|
+
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }).current;
|
|
58
|
+
|
|
59
|
+
const isLastSlide = activeIndex === slides.length - 1;
|
|
60
|
+
|
|
61
|
+
const goNext = () => {
|
|
62
|
+
if (isLastSlide) {
|
|
63
|
+
router.replace("/home");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
listRef.current?.scrollToIndex({ index: activeIndex + 1, animated: true });
|
|
67
|
+
};
|
|
2
68
|
|
|
3
|
-
export default function HomeScreen() {
|
|
4
69
|
return (
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
|
|
70
|
+
<SafeAreaView style={styles.safeArea}>
|
|
71
|
+
<FlatList
|
|
72
|
+
ref={listRef}
|
|
73
|
+
data={slides}
|
|
74
|
+
horizontal
|
|
75
|
+
pagingEnabled
|
|
76
|
+
bounces={false}
|
|
77
|
+
keyExtractor={(item) => item.id}
|
|
78
|
+
showsHorizontalScrollIndicator={false}
|
|
79
|
+
renderItem={({ item }) => (
|
|
80
|
+
<View style={[styles.slide, { width }]}>
|
|
81
|
+
<Image source={item.image} style={styles.image} resizeMode="contain" />
|
|
82
|
+
<Text style={styles.title}>{item.title}</Text>
|
|
83
|
+
<Text style={styles.description}>{item.description}</Text>
|
|
84
|
+
</View>
|
|
85
|
+
)}
|
|
86
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
87
|
+
viewabilityConfig={viewabilityConfig}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
<View style={styles.footer}>
|
|
91
|
+
<View style={styles.dotsRow}>
|
|
92
|
+
{slides.map((slide, index) => (
|
|
93
|
+
<View
|
|
94
|
+
key={slide.id}
|
|
95
|
+
style={[styles.dot, index === activeIndex && styles.dotActive]}
|
|
96
|
+
/>
|
|
97
|
+
))}
|
|
98
|
+
</View>
|
|
99
|
+
|
|
100
|
+
<Pressable style={styles.button} onPress={goNext}>
|
|
101
|
+
<Text style={styles.buttonLabel}>{isLastSlide ? "Get Started" : "Next"}</Text>
|
|
102
|
+
</Pressable>
|
|
103
|
+
</View>
|
|
104
|
+
</SafeAreaView>
|
|
8
105
|
);
|
|
9
106
|
}
|
|
107
|
+
|
|
108
|
+
const styles = StyleSheet.create({
|
|
109
|
+
safeArea: {
|
|
110
|
+
flex: 1,
|
|
111
|
+
backgroundColor: "#F7F7F9",
|
|
112
|
+
},
|
|
113
|
+
slide: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
paddingHorizontal: 24,
|
|
116
|
+
justifyContent: "center",
|
|
117
|
+
alignItems: "center",
|
|
118
|
+
},
|
|
119
|
+
image: {
|
|
120
|
+
width: 220,
|
|
121
|
+
height: 220,
|
|
122
|
+
marginBottom: 28,
|
|
123
|
+
},
|
|
124
|
+
title: {
|
|
125
|
+
fontSize: 28,
|
|
126
|
+
fontWeight: "700",
|
|
127
|
+
color: "#111827",
|
|
128
|
+
textAlign: "center",
|
|
129
|
+
marginBottom: 12,
|
|
130
|
+
},
|
|
131
|
+
description: {
|
|
132
|
+
fontSize: 16,
|
|
133
|
+
lineHeight: 24,
|
|
134
|
+
color: "#4B5563",
|
|
135
|
+
textAlign: "center",
|
|
136
|
+
maxWidth: 320,
|
|
137
|
+
},
|
|
138
|
+
footer: {
|
|
139
|
+
paddingHorizontal: 24,
|
|
140
|
+
paddingBottom: 28,
|
|
141
|
+
gap: 20,
|
|
142
|
+
},
|
|
143
|
+
dotsRow: {
|
|
144
|
+
flexDirection: "row",
|
|
145
|
+
justifyContent: "center",
|
|
146
|
+
gap: 8,
|
|
147
|
+
},
|
|
148
|
+
dot: {
|
|
149
|
+
width: 8,
|
|
150
|
+
height: 8,
|
|
151
|
+
borderRadius: 999,
|
|
152
|
+
backgroundColor: "#D1D5DB",
|
|
153
|
+
},
|
|
154
|
+
dotActive: {
|
|
155
|
+
width: 24,
|
|
156
|
+
backgroundColor: "#111827",
|
|
157
|
+
},
|
|
158
|
+
button: {
|
|
159
|
+
height: 52,
|
|
160
|
+
borderRadius: 12,
|
|
161
|
+
backgroundColor: "#111827",
|
|
162
|
+
alignItems: "center",
|
|
163
|
+
justifyContent: "center",
|
|
164
|
+
},
|
|
165
|
+
buttonLabel: {
|
|
166
|
+
color: "#FFFFFF",
|
|
167
|
+
fontSize: 16,
|
|
168
|
+
fontWeight: "600",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
@@ -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
|
|
|
@@ -7,22 +8,56 @@ export default function LoginScreen() {
|
|
|
7
8
|
const { login } = useContext(AuthContext);
|
|
8
9
|
const [email, setEmail] = useState("");
|
|
9
10
|
const [password, setPassword] = useState("");
|
|
11
|
+
const [error, setError] = useState("");
|
|
10
12
|
|
|
11
13
|
const handleLogin = async () => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
try {
|
|
15
|
+
setError("");
|
|
16
|
+
const token = await loginApi(email, password);
|
|
17
|
+
await login(token);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
setError(err instanceof Error ? err.message : "Login failed");
|
|
20
|
+
}
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
return (
|
|
17
|
-
<
|
|
18
|
-
<TextInput
|
|
24
|
+
<ScreenLayout>
|
|
25
|
+
<TextInput
|
|
26
|
+
placeholder="Email"
|
|
27
|
+
value={email}
|
|
28
|
+
onChangeText={setEmail}
|
|
29
|
+
autoCapitalize="none"
|
|
30
|
+
autoCorrect={false}
|
|
31
|
+
keyboardType="email-address"
|
|
32
|
+
placeholderTextColor="#888"
|
|
33
|
+
style={{
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderColor: "#ccc",
|
|
36
|
+
borderRadius: 8,
|
|
37
|
+
paddingHorizontal: 12,
|
|
38
|
+
paddingVertical: 10,
|
|
39
|
+
color: "#111",
|
|
40
|
+
marginBottom: 12,
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
19
43
|
<TextInput
|
|
20
44
|
placeholder="Password"
|
|
21
45
|
value={password}
|
|
22
46
|
onChangeText={setPassword}
|
|
23
47
|
secureTextEntry
|
|
48
|
+
placeholderTextColor="#888"
|
|
49
|
+
style={{
|
|
50
|
+
borderWidth: 1,
|
|
51
|
+
borderColor: "#ccc",
|
|
52
|
+
borderRadius: 8,
|
|
53
|
+
paddingHorizontal: 12,
|
|
54
|
+
paddingVertical: 10,
|
|
55
|
+
color: "#111",
|
|
56
|
+
marginBottom: 12,
|
|
57
|
+
}}
|
|
24
58
|
/>
|
|
59
|
+
{!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
|
|
25
60
|
<Button title="Login" onPress={() => void handleLogin()} />
|
|
26
|
-
</
|
|
61
|
+
</ScreenLayout>
|
|
27
62
|
);
|
|
28
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
|
|
|
@@ -7,22 +8,56 @@ export default function RegisterScreen() {
|
|
|
7
8
|
const { login } = useContext(AuthContext);
|
|
8
9
|
const [email, setEmail] = useState("");
|
|
9
10
|
const [password, setPassword] = useState("");
|
|
11
|
+
const [error, setError] = useState("");
|
|
10
12
|
|
|
11
13
|
const handleRegister = async () => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
try {
|
|
15
|
+
setError("");
|
|
16
|
+
const token = await registerApi(email, password);
|
|
17
|
+
await login(token);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
setError(err instanceof Error ? err.message : "Registration failed");
|
|
20
|
+
}
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
return (
|
|
17
|
-
<
|
|
18
|
-
<TextInput
|
|
24
|
+
<ScreenLayout>
|
|
25
|
+
<TextInput
|
|
26
|
+
placeholder="Email"
|
|
27
|
+
value={email}
|
|
28
|
+
onChangeText={setEmail}
|
|
29
|
+
autoCapitalize="none"
|
|
30
|
+
autoCorrect={false}
|
|
31
|
+
keyboardType="email-address"
|
|
32
|
+
placeholderTextColor="#888"
|
|
33
|
+
style={{
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderColor: "#ccc",
|
|
36
|
+
borderRadius: 8,
|
|
37
|
+
paddingHorizontal: 12,
|
|
38
|
+
paddingVertical: 10,
|
|
39
|
+
color: "#111",
|
|
40
|
+
marginBottom: 12,
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
19
43
|
<TextInput
|
|
20
44
|
placeholder="Password"
|
|
21
45
|
value={password}
|
|
22
46
|
onChangeText={setPassword}
|
|
23
47
|
secureTextEntry
|
|
48
|
+
placeholderTextColor="#888"
|
|
49
|
+
style={{
|
|
50
|
+
borderWidth: 1,
|
|
51
|
+
borderColor: "#ccc",
|
|
52
|
+
borderRadius: 8,
|
|
53
|
+
paddingHorizontal: 12,
|
|
54
|
+
paddingVertical: 10,
|
|
55
|
+
color: "#111",
|
|
56
|
+
marginBottom: 12,
|
|
57
|
+
}}
|
|
24
58
|
/>
|
|
59
|
+
{!!error && <Text style={{ color: "red", marginBottom: 8 }}>{error}</Text>}
|
|
25
60
|
<Button title="Register" onPress={() => void handleRegister()} />
|
|
26
|
-
</
|
|
61
|
+
</ScreenLayout>
|
|
27
62
|
);
|
|
28
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 SettingsScreen() {
|
|
5
6
|
return (
|
|
6
|
-
<
|
|
7
|
+
<ScreenLayout>
|
|
7
8
|
<Text>This is the Settings Screen</Text>
|
|
8
|
-
</
|
|
9
|
+
</ScreenLayout>
|
|
9
10
|
);
|
|
10
11
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
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 WelcomeScreenProps = {
|
|
16
|
+
onContinue: () => void;
|
|
17
|
+
};
|
|
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: "Build faster with a clean, production-ready app foundation.",
|
|
31
|
+
image: require("../assets/images/react-logo.png"),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "2",
|
|
35
|
+
title: "Structure That Scales",
|
|
36
|
+
description: "Auth, state, and API setup are organized for real-world apps.",
|
|
37
|
+
image: require("../assets/images/partial-react-logo.png"),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "3",
|
|
41
|
+
title: "Make It Yours",
|
|
42
|
+
description: "Swap this welcome flow with your own brand and product journey.",
|
|
43
|
+
image: require("../assets/images/icon.png"),
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export default function WelcomeScreen({ onContinue }: WelcomeScreenProps) {
|
|
48
|
+
const { width } = useWindowDimensions();
|
|
49
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
50
|
+
const listRef = useRef<FlatList<Slide>>(null);
|
|
51
|
+
|
|
52
|
+
const onViewableItemsChanged = useRef(
|
|
53
|
+
({ viewableItems }: { viewableItems: Array<ViewToken> }) => {
|
|
54
|
+
if (viewableItems[0]?.index != null) {
|
|
55
|
+
setActiveIndex(viewableItems[0].index);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
).current;
|
|
59
|
+
|
|
60
|
+
const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }).current;
|
|
61
|
+
|
|
62
|
+
const handleNext = () => {
|
|
63
|
+
const nextIndex = activeIndex + 1;
|
|
64
|
+
if (nextIndex < slides.length) {
|
|
65
|
+
listRef.current?.scrollToIndex({ index: nextIndex, animated: true });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
onContinue();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const isLastSlide = activeIndex === slides.length - 1;
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<SafeAreaView style={styles.safeArea}>
|
|
75
|
+
<FlatList
|
|
76
|
+
ref={listRef}
|
|
77
|
+
data={slides}
|
|
78
|
+
horizontal
|
|
79
|
+
pagingEnabled
|
|
80
|
+
bounces={false}
|
|
81
|
+
keyExtractor={(item) => item.id}
|
|
82
|
+
showsHorizontalScrollIndicator={false}
|
|
83
|
+
renderItem={({ item }) => (
|
|
84
|
+
<View style={[styles.slide, { width }]}>
|
|
85
|
+
<Image source={item.image} style={styles.image} resizeMode="contain" />
|
|
86
|
+
<Text style={styles.title}>{item.title}</Text>
|
|
87
|
+
<Text style={styles.description}>{item.description}</Text>
|
|
88
|
+
</View>
|
|
89
|
+
)}
|
|
90
|
+
onViewableItemsChanged={onViewableItemsChanged}
|
|
91
|
+
viewabilityConfig={viewabilityConfig}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
<View style={styles.footer}>
|
|
95
|
+
<View style={styles.dotsRow}>
|
|
96
|
+
{slides.map((slide, index) => (
|
|
97
|
+
<View
|
|
98
|
+
key={slide.id}
|
|
99
|
+
style={[styles.dot, index === activeIndex && styles.dotActive]}
|
|
100
|
+
/>
|
|
101
|
+
))}
|
|
102
|
+
</View>
|
|
103
|
+
|
|
104
|
+
<Pressable style={styles.button} onPress={handleNext}>
|
|
105
|
+
<Text style={styles.buttonLabel}>{isLastSlide ? "Get Started" : "Next"}</Text>
|
|
106
|
+
</Pressable>
|
|
107
|
+
</View>
|
|
108
|
+
</SafeAreaView>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const styles = StyleSheet.create({
|
|
113
|
+
safeArea: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
backgroundColor: "#F7F7F9",
|
|
116
|
+
},
|
|
117
|
+
slide: {
|
|
118
|
+
flex: 1,
|
|
119
|
+
paddingHorizontal: 24,
|
|
120
|
+
justifyContent: "center",
|
|
121
|
+
alignItems: "center",
|
|
122
|
+
},
|
|
123
|
+
image: {
|
|
124
|
+
width: 220,
|
|
125
|
+
height: 220,
|
|
126
|
+
marginBottom: 28,
|
|
127
|
+
},
|
|
128
|
+
title: {
|
|
129
|
+
fontSize: 28,
|
|
130
|
+
fontWeight: "700",
|
|
131
|
+
color: "#111827",
|
|
132
|
+
textAlign: "center",
|
|
133
|
+
marginBottom: 12,
|
|
134
|
+
},
|
|
135
|
+
description: {
|
|
136
|
+
fontSize: 16,
|
|
137
|
+
lineHeight: 24,
|
|
138
|
+
color: "#4B5563",
|
|
139
|
+
textAlign: "center",
|
|
140
|
+
maxWidth: 320,
|
|
141
|
+
},
|
|
142
|
+
footer: {
|
|
143
|
+
paddingHorizontal: 24,
|
|
144
|
+
paddingBottom: 28,
|
|
145
|
+
gap: 20,
|
|
146
|
+
},
|
|
147
|
+
dotsRow: {
|
|
148
|
+
flexDirection: "row",
|
|
149
|
+
justifyContent: "center",
|
|
150
|
+
gap: 8,
|
|
151
|
+
},
|
|
152
|
+
dot: {
|
|
153
|
+
width: 8,
|
|
154
|
+
height: 8,
|
|
155
|
+
borderRadius: 999,
|
|
156
|
+
backgroundColor: "#D1D5DB",
|
|
157
|
+
},
|
|
158
|
+
dotActive: {
|
|
159
|
+
width: 24,
|
|
160
|
+
backgroundColor: "#111827",
|
|
161
|
+
},
|
|
162
|
+
button: {
|
|
163
|
+
height: 52,
|
|
164
|
+
borderRadius: 12,
|
|
165
|
+
backgroundColor: "#111827",
|
|
166
|
+
alignItems: "center",
|
|
167
|
+
justifyContent: "center",
|
|
168
|
+
},
|
|
169
|
+
buttonLabel: {
|
|
170
|
+
color: "#FFFFFF",
|
|
171
|
+
fontSize: 16,
|
|
172
|
+
fontWeight: "600",
|
|
173
|
+
},
|
|
174
|
+
});
|