@sqliteai/todoapp 1.0.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.
@@ -0,0 +1,99 @@
1
+ import { useState, useEffect, useCallback } from "react";
2
+ import { db } from "../db/dbConnection";
3
+ import { randomUUID } from 'expo-crypto';
4
+ import { useSyncContext } from '../components/SyncContext';
5
+
6
+ const useTasks = (tag = null) => {
7
+ const [taskList, setTaskList] = useState([]);
8
+ const { registerRefreshCallback } = useSyncContext();
9
+
10
+ const getTasks = useCallback(async () => {
11
+ try {
12
+ let result;
13
+ if (tag) {
14
+ result = await db.execute(
15
+ `
16
+ SELECT tasks.*, tags.uuid AS tag_uuid, tags.name AS tag_name
17
+ FROM tasks
18
+ JOIN tasks_tags ON tasks.uuid = tasks_tags.task_uuid
19
+ JOIN tags ON tags.uuid = tasks_tags.tag_uuid
20
+ WHERE tag_name=?`,
21
+ [tag]
22
+ );
23
+ setTaskList(result.rows);
24
+ } else {
25
+ result = await db.execute(`
26
+ SELECT tasks.*, tags.uuid AS tag_uuid, tags.name AS tag_name
27
+ FROM tasks
28
+ JOIN tasks_tags ON tasks.uuid = tasks_tags.task_uuid
29
+ JOIN tags ON tags.uuid = tasks_tags.tag_uuid`);
30
+ setTaskList(result.rows);
31
+ }
32
+ } catch (error) {
33
+ console.error("Error getting tasks", error);
34
+ }
35
+ }, [tag]);
36
+
37
+ const updateTask = async (completedStatus, taskUuid) => {
38
+ try {
39
+ await db.execute("UPDATE tasks SET isCompleted=? WHERE uuid=?", [completedStatus, taskUuid]);
40
+ db.execute('SELECT cloudsync_network_send_changes();')
41
+ setTaskList(prevTasks =>
42
+ prevTasks.map(task =>
43
+ task.uuid === taskUuid ? { ...task, isCompleted: completedStatus } : task
44
+ )
45
+ );
46
+ } catch (error) {
47
+ console.error("Error updating tasks", error);
48
+ }
49
+ };
50
+
51
+ const addTaskTag = async (newTask, tag) => {
52
+ try {
53
+ if (tag.uuid) {
54
+ const addNewTask = await db.execute("INSERT INTO tasks (uuid, title, isCompleted) VALUES (?, ?, ?) RETURNING *", [randomUUID(), newTask.title, newTask.isCompleted]);
55
+ addNewTask.rows[0].tag_uuid = tag.uuid;
56
+ addNewTask.rows[0].tag_name = tag.name;
57
+ setTaskList([...taskList, addNewTask.rows[0]]);
58
+ await db.execute("INSERT INTO tasks_tags (uuid, task_uuid, tag_uuid) VALUES (?, ?, ?)", [randomUUID(), addNewTask.rows[0].uuid, tag.uuid]);
59
+ } else {
60
+ const addNewTaskNoTag = await db.execute("INSERT INTO tasks (uuid, title, isCompleted) VALUES (?, ?, ?) RETURNING *", [randomUUID(), newTask.title, newTask.isCompleted]);
61
+ setTaskList([...taskList, addNewTaskNoTag.rows[0]]);
62
+ }
63
+ db.execute('SELECT cloudsync_network_send_changes();')
64
+ } catch (error) {
65
+ console.error("Error adding task to database", error);
66
+ }
67
+ };
68
+
69
+ const deleteTask = async (taskUuid) => {
70
+ try {
71
+ await db.execute("DELETE FROM tasks_tags WHERE task_uuid=?", [taskUuid]);
72
+ await db.execute("DELETE FROM tasks WHERE uuid=?", [taskUuid]);
73
+ db.execute('SELECT cloudsync_network_send_changes();')
74
+ setTaskList(taskList.filter(task => task.uuid !== taskUuid));
75
+ } catch (error) {
76
+ console.error("Error deleting task", error);
77
+ }
78
+ };
79
+
80
+ useEffect(() => {
81
+ getTasks();
82
+ }, [getTasks]);
83
+
84
+ useEffect(() => {
85
+ return registerRefreshCallback(() => {
86
+ getTasks();
87
+ });
88
+ }, [registerRefreshCallback, getTasks]);
89
+
90
+ return {
91
+ taskList,
92
+ updateTask,
93
+ addTaskTag,
94
+ deleteTask,
95
+ refreshTasks: getTasks,
96
+ };
97
+ };
98
+
99
+ export default useTasks;
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@sqliteai/todoapp",
3
+ "version": "1.0.0",
4
+ "description": "An Expo template for building apps with the SQLite CloudSync extension",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/sqliteai/sqlite-sync.git"
8
+ },
9
+ "author": "SQLiteAI",
10
+ "keywords": ["expo-template", "sqlite", "cloudsync", "todo", "sync", "react-native", "expo", "template"],
11
+ "main": "expo/AppEntry.js",
12
+ "scripts": {
13
+ "start": "expo start",
14
+ "android": "expo run:android",
15
+ "ios": "expo run:ios",
16
+ "clean": "cp .env .env.backup 2>/dev/null || true; git clean -fdX; mv .env.backup .env 2>/dev/null || true"
17
+ },
18
+ "expo": {
19
+ "entryPoint": "expo/AppEntry.js"
20
+ },
21
+ "dependencies": {
22
+ "@op-engineering/op-sqlite": "14.1.4",
23
+ "@react-native-picker/picker": "2.11.1",
24
+ "@react-navigation/native": "^7.1.17",
25
+ "@react-navigation/stack": "^7.4.7",
26
+ "expo": "^53.0.22",
27
+ "expo-crypto": "~14.1.5",
28
+ "expo-status-bar": "~2.2.3",
29
+ "react": "19.0.0",
30
+ "react-native": "0.79.5",
31
+ "react-native-gesture-handler": "~2.24.0",
32
+ "react-native-paper": "5.14.5",
33
+ "react-native-picker-select": "^9.3.1",
34
+ "react-native-safe-area-context": "5.4.0",
35
+ "react-native-screens": "~4.11.1",
36
+ "react-native-vector-icons": "^10.3.0"
37
+ },
38
+ "devDependencies": {
39
+ "@babel/core": "7.28.3",
40
+ "react-native-dotenv": "3.4.11"
41
+ }
42
+ }
@@ -0,0 +1,284 @@
1
+ const { withDangerousMod, withXcodeProject } = require('@expo/config-plugins');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const https = require('https');
5
+ const { execSync } = require('child_process');
6
+
7
+ /**
8
+ * Download file from URL
9
+ */
10
+ async function downloadFile(url, dest) {
11
+ return new Promise((resolve, reject) => {
12
+ const file = fs.createWriteStream(dest);
13
+
14
+ https.get(url, (response) => {
15
+
16
+ if (response.statusCode === 302 || response.statusCode === 301) {
17
+ file.close();
18
+ fs.unlinkSync(dest);
19
+ // Handle redirect
20
+ return downloadFile(response.headers.location, dest).then(resolve).catch(reject);
21
+ }
22
+
23
+ if (response.statusCode !== 200) {
24
+ file.close();
25
+ fs.unlinkSync(dest);
26
+ return reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
27
+ }
28
+
29
+ response.pipe(file);
30
+
31
+ file.on('finish', () => {
32
+ file.close();
33
+ resolve();
34
+ });
35
+
36
+ file.on('error', (err) => {
37
+ fs.unlink(dest, () => {}); // Delete the file async
38
+ reject(err);
39
+ });
40
+ }).on('error', (err) => {
41
+ reject(err);
42
+ });
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Get latest release URL from GitHub API
48
+ */
49
+ async function getLatestReleaseUrl(asset_pattern) {
50
+ return new Promise((resolve, reject) => {
51
+ const options = {
52
+ hostname: 'api.github.com',
53
+ path: '/repos/sqliteai/sqlite-sync/releases/latest',
54
+ headers: {
55
+ 'User-Agent': 'expo-cloudsync-plugin'
56
+ }
57
+ };
58
+
59
+ https.get(options, (response) => {
60
+ let data = '';
61
+
62
+ response.on('data', (chunk) => {
63
+ data += chunk;
64
+ });
65
+
66
+ response.on('end', () => {
67
+ try {
68
+ const release = JSON.parse(data);
69
+ const asset = release.assets.find(asset =>
70
+ asset.name.includes(asset_pattern) && asset.name.endsWith('.zip')
71
+ );
72
+
73
+ if (asset) {
74
+ resolve(asset.browser_download_url);
75
+ } else {
76
+ reject(new Error(`Asset with pattern ${asset_pattern} not found`));
77
+ }
78
+ } catch (error) {
79
+ reject(error);
80
+ }
81
+ });
82
+ }).on('error', (err) => {
83
+ reject(err);
84
+ });
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Extract zip file and return extracted directory
90
+ */
91
+ function extractZip(zipPath, extractTo) {
92
+ try {
93
+ // Create extraction directory
94
+ fs.mkdirSync(extractTo, { recursive: true });
95
+
96
+ // Use system unzip command
97
+ execSync(`unzip -o "${zipPath}" -d "${extractTo}"`, { stdio: 'pipe' });
98
+
99
+ return extractTo;
100
+ } catch (error) {
101
+ throw new Error(`Failed to extract ${zipPath}: ${error.message}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Setup Android native libraries
107
+ */
108
+ async function setupAndroidLibraries(projectRoot) {
109
+
110
+ const tempDir = path.join(projectRoot, 'temp_downloads');
111
+ fs.mkdirSync(tempDir, { recursive: true });
112
+
113
+ try {
114
+ // Download Android x86_64 build
115
+ const x86_64Url = await getLatestReleaseUrl('cloudsync-android-x86_64');
116
+ const x86_64ZipPath = path.join(tempDir, 'android-x86_64.zip');
117
+ await downloadFile(x86_64Url, x86_64ZipPath);
118
+
119
+ // Download Android arm64 build
120
+ const arm64Url = await getLatestReleaseUrl('cloudsync-android-arm64-v8a');
121
+ const arm64ZipPath = path.join(tempDir, 'android-arm64.zip');
122
+ await downloadFile(arm64Url, arm64ZipPath);
123
+
124
+ // Extract both archives
125
+ const x86_64ExtractPath = path.join(tempDir, 'x86_64');
126
+ const arm64ExtractPath = path.join(tempDir, 'arm64');
127
+
128
+ extractZip(x86_64ZipPath, x86_64ExtractPath);
129
+ extractZip(arm64ZipPath, arm64ExtractPath);
130
+
131
+ // Setup jniLibs directory structure
132
+ const jniLibsDir = path.join(projectRoot, 'android', 'app', 'src', 'main', 'jniLibs');
133
+ const x86_64LibDir = path.join(jniLibsDir, 'x86_64');
134
+ const arm64LibDir = path.join(jniLibsDir, 'arm64-v8a');
135
+
136
+ fs.mkdirSync(x86_64LibDir, { recursive: true });
137
+ fs.mkdirSync(arm64LibDir, { recursive: true });
138
+
139
+ // Find and copy cloudsync.so files
140
+ const findSoFile = (dir) => {
141
+ const files = fs.readdirSync(dir, { recursive: true });
142
+ const soFile = files.find(file => file.toString().endsWith('cloudsync.so'));
143
+ return soFile ? path.join(dir, soFile) : null;
144
+ };
145
+
146
+ const x86_64SoFile = findSoFile(x86_64ExtractPath);
147
+ const arm64SoFile = findSoFile(arm64ExtractPath);
148
+
149
+ if (x86_64SoFile) {
150
+ fs.copyFileSync(x86_64SoFile, path.join(x86_64LibDir, 'cloudsync.so'));
151
+ }
152
+
153
+ if (arm64SoFile) {
154
+ fs.copyFileSync(arm64SoFile, path.join(arm64LibDir, 'cloudsync.so'));
155
+ }
156
+
157
+ } finally {
158
+ // Cleanup temp directory
159
+ if (fs.existsSync(tempDir)) {
160
+ fs.rmSync(tempDir, { recursive: true, force: true });
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Setup iOS framework
167
+ */
168
+ async function setupIOSFramework(projectRoot) {
169
+
170
+ const tempDir = path.join(projectRoot, 'temp_downloads');
171
+ fs.mkdirSync(tempDir, { recursive: true });
172
+
173
+ try {
174
+ // Download iOS xcframework
175
+ const xcframeworkUrl = await getLatestReleaseUrl('cloudsync-apple-xcframework');
176
+ const xcframeworkZipPath = path.join(tempDir, 'apple-xcframework.zip');
177
+ await downloadFile(xcframeworkUrl, xcframeworkZipPath);
178
+
179
+ // Extract xcframework
180
+ const extractPath = path.join(tempDir, 'xcframework');
181
+ extractZip(xcframeworkZipPath, extractPath);
182
+
183
+ // Find CloudSync.xcframework directory
184
+ const findXcframework = (dir) => {
185
+ const files = fs.readdirSync(dir, { recursive: true });
186
+ const xcframeworkPath = files.find(file => file.toString().endsWith('CloudSync.xcframework'));
187
+ return xcframeworkPath ? path.join(dir, xcframeworkPath) : null;
188
+ };
189
+
190
+ const xcframeworkPath = findXcframework(extractPath);
191
+
192
+ if (xcframeworkPath && fs.statSync(xcframeworkPath).isDirectory()) {
193
+ // Get project name from app.json
194
+ const appJson = JSON.parse(fs.readFileSync(path.join(projectRoot, 'app.json'), 'utf8'));
195
+ const projectName = appJson.expo.name;
196
+
197
+ const frameworksDir = path.join(projectRoot, 'ios', projectName, 'Frameworks');
198
+ const targetFrameworkPath = path.join(frameworksDir, 'CloudSync.xcframework');
199
+
200
+ // Create Frameworks directory
201
+ fs.mkdirSync(frameworksDir, { recursive: true });
202
+
203
+ // Copy xcframework
204
+ fs.cpSync(xcframeworkPath, targetFrameworkPath, { recursive: true });
205
+
206
+ return targetFrameworkPath;
207
+ } else {
208
+ throw new Error('CloudSync.xcframework not found in extracted archive');
209
+ }
210
+
211
+ } finally {
212
+ // Cleanup temp directory
213
+ if (fs.existsSync(tempDir)) {
214
+ fs.rmSync(tempDir, { recursive: true, force: true });
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Add framework to Xcode project
221
+ */
222
+ const withCloudSyncFramework = (config) => {
223
+ return withXcodeProject(config, (config) => {
224
+ const xcodeProject = config.modResults;
225
+ const projectName = config.modRequest.projectName || config.name;
226
+ const target = xcodeProject.getFirstTarget().uuid;
227
+
228
+ const frameworkPath = `${projectName}/Frameworks/CloudSync.xcframework`;
229
+
230
+ // Check if framework already exists
231
+ if (xcodeProject.hasFile(frameworkPath)) {
232
+ return config;
233
+ }
234
+
235
+ // First check if "Embed Frameworks" build phase exists, if not create it
236
+ let embedPhase = xcodeProject.pbxEmbedFrameworksBuildPhaseObj(target);
237
+
238
+ if (!embedPhase) {
239
+ // Create the embed frameworks build phase with correct parameters for frameworks
240
+ xcodeProject.addBuildPhase([], 'PBXCopyFilesBuildPhase', 'Embed Frameworks', target, 'frameworks');
241
+ embedPhase = xcodeProject.pbxEmbedFrameworksBuildPhaseObj(target);
242
+ }
243
+
244
+ // Add framework to project with embed
245
+ const frameworkFile = xcodeProject.addFramework(frameworkPath, {
246
+ target: target,
247
+ embed: true,
248
+ customFramework: true
249
+ });
250
+
251
+
252
+ return config;
253
+ });
254
+ };
255
+
256
+ /**
257
+ * Main plugin function
258
+ */
259
+ const withCloudSync = (config) => {
260
+ // Android setup
261
+ config = withDangerousMod(config, [
262
+ 'android',
263
+ async (config) => {
264
+ await setupAndroidLibraries(config.modRequest.projectRoot);
265
+ return config;
266
+ }
267
+ ]);
268
+
269
+ // iOS setup - download and place framework
270
+ config = withDangerousMod(config, [
271
+ 'ios',
272
+ async (config) => {
273
+ await setupIOSFramework(config.modRequest.projectRoot);
274
+ return config;
275
+ }
276
+ ]);
277
+
278
+ // iOS setup - add to Xcode project
279
+ config = withCloudSyncFramework(config);
280
+
281
+ return config;
282
+ };
283
+
284
+ module.exports = withCloudSync;
@@ -0,0 +1,220 @@
1
+ import React, { useState } from "react";
2
+ import { ScrollView, StyleSheet, View } from "react-native";
3
+ import { Avatar, Card, Text, Modal, Portal, Button, TextInput } from "react-native-paper";
4
+ import { useFocusEffect } from '@react-navigation/native';
5
+ import useCategories from "../hooks/useCategories";
6
+ import { useSyncContext } from "../components/SyncContext";
7
+
8
+ const Categories = ({ navigation }) => {
9
+ const today = new Date();
10
+ const days = [
11
+ "Sunday",
12
+ "Monday",
13
+ "Tuesday",
14
+ "Wednesday",
15
+ "Thursday",
16
+ "Friday",
17
+ "Saturday",
18
+ ];
19
+ const dayIndex = today.getDay();
20
+ const monthDate = today.toLocaleDateString("en-US", {
21
+ month: "long",
22
+ day: "numeric",
23
+ });
24
+
25
+ const { moreCategories, addCategory } = useCategories();
26
+ const { setSync } = useSyncContext();
27
+
28
+ const [newCategory, setNewCategory] = useState("");
29
+ const [visible, setVisible] = React.useState(false);
30
+
31
+ useFocusEffect(
32
+ React.useCallback(() => {
33
+ setSync(true);
34
+ }, [setSync])
35
+ );
36
+
37
+ const showModal = () => setVisible(true);
38
+ const hideModal = () => setVisible(false);
39
+
40
+ function handleAddCategory() {
41
+ if (newCategory) {
42
+ addCategory(newCategory);
43
+ }
44
+ setNewCategory("");
45
+ hideModal();
46
+ }
47
+
48
+ return (
49
+ <>
50
+ <Portal>
51
+ <Modal
52
+ visible={visible}
53
+ onDismiss={hideModal}
54
+ style={{ backgroundColor: "white", padding: 20 }}
55
+ >
56
+ <TextInput
57
+ style={styles.textInput}
58
+ label="Enter a new category"
59
+ value={newCategory}
60
+ onChangeText={(newCategory) => setNewCategory(newCategory)}
61
+ keyboardType="default"
62
+ activeUnderlineColor="#6ba2ea"
63
+ underlineColor="none"
64
+ activeOutlineColor="#fff"
65
+ outlineColor="none"
66
+ />
67
+ <Button
68
+ style={styles.button}
69
+ buttonColor={styles.button.backgroundColor}
70
+ textColor={styles.button.color}
71
+ onPress={handleAddCategory}
72
+ >
73
+ Add
74
+ </Button>
75
+ </Modal>
76
+ </Portal>
77
+ <ScrollView
78
+ style={styles.container}
79
+ contentContainerStyle={styles.content}
80
+ >
81
+ <Text
82
+ variant="bodyLarge"
83
+ style={[
84
+ styles.content,
85
+ {
86
+ color: "#6b7280",
87
+ },
88
+ ]}
89
+ >
90
+ {days[dayIndex]}
91
+ </Text>
92
+ <Text
93
+ variant="headlineSmall"
94
+ style={[
95
+ styles.content,
96
+ {
97
+ color: "#000",
98
+ },
99
+ ]}
100
+ >
101
+ {monthDate}
102
+ </Text>
103
+ <View style={styles.cardRow}>
104
+ <Card
105
+ style={styles.card}
106
+ onPress={() => navigation.navigate("Tasks")}
107
+ mode="contained"
108
+ >
109
+ <Card.Title
110
+ left={(props) => (
111
+ <Avatar.Icon
112
+ {...props}
113
+ icon="inbox-outline"
114
+ color={styles.icon.color}
115
+ style={styles.icon}
116
+ />
117
+ )}
118
+ />
119
+
120
+ <Text variant="bodyMedium" style={styles.text}>
121
+ Inbox
122
+ </Text>
123
+ </Card>
124
+
125
+ {moreCategories.map((category, index) => (
126
+ <Card
127
+ key={index}
128
+ style={styles.card}
129
+ onPress={() => navigation.navigate("Tasks", { category })}
130
+ mode="contained"
131
+ >
132
+ <Card.Title
133
+ left={(props) => (
134
+ <Avatar.Icon
135
+ {...props}
136
+ icon="tag-outline"
137
+ color={styles.icon.color}
138
+ style={styles.icon}
139
+ />
140
+ )}
141
+ />
142
+
143
+ <Text variant="bodyMedium" numberOfLines={1} style={styles.text}>
144
+ {category}
145
+ </Text>
146
+ </Card>
147
+ ))}
148
+
149
+ <Card style={styles.addCard} onPress={showModal} mode="contained">
150
+ <Card.Title
151
+ left={(props) => (
152
+ <Avatar.Icon
153
+ {...props}
154
+ icon="plus-circle-outline"
155
+ color={styles.addIcon.color}
156
+ style={styles.addIcon}
157
+ />
158
+ )}
159
+ />
160
+
161
+ <Text variant="bodyMedium" style={styles.text}>
162
+ {" "}
163
+ </Text>
164
+ </Card>
165
+ </View>
166
+ </ScrollView>
167
+ </>
168
+ );
169
+ };
170
+
171
+ Categories.title = "Categories";
172
+
173
+ const styles = StyleSheet.create({
174
+ container: {
175
+ backgroundColor: "#fff",
176
+ },
177
+ content: {
178
+ padding: 10,
179
+ },
180
+ cardRow: {
181
+ flexDirection: "row",
182
+ flexWrap: "wrap",
183
+ justifyContent: "space-between",
184
+ },
185
+ card: {
186
+ backgroundColor: "#cfe2f8",
187
+ margin: 5,
188
+ width: "47%",
189
+ },
190
+ addCard: {
191
+ backgroundColor: "#fff",
192
+ margin: 5,
193
+ borderWidth: 1,
194
+ borderStyle: "dashed",
195
+ borderColor: "#6BA2EA",
196
+ width: "47%",
197
+ },
198
+ icon: {
199
+ backgroundColor: "#cfe2f8",
200
+ color: "#6b7280",
201
+ },
202
+ addIcon: {
203
+ backgroundColor: "#fff",
204
+ color: "#6BA2EA",
205
+ },
206
+ text: {
207
+ color: "#6b7280",
208
+ padding: 15,
209
+ },
210
+ button: {
211
+ borderRadius: "none",
212
+ backgroundColor: "#b2cae9",
213
+ color: "#000",
214
+ },
215
+ textInput: {
216
+ backgroundColor: "#f0f5fd",
217
+ },
218
+ });
219
+
220
+ export default Categories;
@@ -0,0 +1,54 @@
1
+ import { useCallback } from 'react';
2
+ import { StatusBar } from 'expo-status-bar';
3
+ import { View, Text, StyleSheet } from 'react-native';
4
+ import { Button } from 'react-native-paper';
5
+ import { useFocusEffect } from '@react-navigation/native';
6
+ import { useSyncContext } from '../components/SyncContext';
7
+
8
+ export default Cover = ({ navigation }) => {
9
+ const { setSync } = useSyncContext();
10
+
11
+ useFocusEffect(
12
+ useCallback(() => {
13
+ setSync(false);
14
+ }, [setSync])
15
+ );
16
+
17
+ return (
18
+ <View style={styles.container}>
19
+ <Text style={styles.heading}>Organize Your</Text>
20
+ <Text style={styles.heading}>Tasks with SQLite</Text>
21
+ <Text>Designed for Happiness, Not Just Productivity.</Text>
22
+ <Text>Enjoy a Stress-free Way to Manage Your Day.</Text>
23
+ <Button
24
+ style={styles.button}
25
+ buttonColor="#6BA2EA"
26
+ textColor="white"
27
+ onPress={() => navigation.navigate('Categories')}
28
+ >
29
+ Get started
30
+ </Button>
31
+ <StatusBar style="auto" />
32
+ </View>
33
+ );
34
+ };
35
+
36
+ const styles = StyleSheet.create({
37
+ container: {
38
+ flex: 1,
39
+ backgroundColor: '#fff',
40
+ alignItems: 'flex-start',
41
+ justifyContent: 'center',
42
+ paddingLeft: 15,
43
+ },
44
+ heading: {
45
+ fontWeight: 'bold',
46
+ fontSize: 40,
47
+ marginBottom: 5,
48
+ },
49
+ button: {
50
+ position: 'absolute',
51
+ bottom: 70,
52
+ right: 20,
53
+ },
54
+ });