login and logout implemented for radiologist

This commit is contained in:
yashwin-foxy 2025-07-21 18:47:22 +05:30
parent c3d92f11ea
commit 1e2e4eba47
51 changed files with 1361 additions and 845 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
BASE_URL='https://neoscan-backend.tech4bizsolutions.com'
# BASE_URL='http://192.168.1.87:3000'

32
App.tsx
View File

@ -15,15 +15,14 @@ import {
useColorScheme,
View,
} from 'react-native';
import { Provider } from 'react-redux'; // Redux Provider
import store from './app/redux/store'; // Redux store
import {
Colors,
DebugInstructions,
Header,
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
import TabNavigator from './app/navigation/TabNavigator';
import AppNavigator from './app/navigation/AppNavigator';
import Toast from 'react-native-toast-message'; // Import Toast
type SectionProps = PropsWithChildren<{
title: string;
@ -60,6 +59,8 @@ function App(): React.JSX.Element {
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
flex:1,
justifyContent:'center'
};
/*
@ -73,18 +74,25 @@ function App(): React.JSX.Element {
*/
const safePadding = '5%';
// Wrap the app with Redux Provider to enable global state management
return (
<View style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<TabNavigator/>
</View>
<Provider store={store}>
<View style={styles.container}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<AppNavigator />
</View>
<Toast />
</Provider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,

View File

@ -1,6 +1,7 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
/**
* This is the configuration block to customize your React Native Android app.
@ -117,3 +118,4 @@ dependencies {
implementation jscFlavor
}
}
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")

View File

@ -1,6 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<application
android:name=".MainApplication"

3
app/constants/URLS.ts Normal file
View File

@ -0,0 +1,3 @@
import Config from 'react-native-config';
export const BASE_URL=Config.BASE_URL;

View File

@ -1,69 +0,0 @@
/*
* File: PinInput.tsx
* Description: Component for PIN entry
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import { View, StyleSheet, TextInput as RNTextInput } from 'react-native';
import { Colors, Spacing, BorderRadius, Typography } from 'shared/src/theme';
interface PinInputProps {
value: string;
onChange: (val: string) => void;
length?: number;
}
/**
* PinInput - input for entering PIN code
*/
const PinInput: React.FC<PinInputProps> = ({ value, onChange, length = 4 }) => {
return (
<View style={styles.container}>
{Array.from({ length }).map((_, idx) => (
<RNTextInput
key={idx}
style={styles.input}
value={value[idx] || ''}
onChangeText={text => {
const newValue = value.substring(0, idx) + text + value.substring(idx + 1);
onChange(newValue);
}}
maxLength={1}
keyboardType="number-pad"
secureTextEntry
/>
))}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
justifyContent: 'center',
marginVertical: Spacing.md,
},
input: {
width: 40,
height: 48,
marginHorizontal: Spacing.xs,
backgroundColor: Colors.backgroundAlt,
borderRadius: BorderRadius.md,
textAlign: 'center',
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.lg,
color: Colors.textPrimary,
borderWidth: 1,
borderColor: Colors.border,
},
});
export default PinInput;
/*
* End of File: PinInput.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -6,7 +6,7 @@
*/
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { LoginScreen, SetupBiometricScreen } from '../screens';
import { Colors } from '../../../../shared/src/theme';
@ -15,7 +15,7 @@ export type AuthStackParamList = {
SetupBiometric: undefined;
};
const Stack = createStackNavigator<AuthStackParamList>();
const Stack = createNativeStackNavigator<AuthStackParamList>();
/**
* AuthNavigator sets up stack navigation for Auth module
@ -32,7 +32,7 @@ const AuthNavigator: React.FC = () => (
<Stack.Screen
name="Login"
component={LoginScreen}
options={{ title: 'Login' }}
options={{ title: 'Login',headerShown:false }}
/>
<Stack.Screen
name="SetupBiometric"

View File

@ -17,9 +17,11 @@ export const login = createAsyncThunk(
async (credentials: { email: string; password: string }, { dispatch, rejectWithValue }) => {
try {
dispatch(loginStart());
const response = await authAPI.login(credentials.email, credentials.password);
if (response.ok && response.data) {
dispatch(loginSuccess(response.data));
const response:any = await authAPI.login(credentials.email, credentials.password,'web');
console.log('user response',response)
if (response.ok && response.data &&response.data.data) {
//@ts-ignore
dispatch(loginSuccess({...response.data.data.user,access_token:response.data.data.access_token}));
} else {
dispatch(loginFailure(response.problem || 'Unknown error'));
return rejectWithValue(response.problem || 'Unknown error');

View File

@ -5,7 +5,7 @@
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { RootState } from 'app/redux/rootReducer';
import { RootState } from '../../../../app/redux/rootReducer';
export const selectUser = (state: RootState) => state.auth.user;
export const selectAuthLoading = (state: RootState) => state.auth.loading;

View File

@ -8,15 +8,50 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
// Sample user type
export interface User {
id: string;
name: string;
interface NotificationPreferences {
critical_alerts: {
email: boolean;
in_app: boolean;
push: boolean;
sms: boolean;
};
system_notifications: {
push: boolean;
email: boolean;
in_app: boolean;
};
}
interface DashboardSettings {
theme: string;
language: string;
timezone: string;
}
interface UserProfile {
accent_color: string | null;
dashboard_role: string;
dashboard_settings: DashboardSettings;
display_name: string;
email: string;
first_name: string;
last_name: string;
hospital_id: string;
notification_preferences: NotificationPreferences;
onboarded: boolean;
onboarding_completed: boolean;
onboarding_message: string;
onboarding_step: number;
profile_photo_url: string | null;
theme_color: string | null;
user_id: string;
access_token: string;
}
// Auth state type
interface AuthState {
user: User | null;
user: UserProfile | null;
loading: boolean;
error: string | null;
isAuthenticated: boolean;
@ -40,7 +75,7 @@ const authSlice = createSlice({
state.loading = true;
state.error = null;
},
loginSuccess(state, action: PayloadAction<User>) {
loginSuccess(state, action: PayloadAction<UserProfile>) {
state.user = action.payload;
state.isAuthenticated = true;
state.loading = false;

View File

@ -1,50 +1,99 @@
/*
* File: LoginScreen.tsx
* Description: Login screen for user authentication
* Description: Login screen with a loading indicator on the submit button.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { TextInput } from '../../../../shared/src/components/Input';
import {
View,
Text,
StyleSheet,
TouchableWithoutFeedback,
Keyboard,
TouchableOpacity,
TextInput, // Using default TextInput for container styling
} from 'react-native';
import { Button } from '../../../../shared/src/components/Button';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
import { useDispatch } from 'react-redux';
import { useAppDispatch } from '../../../redux/hooks';
import { login } from '../redux/authActions';
import { showError } from '../../../../shared/src/utils/helpers/Toast';
import { validateEmail } from '../../../../shared/src/utils/validation/validators';
import Icon from 'react-native-vector-icons/Feather';
/**
* LoginScreen - allows user to login with email and password
*/
const LoginScreen: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const dispatch = useDispatch();
const [isPasswordVisible, setPasswordVisible] = useState(false);
const [loading, setLoading] = useState(false); // Add loading state
const dispatch = useAppDispatch();
const handleLogin = () => {
if (!email.trim() || !password.trim()) {
showError('Validation Error', 'Email and password are required.');
return;
}
if (!validateEmail(email)) {
showError('Validation Error', 'Please enter a valid email address.');
return;
}
setLoading(true); // Start loading
dispatch(login({ email, password }));
// Simulate a network request for 4 seconds
setTimeout(() => {
setLoading(false); // Stop loading after 4 seconds
}, 4000);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Radiologist Portal</Text>
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
style={styles.input}
autoCapitalize="none"
keyboardType="email-address"
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
style={styles.input}
secureTextEntry
/>
<Button title="Login" onPress={handleLogin} style={styles.button} />
</View>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.container}>
<Text style={styles.title}>Radiologist Portal</Text>
{/* Email Input */}
<View style={styles.inputContainer}>
<Icon name="mail" size={20} color={Colors.textSecondary} style={styles.inputIcon} />
<TextInput
placeholder="Email"
value={email}
onChangeText={setEmail}
style={styles.inputField}
autoCapitalize="none"
keyboardType="email-address"
placeholderTextColor={Colors.textMuted}
/>
</View>
{/* Password Input */}
<View style={styles.inputContainer}>
<Icon name="lock" size={20} color={Colors.textSecondary} style={styles.inputIcon} />
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
style={styles.inputField}
secureTextEntry={!isPasswordVisible}
placeholderTextColor={Colors.textMuted}
/>
<TouchableOpacity
onPress={() => setPasswordVisible(!isPasswordVisible)}
style={styles.eyeIcon}>
<Icon name={isPasswordVisible ? 'eye-off' : 'eye'} size={22} color={Colors.textSecondary} />
</TouchableOpacity>
</View>
<Button
title="Login"
onPress={handleLogin}
style={styles.button}
loading={loading} // Pass loading state to button
/>
</View>
</TouchableWithoutFeedback>
);
};
@ -62,8 +111,28 @@ const styles = StyleSheet.create({
marginBottom: Spacing.xl,
textAlign: 'center',
},
input: {
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.backgroundAlt,
borderWidth: 1,
borderColor: Colors.border,
borderRadius: 8,
marginBottom: Spacing.md,
paddingHorizontal: Spacing.md,
paddingVertical:2
},
inputIcon: {
marginRight: Spacing.sm,
},
inputField: {
flex: 1,
paddingVertical: Spacing.md,
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
},
eyeIcon: {
paddingLeft: Spacing.sm,
},
button: {
marginTop: Spacing.md,

View File

@ -1,66 +1,192 @@
/*
* File: SetupBiometricScreen.tsx
* Description: Screen for setting up biometric authentication
* Description: Screen for setting up and authenticating with device biometrics or PIN.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Button } from '../../../../shared/src/components/Button';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
import { useBiometric } from '../hooks/useBiometric';
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
Alert,
StyleSheet,
SafeAreaView,
} from 'react-native';
import ReactNativeBiometrics, {
BiometryTypes,
BiometryType,
} from 'react-native-biometrics';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Colors, Spacing, Typography, Shadows } from '../../../../shared/src/theme';
import { BASE_URL } from '../../../constants/URLS';
const PAYLOAD = 'neo-scan-secure-payload'; // A static payload for signature
/**
* SetupBiometricScreen - allows user to setup biometric authentication
* SetupBiometricScreen - Manages user authentication using the device's
* built-in biometrics or PIN/password.
*/
const SetupBiometricScreen: React.FC = () => {
const { authenticate } = useBiometric();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [biometryType, setBiometryType] = useState<BiometryType | null>(null);
const [isAvailable, setIsAvailable] = useState(false);
const rnBiometrics = new ReactNativeBiometrics({ allowDeviceCredentials: true });
console.log('base url',BASE_URL)
useEffect(() => {
checkBiometricAvailability();
checkAuthStatus();
}, []);
const handleSetup = async () => {
await authenticate();
// TODO: Handle result and navigate
const checkBiometricAvailability = async () => {
try {
const { available, biometryType: bioType } = await rnBiometrics.isSensorAvailable();
setIsAvailable(available);
setBiometryType(bioType || null);
} catch (error) {
console.error('Biometric check failed:', error);
Alert.alert('Error', 'Could not check for biometric features.');
}
};
const checkAuthStatus = async () => {
try {
const authStatus = await AsyncStorage.getItem('isAuthenticated');
setIsAuthenticated(authStatus === 'true');
} catch (error) {
console.error('Auth status check failed:', error);
}
};
const handleSuccessfulAuth = async () => {
setIsAuthenticated(true);
await AsyncStorage.setItem('isAuthenticated', 'true');
Alert.alert('Success', 'Authentication successful!');
};
const authenticateUser = async () => {
if (!isAvailable) {
Alert.alert('Not Available', 'Biometrics are not supported on this device.');
return;
}
try {
// Check if keys exist. If not, create them.
const { keysExist } = await rnBiometrics.biometricKeysExist();
if (!keysExist) {
await rnBiometrics.createKeys();
}
// Create a signature to authenticate. This will fallback to device PIN.
const { success, signature } = await rnBiometrics.createSignature({
promptMessage: 'Authenticate',
payload: PAYLOAD,
});
if (success && signature) {
// In a real app, you would verify the signature on your server.
// For this example, we'll assume it's valid.
await handleSuccessfulAuth();
} else {
Alert.alert('Authentication Failed', 'You could not be authenticated.');
}
} catch (error) {
console.error('Authentication failed:', error);
Alert.alert('Error', 'An unexpected error occurred during authentication.');
}
};
const getBiometricTypeText = () => {
switch (biometryType) {
case BiometryTypes.TouchID: return 'Touch ID';
case BiometryTypes.FaceID: return 'Face ID';
case BiometryTypes.Biometrics: return 'Fingerprint';
default: return 'Device Credentials';
}
};
const logout = async () => {
try {
setIsAuthenticated(false);
await AsyncStorage.setItem('isAuthenticated', 'false');
} catch (error) {
console.error('Logout failed:', error);
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>Setup Biometric Login</Text>
<Text style={styles.info}>Enable Face ID or Touch ID for quick and secure login.</Text>
<Button title="Enable Biometric" onPress={handleSetup} style={styles.button} />
</View>
<SafeAreaView style={styles.container}>
{isAuthenticated ? (
<View style={styles.content}>
<Text style={styles.title}>Welcome!</Text>
<Text style={styles.subtitle}>You are successfully authenticated.</Text>
<TouchableOpacity style={styles.button} onPress={logout}>
<Text style={styles.buttonText}>Logout</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.content}>
<Text style={styles.title}>Authentication Required</Text>
{isAvailable ? (
<Text style={styles.subtitle}>
Use {getBiometricTypeText()} or your device PIN to continue.
</Text>
) : (
<Text style={styles.subtitle}>
Biometrics not available. Please use your device PIN to continue.
</Text>
)}
<TouchableOpacity style={styles.button} onPress={authenticateUser}>
<Text style={styles.buttonText}>Authenticate</Text>
</TouchableOpacity>
</View>
)}
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.background,
backgroundColor: Colors.backgroundAlt,
},
content: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: Spacing.lg,
},
title: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.title,
color: Colors.primary,
marginBottom: Spacing.lg,
color: Colors.textPrimary,
marginBottom: Spacing.md,
textAlign: 'center',
},
info: {
subtitle: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
marginBottom: Spacing.xl,
textAlign: 'center',
marginBottom: Spacing.xl,
paddingHorizontal: Spacing.lg,
},
button: {
marginTop: Spacing.md,
backgroundColor: Colors.primary,
paddingHorizontal: Spacing.xl,
paddingVertical: Spacing.md,
borderRadius: 8,
marginVertical: Spacing.sm,
...Shadows.sm,
minWidth: '80%',
alignItems: 'center',
},
buttonText: {
color: Colors.background,
fontSize: Typography.fontSize.md,
fontFamily: Typography.fontFamily.bold,
},
});
export default SetupBiometricScreen;
/*
* End of File: SetupBiometricScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -6,17 +6,19 @@
*/
import { create } from 'apisauce';
import { BASE_URL } from '../../../constants/URLS';
import { buildHeaders } from '../../../../shared/src/utils/helpers/Api';
const api = create({
baseURL: 'https://api.example.com', // TODO: Replace with actual endpoint
timeout: 10000,
baseURL: BASE_URL, // TODO: Replace with actual endpoint
timeout: 3000,
});
/**
* login - authenticates user with email and password
*/
export const authAPI = {
login: (email: string, password: string) => api.post('/login', { email, password }),
login: (email: string, password: string,platform:string) => api.post('/api/auth/auth/login', { email, password,platform },buildHeaders()),
// Add more endpoints as needed
};

