expotesting2 4.1.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.
Files changed (77) hide show
  1. package/README.md +289 -0
  2. package/apps/expo-app/app.json +60 -0
  3. package/apps/expo-app/babel.config.js +7 -0
  4. package/apps/expo-app/index.js +6 -0
  5. package/apps/expo-app/package.json +46 -0
  6. package/apps/expo-app/src/App.jsx +37 -0
  7. package/apps/expo-app/src/navigation/RootNavigator.jsx +82 -0
  8. package/apps/expo-app/src/navigation/types.js +5 -0
  9. package/apps/expo-app/src/screens/HomeScreen.jsx +178 -0
  10. package/package.json +24 -0
  11. package/packages/animations/package.json +20 -0
  12. package/packages/animations/src/components/FadeView.jsx +42 -0
  13. package/packages/animations/src/components/ScaleView.jsx +28 -0
  14. package/packages/animations/src/components/SlideView.jsx +32 -0
  15. package/packages/animations/src/hooks/useFade.js +50 -0
  16. package/packages/animations/src/hooks/useScale.js +59 -0
  17. package/packages/animations/src/hooks/useSlide.js +53 -0
  18. package/packages/animations/src/index.js +21 -0
  19. package/packages/animations/src/reanimated.js +83 -0
  20. package/packages/core/package.json +22 -0
  21. package/packages/core/src/components/Button.jsx +92 -0
  22. package/packages/core/src/components/Card.jsx +47 -0
  23. package/packages/core/src/components/Container.jsx +61 -0
  24. package/packages/core/src/components/Input.jsx +83 -0
  25. package/packages/core/src/components/List.jsx +80 -0
  26. package/packages/core/src/components/index.js +9 -0
  27. package/packages/core/src/hooks/index.js +5 -0
  28. package/packages/core/src/hooks/useAsync.js +60 -0
  29. package/packages/core/src/hooks/useCounter.js +36 -0
  30. package/packages/core/src/hooks/useToggle.js +18 -0
  31. package/packages/core/src/index.js +5 -0
  32. package/packages/core/src/theme/index.js +67 -0
  33. package/packages/core/src/utils/helpers.js +93 -0
  34. package/packages/core/src/utils/index.js +10 -0
  35. package/packages/device/package.json +24 -0
  36. package/packages/device/src/hooks/useCamera.js +45 -0
  37. package/packages/device/src/hooks/useGallery.js +70 -0
  38. package/packages/device/src/hooks/useLocation.js +99 -0
  39. package/packages/device/src/index.js +5 -0
  40. package/packages/examples/package.json +36 -0
  41. package/packages/examples/src/experiments/animations-device/AnimationsDeviceScreen.jsx +291 -0
  42. package/packages/examples/src/experiments/basic-app/BasicAppScreen.jsx +162 -0
  43. package/packages/examples/src/experiments/components-props-state/ComponentsStateScreen.jsx +280 -0
  44. package/packages/examples/src/experiments/navigation/NavigationScreen.jsx +202 -0
  45. package/packages/examples/src/experiments/network-storage/NetworkStorageScreen.jsx +367 -0
  46. package/packages/examples/src/experiments/state-management/StateManagementScreen.jsx +255 -0
  47. package/packages/examples/src/index.js +76 -0
  48. package/packages/navigation/package.json +20 -0
  49. package/packages/navigation/src/DrawerNavigator.jsx +35 -0
  50. package/packages/navigation/src/StackNavigator.jsx +51 -0
  51. package/packages/navigation/src/TabNavigator.jsx +44 -0
  52. package/packages/navigation/src/createAppNavigator.jsx +48 -0
  53. package/packages/navigation/src/index.js +8 -0
  54. package/packages/navigation/src/types.js +18 -0
  55. package/packages/network/package.json +19 -0
  56. package/packages/network/src/apiClient.js +90 -0
  57. package/packages/network/src/fetchHelpers.js +97 -0
  58. package/packages/network/src/hooks/useFetch.js +56 -0
  59. package/packages/network/src/index.js +3 -0
  60. package/packages/network/src/types.js +4 -0
  61. package/packages/state/package.json +22 -0
  62. package/packages/state/src/context/AuthContext.jsx +94 -0
  63. package/packages/state/src/context/ThemeContext.jsx +79 -0
  64. package/packages/state/src/context/index.js +3 -0
  65. package/packages/state/src/index.js +5 -0
  66. package/packages/state/src/redux/hooks.js +12 -0
  67. package/packages/state/src/redux/index.js +7 -0
  68. package/packages/state/src/redux/slices/counterSlice.js +39 -0
  69. package/packages/state/src/redux/slices/postsSlice.js +92 -0
  70. package/packages/state/src/redux/store.js +32 -0
  71. package/packages/storage/package.json +24 -0
  72. package/packages/storage/src/asyncStorage.js +82 -0
  73. package/packages/storage/src/index.js +2 -0
  74. package/packages/storage/src/sqlite/database.js +65 -0
  75. package/packages/storage/src/sqlite/index.js +3 -0
  76. package/packages/storage/src/sqlite/operations.js +112 -0
  77. package/packages/storage/src/sqlite/useSQLite.js +45 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * ThemeContext — provides light/dark theme throughout the app.
