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.
- package/README.md +289 -0
- package/apps/expo-app/app.json +60 -0
- package/apps/expo-app/babel.config.js +7 -0
- package/apps/expo-app/index.js +6 -0
- package/apps/expo-app/package.json +46 -0
- package/apps/expo-app/src/App.jsx +37 -0
- package/apps/expo-app/src/navigation/RootNavigator.jsx +82 -0
- package/apps/expo-app/src/navigation/types.js +5 -0
- package/apps/expo-app/src/screens/HomeScreen.jsx +178 -0
- package/package.json +24 -0
- package/packages/animations/package.json +20 -0
- package/packages/animations/src/components/FadeView.jsx +42 -0
- package/packages/animations/src/components/ScaleView.jsx +28 -0
- package/packages/animations/src/components/SlideView.jsx +32 -0
- package/packages/animations/src/hooks/useFade.js +50 -0
- package/packages/animations/src/hooks/useScale.js +59 -0
- package/packages/animations/src/hooks/useSlide.js +53 -0
- package/packages/animations/src/index.js +21 -0
- package/packages/animations/src/reanimated.js +83 -0
- package/packages/core/package.json +22 -0
- package/packages/core/src/components/Button.jsx +92 -0
- package/packages/core/src/components/Card.jsx +47 -0
- package/packages/core/src/components/Container.jsx +61 -0
- package/packages/core/src/components/Input.jsx +83 -0
- package/packages/core/src/components/List.jsx +80 -0
- package/packages/core/src/components/index.js +9 -0
- package/packages/core/src/hooks/index.js +5 -0
- package/packages/core/src/hooks/useAsync.js +60 -0
- package/packages/core/src/hooks/useCounter.js +36 -0
- package/packages/core/src/hooks/useToggle.js +18 -0
- package/packages/core/src/index.js +5 -0
- package/packages/core/src/theme/index.js +67 -0
- package/packages/core/src/utils/helpers.js +93 -0
- package/packages/core/src/utils/index.js +10 -0
- package/packages/device/package.json +24 -0
- package/packages/device/src/hooks/useCamera.js +45 -0
- package/packages/device/src/hooks/useGallery.js +70 -0
- package/packages/device/src/hooks/useLocation.js +99 -0
- package/packages/device/src/index.js +5 -0
- package/packages/examples/package.json +36 -0
- package/packages/examples/src/experiments/animations-device/AnimationsDeviceScreen.jsx +291 -0
- package/packages/examples/src/experiments/basic-app/BasicAppScreen.jsx +162 -0
- package/packages/examples/src/experiments/components-props-state/ComponentsStateScreen.jsx +280 -0
- package/packages/examples/src/experiments/navigation/NavigationScreen.jsx +202 -0
- package/packages/examples/src/experiments/network-storage/NetworkStorageScreen.jsx +367 -0
- package/packages/examples/src/experiments/state-management/StateManagementScreen.jsx +255 -0
- package/packages/examples/src/index.js +76 -0
- package/packages/navigation/package.json +20 -0
- package/packages/navigation/src/DrawerNavigator.jsx +35 -0
- package/packages/navigation/src/StackNavigator.jsx +51 -0
- package/packages/navigation/src/TabNavigator.jsx +44 -0
- package/packages/navigation/src/createAppNavigator.jsx +48 -0
- package/packages/navigation/src/index.js +8 -0
- package/packages/navigation/src/types.js +18 -0
- package/packages/network/package.json +19 -0
- package/packages/network/src/apiClient.js +90 -0
- package/packages/network/src/fetchHelpers.js +97 -0
- package/packages/network/src/hooks/useFetch.js +56 -0
- package/packages/network/src/index.js +3 -0
- package/packages/network/src/types.js +4 -0
- package/packages/state/package.json +22 -0
- package/packages/state/src/context/AuthContext.jsx +94 -0
- package/packages/state/src/context/ThemeContext.jsx +79 -0
- package/packages/state/src/context/index.js +3 -0
- package/packages/state/src/index.js +5 -0
- package/packages/state/src/redux/hooks.js +12 -0
- package/packages/state/src/redux/index.js +7 -0
- package/packages/state/src/redux/slices/counterSlice.js +39 -0
- package/packages/state/src/redux/slices/postsSlice.js +92 -0
- package/packages/state/src/redux/store.js +32 -0
- package/packages/storage/package.json +24 -0
- package/packages/storage/src/asyncStorage.js +82 -0
- package/packages/storage/src/index.js +2 -0
- package/packages/storage/src/sqlite/database.js +65 -0
- package/packages/storage/src/sqlite/index.js +3 -0
- package/packages/storage/src/sqlite/operations.js +112 -0
- package/packages/storage/src/sqlite/useSQLite.js +45 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expotesting/navigation",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pre-built navigators and navigation utilities for expotesting",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"react": ">=18",
|
|
13
|
+
"react-native": ">=0.73",
|
|
14
|
+
"@react-navigation/native": "^6",
|
|
15
|
+
"@react-navigation/native-stack": "^6",
|
|
16
|
+
"@react-navigation/bottom-tabs": "^6",
|
|
17
|
+
"@react-navigation/drawer": "^6"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppDrawerNavigator — wraps @react-navigation/drawer with consistent styling.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* <AppDrawerNavigator
|
|
6
|
+
* screens={[
|
|
7
|
+
* { name: 'Home', component: HomeScreen },
|
|
8
|
+
* { name: 'Profile', component: ProfileScreen },
|
|
9
|
+
* ]}
|
|
10
|
+
* />
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { createDrawerNavigator } from '@react-navigation/drawer';
|
|
15
|
+
|
|
16
|
+
const Drawer = createDrawerNavigator();
|
|
17
|
+
|
|
18
|
+
export function AppDrawerNavigator({
|
|
19
|
+
screens,
|
|
20
|
+
screenOptions,
|
|
21
|
+
drawerPosition = 'left' }){
|
|
22
|
+
return (
|
|
23
|
+
<Drawer.Navigator
|
|
24
|
+
screenOptions={{
|
|
25
|
+
drawerPosition,
|
|
26
|
+
drawerType: 'slide',
|
|
27
|
+
overlayColor: 'rgba(0,0,0,0.4)',
|
|
28
|
+
...screenOptions }}
|
|
29
|
+
>
|
|
30
|
+
{screens.map(({ name, component, options }) => (
|
|
31
|
+
<Drawer.Screen key={name} name={name} component={component} options={options} />
|
|
32
|
+
))}
|
|
33
|
+
</Drawer.Navigator>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppStackNavigator — wraps @react-navigation/native-stack with common defaults.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Typed screen registration
|
|
6
|
+
* - Custom header styling via `headerStyle` prop
|
|
7
|
+
* - Convenient `screens` array API (alternative to boilerplate Screen tags)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { AppStackNavigator } from '@expotesting/navigation';
|
|
11
|
+
*
|
|
12
|
+
* export default function App() {
|
|
13
|
+
* return (
|
|
14
|
+
* <NavigationContainer>
|
|
15
|
+
* <AppStackNavigator
|
|
16
|
+
* screens={[
|
|
17
|
+
* { name: 'Home', component: HomeScreen },
|
|
18
|
+
* { name: 'Details', component: DetailsScreen },
|
|
19
|
+
* ]}
|
|
20
|
+
* />
|
|
21
|
+
* </NavigationContainer>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import React from 'react';
|
|
27
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
28
|
+
|
|
29
|
+
const Stack = createNativeStackNavigator();
|
|
30
|
+
|
|
31
|
+
export function AppStackNavigator({
|
|
32
|
+
screens,
|
|
33
|
+
screenOptions }){
|
|
34
|
+
return (
|
|
35
|
+
<Stack.Navigator
|
|
36
|
+
screenOptions={{
|
|
37
|
+
headerBackTitleVisible: false,
|
|
38
|
+
...screenOptions }}
|
|
39
|
+
>
|
|
40
|
+
{screens.map(({ name, component, options, initialParams }) => (
|
|
41
|
+
<Stack.Screen
|
|
42
|
+
key={name}
|
|
43
|
+
name={name}
|
|
44
|
+
component={component}
|
|
45
|
+
options={options}
|
|
46
|
+
initialParams={initialParams}
|
|
47
|
+
/>
|
|
48
|
+
))}
|
|
49
|
+
</Stack.Navigator>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AppTabNavigator — wraps @react-navigation/bottom-tabs with icon + label support.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* <AppTabNavigator
|
|
6
|
+
* tabs={[
|
|
7
|
+
* { name: 'Home', component, icon: 'home' },
|
|
8
|
+
* { name: 'Settings', component, icon: 'settings' },
|
|
9
|
+
* ]}
|
|
10
|
+
* />
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|
15
|
+
|
|
16
|
+
const Tab = createBottomTabNavigator();
|
|
17
|
+
|
|
18
|
+
export function AppTabNavigator({
|
|
19
|
+
tabs,
|
|
20
|
+
screenOptions,
|
|
21
|
+
activeColor = '#6200EE',
|
|
22
|
+
inactiveColor = '#757575' }){
|
|
23
|
+
return (
|
|
24
|
+
<Tab.Navigator
|
|
25
|
+
screenOptions={{
|
|
26
|
+
tabBarActiveTintColor: activeColor,
|
|
27
|
+
tabBarInactiveTintColor: inactiveColor,
|
|
28
|
+
tabBarStyle: { paddingBottom: 4, height: 56 },
|
|
29
|
+
headerShown: false,
|
|
30
|
+
...screenOptions }}
|
|
31
|
+
>
|
|
32
|
+
{tabs.map(({ name, component, options, tabBarIcon }) => (
|
|
33
|
+
<Tab.Screen
|
|
34
|
+
key={name}
|
|
35
|
+
name={name}
|
|
36
|
+
component={component}
|
|
37
|
+
options={{
|
|
38
|
+
...(tabBarIcon && { tabBarIcon }),
|
|
39
|
+
...options }}
|
|
40
|
+
/>
|
|
41
|
+
))}
|
|
42
|
+
</Tab.Navigator>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createAppNavigator — factory that composes Stack + Tab + Drawer navigators
|
|
3
|
+
* based on a declarative config object.
|
|
4
|
+
*
|
|
5
|
+
* Supported types: 'stack' | 'tabs' | 'drawer'
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const AppNavigator = createAppNavigator({
|
|
9
|
+
* type: 'tabs',
|
|
10
|
+
* screens: [
|
|
11
|
+
* { name: 'Home', component: HomeScreen },
|
|
12
|
+
* { name: 'Profile', component: ProfileScreen },
|
|
13
|
+
* ],
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* export default function App() {
|
|
17
|
+
* return (
|
|
18
|
+
* <NavigationContainer>
|
|
19
|
+
* <AppNavigator />
|
|
20
|
+
* </NavigationContainer>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import React from 'react';
|
|
26
|
+
import { AppStackNavigator } from './StackNavigator';
|
|
27
|
+
import { AppTabNavigator } from './TabNavigator';
|
|
28
|
+
import { AppDrawerNavigator } from './DrawerNavigator';
|
|
29
|
+
|
|
30
|
+
export function createAppNavigator(config): () => React.ReactElement {
|
|
31
|
+
return function AppNavigator(){
|
|
32
|
+
switch (config.type) {
|
|
33
|
+
case 'tabs':
|
|
34
|
+
return (
|
|
35
|
+
<AppTabNavigator
|
|
36
|
+
tabs={config.screens]}
|
|
37
|
+
activeColor={config.activeColor}
|
|
38
|
+
inactiveColor={config.inactiveColor}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
case 'drawer':
|
|
42
|
+
return <AppDrawerNavigator screens={config.screens]} />;
|
|
43
|
+
case 'stack':
|
|
44
|
+
default:
|
|
45
|
+
return <AppStackNavigator screens={config.screens]} />;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared navigation types used across stack, tab, and drawer navigators.
|
|
3
|
+
*
|
|
4
|
+
* Consumers should augment these types via TypeScript declaration merging
|
|
5
|
+
* to get fully-typed navigation for their specific routes.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // In your app:
|
|
9
|
+
* declare module '@expotesting/navigation' {
|
|
10
|
+
* interface AppParamList {
|
|
11
|
+
* Home: undefined;
|
|
12
|
+
* Profile: { userId: string };
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Generic param list — extend this in your app
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expotesting/network",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Axios API client, fetch helpers, and useFetch hook for expotesting",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.7.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": ">=18",
|
|
16
|
+
"react-native": ">=0.73"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT"
|
|
19
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apiClient — pre-configured Axios instance with:
|
|
3
|
+
* - Request/response interceptors
|
|
4
|
+
* - Timeout handling
|
|
5
|
+
* - Consistent error normalisation
|
|
6
|
+
* - Bearer token injection (call setAuthToken to configure)
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* import { apiClient } from '@expotesting/network';
|
|
10
|
+
*
|
|
11
|
+
* const posts = await apiClient.get('/posts?_limit=10');
|
|
12
|
+
* const newPost = await apiClient.post('/posts', { title: 'Hello', body: '...' });
|
|
13
|
+
* await apiClient.delete('/posts/1');
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import axios, {
|
|
17
|
+
isAxiosError } from 'axios';
|
|
18
|
+
|
|
19
|
+
const DEFAULT_BASE_URL = 'https://jsonplaceholder.typicode.com';
|
|
20
|
+
const DEFAULT_TIMEOUT = 10_000;
|
|
21
|
+
|
|
22
|
+
function createApiClient(baseURL = DEFAULT_BASE_URL){
|
|
23
|
+
const instance = axios.create({
|
|
24
|
+
baseURL,
|
|
25
|
+
timeout: DEFAULT_TIMEOUT,
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
Accept: 'application/json' } });
|
|
29
|
+
|
|
30
|
+
// --- Request interceptor: attach auth token if available ---
|
|
31
|
+
instance.interceptors.request.use(
|
|
32
|
+
(config) => {
|
|
33
|
+
const token = getAuthToken();
|
|
34
|
+
if (token && config.headers) {
|
|
35
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
},
|
|
39
|
+
(error) => Promise.reject(error),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// --- Response interceptor: normalise errors ---
|
|
43
|
+
instance.interceptors.response.use(
|
|
44
|
+
(response) => response,
|
|
45
|
+
(error) => {
|
|
46
|
+
const apiError= { message: 'An unexpected error occurred' };
|
|
47
|
+
|
|
48
|
+
if (isAxiosError(error)) {
|
|
49
|
+
apiError.status = error.response?.status;
|
|
50
|
+
apiError.code = error.code;
|
|
51
|
+
|
|
52
|
+
if (error.response?.data && typeof error.response.data === 'object') {
|
|
53
|
+
const data = error.response.data;
|
|
54
|
+
apiError.message =
|
|
55
|
+
typeof data['message'] === 'string'
|
|
56
|
+
? data['message']
|
|
57
|
+
: `HTTP ${apiError.status}`;
|
|
58
|
+
} else if (error.code === 'ECONNABORTED') {
|
|
59
|
+
apiError.message = 'Request timed out. Please try again.';
|
|
60
|
+
} else if (!error.response) {
|
|
61
|
+
apiError.message = 'Network error. Check your connection.';
|
|
62
|
+
} else {
|
|
63
|
+
apiError.message = `HTTP ${apiError.status}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Promise.reject(apiError);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return instance;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// In-memory token store — in production use SecureStore or AsyncStorage
|
|
75
|
+
let _authToken= null;
|
|
76
|
+
|
|
77
|
+
export function setAuthToken(token){
|
|
78
|
+
_authToken = token;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getAuthToken(){
|
|
82
|
+
return _authToken;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const apiClient = createApiClient();
|
|
86
|
+
|
|
87
|
+
/** Create a custom instance pointing to a different base URL. */
|
|
88
|
+
export function createClient(baseURL){
|
|
89
|
+
return createApiClient(baseURL);
|
|
90
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fetchHelpers — lightweight wrappers around the native Fetch API.
|
|
3
|
+
*
|
|
4
|
+
* Use these when you prefer Fetch over Axios or need a smaller bundle.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const posts = await fetchGet<Post[]>('/posts?_limit=5', {
|
|
8
|
+
* baseURL: 'https://jsonplaceholder.typicode.com',
|
|
9
|
+
* });
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
class HttpError extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
message,
|
|
15
|
+
status,
|
|
16
|
+
statusText,
|
|
17
|
+
) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = 'HttpError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { HttpError };
|
|
24
|
+
|
|
25
|
+
async function fetchWithTimeout(
|
|
26
|
+
url,
|
|
27
|
+
options = {},
|
|
28
|
+
){
|
|
29
|
+
const { timeout = 10_000, baseURL = '', ...fetchOptions } = options;
|
|
30
|
+
const fullUrl = url.startsWith('http') ? url : `${baseURL}${url}`;
|
|
31
|
+
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetch(fullUrl, {
|
|
37
|
+
...fetchOptions,
|
|
38
|
+
signal: controller.signal });
|
|
39
|
+
return response;
|
|
40
|
+
} finally {
|
|
41
|
+
clearTimeout(timeoutId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function parseResponse(response){
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new HttpError(
|
|
48
|
+
`HTTP ${response.status}: ${response.statusText}`,
|
|
49
|
+
response.status,
|
|
50
|
+
response.statusText,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
54
|
+
if (contentType.includes('application/json')) {
|
|
55
|
+
return response.json();
|
|
56
|
+
}
|
|
57
|
+
return response.text();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function fetchGet(url, options){
|
|
61
|
+
const response = await fetchWithTimeout(url, { ...options, method: 'GET' });
|
|
62
|
+
return parseResponse(response);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function fetchPost(
|
|
66
|
+
url,
|
|
67
|
+
body,
|
|
68
|
+
options,
|
|
69
|
+
){
|
|
70
|
+
const response = await fetchWithTimeout(url, {
|
|
71
|
+
...options,
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
74
|
+
body: JSON.stringify(body) });
|
|
75
|
+
return parseResponse(response);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function fetchPut(
|
|
79
|
+
url,
|
|
80
|
+
body,
|
|
81
|
+
options,
|
|
82
|
+
){
|
|
83
|
+
const response = await fetchWithTimeout(url, {
|
|
84
|
+
...options,
|
|
85
|
+
method: 'PUT',
|
|
86
|
+
headers: { 'Content-Type': 'application/json', ...options?.headers },
|
|
87
|
+
body: JSON.stringify(body) });
|
|
88
|
+
return parseResponse(response);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function fetchDelete(
|
|
92
|
+
url,
|
|
93
|
+
options,
|
|
94
|
+
){
|
|
95
|
+
const response = await fetchWithTimeout(url, { ...options, method: 'DELETE' });
|
|
96
|
+
return parseResponse(response);
|
|
97
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useFetch — React hook for fetching data with loading/error/refetch support.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* const { data, loading, error, refetch } = useFetch<Post[]>(
|
|
6
|
+
* 'https://jsonplaceholder.typicode.com/posts?_limit=10',
|
|
7
|
+
* );
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
11
|
+
|
|
12
|
+
export function useFetch(url, init){
|
|
13
|
+
const [data, setData] = useState(null);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [error, setError] = useState(null);
|
|
16
|
+
const abortRef = useRef(null);
|
|
17
|
+
|
|
18
|
+
const execute = useCallback(async () => {
|
|
19
|
+
// Cancel any in-flight request
|
|
20
|
+
abortRef.current?.abort();
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
abortRef.current = controller;
|
|
23
|
+
|
|
24
|
+
setLoading(true);
|
|
25
|
+
setError(null);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch(url, { ...init, signal: controller.signal });
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
const result= await response.json();
|
|
33
|
+
if (!controller.signal.aborted) {
|
|
34
|
+
setData(result);
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (!controller.signal.aborted) {
|
|
38
|
+
setError(err instanceof Error ? err.message : 'Fetch failed');
|
|
39
|
+
}
|
|
40
|
+
} finally {
|
|
41
|
+
if (!controller.signal.aborted) {
|
|
42
|
+
setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, [url]);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
void execute();
|
|
50
|
+
return () => {
|
|
51
|
+
abortRef.current?.abort();
|
|
52
|
+
};
|
|
53
|
+
}, [execute]);
|
|
54
|
+
|
|
55
|
+
return { data, loading, error, refetch: execute };
|
|
56
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@expotesting/state",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Context API and Redux Toolkit state management for expotesting",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./context": "./src/context/index.ts",
|
|
10
|
+
"./redux": "./src/redux/index.ts"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@reduxjs/toolkit": "^2.3.0",
|
|
15
|
+
"react-redux": "^9.1.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"react": ">=18",
|
|
19
|
+
"react-native": ">=0.73"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AuthContext — minimal auth state (authenticated/unauthenticated + user profile).
|
|
3
|
+
*
|
|
4
|
+
* In production, replace the mock login with real API calls and secure token storage.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* function App() {
|
|
8
|
+
* return (
|
|
9
|
+
* <AuthProvider>
|
|
10
|
+
* <RootNavigator />
|
|
11
|
+
* </AuthProvider>
|
|
12
|
+
* );
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* function ProfileScreen() {
|
|
16
|
+
* const { user, logout } = useAuth();
|
|
17
|
+
* return <Text>Hello, {user?.name}</Text>;
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import React, {
|
|
22
|
+
createContext,
|
|
23
|
+
useCallback,
|
|
24
|
+
useContext,
|
|
25
|
+
useMemo,
|
|
26
|
+
useState } from 'react';
|
|
27
|
+
|
|
28
|
+
const AuthContext = createContext(null);
|
|
29
|
+
|
|
30
|
+
// Simulates an API delay
|
|
31
|
+
function mockLogin(email, password){
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
if (password.length < 6) {
|
|
35
|
+
reject(new Error('Password must be at least 6 characters'));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
resolve({
|
|
39
|
+
id: '1',
|
|
40
|
+
name: email.split('@')[0] ?? 'User',
|
|
41
|
+
email,
|
|
42
|
+
avatarUrl: `https://api.dicebear.com/7.x/initials/svg?seed=${encodeURIComponent(email)}` });
|
|
43
|
+
}, 800);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function AuthProvider({ children }){
|
|
48
|
+
const [user, setUser] = useState(null);
|
|
49
|
+
const [loading, setLoading] = useState(false);
|
|
50
|
+
const [error, setError] = useState(null);
|
|
51
|
+
|
|
52
|
+
const login = useCallback(async (email, password) => {
|
|
53
|
+
setLoading(true);
|
|
54
|
+
setError(null);
|
|
55
|
+
try {
|
|
56
|
+
const loggedInUser = await mockLogin(email, password);
|
|
57
|
+
setUser(loggedInUser);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
setError(err instanceof Error ? err.message : 'Login failed');
|
|
60
|
+
} finally {
|
|
61
|
+
setLoading(false);
|
|
62
|
+
}
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const logout = useCallback(() => {
|
|
66
|
+
setUser(null);
|
|
67
|
+
setError(null);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const clearError = useCallback(() => setError(null), []);
|
|
71
|
+
|
|
72
|
+
const value = useMemo(
|
|
73
|
+
() => ({
|
|
74
|
+
user,
|
|
75
|
+
isAuthenticated: user !== null,
|
|
76
|
+
loading,
|
|
77
|
+
error,
|
|
78
|
+
login,
|
|
79
|
+
logout,
|
|
80
|
+
clearError }),
|
|
81
|
+
[user, loading, error, login, logout, clearError],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Hook to consume AuthContext — must be used inside <AuthProvider>. */
|
|
88
|
+
export function useAuth(){
|
|
89
|
+
const ctx = useContext(AuthContext);
|
|
90
|
+
if (!ctx) {
|
|
91
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
92
|
+
}
|
|
93
|
+
return ctx;
|
|
94
|
+
}
|