View File

@ -5,7 +5,7 @@
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export * from './authAPI';
export * from './';
export * from './biometricService';
/*

View File

@ -26,14 +26,12 @@ const CaseReviewNavigator: React.FC = () => (
initialRouteName="CaseDetails"
screenOptions={{
headerStyle: { backgroundColor: Colors.primary },
headerTintColor: Colors.background,
headerTitleStyle: { fontWeight: 'bold' },
}}
>
<Stack.Screen
name="CaseDetails"
component={CaseDetailsScreen}
options={{ title: 'Case Details' }}
options={{ title: 'Case Details',headerShown:false }}
/>
<Stack.Screen
name="DICOMViewer"

View File

@ -5,7 +5,7 @@
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { RootState } from 'app/redux/rootReducer';
import { RootState } from '../../../../app/redux/rootReducer';
export const selectSelectedCase = (state: RootState) => state.caseReview.selectedCase;
export const selectCaseReviewLoading = (state: RootState) => state.caseReview.loading;

View File

@ -18,22 +18,23 @@ import { useAIOverlay } from '../hooks/useAIOverlay';
* CaseDetailsScreen - shows patient info, AI findings, and navigation to DICOM viewer
*/
const CaseDetailsScreen: React.FC<any> = ({ navigation }) => {
const caseData = useSelector(selectSelectedCase);
const { findings, confidence } = useAIOverlay();
// const caseData = useSelector(selectSelectedCase);
// const { findings, confidence } = useAIOverlay();
if (!caseData) {
return <View style={styles.container}><Text>No case selected.</Text></View>;
}
// if (!caseData) {
// return <View style={styles.container}><Text>No case selected.</Text></View>;
// }
return (
<View style={styles.container}>
<Card>
{/* <Card>
<Text style={styles.title}>{caseData.patientName}</Text>
<Text style={styles.label}>AI Findings:</Text>
<Text style={styles.findings}>{findings}</Text>
<Text style={styles.confidence}>Confidence: {confidence}%</Text>
<Button title="View DICOM Images" onPress={() => navigation.navigate('DICOMViewer')} style={styles.button} />
</Card>
</Card> */}
<Text style={{color:'#000',fontWeight:'600'}} >Coming Soon..</Text>
</View>
);
};
@ -44,6 +45,7 @@ const styles = StyleSheet.create({
backgroundColor: Colors.background,
justifyContent: 'center',
padding: Spacing.lg,
alignItems:'center'
},
title: {
fontFamily: Typography.fontFamily.bold,

View File

@ -25,14 +25,12 @@ const DashboardNavigator: React.FC = () => (
initialRouteName="Dashboard"
screenOptions={{
headerStyle: { backgroundColor: Colors.primary },
headerTintColor: Colors.background,
headerTitleStyle: { fontWeight: 'bold' },
}}
>
<Stack.Screen
name="Dashboard"
component={DashboardScreen}
options={{ title: 'Dashboard' }}
options={{ title: 'Dashboard' ,headerShown:false}}
/>
{/* Add more screens here */}
</Stack.Navigator>

View File

@ -5,7 +5,7 @@
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { RootState } from 'app/redux/rootReducer';
import { RootState } from '../../../../app/redux/rootReducer';
export const selectCaseQueue = (state: RootState) => state.dashboard.caseQueue;
export const selectDashboardLoading = (state: RootState) => state.dashboard.loading;

View File

@ -0,0 +1,44 @@
/*
* File: DashboardScreen.tsx
* Description: Sample dashboard screen using Clinical Blue Interface and shared components
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
/**
* DashboardScreen demonstrates usage of all shared, themed components.
* This is a reference implementation for other modules.
*/
const DashBoardDetail: React.FC = () => {
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={{color:'#FFFFFF'}} >Dashboard Detail screen</Text>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.background,
},
content: {
padding: Spacing.lg,
flex:1
}
});
export default DashBoardDetail;
/*
* End of File: DashboardScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -1,25 +1,26 @@
/*
* File: DashboardScreen.tsx
* Description: Sample dashboard screen using Clinical Blue Interface and shared components
* Description: Main dashboard screen with a custom header and themed components.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import { View, Text, StyleSheet, ScrollView } from 'react-native';
import { Button } from 'shared/src/components/Button';
import { Card, InfoCard } from 'shared/src/components/Card';
import { TextInput, SearchInput } from 'shared/src/components/Input';
import { Modal, ConfirmModal, AlertModal } from 'shared/src/components/Modal';
import { Spinner, LoadingOverlay } from 'shared/src/components/Loading';
import { CustomIcon, IconButton } from 'shared/src/components/Icons';
import { Colors, Spacing, Typography } from 'shared/src/theme';
import { Button } from '../../../../shared/src/components/Button';
import { Card, InfoCard } from '../../../../shared/src/components/Card';
import { TextInput, SearchInput } from '../../../../shared/src/components/Input';
import { Modal, ConfirmModal, AlertModal } from '../../../../shared/src/components/Modal';
import { Spinner, LoadingOverlay } from '../../../../shared/src/components/Loading';
import { CustomIcon, IconButton } from '../../../../shared/src/components/Icons';
import { CustomHeader } from '../../../../shared/src/components/Header'; // Import CustomHeader
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
/**
* DashboardScreen demonstrates usage of all shared, themed components.
* This is a reference implementation for other modules.
* DashboardScreen - The primary landing screen after login, featuring a custom header,
* case summaries, and navigation to other parts of the app.
*/
const DashboardScreen: React.FC = () => {
const DashboardScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
const [modalVisible, setModalVisible] = useState(false);
const [confirmVisible, setConfirmVisible] = useState(false);
const [alertVisible, setAlertVisible] = useState(false);
@ -28,66 +29,96 @@ const DashboardScreen: React.FC = () => {
const [input, setInput] = useState('');
return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.header}>Dashboard</Text>
<SearchInput
placeholder="Search cases..."
value={search}
onChangeText={setSearch}
containerStyle={styles.search}
<View style={styles.container}>
<CustomHeader
title="Dashboard"
showBackButton={false}
/>
<InfoCard>
<Text style={styles.infoTitle}>Welcome, Dr. Smith</Text>
<Text style={styles.infoText}>On-Call Status: <Text style={styles.active}>ACTIVE</Text></Text>
</InfoCard>
<Card>
<Text style={styles.cardTitle}>Critical Cases</Text>
<View style={styles.row}>
<CustomIcon name="alert" color={Colors.error} size={24} />
<Text style={styles.criticalText}>Bed 3 - Hemorrhage (93% AI)</Text>
<Button title="Review Now" onPress={() => setModalVisible(true)} style={styles.button} />
</View>
</Card>
<Card>
<Text style={styles.cardTitle}>Routine Cases</Text>
<View style={styles.row}>
<CustomIcon name="check-circle" color={Colors.success} size={24} />
<Text style={styles.routineText}>Bed 12 - Headache (99% AI)</Text>
<IconButton name="chevron-right" onPress={() => setConfirmVisible(true)} />
</View>
</Card>
<TextInput
placeholder="Add note..."
value={input}
onChangeText={setInput}
style={styles.input}
/>
<Button title="Show Alert" onPress={() => setAlertVisible(true)} style={styles.button} />
<Button title={loading ? "Loading..." : "Show Loading Overlay"} onPress={() => setLoading(true)} style={styles.button} />
<Spinner style={styles.spinner} />
{/* Modals */}
<Modal visible={modalVisible} onRequestClose={() => setModalVisible(false)}>
<Text style={styles.modalTitle}>Critical Case Details</Text>
<Text>Patient: John Doe, 45M</Text>
<Button title="Close" onPress={() => setModalVisible(false)} style={styles.button} />
</Modal>
<ConfirmModal
visible={confirmVisible}
title="Confirm Action"
message="Are you sure you want to proceed?"
onConfirm={() => { setConfirmVisible(false); }}
onCancel={() => setConfirmVisible(false)}
/>
<AlertModal
visible={alertVisible}
title="Critical Alert"
message="Acute Subdural Hemorrhage detected!"
iconName="alert-circle"
iconColor={Colors.error}
onDismiss={() => setAlertVisible(false)}
/>
<LoadingOverlay visible={loading} />
</ScrollView>
<ScrollView contentContainerStyle={styles.content}>
<SearchInput
placeholder="Search cases..."
value={search}
onChangeText={setSearch}
containerStyle={styles.search}
/>
<InfoCard>
<Text style={styles.infoTitle}>Welcome, Dr. Smith</Text>
<Text style={styles.infoText}>
On-Call Status: <Text style={styles.active}>ACTIVE</Text>
</Text>
</InfoCard>
<Card>
<Text style={styles.cardTitle}>Critical Cases</Text>
<View style={styles.row}>
<CustomIcon name="alert" color={Colors.error} size={24} />
<Text style={styles.criticalText}>Bed 3 - Hemorrhage (93% AI)</Text>
<Button
title="Review Now"
onPress={() => setModalVisible(true)}
style={styles.button}
/>
</View>
</Card>
<Card>
<Text style={styles.cardTitle}>Routine Cases</Text>
<View style={styles.row}>
<CustomIcon name="check-circle" color={Colors.success} size={24} />
<Text style={styles.routineText}>Bed 12 - Headache (99% AI)</Text>
<IconButton
name="chevron-right"
onPress={() => setConfirmVisible(true)}
/>
</View>
</Card>
<TextInput
placeholder="Add note..."
value={input}
onChangeText={setInput}
style={styles.input}
/>
<Button
title="Show Alert"
onPress={() => setAlertVisible(true)}
style={styles.button}
/>
<Button
title={loading ? 'Loading...' : 'Show Loading Overlay'}
onPress={() => setLoading(true)}
style={styles.button}
/>
{/* <Spinner style={styles.spinner} /> */}
{/* Modals */}
<Modal
visible={modalVisible}
onRequestClose={() => setModalVisible(false)}>
<Text style={styles.modalTitle}>Critical Case Details</Text>
<Text>Patient: John Doe, 45M</Text>
<Button
title="Close"
onPress={() => setModalVisible(false)}
style={styles.button}
/>
</Modal>
<ConfirmModal
visible={confirmVisible}
title="Confirm Action"
message="Are you sure you want to proceed?"
onConfirm={() => {
setConfirmVisible(false);
}}
onCancel={() => setConfirmVisible(false)}
/>
<AlertModal
visible={alertVisible}
title="Critical Alert"
message="Acute Subdural Hemorrhage detected!"
iconName="alert-circle"
iconColor={Colors.error}
onDismiss={() => setAlertVisible(false)}
/>
<LoadingOverlay visible={loading} />
</ScrollView>
</View>
);
};
@ -99,13 +130,6 @@ const styles = StyleSheet.create({
content: {
padding: Spacing.lg,
},
header: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.title,
color: Colors.primary,
marginBottom: Spacing.lg,
textAlign: 'center',
},
search: {
marginBottom: Spacing.md,
},

View File

@ -6,17 +6,21 @@
*/
import { create } from 'apisauce';
import { BASE_URL } from '../../../constants/URLS';
import { buildHeaders } from '../../../../shared/src/utils/helpers/Api';
const api = create({
baseURL: 'https://api.example.com', // TODO: Replace with actual endpoint
timeout: 10000,
baseURL: BASE_URL, // TODO: Replace with actual endpoint
timeout: 3000,
});
/**
* getCases - fetches the list of cases from the server
*/
export const caseAPI = {
getCases: () => api.get('/cases'),
getCases: (token:string) => api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data',{},buildHeaders({token})),
// Add more endpoints as needed
};

View File

@ -7,7 +7,7 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Colors, Spacing, Typography } from 'shared/src/theme';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
interface PreferencesFormProps {
preferences: any;

View File

@ -7,7 +7,7 @@
import React from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
import { Colors, Spacing, Typography } from 'shared/src/theme';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
interface ProfileHeaderProps {
user: { name: string; avatar?: string };

View File

@ -7,7 +7,7 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Colors, Spacing, Typography } from 'shared/src/theme';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
interface SecuritySettingsProps {
security: any;

View File

@ -7,7 +7,7 @@
import React from 'react';
import { View, Text, StyleSheet, Switch } from 'react-native';
import { Colors, Spacing, Typography } from 'shared/src/theme';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
interface SettingsPanelProps {
settings: { notificationsEnabled: boolean; darkMode: boolean; language: string };

View File

@ -8,7 +8,7 @@
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { ProfileScreen, SettingsScreen, PreferencesScreen } from '../screens';
import { Colors } from 'shared/src/theme';
import { Colors } from '../../../../shared/src/theme';
export type ProfileStackParamList = {
Profile: undefined;
@ -26,14 +26,12 @@ const ProfileNavigator: React.FC = () => (
initialRouteName="Profile"
screenOptions={{
headerStyle: { backgroundColor: Colors.primary },
headerTintColor: Colors.background,
headerTitleStyle: { fontWeight: 'bold' },
}}
>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{ title: 'Profile' }}
options={{ title: 'Profile',headerShown:false }}
/>
<Stack.Screen
name="Settings"

View File

@ -5,9 +5,9 @@
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { RootState } from 'app/redux/rootReducer';
import { RootState } from '../../../../app/redux/rootReducer';
export const selectUserProfile = (state: RootState) => state.profile.userProfile;
export const selectUserProfile = (state: RootState) => state.auth.user;
export const selectProfileLoading = (state: RootState) => state.profile.loading;
export const selectProfileError = (state: RootState) => state.profile.error;
export const selectUserSettings = (state: RootState) => state.settings.settings;

View File

@ -14,6 +14,7 @@ export interface UserProfile {
email: string;
avatar?: string;
role?: string;
location?: string;
}
// Profile state type
@ -24,7 +25,14 @@ interface ProfileState {
}
const initialState: ProfileState = {
userProfile: null,
userProfile: {
id:'1',
name:'laxman h',
email:'laxman.halaki@tech4biz.org',
avatar:'',
role:'doctor'
},
loading: false,
error: null,
};

View File

@ -7,8 +7,8 @@
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { Card } from 'shared/src/components/Card';
import { Colors, Spacing, Typography } from 'shared/src/theme';
import { Card } from '../../../../shared/src/components/Card';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
/**
* PreferencesScreen - shows stub content for preferences form

View File

@ -1,70 +1,494 @@
/*
* File: ProfileScreen.tsx
* Description: Screen for displaying user profile
* Description: Screen for displaying and managing user profile information, with editable name fields.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import { View, Text, StyleSheet, Image } from 'react-native';
import { Button } from 'shared/src/components/Button';
import { Card } from 'shared/src/components/Card';
import { Colors, Spacing, Typography } from 'shared/src/theme';
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
Image,
ScrollView,
TouchableOpacity,
Alert,
Switch,
Modal, // Import Modal
TextInput, // Import TextInput
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { useProfile } from '../hooks/useProfile';
import {
Colors,
Spacing,
Typography,
Shadows,
BorderRadius,
} from '../../../../shared/src/theme';
import { Button } from '../../../../shared/src/components/Button';
import { useDispatch } from 'react-redux';
import { logout } from '../../Auth/redux/authSlice';
/**
* ProfileScreen - shows user info, avatar, and navigation to settings/preferences
*/
const ProfileScreen: React.FC<any> = ({ navigation }) => {
const user = useProfile();
interface InfoRowProps {
icon: string;
label?: string;
value?: string;
onEdit?: () => void;
isNav?: boolean;
isLogout?: boolean;
}
if (!user) {
return <View style={styles.container}><Text>No profile loaded.</Text></View>;
}
const InfoRow: React.FC<InfoRowProps> = ({
icon,
label,
value,
onEdit,
isNav,
isLogout,
}) => (
<TouchableOpacity
style={[styles.infoCard, isLogout && styles.logoutCard]}
onPress={onEdit}
disabled={!onEdit}>
<Icon
name={icon}
size={22}
color={isLogout ? Colors.error : Colors.textSecondary}
style={styles.infoIcon}
/>
<Text
style={[
styles.infoValue,
!value && styles.infoLabel,
isLogout && styles.logoutText,
]}>
{value || label}
</Text>
{onEdit && !isNav && !isLogout && (
<Icon name="edit-2" size={20} color={Colors.textMuted} />
)}
{isNav && (
<Icon name="chevron-right" size={22} color={Colors.textMuted} />
)}
</TouchableOpacity>
);
const SectionHeader: React.FC<{ title: string; icon?: string }> = ({ title, icon }) => (
<View style={styles.sectionHeader}>
{icon && <Icon name={icon} size={18} color={Colors.primary} style={{ marginRight: 8 }} />}
<Text style={styles.sectionHeaderText}>{title}</Text>
</View>
);
// New component for individual notification toggles
const NotificationToggle: React.FC<{
label: string;
isEnabled: boolean;
onToggle: (value: boolean) => void;
}> = ({ label, isEnabled, onToggle }) => (
<View style={styles.notificationRow}>
<Text style={styles.notificationLabel}>{label}</Text>
<Switch
value={isEnabled}
onValueChange={onToggle}
trackColor={{ false: Colors.inactiveState, true: Colors.success }}
thumbColor={Colors.background}
/>
</View>
);
const ProfileScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
const initialUser = useProfile() || {
user_id: '9fe5cd7f-e563-4c7f-93ec-6de64e12b83e',
email: 'radiologist@gmail.com',
first_name: 'Aman',
last_name: 'Kumar',
display_name: 'Aman Kumar',
dashboard_role: 'radiologist',
hospital_id: '0240c6d9-415f-49c9-ae24-7eba4927f939',
dashboard_settings: {
theme: 'light',
language: 'en',
timezone: 'UTC',
},
notification_preferences: {
critical_alerts: {
sms: true,
push: true,
email: true,
in_app: true,
},
system_notifications: {
push: true,
email: true,
in_app: true,
},
},
onboarding_completed: false,
onboarding_step: 0,
onboarding_message: 'You need to complete onboarding (change password and upload profile photo).',
onboarded: false,
profile_photo_url: null,
theme_color: null,
accent_color: null,
};
// State for user data, notifications, and the edit modal
const [user, setUser] = useState(initialUser);
const [notifications, setNotifications] = useState(user.notification_preferences);
const [isModalVisible, setModalVisible] = useState(false);
const [editingField, setEditingField] = useState('');
const [currentValue, setCurrentValue] = useState('');
const dispatch=useDispatch();
const handleLogout = () => {
Alert.alert('Log Out', 'Are you sure you want to log out?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'OK', onPress: () => dispatch(logout()) },
]);
};
// Updated handleEdit to open the modal
const handleEdit = (field: string, value: string) => {
setEditingField(field);
setCurrentValue(value);
setModalVisible(true);
};
// New function to save changes from the modal
const handleSave = () => {
const updatedUser = { ...user };
if (editingField === 'First Name') {
updatedUser.first_name = currentValue;
} else if (editingField === 'Last Name') {
updatedUser.last_name = currentValue;
}
updatedUser.display_name = `${updatedUser.first_name} ${updatedUser.last_name}`;
setUser(updatedUser);
setModalVisible(false);
Alert.alert('Profile Updated', `${editingField} has been changed.`);
};
const handleNotificationToggle = (
category: 'critical_alerts' | 'system_notifications',
channel: string,
) => {
const newPrefs = { ...notifications };
// Asserting the channel type to fix TypeScript indexing error
(newPrefs[category] as any)[channel] = !(newPrefs[category] as any)[channel];
setNotifications(newPrefs);
// In a real app, you would dispatch an action here to save the new preferences.
Alert.alert(
'Preference Updated',
`${channel.toUpperCase()} for ${category.replace('_', ' ')} has been ${
(newPrefs[category] as any)[channel] ? 'enabled' : 'disabled'
}.`,
);
};
return (
<View style={styles.container}>
<Card>
{user.avatar && <Image source={{ uri: user.avatar }} style={styles.avatar} />}
<Text style={styles.name}>{user.name}</Text>
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}>
{/* Profile Section */}
<View style={styles.header}>
<View style={styles.avatarContainer}>
<Image
source={{ uri: user.profile_photo_url || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(user.display_name) }}
style={styles.avatar}
/>
<TouchableOpacity
style={styles.editIconContainer}
onPress={() => handleEdit('Profile Photo', user.profile_photo_url || '')}>
<Icon name="edit-2" size={16} color={Colors.background} />
</TouchableOpacity>
</View>
<Text style={styles.name}>{user.display_name}</Text>
<Text style={styles.role}>{user.dashboard_role}</Text>
<Text style={styles.email}>{user.email}</Text>
<Button title="Settings" onPress={() => navigation.navigate('Settings')} style={styles.button} />
<Button title="Preferences" onPress={() => navigation.navigate('Preferences')} style={styles.button} />
</Card>
</View>
</View>
{/* Onboarding Section */}
{!user.onboarding_completed && (
<View style={styles.onboardingSection}>
<SectionHeader title="Onboarding" icon="alert-circle" />
<Text style={styles.onboardingMessage}>{user.onboarding_message}</Text>
</View>
)}
{/* Personal Information Section - New */}
<View style={styles.infoSection}>
<SectionHeader title="Personal Information" icon="user" />
<InfoRow
icon="user"
label="First Name"
value={user.first_name}
onEdit={() => handleEdit('First Name', user.first_name)}
/>
<InfoRow
icon="user"
label="Last Name"
value={user.last_name}
onEdit={() => handleEdit('Last Name', user.last_name)}
/>
</View>
{/* Settings Section */}
{/* <View style={styles.infoSection}>
<SectionHeader title="Settings" icon="settings" />
<InfoRow icon="sun" label="Theme" value={user.dashboard_settings?.theme || '-'} />
<InfoRow icon="globe" label="Language" value={user.dashboard_settings?.language || '-'} />
<InfoRow icon="clock" label="Timezone" value={user.dashboard_settings?.timezone || '-'} />
</View> */}
{/* Notification Preferences Section - Updated with Toggles */}
<View style={styles.infoSection}>
<SectionHeader title="Notification Preferences" icon="bell" />
<View style={styles.notificationGroup}>
<Text style={styles.notificationCategory}>Critical Alerts</Text>
{Object.entries(notifications.critical_alerts).map(([channel, isEnabled]) => (
<NotificationToggle
key={`critical-${channel}`}
label={channel.toUpperCase()}
isEnabled={isEnabled}
onToggle={() => handleNotificationToggle('critical_alerts', channel)}
/>
))}
</View>
<View style={styles.notificationGroup}>
<Text style={styles.notificationCategory}>System Notifications</Text>
{Object.entries(notifications.system_notifications).map(
([channel, isEnabled]) => (
<NotificationToggle
key={`system-${channel}`}
label={channel.toUpperCase()}
isEnabled={isEnabled}
onToggle={() => handleNotificationToggle('system_notifications', channel)}
/>
),
)}
</View>
</View>
{/* Actions Section */}
<View style={styles.actionsSection}>
<InfoRow
icon="help-circle"
label="Support"
isNav
onEdit={() => navigation.navigate('Support')}
/>
<InfoRow icon="log-out" label="Log Out" isLogout onEdit={handleLogout} />
</View>
{/* Edit Modal */}
<Modal
transparent={true}
visible={isModalVisible}
onRequestClose={() => setModalVisible(false)}>
<View style={styles.modalContainer}>
<View style={styles.modalContent}>
<Text style={styles.modalTitle}>Edit {editingField}</Text>
<TextInput
style={styles.modalInput}
value={currentValue}
onChangeText={setCurrentValue}
autoFocus
/>
<View style={styles.modalActions}>
<Button title="Cancel" onPress={() => setModalVisible(false)} style={styles.modalButton} />
<Button title="Save" onPress={handleSave} style={styles.modalButton} />
</View>
</View>
</View>
</Modal>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: Colors.background,
justifyContent: 'center',
padding: Spacing.lg,
backgroundColor: Colors.backgroundAlt,
},
contentContainer: {
paddingBottom: Spacing.xl,
},
header: {
alignItems: 'center',
paddingVertical: Spacing.xl,
paddingHorizontal: Spacing.lg,
},
avatarContainer: {
position: 'relative',
marginBottom: Spacing.md,
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
alignSelf: 'center',
marginBottom: Spacing.md,
width: 100,
height: 100,
borderRadius: 50,
borderWidth: 3,
borderColor: Colors.primary,
},
editIconContainer: {
position: 'absolute',
bottom: 2,
right: 2,
backgroundColor: Colors.primary,
padding: Spacing.xs,
borderRadius: 15,
borderWidth: 2,
borderColor: Colors.background,
},
name: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.lg,
color: Colors.primary,
fontSize: Typography.fontSize.xl,
color: Colors.textPrimary,
textAlign: 'center',
},
role: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
textAlign: 'center',
marginBottom: Spacing.xs,
},
email: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
color: Colors.textMuted,
textAlign: 'center',
marginBottom: Spacing.md,
},
button: {
marginTop: Spacing.sm,
onboardingSection: {
marginHorizontal: Spacing.lg,
marginBottom: Spacing.md,
backgroundColor: Colors.warning + '22',
borderRadius: BorderRadius.md,
padding: Spacing.md,
},
onboardingMessage: {
color: Colors.warning,
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.sm,
textAlign: 'center',
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: Spacing.xs,
marginTop: Spacing.md,
},
sectionHeaderText: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.md,
color: Colors.primary,
},
infoSection: {
marginHorizontal: Spacing.lg,
},
actionsSection: {
marginHorizontal: Spacing.lg,
marginTop: Spacing.lg,
borderTopWidth: 1,
borderTopColor: Colors.border,
paddingTop: Spacing.lg,
},
infoCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.cardBackground,
padding: Spacing.md,
borderRadius: BorderRadius.md,
marginBottom: Spacing.md,
...Shadows.sm,
},
infoIcon: {
marginRight: Spacing.md,
},
infoLabel: {
flex: 1,
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
},
infoValue: {
flex: 1,
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
},
logoutCard: {
backgroundColor: 'transparent',
...Shadows.none,
},
logoutText: {
color: Colors.error,
fontFamily: Typography.fontFamily.bold,
},
notificationGroup: {
backgroundColor: Colors.cardBackground,
borderRadius: BorderRadius.md,
padding: Spacing.md,
marginBottom: Spacing.md,
...Shadows.sm,
},
notificationCategory: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.md,
color: Colors.textPrimary,
marginBottom: Spacing.sm,
},
notificationRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: Spacing.sm,
borderBottomWidth: 1,
borderBottomColor: Colors.border,
},
notificationLabel: {
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
color: Colors.textSecondary,
},
modalContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0,0,0,0.5)',
},
modalContent: {
width: '80%',
backgroundColor: Colors.cardBackground,
borderRadius: BorderRadius.md,
padding: Spacing.lg,
...Shadows.card,
},
modalTitle: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.lg,
color: Colors.textPrimary,
marginBottom: Spacing.md,
},
modalInput: {
borderWidth: 1,
borderColor: Colors.border,
borderRadius: BorderRadius.sm,
padding: Spacing.sm,
marginBottom: Spacing.lg,
fontFamily: Typography.fontFamily.regular,
fontSize: Typography.fontSize.md,
},
modalActions: {
flexDirection: 'row',
justifyContent: 'flex-end',
},
modalButton: {
marginLeft: Spacing.sm,
},
});

