diff --git a/README.md b/README.md
index dea8b21..46a63c7 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ npm run dev:android
# Web App Screenshot
-
+
# Run it on the web
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 1240fca..c2569bb 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,52 +1,28 @@
import { Slot } from 'expo-router';
import { SafeAreaView } from 'react-native-safe-area-context';
-import { Button, KeyboardAvoidingView, Platform, StyleSheet, View, Text } from 'react-native';
+import { KeyboardAvoidingView, Platform, StyleSheet } from 'react-native';
import React from 'react';
import AppDataProvider from '@/data/providers/AppDataProvider';
-import { PersistenceType } from '@/data/types';
+import store from '@/store';
+import { Provider as ReduxProvider } from 'react-redux';
-const enabledPersistenceTypes = Platform.select({
- web: [PersistenceType.localstorage, PersistenceType.indexedDB, PersistenceType.sqlite],
- default: [PersistenceType.localstorage, PersistenceType.sqlite],
-});
const Root = () => {
- const [persistenceType, setPersistenceType] = React.useState(
- Platform.select({ web: PersistenceType.indexedDB, default: PersistenceType.sqlite })
- );
-
return (
-
-
- Persistence type: {persistenceType}, OS: {Platform.OS}
-
-
- {enabledPersistenceTypes.map((persistenceType) => (
-
-
-
+
+
+
+
+
);
};
const styles = StyleSheet.create({
- buttons: {
- flexDirection: 'row',
- justifyContent: 'space-around',
- alignSelf: 'center',
- width: Platform.select({ web: '50%', default: '100%' }),
- padding: 10,
- },
container: {
flex: 1,
backgroundColor: '#f8f8f8',
@@ -54,11 +30,6 @@ const styles = StyleSheet.create({
keyboardView: {
flex: 1,
},
- title: {
- textAlign: 'center',
- fontSize: 20,
- padding: 10,
- },
});
export default Root;
diff --git a/app/detail/[id].tsx b/app/detail/[id].tsx
index 3500bdc..4e47170 100644
--- a/app/detail/[id].tsx
+++ b/app/detail/[id].tsx
@@ -5,25 +5,18 @@ import { Button, StyleSheet, Text, View } from 'react-native';
import { useDataContext } from '@/data/DataContext';
import { useAppDispatch, useAppSelector } from '@/store';
import { removeTaskHandler, selectTask } from '@/store/taskSlice';
+import Header from '@/Header';
-const Page = () => {
+const Page: React.FC = () => {
const { id } = useLocalSearchParams<{ id: string }>();
const decodedId = parseInt(id);
const { tasksClient } = useDataContext();
const dispatch = useAppDispatch();
const task = useAppSelector(selectTask(decodedId));
- const goBack = () => {
- if (router.canGoBack()) {
- router.back();
- } else {
- router.push('/');
- }
- };
-
const handleDelete = async () => {
dispatch(removeTaskHandler({ tasksClient, id: decodedId }));
- goBack();
+ router.push('/');
};
if (!task) {
@@ -31,12 +24,9 @@ const Page = () => {
}
return (
-
-
-
+
Task: {task.task}
diff --git a/app/index.tsx b/app/index.tsx
index 57db551..196f109 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -1,23 +1,24 @@
-import { FlatList, Text, TextInput, TouchableOpacity, View } from 'react-native';
+import { FlatList, Text, TextInput, TouchableOpacity } from 'react-native';
import React, { useState } from 'react';
import { Task } from '@/types';
-import logger from '@/logger';
import type { ListRenderItem } from '@react-native/virtualized-lists';
import { router } from 'expo-router';
import globalStyles from '@/globalStyles';
import { useDataContext } from '@/data/DataContext';
import { useAppDispatch, useAppSelector } from '@/store';
import { addTaskHandler, selectTasksState } from '@/store/taskSlice';
+import Header from '@/Header';
+import Icon from '@expo/vector-icons/MaterialCommunityIcons';
-const LandingPage = () => {
- const { tasksClient } = useDataContext();
+const LandingPage: React.FC = () => {
+ const { taskClient } = useDataContext();
const dispatch = useAppDispatch();
const [newTask, setNewTask] = useState('');
const tasks = useAppSelector(selectTasksState);
const addTask = async () => {
if (newTask.trim()) {
- dispatch(addTaskHandler({ taskName: newTask, tasksClient }));
+ dispatch(addTaskHandler({ taskName: newTask, taskClient }));
setNewTask('');
}
};
@@ -35,9 +36,20 @@ const LandingPage = () => {
data={tasks}
keyExtractor={(item) => item.id.toString()}
renderItem={renderItem}
- style={globalStyles.taskList}
+ style={globalStyles.root}
ListHeaderComponent={
-
+
+ router.push('/settings')}>
+
+
+
+
{
onChangeText={setNewTask}
/>
- Add
+
-
+
}
/>
);
diff --git a/app/settings.tsx b/app/settings.tsx
new file mode 100644
index 0000000..82eb5b8
--- /dev/null
+++ b/app/settings.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { View, Text, StyleSheet, Platform, Button } from 'react-native';
+import { enabledPersistenceTypes } from '@/config';
+import { useAppDispatch, useAppSelector } from '@/store';
+import { changePersistence, selectedPersistence } from '@/store/settingsSlice';
+import Header from '@/Header';
+import globalStyles from '@/globalStyles';
+import { clearData } from '@/store/globalReset';
+import { useDataContext } from '@/data/DataContext';
+
+const SettingsPage = () => {
+ const dispatch = useAppDispatch();
+ const persistenceType = useAppSelector(selectedPersistence);
+ const { opsClient } = useDataContext();
+
+ return (
+
+
+
+
+ Persistence type: {persistenceType}, OS: {Platform.OS}
+
+ {enabledPersistenceTypes.map((value) => (
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ settingRow: {
+ alignItems: 'flex-start',
+ gap: 5,
+ padding: 16,
+ },
+});
+
+export default SettingsPage;
diff --git a/package-lock.json b/package-lock.json
index faa0c20..925c950 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"@expo/metro-runtime": "~3.2.3",
+ "@expo/vector-icons": "^14.0.3",
"@react-native-async-storage/async-storage": "^2.1.0",
"@reduxjs/toolkit": "^2.4.0",
"eslint-config-prettier": "^9.1.0",
@@ -16,8 +17,11 @@
"expo": "^51.0.34",
"expo-constants": "~16.0.2",
"expo-dev-client": "~4.0.29",
+ "expo-document-picker": "~12.0.2",
+ "expo-file-system": "~17.0.1",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.23",
+ "expo-sharing": "~12.0.1",
"expo-sqlite": "~14.0.6",
"expo-status-bar": "~1.12.1",
"idb": "^8.0.0",
@@ -10791,6 +10795,14 @@
"expo": "*"
}
},
+ "node_modules/expo-document-picker": {
+ "version": "12.0.2",
+ "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-12.0.2.tgz",
+ "integrity": "sha512-tmwuRWoCPv6SmNDSMEWcttMBJ95k8/g5sMWnHdmvOx0UKp0pFXP8FI+55HKtQpo6k2+118MkdDDhQSwKqASVAw==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-file-system": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-17.0.1.tgz",
@@ -11003,6 +11015,14 @@
}
}
},
+ "node_modules/expo-sharing": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-12.0.1.tgz",
+ "integrity": "sha512-wBT+WeXwapj/9NWuLJO01vi9bdlchYu/Q/xD8slL/Ls4vVYku8CPqzkTtDFcjLrjtlJqyeHsdQXwKLvORmBIew==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-splash-screen": {
"version": "0.27.5",
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.27.5.tgz",
@@ -17757,9 +17777,9 @@
}
},
"node_modules/picocolors": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
- "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
"version": "3.0.1",
diff --git a/package.json b/package.json
index f8cd856..42ab5e2 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"eslint-plugin-prettier": "^5.2.1",
"expo": "^51.0.34",
"expo-constants": "~16.0.2",
+ "expo-dev-client": "~4.0.29",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.23",
"expo-sqlite": "~14.0.6",
@@ -40,7 +41,10 @@
"redux-devtools-expo-dev-plugin": "^0.2.1",
"semver": "^7.6.3",
"sql.js": "^1.12.0",
- "expo-dev-client": "~4.0.29"
+ "@expo/vector-icons": "^14.0.3",
+ "expo-document-picker": "~12.0.2",
+ "expo-file-system": "~17.0.1",
+ "expo-sharing": "~12.0.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
diff --git a/preview.png b/preview.png
index bb89d32..54d0c0a 100644
Binary files a/preview.png and b/preview.png differ
diff --git a/src/DbMigrationRunner.spec.ts b/src/DbMigrationRunner.spec.ts
index 2fc8013..1e3c74a 100644
--- a/src/DbMigrationRunner.spec.ts
+++ b/src/DbMigrationRunner.spec.ts
@@ -1,12 +1,13 @@
import { SQLiteDatabase, openDatabaseAsync } from '@/data/sqliteDatabase';
import DbMigrationRunner from '@/DbMigrationRunner';
import migrations from '../migrations';
+import { dbName } from '@/config';
describe('DbMigrationRunner', () => {
let sqlite: SQLiteDatabase;
beforeEach(async () => {
- sqlite = await openDatabaseAsync('test.db');
+ sqlite = await openDatabaseAsync(dbName);
});
it('should migrate', async () => {
diff --git a/src/Header.tsx b/src/Header.tsx
new file mode 100644
index 0000000..1751e02
--- /dev/null
+++ b/src/Header.tsx
@@ -0,0 +1,46 @@
+import { View, StyleSheet, TouchableOpacity } from 'react-native';
+import React from 'react';
+import { router } from 'expo-router';
+import globalStyles from '@/globalStyles';
+import Icon from '@expo/vector-icons/MaterialCommunityIcons';
+
+const Header: React.FC<{ children: React.ReactNode; showBack?: boolean }> = ({ children, showBack }) => {
+ return (
+
+ {showBack && (
+ {
+ if (router.canGoBack()) {
+ router.back();
+ } else {
+ router.push('/');
+ }
+ }}>
+
+
+ )}
+ {children}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ root: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ height: 60,
+ paddingVertical: 12,
+ backgroundColor: '#fff',
+ borderTopWidth: 1,
+ borderTopColor: '#e0e0e0',
+ gap: 5,
+ },
+});
+
+export default Header;
diff --git a/src/clients/IndexedDBOpsClient.ts b/src/clients/IndexedDBOpsClient.ts
new file mode 100644
index 0000000..fd5fe2e
--- /dev/null
+++ b/src/clients/IndexedDBOpsClient.ts
@@ -0,0 +1,24 @@
+import { OpsClient } from '@/clients/types';
+import { IDBPDatabase } from 'idb';
+import { IndexedDBSchema } from '@/data/types';
+
+class IndexedDBOpsClient implements OpsClient {
+ constructor(private db: IDBPDatabase) {}
+
+ async clear(): Promise {
+ const tx = this.db.transaction(this.db.objectStoreNames, 'readwrite');
+
+ const clearedStores = [...this.db.objectStoreNames].map((it) => tx.objectStore(it).clear());
+ await Promise.all([...clearedStores, tx.done]);
+ }
+
+ backup(): Promise {
+ throw new Error('Method not implemented.');
+ }
+
+ restore(): Promise {
+ throw new Error('Method not implemented.');
+ }
+}
+
+export default IndexedDBOpsClient;
diff --git a/src/taskClient/IndexedDBClient.ts b/src/clients/IndexedDBTaskClient.ts
similarity index 88%
rename from src/taskClient/IndexedDBClient.ts
rename to src/clients/IndexedDBTaskClient.ts
index d82a005..fe6d04e 100644
--- a/src/taskClient/IndexedDBClient.ts
+++ b/src/clients/IndexedDBTaskClient.ts
@@ -1,4 +1,4 @@
-import { TaskClient } from '@/taskClient/types';
+import { TaskClient } from '@/clients/types';
import { Task } from '@/types';
import { IDBPDatabase } from 'idb';
import { IndexedDBSchema } from '@/data/types';
@@ -9,7 +9,7 @@ const toTask = (dbObject: IndexedDBSchema['tasks']['value']): Task => ({
updatedAt: new Date(dbObject.updatedAt),
});
-class IndexedDBClient implements TaskClient {
+class IndexedDBTaskClient implements TaskClient {
constructor(private db: IDBPDatabase) {}
async add(taskName: string): Promise {
@@ -42,4 +42,4 @@ class IndexedDBClient implements TaskClient {
}
}
-export default IndexedDBClient;
+export default IndexedDBTaskClient;
diff --git a/src/clients/LocalStorageOpsClient.ts b/src/clients/LocalStorageOpsClient.ts
new file mode 100644
index 0000000..0a42b8d
--- /dev/null
+++ b/src/clients/LocalStorageOpsClient.ts
@@ -0,0 +1,16 @@
+import { OpsClient } from '@/clients/types';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+class LocalStorageOpsClient implements OpsClient {
+ async clear(): Promise {
+ await AsyncStorage.clear();
+ }
+ backup(): Promise {
+ throw new Error('Method not implemented.');
+ }
+ restore(): Promise {
+ throw new Error('Method not implemented.');
+ }
+}
+
+export default LocalStorageOpsClient;
diff --git a/src/taskClient/LocalStorageTaskClient.ts b/src/clients/LocalStorageTaskClient.ts
similarity index 96%
rename from src/taskClient/LocalStorageTaskClient.ts
rename to src/clients/LocalStorageTaskClient.ts
index 3c6ed59..7907537 100644
--- a/src/taskClient/LocalStorageTaskClient.ts
+++ b/src/clients/LocalStorageTaskClient.ts
@@ -1,4 +1,4 @@
-import { TaskClient } from '@/taskClient/types';
+import { TaskClient } from '@/clients/types';
import { Task } from '@/types';
import AsyncStorage from '@react-native-async-storage/async-storage';
diff --git a/src/clients/SQLiteOpsClient.ts b/src/clients/SQLiteOpsClient.ts
new file mode 100644
index 0000000..03f000f
--- /dev/null
+++ b/src/clients/SQLiteOpsClient.ts
@@ -0,0 +1,29 @@
+import { OpsClient } from '@/clients/types';
+import { SQLiteDatabase } from '@/data/sqliteDatabase';
+
+class SQLiteOpsClient implements OpsClient {
+ constructor(private db: SQLiteDatabase) {}
+
+ async clear(): Promise {
+ const tables = await this.db.getAllAsync<{ name: string }>(`SELECT name FROM sqlite_master WHERE type='table';`);
+ const deleteStatements = tables.map((table) => `DELETE FROM ${table.name};`).join('\n');
+ await this.db.execAsync(`
+PRAGMA foreign_keys=off;
+BEGIN TRANSACTION;
+
+DELETE FROM sqlite_sequence;
+
+${deleteStatements}
+
+PRAGMA foreign_keys=on;
+COMMIT;`);
+ }
+ async backup(): Promise {
+ throw new Error('Method not implemented.');
+ }
+ async restore(): Promise {
+ throw new Error('Method not implemented.');
+ }
+}
+
+export default SQLiteOpsClient;
diff --git a/src/taskClient/SQLiteTaskClient.spec.ts b/src/clients/SQLiteTaskClient.spec.ts
similarity index 86%
rename from src/taskClient/SQLiteTaskClient.spec.ts
rename to src/clients/SQLiteTaskClient.spec.ts
index 98a6069..6861814 100644
--- a/src/taskClient/SQLiteTaskClient.spec.ts
+++ b/src/clients/SQLiteTaskClient.spec.ts
@@ -1,13 +1,14 @@
-import SQLiteTaskClient from '@/taskClient/SQLiteTaskClient';
+import SQLiteTaskClient from '@/clients/SQLiteTaskClient';
import DbMigrationRunner from '@/DbMigrationRunner';
import migration1 from '../../migrations/001_initial';
import { SQLiteDatabase, openDatabaseAsync } from '@/data/sqliteDatabase';
+import { dbName } from '@/config';
describe('SQLiteTaskClient', () => {
let sqlite: SQLiteDatabase;
beforeEach(async () => {
- sqlite = await openDatabaseAsync('test.db');
+ sqlite = await openDatabaseAsync(dbName);
await new DbMigrationRunner(sqlite).apply([migration1]);
});
diff --git a/src/taskClient/SQLiteTaskClient.ts b/src/clients/SQLiteTaskClient.ts
similarity index 96%
rename from src/taskClient/SQLiteTaskClient.ts
rename to src/clients/SQLiteTaskClient.ts
index e0fc0ed..c464e1e 100644
--- a/src/taskClient/SQLiteTaskClient.ts
+++ b/src/clients/SQLiteTaskClient.ts
@@ -1,5 +1,5 @@
import { Task } from '@/types';
-import { TaskClient } from '@/taskClient/types';
+import { TaskClient } from '@/clients/types';
import { SQLiteDatabase } from '@/data/sqliteDatabase';
class SQLiteTaskClient implements TaskClient {
diff --git a/src/clients/types.ts b/src/clients/types.ts
new file mode 100644
index 0000000..50c2cff
--- /dev/null
+++ b/src/clients/types.ts
@@ -0,0 +1,18 @@
+import { Task } from '@/types';
+
+export interface TaskClient {
+ tasks(): Promise;
+ task(id: number): Promise;
+ add(task: string): Promise;
+ delete(id: number): Promise;
+}
+
+/**
+ * OpsClient provides a set of operational utilities
+ * for managing system persistence.
+ */
+export interface OpsClient {
+ clear(): Promise;
+ backup(): Promise;
+ restore(): Promise;
+}
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..f526675
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,8 @@
+import { Platform } from 'react-native';
+import { PersistenceType } from '@/data/types';
+
+export const dbName = 'test.db';
+export const enabledPersistenceTypes = Platform.select({
+ web: [PersistenceType.localstorage, PersistenceType.indexedDB, PersistenceType.sqlite],
+ default: [PersistenceType.localstorage, PersistenceType.sqlite],
+});
diff --git a/src/data/DataContext.ts b/src/data/DataContext.ts
index 732931e..26c8a6f 100644
--- a/src/data/DataContext.ts
+++ b/src/data/DataContext.ts
@@ -1,11 +1,7 @@
-import { TaskClient } from '@/taskClient/types';
import React from 'react';
+import { DataContextValue } from '@/data/types';
-type Value = {
- tasksClient: TaskClient;
-};
-
-export const DataContext = React.createContext(null);
+export const DataContext = React.createContext(null);
export const useDataContext = () => {
const context = React.useContext(DataContext);
diff --git a/src/data/providers/AppDataProvider.tsx b/src/data/providers/AppDataProvider.tsx
index 8e250c5..4b10668 100644
--- a/src/data/providers/AppDataProvider.tsx
+++ b/src/data/providers/AppDataProvider.tsx
@@ -1,13 +1,13 @@
-import { DataProviderProps, PersistenceType } from '@/data/types';
+import { DataContextValue, DataProviderProps, PersistenceType } from '@/data/types';
import React from 'react';
import LocalStorageDataProvider from '@/data/providers/LocalStorageDataProvider';
import SQLiteDataProvider from '@/data/providers/SQLiteDataProvider';
import { DataContext } from '@/data/DataContext';
import IndexedDBDataProvider from '@/data/providers/IndexedDBDataProvider';
-import { Provider as ReduxProvider } from 'react-redux';
-import store from '@/store';
-import { TaskClient } from '@/taskClient/types';
+
+import { useAppDispatch, useAppSelector } from '@/store';
import { initializeTasks } from '@/store/taskSlice';
+import { selectedPersistence } from '@/store/settingsSlice';
const PersistenceProviderWrapper: React.FC = ({
persistenceType,
@@ -22,28 +22,32 @@ const PersistenceProviderWrapper: React.FC;
};
-const DataContextProvider: React.FC> = ({
- tasksClient,
+const DataContextProvider: React.FC> = ({
+ taskClient,
+ opsClient,
children,
}) => {
+ const dispatch = useAppDispatch();
React.useEffect(() => {
- store.dispatch(initializeTasks(tasksClient));
- }, [tasksClient]);
+ dispatch(initializeTasks(taskClient));
+ }, [taskClient]);
- return (
-
- {children}
-
- );
+ return {children};
};
const AppDataProvider: React.FC<{
children: React.ReactNode;
- persistenceType: PersistenceType;
-}> = ({ children, persistenceType }) => {
+}> = ({ children }) => {
+ const persistenceType = useAppSelector(selectedPersistence);
return (
- {(props) => {children}}
+ {(props) => (
+
+ {children}
+
+ )}
);
};
diff --git a/src/data/providers/IndexedDBDataProvider.tsx b/src/data/providers/IndexedDBDataProvider.tsx
index 48b9a5a..43d0269 100644
--- a/src/data/providers/IndexedDBDataProvider.tsx
+++ b/src/data/providers/IndexedDBDataProvider.tsx
@@ -1,8 +1,9 @@
import { DataProviderProps, IndexedDBSchema } from '@/data/types';
import React from 'react';
-import IndexedDBClient from '@/taskClient/IndexedDBClient';
+import IndexedDBTaskClient from '@/clients/IndexedDBTaskClient';
import { IDBPDatabase, openDB } from 'idb';
import logger from '@/logger';
+import IndexedDBOpsClient from '@/clients/IndexedDBOpsClient';
const IndexedDBDataProvider: React.FC = ({ children }) => {
const [db, setDb] = React.useState | null>(null);
@@ -35,7 +36,7 @@ const IndexedDBDataProvider: React.FC = ({ children }) => {
return null;
}
- return <>{children({ taskClient: new IndexedDBClient(db) })}>;
+ return <>{children({ taskClient: new IndexedDBTaskClient(db), opsClient: new IndexedDBOpsClient(db) })}>;
};
export default IndexedDBDataProvider;
diff --git a/src/data/providers/LocalStorageDataProvider.tsx b/src/data/providers/LocalStorageDataProvider.tsx
index 1ff7f24..74281bd 100644
--- a/src/data/providers/LocalStorageDataProvider.tsx
+++ b/src/data/providers/LocalStorageDataProvider.tsx
@@ -1,9 +1,10 @@
import { DataProviderProps } from '@/data/types';
import React from 'react';
-import LocalStorageTaskClient from '@/taskClient/LocalStorageTaskClient';
+import LocalStorageTaskClient from '@/clients/LocalStorageTaskClient';
+import LocalStorageOpsClient from '@/clients/LocalStorageOpsClient';
const LocalStorageDataProvider: React.FC = ({ children }) => {
- return <>{children({ taskClient: new LocalStorageTaskClient() })}>;
+ return <>{children({ taskClient: new LocalStorageTaskClient(), opsClient: new LocalStorageOpsClient() })}>;
};
export default LocalStorageDataProvider;
diff --git a/src/data/providers/SQLiteDataProvider.tsx b/src/data/providers/SQLiteDataProvider.tsx
index 8fcbe22..f7e0484 100644
--- a/src/data/providers/SQLiteDataProvider.tsx
+++ b/src/data/providers/SQLiteDataProvider.tsx
@@ -3,9 +3,11 @@ import migrations from '../../../migrations';
import logger from '@/logger';
import React from 'react';
import { DataProviderProps } from '@/data/types';
-import SQLiteTaskClient from '@/taskClient/SQLiteTaskClient';
-import SQLiteProvider from '@/SQLiteProvider';
+import SQLiteTaskClient from '@/clients/SQLiteTaskClient';
+import SQLiteProvider from '@/data/providers/SQLiteProvider';
import { SQLiteDatabase } from '@/data/sqliteDatabase';
+import { dbName } from '@/config';
+import SQLiteOpsClient from '@/clients/SQLiteOpsClient';
const SQLiteDataProvider: React.FC = ({ children }) => {
const [db, setDb] = React.useState(null);
@@ -22,9 +24,9 @@ const SQLiteDataProvider: React.FC = ({ children }) => {
return (
- {!!db && children({ taskClient: new SQLiteTaskClient(db) })}
+ {!!db && children({ taskClient: new SQLiteTaskClient(db), opsClient: new SQLiteOpsClient(db) })}
);
};
diff --git a/src/SQLiteProvider.tsx b/src/data/providers/SQLiteProvider.tsx
similarity index 96%
rename from src/SQLiteProvider.tsx
rename to src/data/providers/SQLiteProvider.tsx
index 47cf616..ec2e1de 100644
--- a/src/SQLiteProvider.tsx
+++ b/src/data/providers/SQLiteProvider.tsx
@@ -2,9 +2,6 @@ import React, { createContext, useEffect, useState } from 'react';
import logger from '@/logger';
import { SQLiteDatabase, openDatabaseAsync } from '@/data/sqliteDatabase';
-/**
- * Create a context for the SQLite database
- */
const SQLiteContext = createContext(null);
function SQLiteProvider({
databaseName,
diff --git a/src/data/types.ts b/src/data/types.ts
index adb8193..a684ff8 100644
--- a/src/data/types.ts
+++ b/src/data/types.ts
@@ -1,4 +1,4 @@
-import { TaskClient } from '@/taskClient/types';
+import { OpsClient, TaskClient } from '@/clients/types';
import React from 'react';
export enum PersistenceType {
@@ -7,8 +7,13 @@ export enum PersistenceType {
localstorage = 'localstorage',
}
+export type DataContextValue = {
+ taskClient: TaskClient;
+ opsClient: OpsClient;
+};
+
export type DataProviderProps = {
- children: (props: { taskClient: TaskClient }) => React.ReactNode;
+ children: (props: DataContextValue) => React.ReactNode;
};
export type IndexedDBSchema = {
diff --git a/src/globalStyles.ts b/src/globalStyles.ts
index e51e442..98004a0 100644
--- a/src/globalStyles.ts
+++ b/src/globalStyles.ts
@@ -1,7 +1,7 @@
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
- taskList: {
+ root: {
flex: 1,
},
taskItem: {
@@ -29,24 +29,39 @@ const styles = StyleSheet.create({
},
input: {
flex: 1,
- height: 40,
borderColor: '#ddd',
borderWidth: 1,
paddingHorizontal: 8,
borderRadius: 5,
- marginRight: 8,
+ height: '100%',
},
- addButton: {
+ divider: {
+ borderBottomColor: '#1e90ff',
+ borderBottomWidth: 1,
+ width: '100%',
+ opacity: 0.1,
+ padding: 5,
+ },
+ button: {
backgroundColor: '#1e90ff',
paddingHorizontal: 16,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 5,
+ height: '100%',
},
- addButtonText: {
+ buttonText: {
color: '#fff',
fontWeight: 'bold',
},
+ icon: {
+ color: '#fff',
+ fontSize: 20,
+ },
+ headerText: {
+ fontSize: 20,
+ paddingHorizontal: 8,
+ },
});
export default styles;
diff --git a/src/store/globalReset.ts b/src/store/globalReset.ts
new file mode 100644
index 0000000..4214a1b
--- /dev/null
+++ b/src/store/globalReset.ts
@@ -0,0 +1,13 @@
+import { createAction, createAsyncThunk } from '@reduxjs/toolkit';
+import { OpsClient } from '@/clients/types';
+
+export const resetState = createAction('app/reset');
+
+export const clearData = createAsyncThunk(
+ 'app/clearData',
+ async ({ opsClient }: { opsClient: OpsClient }, thunkApi) => {
+ console.log('clean');
+ await opsClient.clear();
+ thunkApi.dispatch(resetState());
+ }
+);
diff --git a/src/store/index.ts b/src/store/index.ts
index 7561227..019776a 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,11 +1,14 @@
import { combineReducers, configureStore, Middleware, PayloadAction } from '@reduxjs/toolkit';
import tasksSlice from './taskSlice';
+import settingsSlice from './settingsSlice';
import { useDispatch, useSelector } from 'react-redux';
import logger from '@/logger';
import devToolsEnhancer from 'redux-devtools-expo-dev-plugin';
+import { Platform } from 'react-native';
const rootReducer = combineReducers({
[tasksSlice.name]: tasksSlice.reducer,
+ [settingsSlice.name]: settingsSlice.reducer,
});
export type RootState = ReturnType;
@@ -21,7 +24,7 @@ if (__DEV__) {
const enhancers: any[] = [];
-if (__DEV__) {
+if (__DEV__ && Platform.OS !== 'web') {
enhancers.push(devToolsEnhancer());
}
diff --git a/src/store/settingsSlice.ts b/src/store/settingsSlice.ts
new file mode 100644
index 0000000..8f6fd04
--- /dev/null
+++ b/src/store/settingsSlice.ts
@@ -0,0 +1,27 @@
+import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
+import { Settings } from '@/types';
+import { PersistenceType } from '@/data/types';
+import { Platform } from 'react-native';
+import { RootState } from '@/store/index';
+import { resetState } from './globalReset';
+
+const initialState: Settings = {
+ persistenceType: Platform.select({ web: PersistenceType.indexedDB, default: PersistenceType.sqlite }),
+};
+const settingsSlice = createSlice({
+ initialState,
+ name: 'settings',
+ reducers: {
+ changePersistence: (state, action: PayloadAction<{ value: PersistenceType }>) => {
+ return { ...state, persistenceType: action.payload.value };
+ },
+ },
+ extraReducers: (builder) => {
+ builder.addCase(resetState, () => initialState);
+ },
+});
+
+export const { changePersistence } = settingsSlice.actions;
+export const selectSettings = (state: RootState) => state.settings;
+export const selectedPersistence = createSelector(selectSettings, (it) => it.persistenceType);
+export default settingsSlice;
diff --git a/src/store/taskSlice.ts b/src/store/taskSlice.ts
index 9926125..0ef5e4a 100644
--- a/src/store/taskSlice.ts
+++ b/src/store/taskSlice.ts
@@ -1,7 +1,8 @@
import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { TaskClient } from '@/taskClient/types';
+import { TaskClient } from '@/clients/types';
import { Task } from '@/types';
import { RootState } from '@/store/index';
+import { resetState } from '@/store/globalReset';
const taskSlice = createSlice({
initialState: [],
@@ -17,6 +18,9 @@ const taskSlice = createSlice({
return state.filter((task) => task.id !== action.payload.id);
},
},
+ extraReducers: (builder) => {
+ builder.addCase(resetState, () => []);
+ },
});
const { initialize, addTask, removeTask } = taskSlice.actions;
@@ -32,16 +36,16 @@ const initializeTasks = createAsyncThunk('tasks/initialize', async (taskClient:
const addTaskHandler = createAsyncThunk(
'tasks/add',
- async ({ tasksClient, taskName }: { tasksClient: TaskClient; taskName: string }, thunkApi) => {
- const task = await tasksClient.add(taskName);
+ async ({ taskClient, taskName }: { taskClient: TaskClient; taskName: string }, thunkApi) => {
+ const task = await taskClient.add(taskName);
thunkApi.dispatch(addTask({ task }));
}
);
const removeTaskHandler = createAsyncThunk(
'tasks/remove',
- async ({ tasksClient, id }: { tasksClient: TaskClient; id: Task['id'] }, thunkApi) => {
- await tasksClient.delete(id);
+ async ({ taskClient, id }: { taskClient: TaskClient; id: Task['id'] }, thunkApi) => {
+ await taskClient.delete(id);
thunkApi.dispatch(removeTask({ id }));
}
);
diff --git a/src/taskClient/types.ts b/src/taskClient/types.ts
deleted file mode 100644
index e7a7bd4..0000000
--- a/src/taskClient/types.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Task } from '@/types';
-
-export interface TaskClient {
- tasks(): Promise;
- task(id: number): Promise;
- add(task: string): Promise;
- delete(id: number): Promise;
-}
diff --git a/src/types.ts b/src/types.ts
index d9c0d2e..6341d7d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,4 +1,5 @@
import { SQLiteDatabase } from '@/data/sqliteDatabase';
+import { PersistenceType } from '@/data/types';
export type UserVersion = {
user_version: number;
@@ -15,3 +16,7 @@ export type Task = {
createdAt: Date;
updatedAt: Date;
};
+
+export type Settings = {
+ persistenceType: PersistenceType;
+};