@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
package/.env.example
ADDED
package/App.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PaperProvider } from "react-native-paper";
|
|
2
|
+
import { NavigationContainer } from "@react-navigation/native";
|
|
3
|
+
import { createStackNavigator } from "@react-navigation/stack";
|
|
4
|
+
import Cover from "./screens/Cover";
|
|
5
|
+
import Home from "./screens/Home";
|
|
6
|
+
import AddTaskModal from "./components/AddTaskModal";
|
|
7
|
+
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
8
|
+
import Categories from './screens/Categories';
|
|
9
|
+
import { SyncProvider } from './components/SyncContext';
|
|
10
|
+
|
|
11
|
+
export default function App() {
|
|
12
|
+
const Stack = createStackNavigator();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
16
|
+
<SyncProvider>
|
|
17
|
+
<NavigationContainer>
|
|
18
|
+
<PaperProvider>
|
|
19
|
+
<Stack.Navigator>
|
|
20
|
+
<Stack.Screen name="Cover" component={Cover} />
|
|
21
|
+
<Stack.Screen name="Categories" component={Categories} />
|
|
22
|
+
<Stack.Screen name="Tasks" component={Home} />
|
|
23
|
+
<Stack.Group screenOptions={{ presentation: 'modal' }}>
|
|
24
|
+
<Stack.Screen name="Add Task" component={AddTaskModal} />
|
|
25
|
+
</Stack.Group>
|
|
26
|
+
</Stack.Navigator>
|
|
27
|
+
</PaperProvider>
|
|
28
|
+
</NavigationContainer>
|
|
29
|
+
</SyncProvider>
|
|
30
|
+
</GestureHandlerRootView>
|
|
31
|
+
);
|
|
32
|
+
}
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Expo CloudSync Example
|
|
2
|
+
|
|
3
|
+
A simple Expo example demonstrating SQLite synchronization with CloudSync. Build cross-platform apps that sync data seamlessly across devices.
|
|
4
|
+
|
|
5
|
+
## 🚀 Quick Start
|
|
6
|
+
|
|
7
|
+
### 1. Clone the template
|
|
8
|
+
|
|
9
|
+
Create a new project using this template:
|
|
10
|
+
```bash
|
|
11
|
+
npx create-expo-app MyApp --template @sqliteai/todoapp
|
|
12
|
+
cd MyApp
|
|
13
|
+
npm install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 2. Database Setup
|
|
17
|
+
|
|
18
|
+
1. Create database in [SQLite Cloud Dashboard](https://dashboard.sqlitecloud.io/).
|
|
19
|
+
2. Execute the exact schema from `to-do-app.sql`.
|
|
20
|
+
3. Enable OffSync for all tables on the remote database from the **SQLite Cloud Dashboard -> Databases**.
|
|
21
|
+
|
|
22
|
+
### 3. Environment Configuration
|
|
23
|
+
|
|
24
|
+
Rename the `.env.example` into `.env` and fill with your values.
|
|
25
|
+
|
|
26
|
+
> **⚠️ SECURITY WARNING**: This example puts database connection strings directly in `.env` files for demonstration purposes only. **Do not use this pattern in production.**
|
|
27
|
+
>
|
|
28
|
+
> **Why this is unsafe:**
|
|
29
|
+
> - Connection strings contain sensitive credentials
|
|
30
|
+
> - Client-side apps expose all environment variables to users
|
|
31
|
+
> - Anyone can inspect your app and extract database credentials
|
|
32
|
+
>
|
|
33
|
+
> **For production apps:**
|
|
34
|
+
> - Use the secure [sport-tracker-app](https://github.com/sqliteai/sqlite-sync/tree/main/examples/sport-tracker-app) pattern with authentication tokens and row-level security
|
|
35
|
+
> - Never embed database credentials in client applications
|
|
36
|
+
|
|
37
|
+
### 4. Build and run the App
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx expo prebuild # run once
|
|
41
|
+
npm start
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## ✨ Features
|
|
45
|
+
|
|
46
|
+
- **Add Tasks** - Create new tasks with titles and optional tags.
|
|
47
|
+
- **Edit Task Status** - Update task status when completed.
|
|
48
|
+
- **Delete Tasks** - Remove tasks from your list.
|
|
49
|
+
- **Dropdown Menu** - Select categories for tasks from a predefined list.
|
|
50
|
+
- **Cross-Platform** - Works on iOS and Android
|
|
51
|
+
- **Offline Support** - Works offline, syncs when connection returns
|
|
52
|
+
|
package/app.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"expo": {
|
|
3
|
+
"name": "todoapp",
|
|
4
|
+
"slug": "todoapp",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"orientation": "portrait",
|
|
7
|
+
"icon": "./assets/icon.png",
|
|
8
|
+
"userInterfaceStyle": "light",
|
|
9
|
+
"description": "A todo app demonstrating SQLite CloudSync extension functionalities",
|
|
10
|
+
"keywords": ["sqlite", "cloudsync", "todo", "sync"],
|
|
11
|
+
"privacy": "public",
|
|
12
|
+
"platforms": ["ios", "android"],
|
|
13
|
+
"plugins": [
|
|
14
|
+
"./plugins/CloudSyncSetup.js"
|
|
15
|
+
],
|
|
16
|
+
"splash": {
|
|
17
|
+
"image": "./assets/splash.png",
|
|
18
|
+
"resizeMode": "contain",
|
|
19
|
+
"backgroundColor": "#ffffff"
|
|
20
|
+
},
|
|
21
|
+
"ios": {
|
|
22
|
+
"supportsTablet": true,
|
|
23
|
+
"bundleIdentifier": "ai.sqlite.todoapp"
|
|
24
|
+
},
|
|
25
|
+
"android": {
|
|
26
|
+
"adaptiveIcon": {
|
|
27
|
+
"foregroundImage": "./assets/adaptive-icon.png",
|
|
28
|
+
"backgroundColor": "#ffffff"
|
|
29
|
+
},
|
|
30
|
+
"package": "ai.sqlite.todoapp"
|
|
31
|
+
},
|
|
32
|
+
"extra": {
|
|
33
|
+
"eas": {
|
|
34
|
+
"projectId": "faffd54b-fc15-4368-987a-a73b08640cfd"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"owner": "sqliteai"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
Binary file
|
package/assets/icon.png
ADDED
|
Binary file
|
|
Binary file
|
package/babel.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module.exports = function (api) {
|
|
2
|
+
api.cache(false);
|
|
3
|
+
return {
|
|
4
|
+
presets: ["babel-preset-expo"],
|
|
5
|
+
plugins: [
|
|
6
|
+
"react-native-paper/babel",
|
|
7
|
+
[
|
|
8
|
+
"module:react-native-dotenv",
|
|
9
|
+
{
|
|
10
|
+
moduleName: "@env",
|
|
11
|
+
path: ".env",
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { View, StyleSheet, Alert } from "react-native";
|
|
3
|
+
import { TextInput, Button, Modal } from "react-native-paper";
|
|
4
|
+
import DropdownMenu from "./DropdownMenu";
|
|
5
|
+
import { db } from "../db/dbConnection";
|
|
6
|
+
|
|
7
|
+
export default AddTaskModal = ({
|
|
8
|
+
modalVisible,
|
|
9
|
+
addTaskTag,
|
|
10
|
+
setModalVisible,
|
|
11
|
+
}) => {
|
|
12
|
+
const [taskTitle, setTaskTitle] = useState("");
|
|
13
|
+
const [tagsList, setTagsList] = useState([]);
|
|
14
|
+
const [selectedTag, setSelectedTag] = useState({});
|
|
15
|
+
|
|
16
|
+
const closeModal = () => {
|
|
17
|
+
setModalVisible(false);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const handleAddTask = () => {
|
|
21
|
+
if (taskTitle.trim()) {
|
|
22
|
+
addTaskTag({ title: taskTitle.trim(), isCompleted: false }, selectedTag);
|
|
23
|
+
setTaskTitle("");
|
|
24
|
+
setSelectedTag({});
|
|
25
|
+
closeModal();
|
|
26
|
+
} else {
|
|
27
|
+
Alert.alert("Please add a new task.");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getTags = async () => {
|
|
32
|
+
try {
|
|
33
|
+
const tags = await db.execute("SELECT * FROM tags");
|
|
34
|
+
setTagsList(tags.rows);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("Error getting tags", error);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
getTags();
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Modal
|
|
46
|
+
style={styles.modalContainer}
|
|
47
|
+
visible={modalVisible}
|
|
48
|
+
onDismiss={closeModal}
|
|
49
|
+
>
|
|
50
|
+
<View>
|
|
51
|
+
<View style={styles.newTaskBox}>
|
|
52
|
+
<TextInput
|
|
53
|
+
mode="flat"
|
|
54
|
+
style={[
|
|
55
|
+
styles.textInput
|
|
56
|
+
]}
|
|
57
|
+
contentStyle={styles.textInputContent}
|
|
58
|
+
underlineColor="transparent"
|
|
59
|
+
activeUnderlineColor="#6BA2EA"
|
|
60
|
+
value={taskTitle}
|
|
61
|
+
onChangeText={setTaskTitle}
|
|
62
|
+
label="Enter a new task"
|
|
63
|
+
keyboardType="default"
|
|
64
|
+
/>
|
|
65
|
+
</View>
|
|
66
|
+
<DropdownMenu tagsList={tagsList} setSelectedTag={setSelectedTag} />
|
|
67
|
+
<Button
|
|
68
|
+
style={styles.addTaskButton}
|
|
69
|
+
textColor="black"
|
|
70
|
+
onPress={handleAddTask}
|
|
71
|
+
>
|
|
72
|
+
Add task
|
|
73
|
+
</Button>
|
|
74
|
+
</View>
|
|
75
|
+
</Modal>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const styles = StyleSheet.create({
|
|
80
|
+
modalContainer: {
|
|
81
|
+
flex: 1,
|
|
82
|
+
backgroundColor: "white",
|
|
83
|
+
padding: 10,
|
|
84
|
+
},
|
|
85
|
+
newTaskBox: {
|
|
86
|
+
flexDirection: "row",
|
|
87
|
+
alignItems: "center",
|
|
88
|
+
justifyContent: "center",
|
|
89
|
+
borderWidth: 1,
|
|
90
|
+
borderColor: "lightgray",
|
|
91
|
+
backgroundColor: "#f0f5fd",
|
|
92
|
+
marginBottom: 10,
|
|
93
|
+
},
|
|
94
|
+
textInput: {
|
|
95
|
+
width: "100%",
|
|
96
|
+
backgroundColor: "transparent",
|
|
97
|
+
height: 50,
|
|
98
|
+
},
|
|
99
|
+
textInputContent: {
|
|
100
|
+
backgroundColor: "transparennt",
|
|
101
|
+
borderWidth: 0,
|
|
102
|
+
paddingLeft: 10,
|
|
103
|
+
},
|
|
104
|
+
button: {
|
|
105
|
+
height: 50,
|
|
106
|
+
width: 50,
|
|
107
|
+
justifyContent: "center",
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
},
|
|
110
|
+
closeButton: {
|
|
111
|
+
alignItems: "flex-start",
|
|
112
|
+
bottom: 180,
|
|
113
|
+
left: -10,
|
|
114
|
+
zIndex: 1,
|
|
115
|
+
},
|
|
116
|
+
addTaskButton: {
|
|
117
|
+
backgroundColor: "#b2cae9",
|
|
118
|
+
marginTop: 10,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { View, StyleSheet } from "react-native";
|
|
2
|
+
import RNPickerSelect from "react-native-picker-select";
|
|
3
|
+
|
|
4
|
+
export default DropdownMenu = ({ tagsList, selectedTag, setSelectedTag }) => {
|
|
5
|
+
const TAGS = tagsList.map((tag) => {
|
|
6
|
+
return { label: tag.name, value: tag.name };
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const getTagUuid = (value) => {
|
|
10
|
+
const tagUuid = tagsList.filter((tag) => {
|
|
11
|
+
return tag.name === value;
|
|
12
|
+
});
|
|
13
|
+
return tagUuid[0]?.uuid;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<View style={styles.container}>
|
|
18
|
+
<RNPickerSelect
|
|
19
|
+
items={TAGS}
|
|
20
|
+
onValueChange={(value) =>
|
|
21
|
+
setSelectedTag({ uuid: getTagUuid(value), name: value })
|
|
22
|
+
}
|
|
23
|
+
placeholder={{ label: "Select a category", value: null }}
|
|
24
|
+
value={selectedTag}
|
|
25
|
+
style={pickerSelectStyles}
|
|
26
|
+
/>
|
|
27
|
+
</View>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const styles = StyleSheet.create({
|
|
32
|
+
container: {
|
|
33
|
+
borderColor: "lightgray",
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderRadius: 5,
|
|
36
|
+
backgroundColor: "#f0f5fd",
|
|
37
|
+
marginBottom: 10,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const pickerSelectStyles = StyleSheet.create({
|
|
42
|
+
inputIOSContainer: { pointerEvents: "none" },
|
|
43
|
+
inputIOS: {
|
|
44
|
+
height: 50,
|
|
45
|
+
fontSize: 16,
|
|
46
|
+
paddingVertical: 12,
|
|
47
|
+
paddingHorizontal: 10,
|
|
48
|
+
borderWidth: 1,
|
|
49
|
+
borderColor: "lightgray",
|
|
50
|
+
borderRadius: 4,
|
|
51
|
+
color: "black",
|
|
52
|
+
backgroundColor: "#f0f5fd",
|
|
53
|
+
paddingRight: 30,
|
|
54
|
+
},
|
|
55
|
+
inputAndroid: {
|
|
56
|
+
height: 50,
|
|
57
|
+
fontSize: 16,
|
|
58
|
+
paddingHorizontal: 10,
|
|
59
|
+
paddingVertical: 8,
|
|
60
|
+
borderWidth: 1,
|
|
61
|
+
borderColor: "lightgray",
|
|
62
|
+
borderRadius: 4,
|
|
63
|
+
color: "black",
|
|
64
|
+
backgroundColor: "#f0f5fd",
|
|
65
|
+
paddingRight: 30,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useState, useRef } from 'react';
|
|
2
|
+
import { InteractionManager, AppState } from 'react-native';
|
|
3
|
+
import { db } from '../db/dbConnection';
|
|
4
|
+
|
|
5
|
+
const SyncContext = createContext();
|
|
6
|
+
|
|
7
|
+
export const SyncProvider = ({ children }) => {
|
|
8
|
+
const refreshCallbacks = useRef(new Set());
|
|
9
|
+
const [isSyncEnabled, setIsSyncEnabled] = useState(false);
|
|
10
|
+
const isCheckingRef = useRef(false);
|
|
11
|
+
const lastCheckTimeRef = useRef(0);
|
|
12
|
+
const appStateRef = useRef(AppState.currentState);
|
|
13
|
+
const timeoutIdRef = useRef(null);
|
|
14
|
+
|
|
15
|
+
const registerRefreshCallback = (callback) => {
|
|
16
|
+
refreshCallbacks.current.add(callback);
|
|
17
|
+
return () => refreshCallbacks.current.delete(callback);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const triggerRefresh = () => {
|
|
21
|
+
refreshCallbacks.current.forEach(callback => callback());
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const setSync = (enabled) => {
|
|
25
|
+
setIsSyncEnabled(enabled);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!isSyncEnabled) return;
|
|
30
|
+
|
|
31
|
+
const checkForChanges = () => {
|
|
32
|
+
// Skip if already checking or app is in background
|
|
33
|
+
if (isCheckingRef.current || appStateRef.current !== 'active') {
|
|
34
|
+
timeoutIdRef.current = setTimeout(checkForChanges, 1000);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Throttle checks - don't check more than once every 2 seconds
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
if (now - lastCheckTimeRef.current < 2000) {
|
|
41
|
+
timeoutIdRef.current = setTimeout(checkForChanges, 2000 - (now - lastCheckTimeRef.current));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Use InteractionManager to wait for UI interactions to complete
|
|
46
|
+
InteractionManager.runAfterInteractions(async () => {
|
|
47
|
+
if (isCheckingRef.current) return;
|
|
48
|
+
|
|
49
|
+
isCheckingRef.current = true;
|
|
50
|
+
lastCheckTimeRef.current = Date.now();
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Use a timeout for the database query to prevent hanging
|
|
54
|
+
const queryPromise = db.execute('SELECT cloudsync_network_check_changes();');
|
|
55
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
56
|
+
setTimeout(() => reject(new Error('Query timeout')), 5000)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const result = await Promise.race([queryPromise, timeoutPromise]);
|
|
60
|
+
|
|
61
|
+
if (result.rows && result.rows.length > 0 && result.rows[0]['cloudsync_network_check_changes()'] > 0) {
|
|
62
|
+
console.log(`${result.rows[0]['cloudsync_network_check_changes()']} changes detected, triggering refresh`);
|
|
63
|
+
// Defer refresh to next tick to avoid blocking current interaction
|
|
64
|
+
setTimeout(() => triggerRefresh(), 0);
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Error checking for changes:', error);
|
|
68
|
+
} finally {
|
|
69
|
+
isCheckingRef.current = false;
|
|
70
|
+
|
|
71
|
+
// Schedule next check with adaptive interval
|
|
72
|
+
const nextInterval = appStateRef.current === 'active' ? 2500 : 10000;
|
|
73
|
+
timeoutIdRef.current = setTimeout(checkForChanges, nextInterval);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Handle app state changes
|
|
79
|
+
const handleAppStateChange = (nextAppState) => {
|
|
80
|
+
appStateRef.current = nextAppState;
|
|
81
|
+
|
|
82
|
+
// If app becomes active and sync is enabled, check immediately
|
|
83
|
+
if (nextAppState === 'active' && isSyncEnabled && !isCheckingRef.current) {
|
|
84
|
+
if (timeoutIdRef.current) {
|
|
85
|
+
clearTimeout(timeoutIdRef.current);
|
|
86
|
+
}
|
|
87
|
+
timeoutIdRef.current = setTimeout(checkForChanges, 100);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const subscription = AppState.addEventListener('change', handleAppStateChange);
|
|
92
|
+
|
|
93
|
+
// Start checking after a small delay to let UI settle
|
|
94
|
+
timeoutIdRef.current = setTimeout(checkForChanges, 1000);
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
if (timeoutIdRef.current) {
|
|
98
|
+
clearTimeout(timeoutIdRef.current);
|
|
99
|
+
timeoutIdRef.current = null;
|
|
100
|
+
}
|
|
101
|
+
isCheckingRef.current = false;
|
|
102
|
+
subscription?.remove();
|
|
103
|
+
};
|
|
104
|
+
}, [isSyncEnabled]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<SyncContext.Provider value={{ registerRefreshCallback, setSync }}>
|
|
108
|
+
{children}
|
|
109
|
+
</SyncContext.Provider>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const useSyncContext = () => {
|
|
114
|
+
const context = useContext(SyncContext);
|
|
115
|
+
if (!context) {
|
|
116
|
+
throw new Error('useSyncContext must be used within a SyncProvider');
|
|
117
|
+
}
|
|
118
|
+
return context;
|
|
119
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
|
|
3
|
+
import Icon from "react-native-vector-icons/FontAwesome";
|
|
4
|
+
import { Swipeable } from "react-native-gesture-handler";
|
|
5
|
+
|
|
6
|
+
export default TaskRow = ({ task, updateTask, handleDelete }) => {
|
|
7
|
+
const { uuid, title, isCompleted, tag_uuid, tag_name } = task;
|
|
8
|
+
const [checked, setChecked] = useState(isCompleted);
|
|
9
|
+
const swipableRef = useRef(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setChecked(isCompleted);
|
|
13
|
+
}, [isCompleted]);
|
|
14
|
+
|
|
15
|
+
const handleIconPress = () => {
|
|
16
|
+
const newCompletedStatus = checked === 1 ? 0 : 1;
|
|
17
|
+
setChecked(newCompletedStatus);
|
|
18
|
+
updateTask(newCompletedStatus, uuid);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const renderLeftActions = () => {
|
|
22
|
+
return (
|
|
23
|
+
<TouchableOpacity
|
|
24
|
+
style={styles.deleteButton}
|
|
25
|
+
onPress={() => {
|
|
26
|
+
handleDelete(uuid);
|
|
27
|
+
if (swipableRef.current) {
|
|
28
|
+
swipableRef.current.close();
|
|
29
|
+
}
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<Text style={styles.deleteButtonText}>Delete</Text>
|
|
33
|
+
</TouchableOpacity>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Swipeable renderLeftActions={renderLeftActions} ref={swipableRef}>
|
|
39
|
+
<View style={styles.taskRow}>
|
|
40
|
+
<View style={styles.taskAndTag}>
|
|
41
|
+
{checked === 0 ? (
|
|
42
|
+
<Text style={styles.text}>{title}</Text>
|
|
43
|
+
) : (
|
|
44
|
+
<Text style={styles.strikethroughText}>{title}</Text>
|
|
45
|
+
)}
|
|
46
|
+
<Text style={styles.tag}>{tag_name}</Text>
|
|
47
|
+
</View>
|
|
48
|
+
<TouchableOpacity onPress={handleIconPress}>
|
|
49
|
+
<Icon
|
|
50
|
+
name={checked === 1 ? "check-circle" : "circle-thin"}
|
|
51
|
+
size={20}
|
|
52
|
+
color={"#6BA2EA"}
|
|
53
|
+
/>
|
|
54
|
+
</TouchableOpacity>
|
|
55
|
+
</View>
|
|
56
|
+
<View style={styles.dottedBox} />
|
|
57
|
+
</Swipeable>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
taskRow: {
|
|
63
|
+
flexDirection: "row",
|
|
64
|
+
justifyContent: "space-between",
|
|
65
|
+
fontSize: 16,
|
|
66
|
+
padding: 10,
|
|
67
|
+
},
|
|
68
|
+
deleteButton: {
|
|
69
|
+
backgroundColor: "#6BA2EA",
|
|
70
|
+
padding: 10,
|
|
71
|
+
alignItems: "center",
|
|
72
|
+
justifyContent: "center",
|
|
73
|
+
},
|
|
74
|
+
dottedBox: {
|
|
75
|
+
borderWidth: 1,
|
|
76
|
+
borderColor: "lightgray",
|
|
77
|
+
borderStyle: "dashed",
|
|
78
|
+
},
|
|
79
|
+
deleteButtonText: {
|
|
80
|
+
color: "white",
|
|
81
|
+
},
|
|
82
|
+
text: {
|
|
83
|
+
fontSize: 16,
|
|
84
|
+
},
|
|
85
|
+
strikethroughText: {
|
|
86
|
+
textDecorationLine: "line-through",
|
|
87
|
+
textDecorationColor: "#6BA2EA",
|
|
88
|
+
fontSize: 16,
|
|
89
|
+
},
|
|
90
|
+
tag: {
|
|
91
|
+
color: "gray",
|
|
92
|
+
fontSize: 12,
|
|
93
|
+
marginTop: 5,
|
|
94
|
+
},
|
|
95
|
+
taskAndTag: {
|
|
96
|
+
flexDirection: "column",
|
|
97
|
+
},
|
|
98
|
+
actions: {
|
|
99
|
+
flexDirection: "row",
|
|
100
|
+
justifyContent: "center",
|
|
101
|
+
alignItems: "center"
|
|
102
|
+
}
|
|
103
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import { db } from "../db/dbConnection";
|
|
4
|
+
import { CONNECTION_STRING } from "@env";
|
|
5
|
+
import { getDylibPath } from "@op-engineering/op-sqlite";
|
|
6
|
+
import { randomUUID } from 'expo-crypto';
|
|
7
|
+
import { useSyncContext } from '../components/SyncContext';
|
|
8
|
+
|
|
9
|
+
const useCategories = () => {
|
|
10
|
+
const [moreCategories, setMoreCategories] = useState(['Work', 'Personal'])
|
|
11
|
+
const { registerRefreshCallback, setSync } = useSyncContext()
|
|
12
|
+
|
|
13
|
+
const getCategories = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const tags = await db.execute('SELECT * FROM tags')
|
|
16
|
+
const allCategories = tags.rows.map(tag => tag.name)
|
|
17
|
+
setMoreCategories(allCategories)
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('Error getting tags/categories', error)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const addCategory = async newCategory => {
|
|
24
|
+
try {
|
|
25
|
+
await db.execute('INSERT INTO tags (uuid, name) VALUES (?, ?) RETURNING *', [randomUUID(), newCategory])
|
|
26
|
+
db.execute('SELECT cloudsync_network_send_changes();')
|
|
27
|
+
setMoreCategories(prevCategories => [...prevCategories, newCategory])
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Error adding category', error)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const initializeTables = async () => {
|
|
34
|
+
let extensionPath;
|
|
35
|
+
|
|
36
|
+
console.log('Loading extension...');
|
|
37
|
+
try {
|
|
38
|
+
if (Platform.OS == "ios") {
|
|
39
|
+
extensionPath = getDylibPath("ai.sqlite.cloudsync", "CloudSync");
|
|
40
|
+
} else {
|
|
41
|
+
extensionPath = 'cloudsync';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
db.loadExtension(extensionPath);
|
|
45
|
+
|
|
46
|
+
const version = await db.execute('SELECT cloudsync_version();');
|
|
47
|
+
console.log(`Cloudsync extension loaded successfully, version: ${version.rows[0]['cloudsync_version()']}`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Error loading cloudsync extension:', error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('Initializing tables and cloudsync...');
|
|
53
|
+
try {
|
|
54
|
+
await db.execute('CREATE TABLE IF NOT EXISTS tasks (uuid TEXT NOT NULL PRIMARY KEY, title TEXT, isCompleted INT NOT NULL DEFAULT 0);')
|
|
55
|
+
await db.execute('CREATE TABLE IF NOT EXISTS tags (uuid TEXT NOT NULL PRIMARY KEY, name TEXT, UNIQUE(name));')
|
|
56
|
+
await db.execute('CREATE TABLE IF NOT EXISTS tasks_tags (uuid TEXT NOT NULL PRIMARY KEY, task_uuid TEXT, tag_uuid TEXT, FOREIGN KEY (task_uuid) REFERENCES tasks(uuid), FOREIGN KEY (tag_uuid) REFERENCES tags(uuid));')
|
|
57
|
+
|
|
58
|
+
await db.execute(`SELECT cloudsync_init('*');`);
|
|
59
|
+
await db.execute('INSERT OR IGNORE INTO tags (uuid, name) VALUES (?, ?)', [randomUUID(), 'Work'])
|
|
60
|
+
await db.execute('INSERT OR IGNORE INTO tags (uuid, name) VALUES (?, ?)', [randomUUID(), 'Personal'])
|
|
61
|
+
|
|
62
|
+
if (CONNECTION_STRING.startsWith('sqlitecloud://')) {
|
|
63
|
+
await db.execute(`SELECT cloudsync_network_init('${CONNECTION_STRING}');`);
|
|
64
|
+
} else {
|
|
65
|
+
console.warn(CONNECTION_STRING)
|
|
66
|
+
throw new Error('No valid CONNECTION_STRING provided, cloudsync_network_init will not be called');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
db.execute('SELECT cloudsync_network_sync(100, 10);')
|
|
70
|
+
getCategories()
|
|
71
|
+
setSync(true)
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Error creating tables', error)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
initializeTables()
|
|
79
|
+
}, [])
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
return registerRefreshCallback(() => {
|
|
83
|
+
getCategories();
|
|
84
|
+
});
|
|
85
|
+
}, [registerRefreshCallback])
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
moreCategories,
|
|
89
|
+
addCategory,
|
|
90
|
+
getCategories
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default useCategories
|