View File

@ -7,8 +7,8 @@
import React from 'react';
import { View, Text, StyleSheet, Switch } from 'react-native';
import { Card } from 'shared/src/components/Card';
import { Colors, Spacing, Typography } from 'shared/src/theme';
import { Card } from '../../../../shared/src/components/Card';
import { Colors, Spacing, Typography } from '../../../../shared/src/theme';
import { useSettings } from '../hooks/useSettings';
/**

View File

@ -11,6 +11,8 @@ import { useSelector } from 'react-redux';
import { selectIsAuthenticated } from '../modules/Auth/redux/authSelectors';
import AuthNavigator from './AuthNavigator';
import TabNavigator from './TabNavigator';
import { LoginScreen, SetupBiometricScreen } from '../modules/Auth/screens';
import { DashboardScreen } from '../modules/Dashboard/screens';
/**
* AppNavigator - root navigator for the app
@ -19,7 +21,7 @@ import TabNavigator from './TabNavigator';
const AppNavigator: React.FC = () => {
const isAuthenticated = useSelector(selectIsAuthenticated);
return (
<NavigationContainer>
<NavigationContainer >
{isAuthenticated ? <TabNavigator /> : <AuthNavigator />}
</NavigationContainer>
);

View File

@ -10,8 +10,8 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { DashboardNavigator } from '../modules/Dashboard/navigation';
import { CaseReviewNavigator } from '../modules/CaseReview/navigation';
import { ProfileNavigator } from '../modules/Profile/navigation';
import { Colors } from 'shared/src/theme';
import { CustomIcon } from 'shared/src/components/Icons';
import { Colors } from '../../shared/src/theme';
import { CustomIcon } from '../../shared/src/components/Icons';
export type TabParamList = {
Dashboard: undefined;

15
app/redux/hooks.ts Normal file
View File

@ -0,0 +1,15 @@
/*
* File: hooks.ts
* Description: Typed hooks for Redux store interaction (useAppDispatch and useAppSelector).
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { useDispatch, useSelector } from 'react-redux';
import type { TypedUseSelectorHook } from 'react-redux';
import type { RootState } from './rootReducer';
import type { AppDispatch } from './store';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

34
app/redux/rootReducer.ts Normal file
View File

@ -0,0 +1,34 @@
/*
* File: rootReducer.ts
* Description: Root reducer combining all feature slices for Redux store
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { combineReducers } from '@reduxjs/toolkit';
// Import feature slices
import profileReducer from '../modules/Profile/redux/profileSlice';
import settingsReducer from '../modules/Profile/redux/settingsSlice';
// Add other slices as needed, e.g.:
import dashboardReducer from '../modules/Dashboard/redux/dashboardSlice';
import authReducer from '../modules/Auth/redux/authSlice';
// RootState type for use throughout the app
export type RootState = ReturnType<typeof rootReducer>;
// Combine all reducers into the rootReducer
const rootReducer = combineReducers({
profile: profileReducer,
settings: settingsReducer,
dashboard:dashboardReducer,
auth:authReducer
// Add other reducers here
});
export default rootReducer;
/*
* End of File: rootReducer.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

25
app/redux/store.ts Normal file
View File

@ -0,0 +1,25 @@
/*
* File: store.ts
* Description: Redux store configuration for the Radiologist App
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';
// Configure the Redux store
const store = configureStore({
reducer: rootReducer,
// You can add middleware, devTools, and other options here if needed
});
// Export store and types for use throughout the app
export type AppDispatch = typeof store.dispatch;
export default store;
/*
* End of File: store.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -1,3 +1,4 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['react-native-reanimated/plugin'],
};

552
package-lock.json generated
View File

@ -7,11 +7,11 @@
"": {
"name": "NeoScan_Radiologist",
"version": "0.0.1",
"hasInstallScript": true,
"dependencies": {
"@react-native-async-storage/async-storage": "^2.1.0",
"@react-native-clipboard/clipboard": "^1.16.1",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-voice/voice": "^3.2.4",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",
@ -22,10 +22,12 @@
"lottie-react-native": "^7.2.2",
"react": "19.0.0",
"react-native": "0.79.0",
"react-native-biometrics": "^3.0.1",
"react-native-blob-util": "^0.22.2",
"react-native-config": "^1.5.5",
"react-native-gesture-handler": "^2.22.1",
"react-native-image-picker": "^7.2.3",
"react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3",
"react-native-permissions": "^5.2.4",
"react-native-raw-bottom-sheet": "^3.0.0",
@ -55,6 +57,7 @@
"@react-native/typescript-config": "0.79.0",
"@types/jest": "^29.5.13",
"@types/react": "^19.0.0",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^19.0.0",
"eslint": "^8.19.0",
"jest": "^29.6.3",
@ -426,92 +429,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz",
"integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.25.9",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/highlight/node_modules/ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"license": "MIT",
"dependencies": {
"color-convert": "^1.9.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"license": "MIT",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/@babel/highlight/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
"license": "MIT"
},
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/@babel/highlight/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/highlight/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@babel/parser": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
@ -2223,229 +2140,6 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@expo/config-plugins": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-2.0.4.tgz",
"integrity": "sha512-JGt/X2tFr7H8KBQrKfbGo9hmCubQraMxq5sj3bqDdKmDOLcE1a/EDCP9g0U4GHsa425J8VDIkQUHYz3h3ndEXQ==",
"license": "MIT",
"dependencies": {
"@expo/config-types": "^41.0.0",
"@expo/json-file": "8.2.30",
"@expo/plist": "0.0.13",
"debug": "^4.3.1",
"find-up": "~5.0.0",
"fs-extra": "9.0.0",
"getenv": "^1.0.0",
"glob": "7.1.6",
"resolve-from": "^5.0.0",
"slash": "^3.0.0",
"xcode": "^3.0.1",
"xml2js": "^0.4.23"
}
},
"node_modules/@expo/config-plugins/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/@expo/config-plugins/node_modules/fs-extra": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz",
"integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==",
"license": "MIT",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@expo/config-plugins/node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@expo/config-plugins/node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/@expo/config-plugins/node_modules/jsonfile/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@expo/config-plugins/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/@expo/config-plugins/node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@expo/config-plugins/node_modules/universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@expo/config-types": {
"version": "41.0.0",
"resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-41.0.0.tgz",
"integrity": "sha512-Ax0pHuY5OQaSrzplOkT9DdpdmNzaVDnq9VySb4Ujq7UJ4U4jriLy8u93W98zunOXpcu0iiKubPsqD6lCiq0pig==",
"license": "MIT"
},
"node_modules/@expo/json-file": {
"version": "8.2.30",
"resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-8.2.30.tgz",
"integrity": "sha512-vrgGyPEXBoFI5NY70IegusCSoSVIFV3T3ry4tjJg1MFQKTUlR7E0r+8g8XR6qC705rc2PawaZQjqXMAVtV6s2A==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "~7.10.4",
"fs-extra": "9.0.0",
"json5": "^1.0.1",
"write-file-atomic": "^2.3.0"
}
},
"node_modules/@expo/json-file/node_modules/@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
"integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==",
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.10.4"
}
},
"node_modules/@expo/json-file/node_modules/fs-extra": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz",
"integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==",
"license": "MIT",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^1.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@expo/json-file/node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.0"
},
"bin": {
"json5": "lib/cli.js"
}
},
"node_modules/@expo/json-file/node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/@expo/json-file/node_modules/jsonfile/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@expo/json-file/node_modules/universalify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz",
"integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@expo/json-file/node_modules/write-file-atomic": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz",
"integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==",
"license": "ISC",
"dependencies": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.2"
}
},
"node_modules/@expo/plist": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.0.13.tgz",
"integrity": "sha512-zGPSq9OrCn7lWvwLLHLpHUUq2E40KptUFXn53xyZXPViI0k9lbApcR9KlonQZ95C+ELsf0BQ3gRficwK92Ivcw==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.2.3",
"xmlbuilder": "^14.0.0",
"xmldom": "~0.5.0"
}
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@ -3503,19 +3197,6 @@
"react-native": ">=0.59"
}
},
"node_modules/@react-native-voice/voice": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@react-native-voice/voice/-/voice-3.2.4.tgz",
"integrity": "sha512-4i3IpB/W5VxCI7BQZO5Nr2VB0ecx0SLvkln2Gy29cAQKqgBl+1ZsCwUBChwHlPbmja6vA3tp/+2ADQGwB1OhHg==",
"license": "MIT",
"dependencies": {
"@expo/config-plugins": "^2.0.0",
"invariant": "^2.2.4"
},
"peerDependencies": {
"react-native": ">= 0.60.2"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.79.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.0.tgz",
@ -4304,6 +3985,27 @@
"@types/react": "*"
}
},
"node_modules/@types/react-native-vector-icons": {
"version": "6.4.18",
"resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.18.tgz",
"integrity": "sha512-YGlNWb+k5laTBHd7+uZowB9DpIK3SXUneZqAiKQaj1jnJCZM0x71GDim5JCTMi4IFkhc9m8H/Gm28T5BjyivUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*",
"@types/react-native": "^0.70"
}
},
"node_modules/@types/react-native-vector-icons/node_modules/@types/react-native": {
"version": "0.70.19",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz",
"integrity": "sha512-c6WbyCgWTBgKKMESj/8b4w+zWcZSsCforson7UdXtXMecG3MxCinYi6ihhrHVPyUrVzORsvEzK8zg32z4pK6Sg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-test-renderer": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-19.1.0.tgz",
@ -4587,15 +4289,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@xmldom/xmldom": {
"version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -4994,15 +4687,6 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
"integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
"license": "ISC",
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -5232,15 +4916,6 @@
],
"license": "MIT"
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"license": "Unlicense",
"engines": {
"node": ">=0.6"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
@ -5301,27 +4976,6 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bplist-creator": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
"integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==",
"license": "MIT",
"dependencies": {
"stream-buffers": "2.2.x"
}
},
"node_modules/bplist-parser": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz",
"integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==",
"license": "MIT",
"dependencies": {
"big-integer": "1.6.x"
},
"engines": {
"node": ">= 5.10.0"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
@ -7572,6 +7226,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
@ -7871,15 +7526,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/getenv": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz",
"integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@ -10119,6 +9765,7 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
@ -10976,15 +10623,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
@ -11366,6 +11004,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
@ -11381,6 +11020,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
@ -11610,29 +11250,6 @@
"node": ">=8"
}
},
"node_modules/plist": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
"integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
"license": "MIT",
"dependencies": {
"@xmldom/xmldom": "^0.8.8",
"base64-js": "^1.5.1",
"xmlbuilder": "^15.1.1"
},
"engines": {
"node": ">=10.4.0"
}
},
"node_modules/plist/node_modules/xmlbuilder": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
"license": "MIT",
"engines": {
"node": ">=8.0"
}
},
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
@ -12004,6 +11621,15 @@
}
}
},
"node_modules/react-native-biometrics": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-native-biometrics/-/react-native-biometrics-3.0.1.tgz",
"integrity": "sha512-Ru80gXRa9KG04sl5AB9HyjLjVbduhqZVjA+AiOSGqr+fNqCDmCu9y5WEksnjbnniNLmq1yGcw+qcLXmR1ddLDQ==",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.60.0"
}
},
"node_modules/react-native-blob-util": {
"version": "0.22.2",
"resolved": "https://registry.npmjs.org/react-native-blob-util/-/react-native-blob-util-0.22.2.tgz",
@ -12087,6 +11713,19 @@
"react-native": "*"
}
},
"node_modules/react-native-keychain": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/react-native-keychain/-/react-native-keychain-10.0.0.tgz",
"integrity": "sha512-YzPKSAnSzGEJ12IK6CctNLU79T1W15WDrElRQ+1/FsOazGX9ucFPTQwgYe8Dy8jiSEDJKM4wkVa3g4lD2Z+Pnw==",
"license": "MIT",
"workspaces": [
"KeychainExample",
"website"
],
"engines": {
"node": ">=16"
}
},
"node_modules/react-native-linear-gradient": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz",
@ -12831,12 +12470,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"license": "ISC"
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@ -13122,17 +12755,6 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/simple-plist": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz",
"integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==",
"license": "MIT",
"dependencies": {
"bplist-creator": "0.1.0",
"bplist-parser": "0.3.1",
"plist": "^3.0.5"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
@ -13315,15 +12937,6 @@
"node": ">= 0.4"
}
},
"node_modules/stream-buffers": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
"integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==",
"license": "Unlicense",
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@ -14089,15 +13702,6 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
"integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
@ -14344,59 +13948,6 @@
"async-limiter": "~1.0.0"
}
},
"node_modules/xcode": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz",
"integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==",
"license": "Apache-2.0",
"dependencies": {
"simple-plist": "^1.1.0",
"uuid": "^7.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/xml2js": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xml2js/node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xmlbuilder": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz",
"integrity": "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==",
"license": "MIT",
"engines": {
"node": ">=8.0"
}
},
"node_modules/xmldom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.5.0.tgz",
"integrity": "sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@ -14465,6 +14016,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10"

View File

@ -7,13 +7,13 @@
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
"test": "jest",
"postinstall": "patch-package"
},
"dependencies": {
"@react-native-async-storage/async-storage": "^2.1.0",
"@react-native-clipboard/clipboard": "^1.16.1",
"@react-native-community/netinfo": "^11.4.1",
"@react-native-voice/voice": "^3.2.4",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@react-navigation/native-stack": "^7.2.0",
@ -24,10 +24,12 @@
"lottie-react-native": "^7.2.2",
"react": "19.0.0",
"react-native": "0.79.0",
"react-native-biometrics": "^3.0.1",
"react-native-blob-util": "^0.22.2",
"react-native-config": "^1.5.5",
"react-native-gesture-handler": "^2.22.1",
"react-native-image-picker": "^7.2.3",
"react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3",
"react-native-permissions": "^5.2.4",
"react-native-raw-bottom-sheet": "^3.0.0",
@ -57,6 +59,7 @@
"@react-native/typescript-config": "0.79.0",
"@types/jest": "^29.5.13",
"@types/react": "^19.0.0",
"@types/react-native-vector-icons": "^6.4.18",
"@types/react-test-renderer": "^19.0.0",
"eslint": "^8.19.0",
"jest": "^29.6.3",

View File

@ -0,0 +1,11 @@
diff --git a/node_modules/@react-native-voice/voice/android/build.gradle b/node_modules/@react-native-voice/voice/android/build.gradle
index 8fce879..bda4ee1 100644
--- a/node_modules/@react-native-voice/voice/android/build.gradle
+++ b/node_modules/@react-native-voice/voice/android/build.gradle
@@ -62,6 +62,5 @@ def supportVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.12'
- implementation "com.android.support:appcompat-v7:${supportVersion}"
implementation 'com.facebook.react:react-native:+'
}

View File

@ -1,75 +1,62 @@
/*
* File: Button.tsx
* Description: Themed button component using Clinical Blue Interface design system
* Description: A reusable button component with optional loading state.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import { TouchableOpacity, Text, StyleSheet, ViewStyle, TextStyle, GestureResponderEvent } from 'react-native';
import { Colors, Typography, Spacing, BorderRadius, Shadows } from '../../theme';
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'; // Import ActivityIndicator
import { Colors, Spacing, Typography, BorderRadius } from '../../theme';
// Button props for reusability and type safety
interface ButtonProps {
title: string;
onPress: (event: GestureResponderEvent) => void;
style?: ViewStyle;
textStyle?: TextStyle;
onPress: () => void;
style?: object;
textStyle?: object;
disabled?: boolean;
loading?: boolean; // Add loading prop
}
/**
* Themed Button component
* - Uses Clinical Blue Interface theme
* - Supports custom styles and disabled state
*/
const Button: React.FC<ButtonProps> = ({ title, onPress, style, textStyle, disabled }) => {
const Button: React.FC<ButtonProps> = ({
title,
onPress,
style,
textStyle,
disabled,
loading, // Destructure loading prop
}) => {
return (
<TouchableOpacity
style={[
styles.button,
disabled ? styles.buttonDisabled : {},
style,
]}
style={[styles.button, style, (disabled || loading) && styles.disabled]}
onPress={onPress}
activeOpacity={0.8}
disabled={disabled}
>
<Text style={[styles.text, textStyle, disabled ? styles.textDisabled : {}]}>{title}</Text>
disabled={disabled || loading}>
{loading ? (
<ActivityIndicator color={Colors.background} />
) : (
<Text style={[styles.text, textStyle]}>{title}</Text>
)}
</TouchableOpacity>
);
};
// Styles use the theme for consistency
const styles = StyleSheet.create({
button: {
backgroundColor: Colors.primary,
paddingVertical: Spacing.md,
paddingHorizontal: Spacing.xl,
paddingHorizontal: Spacing.lg,
borderRadius: BorderRadius.md,
alignItems: 'center',
justifyContent: 'center',
...Shadows.card,
},
buttonDisabled: {
backgroundColor: Colors.inactiveState,
},
text: {
color: Colors.background,
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.md,
fontWeight: Typography.fontWeight.bold as any,
letterSpacing: 0.5,
fontFamily: Typography.fontFamily.bold,
},
textDisabled: {
color: Colors.textMuted,
disabled: {
backgroundColor: Colors.inactiveState,
},
});
export default Button;
/*
* End of File: Button.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -1 +1,25 @@
/*
* File: Card.tsx
* Description: Themed info card using Clinical Blue Interface design system
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { ReactNode } from 'react';
import { View, StyleSheet, ViewStyle } from 'react-native';
import { cardStyles } from './Card.styles';
interface CardProps {
children: ReactNode;
style?: ViewStyle;
}
/**
* Card component
* - Simple card for displaying information
*/
const Card: React.FC<CardProps> = ({ children, style }) => (
<View style={[cardStyles.card, style]}>{children}</View>
);
export default Card;

View File

@ -6,6 +6,7 @@
*/
export { default as Card } from './Card';
export { default as InfoCard } from './InfoCard';
/*
* End of File: index.ts

View File

@ -0,0 +1,73 @@
/*
* File: Header.tsx
* Description: A reusable header component with optional back button, title, and right-side icon.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { useNavigation } from '@react-navigation/native';
import { Colors, Spacing, Typography } from '../../theme';
interface CustomHeaderProps {
title: string;
showBackButton?: boolean;
rightIcon?: {
name: string;
onPress: () => void;
};
}
const CustomHeader: React.FC<CustomHeaderProps> = ({
title,
showBackButton = true,
rightIcon,
}) => {
const navigation = useNavigation();
return (
<View style={styles.container}>
{showBackButton ? (
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.iconButton}>
<Icon name="arrow-left" size={24} color={Colors.textPrimary} />
</TouchableOpacity>
) : <View style={styles.placeholder} /> }
<Text style={styles.title}>{title}</Text>
{rightIcon ? (
<TouchableOpacity onPress={rightIcon.onPress} style={styles.iconButton}>
<Icon name={rightIcon.name} size={24} color={Colors.textPrimary} />
</TouchableOpacity>
) : (
<View style={styles.placeholder} />
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.md,
backgroundColor: Colors.background,
borderBottomWidth: 1,
borderBottomColor: Colors.border,
},
title: {
fontFamily: Typography.fontFamily.bold,
fontSize: Typography.fontSize.lg,
color: Colors.textPrimary,
},
iconButton: {
padding: Spacing.xs,
},
placeholder: {
width: 24 + Spacing.xs * 2, // Match icon button size
},
});
export default CustomHeader;

View File

@ -0,0 +1,8 @@
/*
* File: index.ts
* Description: Barrel file for exporting the CustomHeader component.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export { default as CustomHeader } from './Header';

View File

@ -6,6 +6,7 @@
*/
export { default as TextInput } from './TextInput';
export { default as SearchInput } from './SearchInput';
// Add other input components here as you implement them
/*

View File

@ -14,6 +14,20 @@ export const Shadows = {
shadowRadius: 8,
elevation: 4,
},
sm: {
shadowColor: '#5B7CE6',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
none: {
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
};
// Usage: import { Shadows } from './shadows';

View File

@ -0,0 +1,13 @@
interface BuildHeadersParams {
token?: string;
contentType?: string;
}
export const buildHeaders = ({ token, contentType }: BuildHeadersParams = {}) => {
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
if (contentType) headers['Content-Type'] = contentType;
return { headers };
};

View File

@ -0,0 +1,48 @@
/*
* File: Toast.js
* Description: Utility for displaying toast notifications
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import Toast from 'react-native-toast-message';
//shows success toast
const showSuccess=(text1='Successfull',text2='')=>{
Toast.show({
type: 'success',
text1,
text2
});
}
//shows error text
const showError=(text1='something went wrong',text2='')=>{
Toast.show({
type: 'error',
text1,
text2
});
}
//shows warning text
const showWarning=(text1='you have some warning',text2='')=>{
Toast.show({
type: 'info',
text1,
text2
});
}
const handleError=()=>{
}
export {showSuccess,showError,showWarning,handleError};
/*
* End of File: Toast.js
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,22 @@
/*
* File: validators.ts
* Description: Common input validation functions.
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
/**
* Validates an email address against a standard regex pattern.
* @param email The email address string to validate.
* @returns `true` if the email is valid, `false` otherwise.
*/
export const validateEmail = (email: string): boolean => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
/*
* End of File: validators.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/