3
+ *
4
+ * Usage:
5
+ * 1. Wrap your app in <ThemeProvider>
6
+ * 2. Consume with useTheme()
7
+ *
8
+ * @example
9
+ * function App() {
10
+ * return (
11
+ * <ThemeProvider>
12
+ * <MainNavigator />
13
+ * </ThemeProvider>
14
+ * );
15
+ * }
16
+ *
17
+ * function Screen() {
18
+ * const { colorScheme, toggleTheme, colors } = useTheme();
19
+ * return <View style={{ backgroundColor: colors.background }} />;
20
+ * }
21
+ */
22
+
23
+ import React, {
24
+ createContext,
25
+ useCallback,
26
+ useContext,
27
+ useMemo,
28
+ useState } from 'react';
29
+
30
+ const lightColors= {
31
+ background: '#FFFFFF',
32
+ surface: '#F5F5F5',
33
+ primary: '#6200EE',
34
+ text: '#212121',
35
+ subtext: '#757575',
36
+ border: '#E0E0E0',
37
+ error: '#B00020' };
38
+
39
+ const darkColors= {
40
+ background: '#121212',
41
+ surface: '#1E1E1E',
42
+ primary: '#BB86FC',
43
+ text: '#FFFFFF',
44
+ subtext: '#B0B0B0',
45
+ border: '#333333',
46
+ error: '#CF6679' };
47
+
48
+ const ThemeContext = createContext(null);
49
+
50
+ export function ThemeProvider({
51
+ children,
52
+ initialScheme = 'light' }){
53
+ const [colorScheme, setColorScheme] = useState(initialScheme);
54
+
55
+ const toggleTheme = useCallback(() => {
56
+ setColorScheme((s) => (s === 'light' ? 'dark' : 'light'));
57
+ }, []);
58
+
59
+ const value = useMemo(
60
+ () => ({
61
+ colorScheme,
62
+ colors: colorScheme === 'dark' ? darkColors : lightColors,
63
+ isDark: colorScheme === 'dark',
64
+ toggleTheme,
65
+ setColorScheme }),
66
+ [colorScheme, toggleTheme],
67
+ );
68
+
69
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
70
+ }
71
+
72
+ /** Hook to consume ThemeContext — must be used inside <ThemeProvider>. */
73
+ export function useTheme(){
74
+ const ctx = useContext(ThemeContext);
75
+ if (!ctx) {
76
+ throw new Error('useTheme must be used within a ThemeProvider');
77
+ }
78
+ return ctx;
79
+ }
@@ -0,0 +1,3 @@
1
+ export { ThemeProvider, useTheme } from './ThemeContext';
2
+
3
+ export { AuthProvider, useAuth } from './AuthContext';
@@ -0,0 +1,5 @@
1
+ // Context API exports
2
+ export * from './context/index';
3
+
4
+ // Redux exports
5
+ export * from './redux/index';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Typed hooks for Redux — use these instead of plain useSelector / useDispatch.
3
+ *
4
+ * @example
5
+ * const dispatch = useAppDispatch();
6
+ * const count = useAppSelector(selectCount);
7
+ */
8
+
9
+ import { useDispatch, useSelector } from 'react-redux';
10
+
11
+ export const useAppDispatch = useDispatch;
12
+ export const useAppSelector = useSelector;
@@ -0,0 +1,7 @@
1
+ export { store } from './store';
2
+
3
+ export { useAppDispatch, useAppSelector } from './hooks';
4
+
5
+ export { counterSlice, increment, decrement, reset, incrementByAmount, setStep, selectCount, selectStep } from './slices/counterSlice';
6
+
7
+ export { postsSlice, fetchPosts, fetchPostById, clearPosts, selectPost, selectAllPosts, selectPostsStatus, selectPostsError, selectSelectedPost } from './slices/postsSlice';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * counterSlice — example Redux Toolkit slice demonstrating:
3
+ * - synchronous actions (increment, decrement, reset, incrementByAmount)
4
+ * - derived selectors
5
+ * - used in Module 2: State Management
6
+ */
7
+
8
+ import { createSlice } from '@reduxjs/toolkit';
9
+
10
+ const initialState= {
11
+ value: 0,
12
+ step: 1 };
13
+
14
+ export const counterSlice = createSlice({
15
+ name: 'counter',
16
+ initialState,
17
+ reducers: {
18
+ increment(state) {
19
+ state.value += state.step;
20
+ },
21
+ decrement(state) {
22
+ state.value -= state.step;
23
+ },
24
+ reset(state) {
25
+ state.value = 0;
26
+ },
27
+ incrementByAmount(state, action) {
28
+ state.value += action.payload;
29
+ },
30
+ setStep(state, action) {
31
+ state.step = action.payload;
32
+ } } });
33
+
34
+ export const { increment, decrement, reset, incrementByAmount, setStep } =
35
+ counterSlice.actions;
36
+
37
+ // Typed selectors (kept co-located for discoverability)
38
+ export const selectCount = (state) => state.counter.value;
39
+ export const selectStep = (state) => state.counter.step;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * postsSlice — demonstrates async thunks with createAsyncThunk.
3
+ *
4
+ * Covers:
5
+ * - API fetching with loading/error/success states
6
+ * - Async/Await patterns in Redux
7
+ * - Normalized state (simple array)
8
+ *
9
+ * Derived from Module 2
10
+ */
11
+
12
+ import {
13
+ createAsyncThunk,
14
+ createSlice } from '@reduxjs/toolkit';
15
+
16
+ const POSTS_API = 'https://jsonplaceholder.typicode.com/posts';
17
+
18
+ /** Fetch paginated posts — pass { page, limit } to control pagination. */
19
+ export const fetchPosts = createAsyncThunk('posts/fetchPosts', async ({ page = 1, limit = 10 }, { rejectWithValue }) => {
20
+ try {
21
+ const start = (page - 1) * limit;
22
+ const response = await fetch(
23
+ `${POSTS_API}?_start=${start}&_limit=${limit}`,
24
+ );
25
+ if (!response.ok) {
26
+ return rejectWithValue(`HTTP ${response.status}: ${response.statusText}`);
27
+ }
28
+ const data= await response.json();
29
+ return data;
30
+ } catch (err) {
31
+ return rejectWithValue(err instanceof Error ? err.message : 'Network error');
32
+ }
33
+ });
34
+
35
+ /** Fetch a single post by id. */
36
+ export const fetchPostById = createAsyncThunk('posts/fetchPostById', async (id, { rejectWithValue }) => {
37
+ try {
38
+ const response = await fetch(`${POSTS_API}/${id}`);
39
+ if (!response.ok) {
40
+ return rejectWithValue(`HTTP ${response.status}: ${response.statusText}`);
41
+ }
42
+ return (await response.json());
43
+ } catch (err) {
44
+ return rejectWithValue(err instanceof Error ? err.message : 'Network error');
45
+ }
46
+ });
47
+
48
+ const initialState= {
49
+ items: [],
50
+ status: 'idle',
51
+ error: null,
52
+ selectedPost: null,
53
+ };
54
+
55
+ export const postsSlice = createSlice({
56
+ name: 'posts',
57
+ initialState,
58
+ reducers: {
59
+ clearPosts(state) {
60
+ state.items = [];
61
+ state.status = 'idle';
62
+ state.error = null;
63
+ },
64
+ selectPost(state, action) {
65
+ state.selectedPost = action.payload;
66
+ } },
67
+ extraReducers(builder) {
68
+ builder
69
+ .addCase(fetchPosts.pending, (state) => {
70
+ state.status = 'loading';
71
+ state.error = null;
72
+ })
73
+ .addCase(fetchPosts.fulfilled, (state, action) => {
74
+ state.status = 'succeeded';
75
+ state.items = action.payload;
76
+ })
77
+ .addCase(fetchPosts.rejected, (state, action) => {
78
+ state.status = 'failed';
79
+ state.error = action.payload ?? 'Unknown error';
80
+ })
81
+ .addCase(fetchPostById.fulfilled, (state, action) => {
82
+ state.selectedPost = action.payload;
83
+ });
84
+ } });
85
+
86
+ export const { clearPosts, selectPost } = postsSlice.actions;
87
+
88
+ // Selectors
89
+ export const selectAllPosts = (state) => state.posts.items;
90
+ export const selectPostsStatus = (state) => state.posts.status;
91
+ export const selectPostsError = (state) => state.posts.error;
92
+ export const selectSelectedPost = (state) => state.posts.selectedPost;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Redux store — pre-configured with counter and posts slices.
3
+ *
4
+ * @example
5
+ * // In your app entry point:
6
+ * import { Provider } from 'react-redux';
7
+ * import { store } from '@expotesting/state/redux';
8
+ *
9
+ * export default function App() {
10
+ * return (
11
+ * <Provider store={store}>
12
+ * <RootNavigator />
13
+ * </Provider>
14
+ * );
15
+ * }
16
+ */
17
+
18
+ import { configureStore } from '@reduxjs/toolkit';
19
+ import { counterSlice } from './slices/counterSlice';
20
+ import { postsSlice } from './slices/postsSlice';
21
+
22
+ export const store = configureStore({
23
+ reducer: {
24
+ counter: counterSlice.reducer,
25
+ posts: postsSlice.reducer },
26
+ middleware: (getDefaultMiddleware) =>
27
+ getDefaultMiddleware({
28
+ // Serializable check is fine for standard payloads
29
+ serializableCheck: {
30
+ ignoredActions: [] } }) });
31
+
32
+ // Inferred types — re-export for use in hooks across the app
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@expotesting/storage",
3
+ "version": "1.0.0",
4
+ "description": "AsyncStorage and SQLite wrappers for expotesting",
5
+ "main": "src/index.js",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./async-storage": "./src/asyncStorage.ts",
10
+ "./sqlite": "./src/sqlite/index.ts"
11
+ },
12
+ "scripts": {},
13
+ "peerDependencies": {
14
+ "react": ">=18",
15
+ "react-native": ">=0.73",
16
+ "@react-native-async-storage/async-storage": ">=1",
17
+ "expo-sqlite": ">=14"
18
+ },
19
+ "devDependencies": {
20
+ "@react-native-async-storage/async-storage": "*",
21
+ "expo-sqlite": "*"
22
+ },
23
+ "license": "MIT"
24
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * asyncStorage — typed wrappers around @react-native-async-storage/async-storage.
3
+ *
4
+ * Why wrap?
5
+ * - Automatic JSON serialisation/deserialisation
6
+ * - Namespace support to avoid key collisions
7
+ * - Consistent error handling
8
+ *
9
+ * @example
10
+ * const storage = createStorage('auth');
11
+ * await storage.setItem('token', 'abc123');
12
+ * const token = await storage.getItem<string>('token');
13
+ * await storage.removeItem('token');
14
+ */
15
+
16
+ import AsyncStorage from '@react-native-async-storage/async-storage';
17
+
18
+ /**
19
+ * Creates a namespaced storage adapter to isolate different domains of data.
20
+ *
21
+ * @param namespace - Prefix added to all keys, e.g. 'auth', 'settings'
22
+ */
23
+ export function createStorage(namespace = 'app'){
24
+ const prefix = `@${namespace}:`;
25
+
26
+ const buildKey = (key) => `${prefix}${key}`;
27
+
28
+ async function setItem(key, value){
29
+ const serialised = JSON.stringify(value);
30
+ await AsyncStorage.setItem(buildKey(key), serialised);
31
+ }
32
+
33
+ async function getItem(key){
34
+ const raw = await AsyncStorage.getItem(buildKey(key));
35
+ if (raw === null) return null;
36
+ try {
37
+ return JSON.parse(raw);
38
+ } catch {
39
+ // If stored value is a plain string (legacy), return as-is
40
+ return raw;
41
+ }
42
+ }
43
+
44
+ async function removeItem(key){
45
+ await AsyncStorage.removeItem(buildKey(key));
46
+ }
47
+
48
+ async function clear(){
49
+ const allKeys = await AsyncStorage.getAllKeys();
50
+ const namespaceKeys = allKeys.filter((k) => k.startsWith(prefix));
51
+ await AsyncStorage.multiRemove(namespaceKeys);
52
+ }
53
+
54
+ async function getAllKeys(){
55
+ const allKeys = await AsyncStorage.getAllKeys();
56
+ return allKeys
57
+ .filter((k) => k.startsWith(prefix))
58
+ .map((k) => k.slice(prefix.length));
59
+ }
60
+
61
+ async function multiGet(keys){
62
+ const fullKeys = keys.map(buildKey);
63
+ const pairs = await AsyncStorage.multiGet(fullKeys);
64
+ const result= {};
65
+ for (const [k, v] of pairs) {
66
+ const shortKey = k.slice(prefix.length);
67
+ try {
68
+ result[shortKey] = v !== null ? (JSON.parse(v)) : null;
69
+ } catch {
70
+ result[shortKey] = v;
71
+ }
72
+ }
73
+ return result;
74
+ }
75
+
76
+ return { setItem, getItem, removeItem, clear, getAllKeys, multiGet };
77
+ }
78
+
79
+ // Default app-level storage instance
80
+ export const appStorage = createStorage('app');
81
+ export const authStorage = createStorage('auth');
82
+ export const settingsStorage = createStorage('settings');
@@ -0,0 +1,2 @@
1
+ export * from './asyncStorage';
2
+ export * from './sqlite/index';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * SQLite database setup using expo-sqlite v14 (async API).
3
+ *
4
+ * expo-sqlite v14 introduced a fully async API via SQLiteDatabase.
5
+ * Use openDatabase() once at app startup; pass the db instance to operations.
6
+ *
7
+ * @example
8
+ * // In your app entry point:
9
+ * const db = await openDatabase('myapp.db');
10
+ * await runMigrations(db);
11
+ *
12
+ * // Later:
13
+ * const users = await queryAll<User>(db, 'SELECT * FROM users');
14
+ */
15
+
16
+ import * as SQLite from 'expo-sqlite';
17
+
18
+ /**
19
+ * Opens (or creates) a SQLite database file.
20
+ * Call this once per app session and share the instance.
21
+ */
22
+ export async function openDatabase(name = 'expotesting.db'){
23
+ const db = await SQLite.openDatabaseAsync(name);
24
+ // Enable WAL mode for better concurrent read performance
25
+ await db.execAsync('PRAGMA journal_mode = WAL;');
26
+ return db;
27
+ }
28
+
29
+ /**
30
+ * Runs raw SQL statements separated by semicolons — ideal for migrations.
31
+ */
32
+ export async function runMigrations(db, sql){
33
+ await db.execAsync(sql);
34
+ }
35
+
36
+ /** Standard CRUD types */
37
+
38
+ /**
39
+ * Generates + executes a CREATE TABLE IF NOT EXISTS statement.
40
+ */
41
+ export async function createTable(
42
+ db,
43
+ tableName,
44
+ columns,
45
+ ){
46
+ const cols = columns.map((c) => {
47
+ let def = `${c.name} ${c.type}`;
48
+ if (c.primaryKey) def += ' PRIMARY KEY';
49
+ if (c.autoIncrement) def += ' AUTOINCREMENT';
50
+ if (c.notNull) def += ' NOT NULL';
51
+ if (c.unique) def += ' UNIQUE';
52
+ if (c.defaultValue !== undefined) def += ` DEFAULT ${c.defaultValue}`;
53
+ return def;
54
+ });
55
+ await db.execAsync(
56
+ `CREATE TABLE IF NOT EXISTS ${tableName} (${cols.join(', ')});`,
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Drops a table if it exists.
62
+ */
63
+ export async function dropTable(db, tableName){
64
+ await db.execAsync(`DROP TABLE IF EXISTS ${tableName};`);
65
+ }
@@ -0,0 +1,3 @@
1
+ export * from './database';
2
+ export * from './operations';
3
+ export * from './useSQLite';
@@ -0,0 +1,112 @@
1
+ /**
2
+ * SQLite CRUD operations — generic helpers for the most common patterns.
3
+ *
4
+ * @example
5
+ * // Insert a record
6
+ * const id = await insertRow(db, 'notes', { title: 'Hello', content: 'World' });
7
+ *
8
+ * // Query all
9
+ * const notes = await queryAll<Note>(db, 'SELECT * FROM notes ORDER BY id DESC');
10
+ *
11
+ * // Query with params
12
+ * const note = await queryOne<Note>(db, 'SELECT * FROM notes WHERE id = ?', [id]);
13
+ *
14
+ * // Update
15
+ * await updateRows(db, 'notes', { title: 'Updated' }, 'id = ?', [id]);
16
+ *
17
+ * // Delete
18
+ * await deleteRows(db, 'notes', 'id = ?', [id]);
19
+ */
20
+
21
+ /**
22
+ * Execute a SELECT query returning all rows.
23
+ */
24
+ export async function queryAll(
25
+ db,
26
+ sql,
27
+ params = [],
28
+ ){
29
+ return db.getAllAsync(sql, params);
30
+ }
31
+
32
+ /**
33
+ * Execute a SELECT query returning the first row or null.
34
+ */
35
+ export async function queryOne(
36
+ db,
37
+ sql,
38
+ params = [],
39
+ ){
40
+ return db.getFirstAsync(sql, params);
41
+ }
42
+
43
+ /**
44
+ * INSERT a row from an object — returns the inserted row's id.
45
+ */
46
+ export async function insertRow(
47
+ db,
48
+ tableName,
49
+ data,
50
+ ){
51
+ const keys = Object.keys(data);
52
+ const placeholders = keys.map(() => '?').join(', ');
53
+ const values = Object.values(data);
54
+ const result = await db.runAsync(
55
+ `INSERT INTO ${tableName} (${keys.join(', ')}) VALUES (${placeholders});`,
56
+ values,
57
+ );
58
+ return result.lastInsertRowId;
59
+ }
60
+
61
+ /**
62
+ * UPDATE rows matching a WHERE clause.
63
+ */
64
+ export async function updateRows(
65
+ db,
66
+ tableName,
67
+ data,
68
+ where,
69
+ whereParams = [],
70
+ ){
71
+ const assignments = Object.keys(data)
72
+ .map((k) => `${k} = ?`)
73
+ .join(', ');
74
+ const values= [...Object.values(data), ...whereParams];
75
+ const result = await db.runAsync(
76
+ `UPDATE ${tableName} SET ${assignments} WHERE ${where};`,
77
+ values,
78
+ );
79
+ return result.changes;
80
+ }
81
+
82
+ /**
83
+ * DELETE rows matching a WHERE clause.
84
+ */
85
+ export async function deleteRows(
86
+ db,
87
+ tableName,
88
+ where,
89
+ whereParams = [],
90
+ ){
91
+ const result = await db.runAsync(
92
+ `DELETE FROM ${tableName} WHERE ${where};`,
93
+ whereParams,
94
+ );
95
+ return result.changes;
96
+ }
97
+
98
+ /**
99
+ * Count rows in a table, optionally filtered.
100
+ */
101
+ export async function countRows(
102
+ db,
103
+ tableName,
104
+ where,
105
+ whereParams = [],
106
+ ){
107
+ const sql = where
108
+ ? `SELECT COUNT(*) as count FROM ${tableName} WHERE ${where};`
109
+ : `SELECT COUNT(*) as count FROM ${tableName};`;
110
+ const row = await db.getFirstAsync(sql, whereParams);
111
+ return row?.count ?? 0;
112
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * useSQLite — React hook that opens a SQLite database and
3
+ * returns the db instance alongside ready/error state.
4
+ *
5
+ * @example
6
+ * const { db, ready, error } = useSQLite('myapp.db');
7
+ *
8
+ * useEffect(() => {
9
+ * if (!ready || !db) return;
10
+ * void db.runAsync('CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY)');
11
+ * }, [ready, db]);
12
+ */
13
+
14
+ import { useEffect, useState } from 'react';
15
+ import { openDatabase } from './database';
16
+
17
+ export function useSQLite(dbName = 'expotesting.db'){
18
+ const [db, setDb] = useState(null);
19
+ const [ready, setReady] = useState(false);
20
+ const [error, setError] = useState(null);
21
+
22
+ useEffect(() => {
23
+ let cancelled = false;
24
+
25
+ void (async () => {
26
+ try {
27
+ const database = await openDatabase(dbName);
28
+ if (!cancelled) {
29
+ setDb(database);
30
+ setReady(true);
31
+ }
32
+ } catch (err) {
33
+ if (!cancelled) {
34
+ setError(err instanceof Error ? err.message : 'Failed to open database');
35
+ }
36
+ }
37
+ })();
38
+
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ }, [dbName]);
43
+
44
+ return { db, ready, error };
45
+ }