@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.
- package/.env.example +3 -0
- package/App.js +32 -0
- package/README.md +52 -0
- package/app.json +39 -0
- package/assets/adaptive-icon.png +0 -0
- package/assets/icon.png +0 -0
- package/assets/splash.png +0 -0
- package/babel.config.js +16 -0
- package/components/AddTaskModal.js +120 -0
- package/components/DropdownMenu.js +67 -0
- package/components/SyncContext.js +119 -0
- package/components/TaskRow.js +103 -0
- package/db/dbConnection.js +3 -0
- package/hooks/useCategories.js +94 -0
- package/hooks/useTasks.js +99 -0
- package/package.json +42 -0
- package/plugins/CloudSyncSetup.js +284 -0
- package/screens/Categories.js +220 -0
- package/screens/Cover.js +54 -0
- package/screens/Home.js +90 -0
- package/to-do-app.sql +3 -0
|
@@ -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;
|
package/screens/Cover.js
ADDED
|
@@ -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
|
+
});
|