This is page 2 of 2. Use http://codebase.md/lallen30/mcp-remote-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .gitignore ├── build │ └── index.js ├── package-lock.json ├── package.json ├── README.md ├── resources │ ├── code-examples │ │ └── react-native │ │ ├── assets │ │ │ └── images │ │ │ ├── event.png │ │ │ ├── qr-codeeee13.png │ │ │ └── [email protected] │ │ ├── components │ │ │ ├── Button.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── LoadingSpinner.tsx │ │ │ └── UpdateModal.tsx │ │ ├── config │ │ │ ├── apiConfig.ts │ │ │ └── environment.ts │ │ ├── hooks │ │ │ ├── useAppUpdate.ts │ │ │ └── useForm.ts │ │ ├── navigation │ │ │ ├── AppNavigator.tsx │ │ │ ├── DrawerNavigator.tsx │ │ │ ├── NavigationWrapper.tsx │ │ │ └── TabNavigator.tsx │ │ ├── screens │ │ │ ├── AboutUs.tsx │ │ │ ├── HomeScreen.js │ │ │ ├── HomeScreen.tsx │ │ │ ├── PostLogin │ │ │ │ ├── AboutUs │ │ │ │ │ ├── AboutUsScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── BluestoneAppsAI │ │ │ │ │ ├── BluestoneAppsAIScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── Calendar │ │ │ │ │ ├── CalendarScreen.tsx │ │ │ │ │ ├── EventDetails.tsx │ │ │ │ │ ├── EventDetailsStyles.ts │ │ │ │ │ └── Styles.ts │ │ │ │ ├── ChangePassword │ │ │ │ │ ├── ChangePasswordScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── Contact │ │ │ │ │ ├── ContactScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── CustomerSupport │ │ │ │ │ ├── CustomerSupportScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── EditProfile │ │ │ │ │ ├── EditProfileScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ ├── Home │ │ │ │ │ └── HomeScreen.tsx │ │ │ │ ├── MyProfile │ │ │ │ │ ├── MyProfileScreen.tsx │ │ │ │ │ └── Styles.ts │ │ │ │ └── Posts │ │ │ │ ├── PostScreen.tsx │ │ │ │ ├── PostsScreen.tsx │ │ │ │ ├── PostStyles.ts │ │ │ │ └── Styles.ts │ │ │ └── PreLogin │ │ │ ├── ForgotPassword │ │ │ │ ├── ForgotPasswordScreen.tsx │ │ │ │ └── Styles.ts │ │ │ ├── Legal │ │ │ │ ├── PrivacyPolicyScreen.tsx │ │ │ │ └── TermsAndConditionsScreen.tsx │ │ │ ├── Login │ │ │ │ ├── LoginScreen.tsx │ │ │ │ └── Styles.tsx │ │ │ ├── SignUp │ │ │ │ ├── SignUpScreen.tsx │ │ │ │ └── Styles.ts │ │ │ └── VerifyEmail │ │ │ ├── Styles.ts │ │ │ └── VerifyEmailScreen.tsx │ │ ├── services │ │ │ ├── apiService.ts │ │ │ ├── authService.ts │ │ │ ├── axiosRequest.ts │ │ │ ├── config.ts │ │ │ ├── NavigationMonitorService.ts │ │ │ ├── storageService.ts │ │ │ ├── types.ts │ │ │ ├── UpdateService.ts │ │ │ └── userService.ts │ │ ├── theme │ │ │ ├── colors_original.ts │ │ │ ├── colors.ts │ │ │ ├── README.md │ │ │ ├── theme.ts │ │ │ └── typography.ts │ │ ├── tsconfig.json │ │ ├── types │ │ │ └── react-native.d.ts │ │ └── utils │ │ ├── axiosUtils.ts │ │ └── LogSuppressor.ts │ └── standards │ ├── api_communication.md │ ├── component_design.md │ ├── project_structure.md │ └── state_management.md ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/SignUp/SignUpScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | TouchableOpacity, 7 | Alert, 8 | Image, 9 | ScrollView, 10 | } from 'react-native'; 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | import axios from 'axios'; 13 | import { styles } from './Styles.ts'; 14 | import { API } from '../../../helper/config'; 15 | import { colors } from '../../../theme/colors'; 16 | 17 | const SignUpScreen = ({ navigation }: any) => { 18 | console.log('SignUpScreen rendered'); 19 | 20 | const [formData, setFormData] = useState({ 21 | email: '', 22 | first_name: '', 23 | last_name: '', 24 | password: '', 25 | confirmPassword: '', 26 | }); 27 | const [acceptTerms, setAcceptTerms] = useState(false); 28 | const [errors, setErrors] = useState<{ [key: string]: string }>({}); 29 | 30 | const validateForm = () => { 31 | const newErrors: { [key: string]: string } = {}; 32 | 33 | // Email validation 34 | const emailRegex = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/; 35 | if (!formData.email) { 36 | newErrors.email = 'Email is required'; 37 | } else if (!emailRegex.test(formData.email)) { 38 | newErrors.email = 'Please enter a valid email'; 39 | } 40 | 41 | // Name validation 42 | if (!formData.first_name) newErrors.first_name = 'First name is required'; 43 | if (!formData.last_name) newErrors.last_name = 'Last name is required'; 44 | 45 | // Password validation 46 | if (!formData.password) { 47 | newErrors.password = 'Password is required'; 48 | } else if (formData.password.length < 5) { 49 | newErrors.password = 'Password must be at least 5 characters'; 50 | } 51 | 52 | // Confirm password validation 53 | if (formData.password !== formData.confirmPassword) { 54 | newErrors.confirmPassword = 'Passwords do not match'; 55 | } 56 | 57 | // Terms validation 58 | if (!acceptTerms) { 59 | newErrors.terms = 'Please accept the terms and conditions'; 60 | } 61 | 62 | setErrors(newErrors); 63 | return Object.keys(newErrors).length === 0; 64 | }; 65 | 66 | const handleSignUp = async () => { 67 | if (!validateForm()) { 68 | return; 69 | } 70 | 71 | try { 72 | const response = await axios.post( 73 | `${API.BASE_URL}${API.ENDPOINTS.MOBILEAPI}/verifyemail_and_send_otp`, 74 | { 75 | ...formData, 76 | emailVerification: 'pending', 77 | acceptTermsConditions: acceptTerms, 78 | } 79 | ); 80 | 81 | if (response.data.status === 'ok') { 82 | Alert.alert('Success', response.data.msg); 83 | navigation.navigate('VerifyEmail', { 84 | registerData: formData, 85 | otp: response.data.otp, 86 | }); 87 | } else { 88 | Alert.alert('Error', response.data.msg || 'Registration failed'); 89 | } 90 | } catch (error: any) { 91 | Alert.alert( 92 | 'Error', 93 | error.response?.data?.msg || 'An error occurred during registration' 94 | ); 95 | } 96 | }; 97 | 98 | return ( 99 | <View style={styles.container}> 100 | <ScrollView contentContainerStyle={styles.scrollContainer}> 101 | <View style={styles.logoContainer}> 102 | <Image 103 | source={require('../../../assets/images/logo.png')} 104 | style={styles.logo} 105 | /> 106 | </View> 107 | 108 | <Text style={styles.title}>Sign Up</Text> 109 | 110 | <TextInput 111 | style={styles.input} 112 | placeholder="Email" 113 | placeholderTextColor="#2c3e50" 114 | value={formData.email} 115 | onChangeText={(text) => setFormData({ ...formData, email: text.toLowerCase() })} 116 | keyboardType="email-address" 117 | autoCapitalize="none" 118 | /> 119 | {errors.email && <Text style={styles.errorText}>{errors.email}</Text>} 120 | 121 | <TextInput 122 | style={styles.input} 123 | placeholder="First Name" 124 | placeholderTextColor="#2c3e50" 125 | value={formData.first_name} 126 | onChangeText={(text) => setFormData({ ...formData, first_name: text })} 127 | /> 128 | {errors.first_name && <Text style={styles.errorText}>{errors.first_name}</Text>} 129 | 130 | <TextInput 131 | style={styles.input} 132 | placeholder="Last Name" 133 | placeholderTextColor="#2c3e50" 134 | value={formData.last_name} 135 | onChangeText={(text) => setFormData({ ...formData, last_name: text })} 136 | /> 137 | {errors.last_name && <Text style={styles.errorText}>{errors.last_name}</Text>} 138 | 139 | <TextInput 140 | style={styles.input} 141 | placeholder="Password" 142 | placeholderTextColor="#2c3e50" 143 | value={formData.password} 144 | onChangeText={(text) => setFormData({ ...formData, password: text })} 145 | secureTextEntry 146 | /> 147 | {errors.password && <Text style={styles.errorText}>{errors.password}</Text>} 148 | 149 | <TextInput 150 | style={styles.input} 151 | placeholder="Confirm Password" 152 | placeholderTextColor="#2c3e50" 153 | value={formData.confirmPassword} 154 | onChangeText={(text) => setFormData({ ...formData, confirmPassword: text })} 155 | secureTextEntry 156 | /> 157 | {errors.confirmPassword && <Text style={styles.errorText}>{errors.confirmPassword}</Text>} 158 | 159 | <View style={styles.checkboxContainer}> 160 | <TouchableOpacity 161 | style={[styles.customCheckbox, acceptTerms && styles.customCheckboxChecked]} 162 | onPress={() => setAcceptTerms(!acceptTerms)} 163 | > 164 | {acceptTerms && ( 165 | <Icon name="checkmark" size={16} color="#fff" /> 166 | )} 167 | </TouchableOpacity> 168 | <Text style={styles.checkboxLabel}> 169 | I accept the{' '} 170 | <Text 171 | style={[styles.checkboxLabel, styles.link]} 172 | onPress={() => navigation.navigate('TermsAndConditions')} 173 | > 174 | Terms and Conditions 175 | </Text> 176 | {' '}and{' '} 177 | <Text 178 | style={[styles.checkboxLabel, styles.link]} 179 | onPress={() => navigation.navigate('PrivacyPolicy')} 180 | > 181 | Privacy Policy 182 | </Text> 183 | </Text> 184 | </View> 185 | {errors.terms && <Text style={styles.errorText}>{errors.terms}</Text>} 186 | 187 | <TouchableOpacity style={styles.button} onPress={handleSignUp}> 188 | <Text style={styles.buttonText}>Sign Up</Text> 189 | </TouchableOpacity> 190 | 191 | <View style={styles.linkContainer}> 192 | <Text style={styles.linkText}>Already have an account?</Text> 193 | <TouchableOpacity onPress={() => navigation.navigate('Login')}> 194 | <Text style={styles.link}>Login</Text> 195 | </TouchableOpacity> 196 | </View> 197 | </ScrollView> 198 | </View> 199 | ); 200 | }; 201 | 202 | export default SignUpScreen; 203 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Contact/ContactScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | TouchableOpacity, 7 | ScrollView, 8 | StyleSheet, 9 | Alert, 10 | KeyboardAvoidingView, 11 | Platform, 12 | SafeAreaView, 13 | Keyboard, 14 | } from 'react-native'; 15 | import { useNavigation } from '@react-navigation/native'; 16 | import AsyncStorage from '@react-native-async-storage/async-storage'; 17 | import { API } from '../../../config/apiConfig'; 18 | import axiosRequest from '../../../utils/axiosUtils'; 19 | import { styles } from './Styles'; 20 | import { colors } from '../../../theme/colors'; 21 | 22 | const ContactScreen = () => { 23 | const navigation = useNavigation(); 24 | const [name, setName] = useState(''); 25 | const [email, setEmail] = useState(''); 26 | const [phone, setPhone] = useState(''); 27 | const [subject, setSubject] = useState(''); 28 | const [message, setMessage] = useState(''); 29 | const [isSubmitting, setIsSubmitting] = useState(false); 30 | 31 | useEffect(() => { 32 | loadUserData(); 33 | }, []); 34 | 35 | const loadUserData = async () => { 36 | try { 37 | const userDataString = await AsyncStorage.getItem('userData'); 38 | if (userDataString) { 39 | const userData = JSON.parse(userDataString); 40 | if (userData.userData) { 41 | setName(userData.userData.display_name || ''); 42 | setEmail(userData.userData.user_email || ''); 43 | setPhone(userData.userData.phone || ''); 44 | } 45 | } 46 | } catch (error) { 47 | console.error('Error loading user data:', error); 48 | } 49 | }; 50 | 51 | const handleSubmit = async () => { 52 | if (isSubmitting) return; 53 | 54 | if (!name || !email || !subject || !message) { 55 | Alert.alert('Error', 'Please fill in all required fields'); 56 | return; 57 | } 58 | 59 | setIsSubmitting(true); 60 | Keyboard.dismiss(); 61 | 62 | try { 63 | const userData = await AsyncStorage.getItem('userData'); 64 | const token = userData ? JSON.parse(userData).token : null; 65 | 66 | // Create URL-encoded form data 67 | const params = new URLSearchParams(); 68 | params.append('name', name.trim()); 69 | params.append('email', email.trim()); 70 | if (phone) params.append('phone', phone.trim()); 71 | params.append('subject', subject.trim()); 72 | params.append('message', message.trim()); 73 | 74 | const response = await axiosRequest.post(`${API.ENDPOINTS.MOBILEAPI}/contact_us`, 75 | params.toString(), 76 | { 77 | headers: { 78 | 'Accept': 'application/json', 79 | 'Content-Type': 'application/x-www-form-urlencoded', 80 | ...(token ? { 'Authorization': `Bearer ${token}` } : {}) 81 | } 82 | } 83 | ); 84 | 85 | if (response?.data?.success) { 86 | // Clear form 87 | setName(''); 88 | setEmail(''); 89 | setPhone(''); 90 | setSubject(''); 91 | setMessage(''); 92 | 93 | // Show success and navigate 94 | Alert.alert( 95 | 'Success', 96 | response.data.message || 'Your message has been sent successfully.', 97 | [{ 98 | text: 'OK', 99 | onPress: () => navigation.navigate('Home'), 100 | style: 'default' 101 | }] 102 | ); 103 | } else { 104 | Alert.alert('Error', response?.data?.message || 'Failed to send message. Please try again.'); 105 | } 106 | } catch (error: any) { 107 | console.error('Contact submission error:', error.message); 108 | Alert.alert('Error', error.response?.data?.message || error.message || 'Failed to send message. Please try again.'); 109 | } finally { 110 | setIsSubmitting(false); 111 | } 112 | }; 113 | 114 | return ( 115 | <SafeAreaView style={styles.safeArea}> 116 | <KeyboardAvoidingView 117 | style={styles.keyboardAvoidingView} 118 | behavior={Platform.OS === 'ios' ? 'padding' : undefined} 119 | keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0} 120 | > 121 | <View style={styles.contentContainer}> 122 | <ScrollView 123 | style={styles.scrollView} 124 | contentContainerStyle={styles.scrollViewContent} 125 | keyboardShouldPersistTaps="handled" 126 | showsVerticalScrollIndicator={false} 127 | > 128 | <View style={styles.formContainer}> 129 | <Text style={styles.label}>Name *</Text> 130 | <TextInput 131 | style={styles.input} 132 | value={name} 133 | onChangeText={setName} 134 | placeholder="Enter your full name" 135 | placeholderTextColor={colors.dark} 136 | returnKeyType="next" 137 | /> 138 | 139 | <Text style={styles.label}>Email *</Text> 140 | <TextInput 141 | style={styles.input} 142 | value={email} 143 | onChangeText={setEmail} 144 | placeholder="Enter your email" 145 | placeholderTextColor={colors.dark} 146 | keyboardType="email-address" 147 | autoCapitalize="none" 148 | returnKeyType="next" 149 | /> 150 | 151 | <Text style={styles.label}>Phone</Text> 152 | <TextInput 153 | style={styles.input} 154 | value={phone} 155 | onChangeText={setPhone} 156 | placeholder="Enter your phone number" 157 | placeholderTextColor={colors.dark} 158 | keyboardType="phone-pad" 159 | returnKeyType="next" 160 | /> 161 | 162 | <Text style={styles.label}>Subject *</Text> 163 | <TextInput 164 | style={styles.input} 165 | value={subject} 166 | onChangeText={setSubject} 167 | placeholder="Enter subject" 168 | placeholderTextColor={colors.dark} 169 | returnKeyType="next" 170 | /> 171 | 172 | <Text style={styles.label}>Message *</Text> 173 | <TextInput 174 | style={[styles.input, styles.messageInput]} 175 | value={message} 176 | onChangeText={setMessage} 177 | placeholder="Enter a brief message" 178 | placeholderTextColor={colors.dark} 179 | multiline 180 | numberOfLines={4} 181 | returnKeyType="done" 182 | onSubmitEditing={Keyboard.dismiss} 183 | /> 184 | </View> 185 | </ScrollView> 186 | 187 | <View style={styles.buttonWrapper}> 188 | <TouchableOpacity 189 | style={[styles.submitButton, isSubmitting && styles.submitButtonDisabled]} 190 | onPress={() => { 191 | Keyboard.dismiss(); 192 | handleSubmit(); 193 | }} 194 | disabled={isSubmitting} 195 | > 196 | <Text style={styles.submitButtonText}> 197 | {isSubmitting ? 'Sending...' : 'Send Message'} 198 | </Text> 199 | </TouchableOpacity> 200 | </View> 201 | </View> 202 | </KeyboardAvoidingView> 203 | </SafeAreaView> 204 | ); 205 | }; 206 | 207 | export default ContactScreen; 208 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/userService.ts: -------------------------------------------------------------------------------- ```typescript 1 | import axiosRequest from './axiosRequest'; 2 | import { API } from './config'; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | 5 | interface UserProfile { 6 | first_name: string; 7 | last_name: string; 8 | email: string; 9 | phone: string; 10 | user_avatar?: string; 11 | access_token: string; 12 | } 13 | 14 | class UserService { 15 | async getProfile(): Promise<UserProfile> { 16 | try { 17 | const userToken = await AsyncStorage.getItem('userToken'); 18 | if (!userToken) { 19 | throw new Error('No auth token found'); 20 | } 21 | 22 | const response = await axiosRequest.post(`${API.ENDPOINTS.MOBILEAPI}/getProfile`, { 23 | token: userToken, 24 | }); 25 | 26 | console.log('Profile API Response:', response); 27 | 28 | if (response.status === 'success' && response.data) { 29 | await AsyncStorage.setItem('userData', JSON.stringify(response.data)); 30 | return response.data; 31 | } 32 | 33 | throw new Error(response.message || 'Failed to get profile'); 34 | } catch (error) { 35 | console.error('Get profile error:', error); 36 | throw error; 37 | } 38 | } 39 | 40 | async changePassword(oldPassword: string, newPassword: string, confirmPassword: string): Promise<any> { 41 | try { 42 | const userDataString = await AsyncStorage.getItem('userData'); 43 | if (!userDataString) { 44 | throw new Error('User data not found'); 45 | } 46 | 47 | const userData = JSON.parse(userDataString); 48 | const token = userData.loginInfo?.token; 49 | const email = userData.loginInfo?.email; 50 | 51 | if (!token || !email) { 52 | throw new Error('User information not found'); 53 | } 54 | 55 | const formData = { 56 | old_password: oldPassword, 57 | password: newPassword, 58 | token: token, 59 | email: email 60 | }; 61 | 62 | console.log('Sending password change request:', formData); 63 | 64 | const response = await axiosRequest.post( 65 | `${API.ENDPOINTS.MOBILEAPI}/updatePassword`, 66 | formData, 67 | { 68 | headers: { 69 | 'Accept': 'application/json', 70 | 'Content-Type': 'application/json' 71 | } 72 | } 73 | ); 74 | 75 | console.log('Raw response:', response); 76 | console.log('Response data:', response.data); 77 | console.log('Response status:', response.status); 78 | 79 | // Handle both direct response and axios wrapped response 80 | const apiResponse = response.data || response; 81 | console.log('API response:', apiResponse); 82 | 83 | // If we have a successful response from the API 84 | if (apiResponse && (apiResponse.status === 'ok' || apiResponse.status === 'success')) { 85 | // Password was changed successfully, now try to re-login 86 | try { 87 | const loginResponse = await axiosRequest.post( 88 | API.ENDPOINTS.LOGIN, 89 | { 90 | email: email, 91 | password: newPassword 92 | }, 93 | { 94 | headers: { 95 | 'Accept': 'application/json', 96 | 'Content-Type': 'application/x-www-form-urlencoded' 97 | } 98 | } 99 | ); 100 | 101 | console.log('Re-login response:', loginResponse); 102 | 103 | if (loginResponse.data?.loginInfo?.token || loginResponse.loginInfo?.token) { 104 | const finalLoginData = loginResponse.data || loginResponse; 105 | await AsyncStorage.setItem('userData', JSON.stringify(finalLoginData)); 106 | return { status: 'ok', message: 'Password changed successfully' }; 107 | } 108 | 109 | // If we get here, login succeeded but didn't return expected data 110 | return { status: 'ok', message: 'Password changed successfully, please log in again' }; 111 | } catch (loginError) { 112 | console.error('Re-login error:', loginError); 113 | // Even if re-login fails, password was changed successfully 114 | return { status: 'ok', message: 'Password changed successfully, please log in again' }; 115 | } 116 | } 117 | 118 | // If we get here, the password change failed 119 | const errorMsg = apiResponse.errormsg || apiResponse.message || 'Failed to change password'; 120 | throw new Error(errorMsg); 121 | } catch (error: any) { 122 | console.error('Change password error:', error); 123 | // If we have a successful response but hit this block, return success 124 | if (error.response?.data?.status === 'ok' || error.response?.status === 'ok') { 125 | return { status: 'ok', message: 'Password changed successfully' }; 126 | } 127 | // Otherwise handle the error 128 | if (error.response?.data?.errormsg) { 129 | throw new Error(error.response.data.errormsg); 130 | } else if (error.response?.data?.message) { 131 | throw new Error(error.response.data.message); 132 | } else if (error.message) { 133 | throw error; 134 | } 135 | throw new Error('Failed to change password'); 136 | } 137 | } 138 | 139 | async updateProfile(data: { 140 | first_name: string; 141 | last_name: string; 142 | phone: string; 143 | profile_img?: any; 144 | }): Promise<any> { 145 | try { 146 | const userDataString = await AsyncStorage.getItem('userData'); 147 | if (!userDataString) { 148 | throw new Error('User data not found'); 149 | } 150 | 151 | const userData = JSON.parse(userDataString); 152 | const token = userData.loginInfo?.token; 153 | 154 | if (!token) { 155 | throw new Error('User token not found'); 156 | } 157 | 158 | const formData = new FormData(); 159 | formData.append('first_name', data.first_name); 160 | formData.append('last_name', data.last_name); 161 | formData.append('phone', data.phone); 162 | formData.append('token', token); 163 | 164 | if (data.profile_img) { 165 | formData.append('profile_img', { 166 | uri: data.profile_img.uri, 167 | type: data.profile_img.type, 168 | name: data.profile_img.fileName || 'profile.jpg', 169 | }); 170 | } 171 | 172 | const response = await axiosRequest.post(API.ENDPOINTS.UPDATE_PROFILE, formData, { 173 | headers: { 174 | 'Content-Type': 'multipart/form-data', 175 | }, 176 | }); 177 | 178 | console.log('Raw API Response:', response); 179 | 180 | // The response might be wrapped in a data property 181 | const responseData = response.data || response; 182 | 183 | // Check if we have a successful response 184 | if (responseData.code === 200 && responseData.status === 'ok') { 185 | // The API returns the complete updated user data 186 | return responseData; 187 | } 188 | 189 | // If we have an error message from the API, use it 190 | if (responseData.errormsg) { 191 | throw new Error(responseData.errormsg); 192 | } 193 | 194 | throw new Error('Failed to update profile'); 195 | } catch (error: any) { 196 | console.error('Update profile error:', error); 197 | // If it's an API error response 198 | if (error.response?.data?.errormsg) { 199 | throw new Error(error.response.data.errormsg); 200 | } 201 | // If it's our own error message 202 | if (error.message) { 203 | throw error; 204 | } 205 | // Generic error 206 | throw new Error('Failed to update profile'); 207 | } 208 | } 209 | } 210 | 211 | export default new UserService(); 212 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PreLogin/Login/LoginScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | TouchableOpacity, 7 | Alert, 8 | Image, 9 | KeyboardAvoidingView, 10 | ScrollView, 11 | Platform, 12 | } from 'react-native'; 13 | import AsyncStorage from '@react-native-async-storage/async-storage'; 14 | import Icon from 'react-native-vector-icons/Ionicons'; 15 | import { styles } from './Styles'; 16 | import authService from '../../../helper/authService'; 17 | import { AuthError, LoginResponse } from '../../../helper/types'; 18 | import { StackNavigationProp } from '@react-navigation/stack'; 19 | 20 | type RootStackParamList = { 21 | Login: undefined; 22 | DrawerNavigator: undefined; 23 | ForgotPassword: undefined; 24 | SignUp: undefined; 25 | }; 26 | 27 | type LoginScreenNavigationProp = StackNavigationProp<RootStackParamList, 'Login'>; 28 | 29 | interface Props { 30 | navigation: LoginScreenNavigationProp; 31 | } 32 | 33 | const LoginScreen: React.FC<Props> = ({ navigation }) => { 34 | const [email, setEmail] = useState(''); 35 | const [password, setPassword] = useState(''); 36 | const [rememberMe, setRememberMe] = useState(false); 37 | const [showPassword, setShowPassword] = useState(false); 38 | 39 | useEffect(() => { 40 | checkPreviousLogin(); 41 | }, []); 42 | 43 | const checkPreviousLogin = async () => { 44 | try { 45 | const rememberMeStatus = await AsyncStorage.getItem('rememberMe'); 46 | if (rememberMeStatus === 'true') { 47 | const savedEmail = await AsyncStorage.getItem('userEmail'); 48 | const savedPassword = await AsyncStorage.getItem('userPassword'); 49 | const userData = await AsyncStorage.getItem('userData'); 50 | 51 | if (savedEmail && savedPassword && userData) { 52 | // If we have all the necessary data and Remember Me is true, 53 | // automatically navigate to DrawerNavigator 54 | navigation.replace('DrawerNavigator'); 55 | } else { 56 | // If we're missing any data, clear everything to ensure a fresh state 57 | await AsyncStorage.removeItem('rememberMe'); 58 | await AsyncStorage.removeItem('userEmail'); 59 | await AsyncStorage.removeItem('userPassword'); 60 | await AsyncStorage.removeItem('userData'); 61 | } 62 | } 63 | } catch (error) { 64 | console.error('Error checking previous login:', error); 65 | } 66 | }; 67 | 68 | const handleLogin = async () => { 69 | try { 70 | const trimmedEmail = email.trim(); 71 | if (!trimmedEmail || !password) { 72 | Alert.alert('Error', 'Please enter both email and password'); 73 | return; 74 | } 75 | 76 | // Basic email validation 77 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 78 | if (!emailRegex.test(trimmedEmail)) { 79 | Alert.alert('Error', 'Please enter a valid email address'); 80 | return; 81 | } 82 | 83 | console.log('Attempting login with:', { email: trimmedEmail }); 84 | 85 | const response = await authService.login(trimmedEmail, password); 86 | 87 | if (response?.data?.loginInfo?.token) { 88 | await AsyncStorage.setItem('rememberMe', rememberMe.toString()); 89 | // Store user credentials if remember me is checked 90 | if (rememberMe) { 91 | await AsyncStorage.setItem('userEmail', trimmedEmail); 92 | await AsyncStorage.setItem('userPassword', password); 93 | } else { 94 | // Clear stored credentials if remember me is unchecked 95 | await AsyncStorage.removeItem('userEmail'); 96 | await AsyncStorage.removeItem('userPassword'); 97 | } 98 | // Store data in the correct format expected by the profile screen 99 | const userData = { loginInfo: response.data.loginInfo }; 100 | console.log('Storing userData:', userData); 101 | await AsyncStorage.setItem('userData', JSON.stringify(userData)); 102 | console.log('Login successful, navigating to DrawerNavigator'); 103 | navigation.replace('DrawerNavigator'); 104 | } else { 105 | console.log('No token in response:', response); 106 | Alert.alert('Error', 'Invalid response from server'); 107 | } 108 | } catch (error) { 109 | const authError = error as AuthError; 110 | console.error('Login error:', authError); 111 | Alert.alert( 112 | 'Error', 113 | authError.response?.data?.message?.replace(/<[^>]*>/g, '') || 114 | authError.response?.data?.errormsg || 115 | 'An error occurred during login. Please try again.' 116 | ); 117 | } 118 | }; 119 | 120 | return ( 121 | <KeyboardAvoidingView 122 | behavior={Platform.OS === 'ios' ? 'padding' : 'height'} 123 | style={{ flex: 1 }} 124 | > 125 | <ScrollView 126 | contentContainerStyle={styles.container} 127 | keyboardShouldPersistTaps="handled" 128 | showsVerticalScrollIndicator={false} 129 | > 130 | <View style={styles.logoContainer}> 131 | <Image 132 | source={require('../../../assets/images/logo.png')} 133 | style={styles.logo} 134 | resizeMode="contain" 135 | /> 136 | </View> 137 | <Text style={styles.title}>Login</Text> 138 | <TextInput 139 | style={styles.input} 140 | placeholder="Email" 141 | placeholderTextColor="#2c3e50" 142 | value={email} 143 | onChangeText={(text) => setEmail(text.toLowerCase().trim())} 144 | keyboardType="email-address" 145 | autoCapitalize="none" 146 | autoCorrect={false} 147 | spellCheck={false} 148 | /> 149 | <View style={styles.passwordContainer}> 150 | <TextInput 151 | style={[styles.input, { marginBottom: 0, color: '#2c3e50' }]} 152 | placeholder="Password" 153 | placeholderTextColor="#2c3e50" 154 | value={password} 155 | onChangeText={setPassword} 156 | secureTextEntry={!showPassword} 157 | autoCapitalize="none" 158 | autoCorrect={false} 159 | /> 160 | <TouchableOpacity 161 | style={styles.eyeIcon} 162 | onPress={() => setShowPassword(!showPassword)} 163 | > 164 | <Icon 165 | name={showPassword ? 'eye-off-outline' : 'eye-outline'} 166 | size={24} 167 | color="#2c3e50" 168 | /> 169 | </TouchableOpacity> 170 | </View> 171 | <View style={styles.checkboxContainer}> 172 | <TouchableOpacity 173 | style={[styles.customCheckbox, rememberMe && styles.customCheckboxChecked]} 174 | onPress={() => setRememberMe(!rememberMe)} 175 | > 176 | {rememberMe && ( 177 | <Icon name="checkmark" size={16} color="#fff" /> 178 | )} 179 | </TouchableOpacity> 180 | <Text style={styles.checkboxLabel}>Remember me</Text> 181 | </View> 182 | <TouchableOpacity style={styles.button} onPress={handleLogin}> 183 | <Text style={styles.buttonText}>Login</Text> 184 | </TouchableOpacity> 185 | <View style={styles.linkContainer}> 186 | <TouchableOpacity onPress={() => navigation.navigate('ForgotPassword')}> 187 | <Text style={styles.link}>Forgot Password?</Text> 188 | </TouchableOpacity> 189 | <TouchableOpacity onPress={() => navigation.navigate('SignUp')}> 190 | <Text style={styles.link}>Sign Up</Text> 191 | </TouchableOpacity> 192 | </View> 193 | </ScrollView> 194 | </KeyboardAvoidingView> 195 | ); 196 | }; 197 | 198 | export default LoginScreen; 199 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/theme/typography.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Typography styles for the application 3 | * 4 | * This file defines all typography styles used throughout the app. 5 | * It establishes a consistent type system with predefined text styles. 6 | */ 7 | 8 | import { Platform, TextStyle } from 'react-native'; 9 | import { colors } from './colors'; 10 | 11 | // Font family definitions 12 | const fontFamily = { 13 | // Primary font 14 | regular: Platform.OS === 'ios' ? 'System' : 'Roboto', 15 | medium: Platform.OS === 'ios' ? 'System' : 'Roboto-Medium', 16 | semiBold: Platform.OS === 'ios' ? 'System' : 'Roboto-Medium', 17 | bold: Platform.OS === 'ios' ? 'System' : 'Roboto-Bold', 18 | 19 | // Monospace font for code or technical content 20 | mono: Platform.OS === 'ios' ? 'Menlo' : 'monospace', 21 | } as const; 22 | 23 | // Font size scale (in pixels) 24 | const fontSize = { 25 | xs: 12, 26 | sm: 14, 27 | md: 16, 28 | lg: 18, 29 | xl: 20, 30 | xxl: 24, 31 | xxxl: 28, 32 | xxxxl: 32, 33 | display: 40, 34 | jumbo: 48, 35 | } as const; 36 | 37 | // Line height scale (multiplier of font size) 38 | const lineHeightMultiplier = { 39 | tight: 1.2, // For headings 40 | normal: 1.4, // For body text 41 | relaxed: 1.6, // For readable paragraphs 42 | } as const; 43 | 44 | // Calculate line heights based on font sizes 45 | const lineHeight = { 46 | xs: Math.round(fontSize.xs * lineHeightMultiplier.normal), 47 | sm: Math.round(fontSize.sm * lineHeightMultiplier.normal), 48 | md: Math.round(fontSize.md * lineHeightMultiplier.normal), 49 | lg: Math.round(fontSize.lg * lineHeightMultiplier.normal), 50 | xl: Math.round(fontSize.xl * lineHeightMultiplier.normal), 51 | xxl: Math.round(fontSize.xxl * lineHeightMultiplier.tight), 52 | xxxl: Math.round(fontSize.xxxl * lineHeightMultiplier.tight), 53 | xxxxl: Math.round(fontSize.xxxxl * lineHeightMultiplier.tight), 54 | display: Math.round(fontSize.display * lineHeightMultiplier.tight), 55 | jumbo: Math.round(fontSize.jumbo * lineHeightMultiplier.tight), 56 | } as const; 57 | 58 | // Font weight definitions 59 | const fontWeight = { 60 | regular: '400', 61 | medium: '500', 62 | semiBold: '600', 63 | bold: '700', 64 | } as const; 65 | 66 | // Letter spacing (tracking) 67 | const letterSpacing = { 68 | tight: -0.5, 69 | normal: 0, 70 | wide: 0.5, 71 | extraWide: 1, 72 | } as const; 73 | 74 | // Text transform options 75 | const textTransform = { 76 | none: 'none', 77 | capitalize: 'capitalize', 78 | uppercase: 'uppercase', 79 | lowercase: 'lowercase', 80 | } as const; 81 | 82 | // Predefined typography styles 83 | export const typography: Record<string, TextStyle> = { 84 | // Headings 85 | h1: { 86 | fontFamily: fontFamily.bold, 87 | fontSize: fontSize.xxxxl, 88 | lineHeight: lineHeight.xxxxl, 89 | fontWeight: fontWeight.bold, 90 | color: colors.textPrimary, 91 | letterSpacing: letterSpacing.tight, 92 | }, 93 | h2: { 94 | fontFamily: fontFamily.bold, 95 | fontSize: fontSize.xxxl, 96 | lineHeight: lineHeight.xxxl, 97 | fontWeight: fontWeight.bold, 98 | color: colors.textPrimary, 99 | letterSpacing: letterSpacing.tight, 100 | }, 101 | h3: { 102 | fontFamily: fontFamily.bold, 103 | fontSize: fontSize.xxl, 104 | lineHeight: lineHeight.xxl, 105 | fontWeight: fontWeight.bold, 106 | color: colors.textPrimary, 107 | letterSpacing: letterSpacing.tight, 108 | }, 109 | h4: { 110 | fontFamily: fontFamily.semiBold, 111 | fontSize: fontSize.xl, 112 | lineHeight: lineHeight.xl, 113 | fontWeight: fontWeight.semiBold, 114 | color: colors.textPrimary, 115 | letterSpacing: letterSpacing.normal, 116 | }, 117 | h5: { 118 | fontFamily: fontFamily.semiBold, 119 | fontSize: fontSize.lg, 120 | lineHeight: lineHeight.lg, 121 | fontWeight: fontWeight.semiBold, 122 | color: colors.textPrimary, 123 | letterSpacing: letterSpacing.normal, 124 | }, 125 | h6: { 126 | fontFamily: fontFamily.medium, 127 | fontSize: fontSize.md, 128 | lineHeight: lineHeight.md, 129 | fontWeight: fontWeight.medium, 130 | color: colors.textPrimary, 131 | letterSpacing: letterSpacing.normal, 132 | }, 133 | 134 | // Body text 135 | bodyLarge: { 136 | fontFamily: fontFamily.regular, 137 | fontSize: fontSize.lg, 138 | lineHeight: lineHeight.lg, 139 | fontWeight: fontWeight.regular, 140 | color: colors.textPrimary, 141 | letterSpacing: letterSpacing.normal, 142 | }, 143 | body: { 144 | fontFamily: fontFamily.regular, 145 | fontSize: fontSize.md, 146 | lineHeight: lineHeight.md, 147 | fontWeight: fontWeight.regular, 148 | color: colors.textPrimary, 149 | letterSpacing: letterSpacing.normal, 150 | }, 151 | bodySmall: { 152 | fontFamily: fontFamily.regular, 153 | fontSize: fontSize.sm, 154 | lineHeight: lineHeight.sm, 155 | fontWeight: fontWeight.regular, 156 | color: colors.textSecondary, 157 | letterSpacing: letterSpacing.normal, 158 | }, 159 | 160 | // Special text styles 161 | caption: { 162 | fontFamily: fontFamily.regular, 163 | fontSize: fontSize.xs, 164 | lineHeight: lineHeight.xs, 165 | fontWeight: fontWeight.regular, 166 | color: colors.textSecondary, 167 | letterSpacing: letterSpacing.normal, 168 | }, 169 | button: { 170 | fontFamily: fontFamily.medium, 171 | fontSize: fontSize.md, 172 | lineHeight: lineHeight.md, 173 | fontWeight: fontWeight.medium, 174 | letterSpacing: letterSpacing.wide, 175 | textTransform: textTransform.none, 176 | }, 177 | buttonSmall: { 178 | fontFamily: fontFamily.medium, 179 | fontSize: fontSize.sm, 180 | lineHeight: lineHeight.sm, 181 | fontWeight: fontWeight.medium, 182 | letterSpacing: letterSpacing.wide, 183 | textTransform: textTransform.none, 184 | }, 185 | label: { 186 | fontFamily: fontFamily.medium, 187 | fontSize: fontSize.sm, 188 | lineHeight: lineHeight.sm, 189 | fontWeight: fontWeight.medium, 190 | color: colors.textPrimary, 191 | letterSpacing: letterSpacing.wide, 192 | }, 193 | link: { 194 | fontFamily: fontFamily.regular, 195 | fontSize: fontSize.md, 196 | lineHeight: lineHeight.md, 197 | fontWeight: fontWeight.regular, 198 | color: colors.link, 199 | letterSpacing: letterSpacing.normal, 200 | }, 201 | 202 | // Display and special cases 203 | display: { 204 | fontFamily: fontFamily.bold, 205 | fontSize: fontSize.display, 206 | lineHeight: lineHeight.display, 207 | fontWeight: fontWeight.bold, 208 | color: colors.textPrimary, 209 | letterSpacing: letterSpacing.tight, 210 | }, 211 | jumbo: { 212 | fontFamily: fontFamily.bold, 213 | fontSize: fontSize.jumbo, 214 | lineHeight: lineHeight.jumbo, 215 | fontWeight: fontWeight.bold, 216 | color: colors.textPrimary, 217 | letterSpacing: letterSpacing.tight, 218 | }, 219 | 220 | // Utility styles 221 | uppercase: { 222 | textTransform: textTransform.uppercase, 223 | }, 224 | capitalize: { 225 | textTransform: textTransform.capitalize, 226 | }, 227 | 228 | // Form elements 229 | input: { 230 | fontFamily: fontFamily.regular, 231 | fontSize: fontSize.md, 232 | lineHeight: lineHeight.md, 233 | fontWeight: fontWeight.regular, 234 | color: colors.textPrimary, 235 | }, 236 | 237 | // Code/monospace 238 | code: { 239 | fontFamily: fontFamily.mono, 240 | fontSize: fontSize.sm, 241 | lineHeight: Math.round(fontSize.sm * 1.5), 242 | fontWeight: fontWeight.regular, 243 | color: colors.textPrimary, 244 | letterSpacing: letterSpacing.tight, 245 | }, 246 | }; 247 | 248 | // Export raw values for custom use cases 249 | export const typeScale = { 250 | fontFamily, 251 | fontSize, 252 | lineHeight, 253 | fontWeight, 254 | letterSpacing, 255 | textTransform, 256 | } as const; 257 | 258 | export type FontFamilyKey = keyof typeof fontFamily; 259 | export type FontSizeKey = keyof typeof fontSize; 260 | export type LineHeightKey = keyof typeof lineHeight; 261 | export type FontWeightKey = keyof typeof fontWeight; 262 | export type LetterSpacingKey = keyof typeof letterSpacing; 263 | export type TextTransformKey = keyof typeof textTransform; 264 | export type TypographyStyleKey = keyof typeof typography; 265 | 266 | export default typography; 267 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/ChangePassword/ChangePasswordScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | TouchableOpacity, 7 | ScrollView, 8 | Alert, 9 | ActivityIndicator, 10 | } from 'react-native'; 11 | import Icon from 'react-native-vector-icons/Ionicons'; 12 | import { styles } from './Styles'; 13 | import userService from '../../../helper/userService'; 14 | import { colors } from '../../../theme/colors'; 15 | 16 | interface FormData { 17 | currentPassword: string; 18 | newPassword: string; 19 | confirmPassword: string; 20 | } 21 | 22 | interface PasswordVisibility { 23 | currentPassword: boolean; 24 | newPassword: boolean; 25 | confirmPassword: boolean; 26 | } 27 | 28 | const ChangePasswordScreen = ({ navigation }: any) => { 29 | const [formData, setFormData] = useState<FormData>({ 30 | currentPassword: '', 31 | newPassword: '', 32 | confirmPassword: '', 33 | }); 34 | 35 | const [showPassword, setShowPassword] = useState<PasswordVisibility>({ 36 | currentPassword: false, 37 | newPassword: false, 38 | confirmPassword: false, 39 | }); 40 | 41 | const [errors, setErrors] = useState<Partial<FormData>>({}); 42 | const [isLoading, setIsLoading] = useState(false); 43 | 44 | useEffect(() => { 45 | // Set up the navigation header 46 | navigation.setOptions({ 47 | headerShown: true, 48 | headerTitle: 'Change Password', 49 | headerLeft: () => ( 50 | <TouchableOpacity 51 | style={{ marginLeft: 10 }} 52 | onPress={() => navigation.navigate('MyProfile')} 53 | > 54 | <Icon style={styles.backButton} name="chevron-back" size={24} /> 55 | </TouchableOpacity> 56 | ), 57 | }); 58 | }, [navigation]); 59 | 60 | const togglePasswordVisibility = (field: keyof PasswordVisibility) => { 61 | setShowPassword(prev => ({ 62 | ...prev, 63 | [field]: !prev[field] 64 | })); 65 | }; 66 | 67 | const validateForm = () => { 68 | const newErrors: Partial<FormData> = {}; 69 | 70 | if (!formData.currentPassword) { 71 | newErrors.currentPassword = 'Current password is required'; 72 | } 73 | 74 | if (!formData.newPassword) { 75 | newErrors.newPassword = 'New password is required'; 76 | } 77 | 78 | if (!formData.confirmPassword) { 79 | newErrors.confirmPassword = 'Confirm password is required'; 80 | } else if (formData.newPassword !== formData.confirmPassword) { 81 | newErrors.confirmPassword = 'Passwords do not match'; 82 | } 83 | 84 | setErrors(newErrors); 85 | return Object.keys(newErrors).length === 0; 86 | }; 87 | 88 | const handleChangePassword = async () => { 89 | if (!validateForm()) { 90 | return; 91 | } 92 | 93 | try { 94 | setIsLoading(true); 95 | const response = await userService.changePassword( 96 | formData.currentPassword, 97 | formData.newPassword, 98 | formData.confirmPassword 99 | ); 100 | 101 | Alert.alert('Success', 'Password changed successfully', [ 102 | { 103 | text: 'OK', 104 | onPress: () => { 105 | navigation.navigate('Login'); 106 | }, 107 | }, 108 | ]); 109 | } catch (error: any) { 110 | Alert.alert('Error', error.message || 'Failed to change password'); 111 | } finally { 112 | setIsLoading(false); 113 | } 114 | }; 115 | 116 | return ( 117 | <ScrollView style={styles.container}> 118 | <View style={styles.header}> 119 | <Text style={styles.title}>Change Password</Text> 120 | <Text style={styles.subtitle}>Please enter your current password and choose a new password</Text> 121 | </View> 122 | 123 | <View style={styles.formContainer}> 124 | <View style={styles.inputGroup}> 125 | <Text style={styles.label}>Current Password</Text> 126 | <View style={styles.inputContainer}> 127 | <Icon name="lock-closed" size={20} color="#666" style={styles.inputIcon} /> 128 | <TextInput 129 | style={styles.input} 130 | value={formData.currentPassword} 131 | onChangeText={(text) => { 132 | setFormData({ ...formData, currentPassword: text }); 133 | if (errors.currentPassword) { 134 | setErrors({ ...errors, currentPassword: '' }); 135 | } 136 | }} 137 | placeholder="Enter current password" 138 | placeholderTextColor={colors.dark} 139 | secureTextEntry={!showPassword.currentPassword} 140 | /> 141 | <TouchableOpacity onPress={() => togglePasswordVisibility('currentPassword')}> 142 | <Icon 143 | name={showPassword.currentPassword ? 'eye-off' : 'eye'} 144 | size={20} 145 | color="#666" 146 | /> 147 | </TouchableOpacity> 148 | </View> 149 | {errors.currentPassword && ( 150 | <Text style={styles.errorText}>{errors.currentPassword}</Text> 151 | )} 152 | </View> 153 | 154 | <View style={styles.inputGroup}> 155 | <Text style={styles.label}>New Password</Text> 156 | <View style={styles.inputContainer}> 157 | <Icon name="lock-closed" size={20} color="#666" style={styles.inputIcon} /> 158 | <TextInput 159 | style={styles.input} 160 | value={formData.newPassword} 161 | onChangeText={(text) => { 162 | setFormData({ ...formData, newPassword: text }); 163 | if (errors.newPassword) { 164 | setErrors({ ...errors, newPassword: '' }); 165 | } 166 | }} 167 | placeholder="Enter new password" 168 | placeholderTextColor={colors.dark} 169 | secureTextEntry={!showPassword.newPassword} 170 | /> 171 | <TouchableOpacity onPress={() => togglePasswordVisibility('newPassword')}> 172 | <Icon 173 | name={showPassword.newPassword ? 'eye-off' : 'eye'} 174 | size={20} 175 | color="#666" 176 | /> 177 | </TouchableOpacity> 178 | </View> 179 | {errors.newPassword && ( 180 | <Text style={styles.errorText}>{errors.newPassword}</Text> 181 | )} 182 | </View> 183 | 184 | <View style={styles.inputGroup}> 185 | <Text style={styles.label}>Confirm New Password</Text> 186 | <View style={styles.inputContainer}> 187 | <Icon name="lock-closed" size={20} color="#666" style={styles.inputIcon} /> 188 | <TextInput 189 | style={styles.input} 190 | value={formData.confirmPassword} 191 | onChangeText={(text) => { 192 | setFormData({ ...formData, confirmPassword: text }); 193 | if (errors.confirmPassword) { 194 | setErrors({ ...errors, confirmPassword: '' }); 195 | } 196 | }} 197 | placeholder="Confirm new password" 198 | placeholderTextColor={colors.dark} 199 | secureTextEntry={!showPassword.confirmPassword} 200 | /> 201 | <TouchableOpacity onPress={() => togglePasswordVisibility('confirmPassword')}> 202 | <Icon 203 | name={showPassword.confirmPassword ? 'eye-off' : 'eye'} 204 | size={20} 205 | color="#666" 206 | /> 207 | </TouchableOpacity> 208 | </View> 209 | {errors.confirmPassword && ( 210 | <Text style={styles.errorText}>{errors.confirmPassword}</Text> 211 | )} 212 | </View> 213 | 214 | <TouchableOpacity 215 | style={[styles.button, isLoading && styles.disabledButton]} 216 | onPress={handleChangePassword} 217 | disabled={isLoading} 218 | > 219 | {isLoading ? ( 220 | <ActivityIndicator color="#fff" /> 221 | ) : ( 222 | <Text style={styles.buttonText}>Change Password</Text> 223 | )} 224 | </TouchableOpacity> 225 | </View> 226 | </ScrollView> 227 | ); 228 | }; 229 | 230 | export default ChangePasswordScreen; 231 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/EditProfile/EditProfileScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | View, 4 | Text, 5 | TextInput, 6 | TouchableOpacity, 7 | Image, 8 | ScrollView, 9 | ActivityIndicator, 10 | Alert, 11 | } from 'react-native'; 12 | import AsyncStorage from '@react-native-async-storage/async-storage'; 13 | import * as ImagePicker from 'react-native-image-picker'; 14 | import Icon from 'react-native-vector-icons/Ionicons'; 15 | import { styles } from './Styles'; 16 | import userService from '../../../services/userService'; 17 | 18 | interface FormData { 19 | first_name: string; 20 | last_name: string; 21 | phone: string; 22 | email: string; 23 | } 24 | 25 | interface FormErrors { 26 | first_name?: string; 27 | last_name?: string; 28 | phone?: string; 29 | } 30 | 31 | const EditProfileScreen = ({ navigation }: any) => { 32 | const [formData, setFormData] = useState<FormData>({ 33 | first_name: '', 34 | last_name: '', 35 | phone: '', 36 | email: '', 37 | }); 38 | const [errors, setErrors] = useState<FormErrors>({}); 39 | const [isLoading, setIsLoading] = useState(false); 40 | const [profileImage, setProfileImage] = useState<any>(null); 41 | const [currentAvatar, setCurrentAvatar] = useState<string>(''); 42 | 43 | useEffect(() => { 44 | // Set up the navigation header 45 | navigation.setOptions({ 46 | headerShown: true, 47 | headerTitle: 'Edit Profile', 48 | headerLeft: () => ( 49 | <TouchableOpacity 50 | style={{ marginLeft: 10 }} 51 | onPress={() => navigation.navigate('MyProfile')} 52 | > 53 | <Icon style={styles.backButton} name="chevron-back" size={24} /> 54 | </TouchableOpacity> 55 | ), 56 | }); 57 | 58 | loadUserData(); 59 | }, [navigation]); 60 | 61 | const loadUserData = async () => { 62 | try { 63 | const userDataString = await AsyncStorage.getItem('userData'); 64 | if (userDataString) { 65 | const userData = JSON.parse(userDataString); 66 | const { loginInfo } = userData; 67 | setFormData({ 68 | first_name: loginInfo.first_name || '', 69 | last_name: loginInfo.last_name || '', 70 | phone: loginInfo.phone || '', 71 | email: loginInfo.email || '', 72 | }); 73 | setCurrentAvatar(loginInfo.user_avatar || ''); 74 | } 75 | } catch (error) { 76 | console.error('Error loading user data:', error); 77 | Alert.alert('Error', 'Failed to load user data'); 78 | } 79 | }; 80 | 81 | const validateForm = () => { 82 | const newErrors: FormErrors = {}; 83 | if (!formData.first_name.trim()) { 84 | newErrors.first_name = 'First name is required'; 85 | } 86 | if (!formData.last_name.trim()) { 87 | newErrors.last_name = 'Last name is required'; 88 | } 89 | if (!formData.phone.trim()) { 90 | newErrors.phone = 'Phone number is required'; 91 | } else if (formData.phone.length < 10) { 92 | newErrors.phone = 'Phone number must be at least 10 digits'; 93 | } 94 | setErrors(newErrors); 95 | return Object.keys(newErrors).length === 0; 96 | }; 97 | 98 | const handleImagePicker = () => { 99 | const options: ImagePicker.ImageLibraryOptions = { 100 | mediaType: 'photo', 101 | quality: 1, 102 | selectionLimit: 1, 103 | }; 104 | 105 | ImagePicker.launchImageLibrary(options, (response) => { 106 | if (response.didCancel) { 107 | return; 108 | } 109 | if (response.errorCode) { 110 | Alert.alert('Error', response.errorMessage || 'Failed to pick image'); 111 | return; 112 | } 113 | if (response.assets && response.assets[0]) { 114 | setProfileImage(response.assets[0]); 115 | } 116 | }); 117 | }; 118 | 119 | const handleSubmit = async () => { 120 | if (!validateForm()) { 121 | return; 122 | } 123 | 124 | try { 125 | setIsLoading(true); 126 | const response = await userService.updateProfile({ 127 | first_name: formData.first_name, 128 | last_name: formData.last_name, 129 | phone: formData.phone, 130 | profile_img: profileImage, 131 | }); 132 | 133 | // The response is already successful if we reach this point 134 | // Store the updated user data 135 | await AsyncStorage.setItem('userData', JSON.stringify(response)); 136 | Alert.alert('Success', 'Profile updated successfully', [ 137 | { 138 | text: 'OK', 139 | onPress: () => { 140 | if (navigation) { 141 | navigation.navigate('MyProfile'); 142 | } 143 | } 144 | }, 145 | ]); 146 | } catch (error: any) { 147 | Alert.alert('Error', error.message || 'Failed to update profile'); 148 | } finally { 149 | setIsLoading(false); 150 | } 151 | }; 152 | 153 | return ( 154 | <ScrollView style={styles.container}> 155 | <View style={styles.header}> 156 | <TouchableOpacity style={styles.avatarContainer} onPress={handleImagePicker}> 157 | {profileImage ? ( 158 | <Image source={{ uri: profileImage.uri }} style={styles.profileImage} /> 159 | ) : currentAvatar ? ( 160 | <Image source={{ uri: currentAvatar }} style={styles.profileImage} /> 161 | ) : ( 162 | <Image 163 | source={require('../../../assets/images/default_profile_pic.png')} 164 | style={styles.profileImage} 165 | /> 166 | )} 167 | <View style={styles.cameraIconContainer}> 168 | <Icon name="camera" size={20} color="#fff" /> 169 | </View> 170 | </TouchableOpacity> 171 | </View> 172 | 173 | <View style={styles.formContainer}> 174 | <View style={styles.inputGroup}> 175 | <Text style={styles.label}>First Name</Text> 176 | <View style={styles.inputContainer}> 177 | <Icon name="person-outline" size={20} color="#666" style={styles.inputIcon} /> 178 | <TextInput 179 | style={styles.input} 180 | placeholder="Enter first name" 181 | value={formData.first_name} 182 | onChangeText={(text) => setFormData({ ...formData, first_name: text })} 183 | /> 184 | </View> 185 | {errors.first_name && <Text style={styles.errorText}>{errors.first_name}</Text>} 186 | </View> 187 | 188 | <View style={styles.inputGroup}> 189 | <Text style={styles.label}>Last Name</Text> 190 | <View style={styles.inputContainer}> 191 | <Icon name="person-outline" size={20} color="#666" style={styles.inputIcon} /> 192 | <TextInput 193 | style={styles.input} 194 | placeholder="Enter last name" 195 | value={formData.last_name} 196 | onChangeText={(text) => setFormData({ ...formData, last_name: text })} 197 | /> 198 | </View> 199 | {errors.last_name && <Text style={styles.errorText}>{errors.last_name}</Text>} 200 | </View> 201 | 202 | <View style={styles.inputGroup}> 203 | <Text style={styles.label}>Phone Number</Text> 204 | <View style={styles.inputContainer}> 205 | <Icon name="call-outline" size={20} color="#666" style={styles.inputIcon} /> 206 | <TextInput 207 | style={styles.input} 208 | placeholder="Enter phone number" 209 | value={formData.phone} 210 | onChangeText={(text) => setFormData({ ...formData, phone: text })} 211 | keyboardType="phone-pad" 212 | /> 213 | </View> 214 | {errors.phone && <Text style={styles.errorText}>{errors.phone}</Text>} 215 | </View> 216 | 217 | <View style={styles.inputGroup}> 218 | <Text style={styles.label}>Email</Text> 219 | <View style={styles.inputContainer}> 220 | <Icon name="mail-outline" size={20} color="#666" style={styles.inputIcon} /> 221 | <TextInput 222 | style={[styles.input, { color: '#666' }]} 223 | value={formData.email} 224 | editable={false} 225 | /> 226 | </View> 227 | </View> 228 | </View> 229 | 230 | <View style={styles.buttonContainer}> 231 | <TouchableOpacity 232 | style={[styles.button, isLoading && styles.disabledButton]} 233 | onPress={handleSubmit} 234 | disabled={isLoading} 235 | > 236 | {isLoading ? ( 237 | <ActivityIndicator color="#fff" /> 238 | ) : ( 239 | <Text style={styles.buttonText}>Update Profile</Text> 240 | )} 241 | </TouchableOpacity> 242 | </View> 243 | </ScrollView> 244 | ); 245 | }; 246 | 247 | export default EditProfileScreen; 248 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/hooks/useAppUpdate.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { useState, useCallback, useEffect } from 'react'; 2 | import UpdateService from '../services/UpdateService'; 3 | import AsyncStorage from '@react-native-async-storage/async-storage'; 4 | import { compareVersions } from 'compare-versions'; 5 | 6 | interface VersionInfo { 7 | latestVersion: string; 8 | minimumVersion: string; 9 | releaseNotes?: string; 10 | } 11 | 12 | /** 13 | * Custom hook for managing app update state and logic 14 | * @param checkOnMount Whether to check for updates when the component mounts 15 | * @param updateCheckInterval Time interval between update checks (in milliseconds) 16 | * @returns Object containing update state and functions 17 | */ 18 | const useAppUpdate = (checkOnMount = true, updateCheckInterval = 60 * 60 * 1000) => { 19 | const [loading, setLoading] = useState(false); 20 | const [error, setError] = useState<string | null>(null); 21 | const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null); 22 | const [isAppStoreVersion, setIsAppStoreVersion] = useState(false); 23 | const [storedAppVersion, setStoredAppVersion] = useState<string | null>(null); 24 | const [updateModalVisible, setUpdateModalVisible] = useState(false); 25 | const [lastUpdateCheck, setLastUpdateCheck] = useState<number | null>(null); 26 | 27 | // Time interval between update checks (in milliseconds) 28 | // Default: 1 hour - adjust this value based on how frequently you want to check for updates 29 | // Lower values will check more frequently but may impact performance 30 | const UPDATE_CHECK_INTERVAL = updateCheckInterval; 31 | 32 | // Check if the app is from the App Store 33 | const checkAppSource = useCallback(async () => { 34 | try { 35 | const isFromAppStore = await UpdateService.isAppStoreVersion(); 36 | setIsAppStoreVersion(isFromAppStore); 37 | return isFromAppStore; 38 | } catch (err) { 39 | console.error('Error checking app source'); 40 | return false; 41 | } 42 | }, []); 43 | 44 | // Get the stored app version 45 | const getStoredAppVersion = useCallback(async () => { 46 | try { 47 | const version = await AsyncStorage.getItem('app_version'); 48 | setStoredAppVersion(version); 49 | return version; 50 | } catch (err) { 51 | console.error('Error getting stored app version'); 52 | return null; 53 | } 54 | }, []); 55 | 56 | // Update the stored app version 57 | const updateStoredAppVersion = useCallback(async (version: string) => { 58 | try { 59 | await AsyncStorage.setItem('app_version', version); 60 | setStoredAppVersion(version); 61 | } catch (err) { 62 | console.error('Error updating stored app version'); 63 | } 64 | }, []); 65 | 66 | /** 67 | * Load the last update check time from storage 68 | */ 69 | const loadLastUpdateCheckTime = useCallback(async () => { 70 | try { 71 | const storedTime = await AsyncStorage.getItem('lastUpdateCheckTime'); 72 | if (storedTime) { 73 | setLastUpdateCheck(parseInt(storedTime, 10)); 74 | } 75 | } catch (error) { 76 | console.error('Error loading last update check time'); 77 | } 78 | }, []); 79 | 80 | /** 81 | * Save the current time as the last update check time 82 | */ 83 | const saveLastUpdateCheckTime = async () => { 84 | try { 85 | const currentTime = Date.now(); 86 | await AsyncStorage.setItem('lastUpdateCheckTime', currentTime.toString()); 87 | setLastUpdateCheck(currentTime); 88 | } catch (error) { 89 | console.error('Error saving last update check time'); 90 | } 91 | }; 92 | 93 | /** 94 | * Check if enough time has passed since the last update check 95 | */ 96 | const shouldCheckForUpdates = () => { 97 | if (!lastUpdateCheck) { 98 | return true; 99 | } 100 | 101 | const currentTime = Date.now(); 102 | const timeSinceLastCheck = currentTime - lastUpdateCheck; 103 | const shouldCheck = timeSinceLastCheck > UPDATE_CHECK_INTERVAL; 104 | 105 | return shouldCheck; 106 | }; 107 | 108 | /** 109 | * Check if app version needs update 110 | * @param silent If true, performs the check silently in the background without showing loading indicators 111 | * @param force If true, checks for updates regardless of when the last check occurred 112 | * 113 | * Usage examples: 114 | * - checkAppVersion(true, true): Silent check that bypasses time interval (ideal for HomeScreen focus) 115 | * - checkAppVersion(false, false): Regular check with loading indicator that respects time interval 116 | * - checkAppVersion(true, false): Silent check that respects time interval (good for background checks) 117 | * @returns Promise that resolves when the check is complete 118 | */ 119 | const checkAppVersion = useCallback(async (silent = false, force = false): Promise<void> => { 120 | try { 121 | // Skip check if it was done recently, unless force is true 122 | if (!force && !shouldCheckForUpdates()) { 123 | return; 124 | } 125 | 126 | if (!silent) { 127 | setLoading(true); 128 | } 129 | 130 | // Get the current app version 131 | const currentVersion = await UpdateService.getCurrentVersion(); 132 | 133 | if (!currentVersion) { 134 | console.error('Failed to get current app version'); 135 | return; 136 | } 137 | 138 | // Get the latest version info from the server 139 | const versionInfo = await UpdateService.checkForUpdates(); 140 | 141 | if (!versionInfo) { 142 | console.error('Failed to get latest version info'); 143 | return; 144 | } 145 | 146 | // Update the last check time 147 | await saveLastUpdateCheckTime(); 148 | 149 | // Set version info state 150 | setVersionInfo(versionInfo); 151 | 152 | // Check if update is needed 153 | const shouldShowModal = UpdateService.shouldShowUpdateModal( 154 | currentVersion, 155 | versionInfo.latestVersion, 156 | versionInfo.minimumVersion 157 | ); 158 | 159 | // Only show modal if update is needed 160 | if (shouldShowModal) { 161 | setUpdateModalVisible(true); 162 | } else { 163 | setUpdateModalVisible(false); 164 | } 165 | } catch (error) { 166 | console.error('Error checking app version'); 167 | // Don't show modal on error 168 | setUpdateModalVisible(false); 169 | } finally { 170 | if (!silent) { 171 | setLoading(false); 172 | } 173 | } 174 | }, [shouldCheckForUpdates, saveLastUpdateCheckTime]); 175 | 176 | /** 177 | * Close the update modal 178 | */ 179 | const closeUpdateModal = useCallback(async () => { 180 | try { 181 | // If there's version info and it's not a required update, mark it as skipped 182 | if (versionInfo) { 183 | const currentVersion = await UpdateService.getCurrentVersion(); 184 | const isRequired = UpdateService.isUpdateRequired(currentVersion, versionInfo.minimumVersion); 185 | 186 | if (!isRequired) { 187 | await UpdateService.skipVersion(versionInfo.latestVersion); 188 | } 189 | } 190 | setUpdateModalVisible(false); 191 | } catch (error) { 192 | console.error('Error closing update modal'); 193 | } 194 | }, [versionInfo]); 195 | 196 | /** 197 | * Navigate to the app store to update the app 198 | */ 199 | const goToUpdate = useCallback(async () => { 200 | try { 201 | await UpdateService.openAppStore(); 202 | } catch (error) { 203 | console.error('Error navigating to app store'); 204 | setError(`Failed to open app store: ${error instanceof Error ? error.message : String(error)}`); 205 | } 206 | }, []); 207 | 208 | // Check for updates on mount if checkOnMount is true 209 | useEffect(() => { 210 | // Always load the last update check time 211 | loadLastUpdateCheckTime(); 212 | 213 | // Only check for updates if checkOnMount is true 214 | if (checkOnMount) { 215 | // Use silent mode for initial check to avoid disrupting the user experience 216 | checkAppVersion(true, false); 217 | } 218 | }, [checkOnMount, loadLastUpdateCheckTime]); 219 | 220 | // Check if the app is from the App Store on mount 221 | useEffect(() => { 222 | checkAppSource(); 223 | }, [checkAppSource]); 224 | 225 | // Get the stored app version on mount 226 | useEffect(() => { 227 | getStoredAppVersion(); 228 | }, [getStoredAppVersion]); 229 | 230 | return { 231 | versionInfo, 232 | isAppStoreVersion, 233 | storedAppVersion, 234 | loading, 235 | error, 236 | updateModalVisible, 237 | setUpdateModalVisible, 238 | checkAppVersion, 239 | checkForUpdates: useCallback((silent = false, force = false): Promise<void> => { 240 | // Directly call checkAppVersion with the provided parameters 241 | return checkAppVersion(silent, force); 242 | }, [checkAppVersion]), 243 | closeUpdateModal, 244 | goToUpdate, 245 | }; 246 | }; 247 | 248 | export default useAppUpdate; 249 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/components/UpdateModal.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useCallback, useEffect } from 'react'; 2 | import { 3 | Modal, 4 | View, 5 | Text, 6 | StyleSheet, 7 | TouchableOpacity, 8 | Platform, 9 | Switch, 10 | Dimensions, 11 | ActivityIndicator 12 | } from 'react-native'; 13 | 14 | // Import the VersionInfo interface from the hook instead of the service 15 | import UpdateService from '../services/UpdateService'; 16 | import { compareVersions } from 'compare-versions'; 17 | 18 | const { width } = Dimensions.get('window'); 19 | 20 | interface UpdateModalProps { 21 | isVisible: boolean; 22 | versionInfo: { 23 | latestVersion: string; 24 | minimumVersion: string; 25 | releaseNotes?: string; 26 | } | null; 27 | onClose: () => void; 28 | onUpdate?: () => Promise<void>; // Optional prop for update handling 29 | } 30 | 31 | const UpdateModal: React.FC<UpdateModalProps> = ({ 32 | isVisible, 33 | versionInfo, 34 | onClose, 35 | onUpdate 36 | }) => { 37 | const [skipVersion, setSkipVersion] = useState(false); 38 | const [isUpdating, setIsUpdating] = useState(false); 39 | const [updateError, setUpdateError] = useState<string | null>(null); 40 | const [currentVersion, setCurrentVersion] = useState<string>(''); 41 | 42 | // Get the current version when the component mounts 43 | React.useEffect(() => { 44 | const fetchCurrentVersion = async () => { 45 | try { 46 | const version = await UpdateService.getCurrentVersion(); 47 | setCurrentVersion(version); 48 | } catch (error) { 49 | console.error('Error getting current version:', error); 50 | } 51 | }; 52 | 53 | fetchCurrentVersion(); 54 | }, []); 55 | 56 | const handleUpdate = useCallback(async () => { 57 | try { 58 | setIsUpdating(true); 59 | setUpdateError(null); 60 | 61 | if (onUpdate) { 62 | // Use the provided update handler if available 63 | await onUpdate(); 64 | } else { 65 | // Default behavior 66 | await UpdateService.openAppStore(); 67 | } 68 | 69 | // Only close modal if update is not required 70 | if (!versionInfo || !currentVersion || !UpdateService.isUpdateRequired(currentVersion, versionInfo.minimumVersion)) { 71 | onClose(); 72 | } 73 | } catch (error) { 74 | const errorMessage = error instanceof Error ? error.message : 'Failed to open app store'; 75 | setUpdateError(errorMessage); 76 | console.error('Update error:', error); 77 | } finally { 78 | setIsUpdating(false); 79 | } 80 | }, [versionInfo, onClose, onUpdate, currentVersion]); 81 | 82 | const handleSkip = useCallback(async () => { 83 | try { 84 | if (skipVersion && versionInfo) { 85 | // Skip this version 86 | await UpdateService.skipVersion(versionInfo.latestVersion); 87 | } 88 | 89 | // Only close modal if update is not required 90 | if (!versionInfo || !currentVersion || !UpdateService.isUpdateRequired(currentVersion, versionInfo.minimumVersion)) { 91 | onClose(); 92 | } 93 | } catch (error) { 94 | console.error('Error skipping version:', error); 95 | } 96 | }, [versionInfo, skipVersion, onClose, currentVersion]); 97 | 98 | // Determine the message to display based on version comparison 99 | const getUpdateMessage = () => { 100 | if (!versionInfo || !currentVersion) return ''; 101 | 102 | const { latestVersion, minimumVersion } = versionInfo; 103 | 104 | // Check if update is required 105 | const isRequired = UpdateService.isUpdateRequired(currentVersion, minimumVersion); 106 | 107 | if (isRequired) { 108 | return 'This update is required to continue using the app.'; 109 | } else if (compareVersions(currentVersion, latestVersion) < 0) { 110 | return 'A new version of the app is available. Would you like to update now?'; 111 | } else { 112 | return 'Your app is up to date.'; 113 | } 114 | }; 115 | 116 | if (!versionInfo) { 117 | return null; 118 | } 119 | 120 | return ( 121 | <Modal 122 | animationType="fade" 123 | transparent={true} 124 | visible={isVisible} 125 | onRequestClose={() => { 126 | if (!versionInfo || !currentVersion || !UpdateService.isUpdateRequired(currentVersion, versionInfo.minimumVersion)) { 127 | onClose(); 128 | } 129 | }} 130 | > 131 | <View style={styles.centeredView}> 132 | <View style={styles.modalView}> 133 | <Text style={styles.modalTitle}> 134 | {versionInfo && currentVersion && UpdateService.isUpdateRequired(currentVersion, versionInfo.minimumVersion) ? 'Required Update' : 'Update Available'} 135 | </Text> 136 | 137 | <Text style={styles.modalText}> 138 | {getUpdateMessage()} 139 | </Text> 140 | 141 | {versionInfo.releaseNotes && ( 142 | <Text style={styles.releaseNotes}> 143 | {versionInfo.releaseNotes} 144 | </Text> 145 | )} 146 | 147 | {updateError && ( 148 | <Text style={styles.errorText}> 149 | {updateError} 150 | </Text> 151 | )} 152 | 153 | {versionInfo && currentVersion && UpdateService.isUpdateRequired(currentVersion, versionInfo.minimumVersion) ? ( 154 | <Text style={styles.warningText}> 155 | This update includes critical changes and is required to continue using the app. 156 | </Text> 157 | ) : ( 158 | <View style={styles.skipContainer}> 159 | <Switch 160 | value={skipVersion} 161 | onValueChange={setSkipVersion} 162 | trackColor={{ false: '#767577', true: '#81b0ff' }} 163 | thumbColor={skipVersion ? '#f5dd4b' : '#f4f3f4'} 164 | /> 165 | <Text style={styles.skipText}>Skip this version</Text> 166 | </View> 167 | )} 168 | 169 | <View style={styles.buttonsContainer}> 170 | {(!versionInfo || !currentVersion || !UpdateService.isUpdateRequired(currentVersion, versionInfo.minimumVersion)) && ( 171 | <TouchableOpacity 172 | style={[styles.button, styles.buttonCancel]} 173 | onPress={handleSkip} 174 | disabled={isUpdating} 175 | > 176 | <Text style={styles.buttonText}>Later</Text> 177 | </TouchableOpacity> 178 | )} 179 | 180 | <TouchableOpacity 181 | style={[styles.button, styles.buttonUpdate]} 182 | onPress={handleUpdate} 183 | disabled={isUpdating} 184 | > 185 | {isUpdating ? ( 186 | <ActivityIndicator size="small" color="#ffffff" /> 187 | ) : ( 188 | <Text style={[styles.buttonText, styles.updateButtonText]}>Update Now</Text> 189 | )} 190 | </TouchableOpacity> 191 | </View> 192 | </View> 193 | </View> 194 | </Modal> 195 | ); 196 | }; 197 | 198 | const styles = StyleSheet.create({ 199 | centeredView: { 200 | flex: 1, 201 | justifyContent: 'center', 202 | alignItems: 'center', 203 | backgroundColor: 'rgba(0, 0, 0, 0.5)', 204 | }, 205 | modalView: { 206 | width: width * 0.85, 207 | backgroundColor: 'white', 208 | borderRadius: 20, 209 | padding: 25, 210 | alignItems: 'center', 211 | shadowColor: '#000', 212 | shadowOffset: { 213 | width: 0, 214 | height: 2, 215 | }, 216 | shadowOpacity: 0.25, 217 | shadowRadius: 4, 218 | elevation: 5, 219 | }, 220 | modalTitle: { 221 | fontSize: 20, 222 | fontWeight: 'bold', 223 | marginBottom: 15, 224 | textAlign: 'center', 225 | }, 226 | modalText: { 227 | marginBottom: 15, 228 | textAlign: 'center', 229 | fontSize: 16, 230 | lineHeight: 22, 231 | }, 232 | releaseNotes: { 233 | marginBottom: 15, 234 | textAlign: 'left', 235 | fontSize: 14, 236 | lineHeight: 20, 237 | padding: 10, 238 | backgroundColor: '#f9f9f9', 239 | borderRadius: 8, 240 | width: '100%', 241 | }, 242 | warningText: { 243 | color: '#d9534f', 244 | marginBottom: 20, 245 | textAlign: 'center', 246 | fontSize: 14, 247 | }, 248 | errorText: { 249 | color: '#d9534f', 250 | marginBottom: 15, 251 | textAlign: 'center', 252 | fontSize: 14, 253 | backgroundColor: '#ffeeee', 254 | padding: 10, 255 | borderRadius: 5, 256 | width: '100%', 257 | }, 258 | skipContainer: { 259 | flexDirection: 'row', 260 | alignItems: 'center', 261 | marginBottom: 20, 262 | }, 263 | skipText: { 264 | marginLeft: 10, 265 | fontSize: 14, 266 | }, 267 | buttonsContainer: { 268 | flexDirection: 'row', 269 | justifyContent: 'space-between', 270 | width: '100%', 271 | }, 272 | button: { 273 | borderRadius: 10, 274 | padding: 12, 275 | elevation: 2, 276 | minWidth: 100, 277 | marginHorizontal: 5, 278 | alignItems: 'center', 279 | }, 280 | buttonCancel: { 281 | backgroundColor: '#f8f9fa', 282 | borderWidth: 1, 283 | borderColor: '#dee2e6', 284 | }, 285 | buttonUpdate: { 286 | backgroundColor: '#007bff', 287 | flex: 1, 288 | }, 289 | buttonText: { 290 | textAlign: 'center', 291 | fontSize: 16, 292 | }, 293 | updateButtonText: { 294 | color: 'white', 295 | fontWeight: 'bold', 296 | }, 297 | }); 298 | 299 | export default UpdateModal; 300 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/services/UpdateService.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Platform, Linking } from 'react-native'; 2 | import AsyncStorage from '@react-native-async-storage/async-storage'; 3 | import axios from 'axios'; 4 | import { API } from '../config/apiConfig'; 5 | import DeviceInfo from 'react-native-device-info'; 6 | import { compareVersions } from 'compare-versions'; 7 | 8 | // Storage keys 9 | const STORAGE_KEYS = { 10 | SKIPPED_VERSIONS: 'skipped_versions', 11 | CURRENT_VERSION: 'current_version', 12 | }; 13 | 14 | // App store URLs and IDs 15 | const APP_STORE = { 16 | IOS_URL: 'https://apps.apple.com/us/app/id6670172327', 17 | IOS_LOOKUP_URL: 'https://itunes.apple.com/lookup?id=6670172327', 18 | ANDROID_URL: 'https://play.google.com/store/apps/details?id=your.package.name', 19 | }; 20 | 21 | interface VersionData { 22 | latestVersion: string; 23 | minimumVersion: string; 24 | releaseNotes?: string; 25 | } 26 | 27 | class UpdateService { 28 | /** 29 | * Get the current app version 30 | * @returns Promise resolving to the current app version 31 | */ 32 | static async getCurrentVersion(): Promise<string> { 33 | try { 34 | return DeviceInfo.getVersion(); 35 | } catch (error) { 36 | console.error('Error getting current version:', error); 37 | return '1.0.0'; // Fallback version 38 | } 39 | } 40 | 41 | /** 42 | * Get the stored app version 43 | * @returns Promise resolving to the stored app version or null if not found 44 | */ 45 | static async getStoredVersion(): Promise<string | null> { 46 | try { 47 | return await AsyncStorage.getItem(STORAGE_KEYS.CURRENT_VERSION); 48 | } catch (error) { 49 | console.error('Error getting stored version:', error); 50 | return null; 51 | } 52 | } 53 | 54 | /** 55 | * Update the stored app version 56 | * @param version The version to store 57 | * @returns Promise resolving when the version is stored 58 | */ 59 | static async updateStoredVersion(version: string): Promise<void> { 60 | try { 61 | await AsyncStorage.setItem(STORAGE_KEYS.CURRENT_VERSION, version); 62 | } catch (error) { 63 | console.error('Error updating stored version:', error); 64 | } 65 | } 66 | 67 | /** 68 | * Check for updates from the server 69 | * @returns Promise resolving to the version data 70 | */ 71 | static async checkForUpdates(): Promise<VersionData | null> { 72 | try { 73 | // First try to get the version from the App Store if on iOS 74 | if (Platform.OS === 'ios') { 75 | const appStoreVersion = await this.fetchAppStoreVersion(); 76 | if (appStoreVersion) { 77 | return { 78 | latestVersion: appStoreVersion, 79 | minimumVersion: appStoreVersion, // Assuming minimum version is the same as latest for App Store 80 | releaseNotes: 'A new version is available in the App Store.', 81 | }; 82 | } 83 | } 84 | 85 | // If App Store check fails or we're not on iOS, fall back to API check 86 | 87 | // Add a timeout to prevent long waits if the server is unresponsive 88 | const response = await axios.get(`${API.BASE_URL}/${API.ENDPOINTS.APP_VERSION}`, { 89 | timeout: 5000, // 5 second timeout 90 | }); 91 | 92 | if (response.data && response.data.success) { 93 | return { 94 | latestVersion: response.data.data.latestVersion || '1.0.0', 95 | minimumVersion: response.data.data.minimumVersion || '1.0.0', 96 | releaseNotes: response.data.data.releaseNotes || '', 97 | }; 98 | } 99 | 100 | // Return a default version info instead of null 101 | return await this.getDefaultVersionInfo(); 102 | } catch (error) { 103 | console.error('Error checking for updates'); 104 | // Instead of just logging the error, provide a fallback 105 | return await this.getDefaultVersionInfo(); 106 | } 107 | } 108 | 109 | /** 110 | * Fetch the latest version from the App Store 111 | * @returns Promise resolving to the App Store version or null if not found 112 | */ 113 | static async fetchAppStoreVersion(): Promise<string | null> { 114 | try { 115 | const response = await axios.get(APP_STORE.IOS_LOOKUP_URL, { 116 | timeout: 5000, // 5 second timeout 117 | }); 118 | 119 | if (response.data && response.data.resultCount > 0) { 120 | const appStoreVersion = response.data.results[0].version; 121 | return appStoreVersion; 122 | } 123 | 124 | return null; 125 | } catch (error) { 126 | console.error('Error fetching app version from App Store'); 127 | return null; 128 | } 129 | } 130 | 131 | /** 132 | * Get default version information when the server is unavailable 133 | * @returns Default version data 134 | */ 135 | private static async getDefaultVersionInfo(): Promise<VersionData> { 136 | // Get the current version from the app 137 | const currentVersion = await this.getCurrentVersion(); 138 | 139 | return { 140 | latestVersion: currentVersion, // Use current version as latest if we can't fetch it 141 | minimumVersion: '1.0.0', // Use a safe default for minimum version 142 | releaseNotes: 'Unable to fetch update information. Please check your internet connection.', 143 | }; 144 | } 145 | 146 | /** 147 | * Check if an update is required 148 | * @param currentVersion The current app version 149 | * @param minimumVersion The minimum required version 150 | * @returns True if an update is required, false otherwise 151 | */ 152 | static isUpdateRequired(currentVersion: string, minimumVersion: string): boolean { 153 | try { 154 | return compareVersions(currentVersion, minimumVersion) < 0; 155 | } catch (error) { 156 | console.error('Error checking if update is required'); 157 | return false; // Default to not required if there's an error 158 | } 159 | } 160 | 161 | /** 162 | * Check if the update modal should be shown 163 | * @param currentVersion Current app version 164 | * @param latestVersion Latest available version 165 | * @param minimumRequiredVersion Minimum required version 166 | * @returns Boolean indicating if the update modal should be shown 167 | */ 168 | static shouldShowUpdateModal( 169 | currentVersion: string, 170 | latestVersion: string, 171 | minimumRequiredVersion: string 172 | ): boolean { 173 | // Check if update is required (current version is less than minimum required) 174 | const isRequired = this.isUpdateRequired(currentVersion, minimumRequiredVersion); 175 | 176 | // Check if update is available (current version is less than latest version) 177 | const isUpdateAvailable = compareVersions(currentVersion, latestVersion) < 0; 178 | 179 | // Only show modal if update is required or an update is available 180 | return isRequired || isUpdateAvailable; 181 | } 182 | 183 | /** 184 | * Skip a version 185 | * @param version The version to skip 186 | * @returns Promise resolving when the version is skipped 187 | */ 188 | static async skipVersion(version: string): Promise<void> { 189 | try { 190 | // Get the current skipped versions 191 | const skippedVersionsString = await AsyncStorage.getItem(STORAGE_KEYS.SKIPPED_VERSIONS); 192 | const skippedVersions = skippedVersionsString ? JSON.parse(skippedVersionsString) : []; 193 | 194 | // Add the version if it's not already skipped 195 | if (!skippedVersions.includes(version)) { 196 | skippedVersions.push(version); 197 | await AsyncStorage.setItem(STORAGE_KEYS.SKIPPED_VERSIONS, JSON.stringify(skippedVersions)); 198 | } 199 | } catch (error) { 200 | console.error('Error skipping version:', error); 201 | } 202 | } 203 | 204 | /** 205 | * Check if a version has been skipped 206 | * @param version The version to check 207 | * @returns Promise resolving to true if the version has been skipped, false otherwise 208 | */ 209 | static async isVersionSkipped(version: string): Promise<boolean> { 210 | try { 211 | const skippedVersionsString = await AsyncStorage.getItem(STORAGE_KEYS.SKIPPED_VERSIONS); 212 | const skippedVersions = skippedVersionsString ? JSON.parse(skippedVersionsString) : []; 213 | 214 | return skippedVersions.includes(version); 215 | } catch (error) { 216 | console.error('Error checking if version is skipped:', error); 217 | return false; 218 | } 219 | } 220 | 221 | /** 222 | * Open the app store 223 | * @returns Promise resolving when the app store is opened 224 | */ 225 | static async openAppStore(): Promise<void> { 226 | try { 227 | const url = Platform.OS === 'ios' ? APP_STORE.IOS_URL : APP_STORE.ANDROID_URL; 228 | 229 | const canOpen = await Linking.canOpenURL(url); 230 | 231 | if (canOpen) { 232 | await Linking.openURL(url); 233 | } else { 234 | console.warn(`Cannot open URL: ${url}`); 235 | } 236 | } catch (error) { 237 | console.error('Error opening app store:', error); 238 | } 239 | } 240 | 241 | /** 242 | * Check if the app is installed from the App Store 243 | * @returns Promise resolving to whether the app is from the App Store 244 | */ 245 | static async isAppStoreVersion(): Promise<boolean> { 246 | try { 247 | if (Platform.OS === 'ios') { 248 | // On iOS, we can use DeviceInfo to determine if the app is running from the App Store 249 | // This is a simplified approach - in a real app you might need a more robust check 250 | const bundleId = DeviceInfo.getBundleId(); 251 | const isTestFlight = await DeviceInfo.isEmulator(); 252 | 253 | // If it's not an emulator and has a valid bundle ID, it's likely from the App Store 254 | return !isTestFlight && !!bundleId; 255 | } 256 | 257 | // For Android, we would need a different approach 258 | return false; 259 | } catch (error) { 260 | console.error('Error determining app source:', error); 261 | return false; 262 | } 263 | } 264 | } 265 | 266 | export default UpdateService; 267 | ``` -------------------------------------------------------------------------------- /resources/standards/state_management.md: -------------------------------------------------------------------------------- ```markdown 1 | # State Management Standards 2 | 3 | BluestoneApps follows these standards for state management in React Native applications. 4 | 5 | ## State Management Strategy 6 | 7 | We use a pragmatic approach to state management with a focus on simplicity and maintainability: 8 | 9 | 1. **Local Component State**: For UI state that doesn't need to be shared 10 | 2. **Custom Hooks**: For reusable state logic 11 | 3. **AsyncStorage**: For persistent data 12 | 4. **Context API**: For state shared across components (when necessary) 13 | 14 | ## Local Component State 15 | 16 | Use React's `useState` and `useEffect` hooks for component-local state: 17 | 18 | ```typescript 19 | // Simple state with useState 20 | const [isLoading, setIsLoading] = useState<boolean>(false); 21 | const [data, setData] = useState<DataType | null>(null); 22 | 23 | // Side effects with useEffect 24 | useEffect(() => { 25 | const fetchData = async () => { 26 | try { 27 | setIsLoading(true); 28 | const result = await apiService.getData(); 29 | setData(result); 30 | } catch (error) { 31 | console.error('Error fetching data:', error); 32 | } finally { 33 | setIsLoading(false); 34 | } 35 | }; 36 | 37 | fetchData(); 38 | }, []); 39 | ``` 40 | 41 | ### When to use local state: 42 | 43 | - Form input values 44 | - UI states like open/closed, visible/hidden 45 | - Component-specific data loading states 46 | - Temporary data that doesn't need persistence 47 | 48 | ## Custom Hooks 49 | 50 | Extract reusable state logic into custom hooks: 51 | 52 | ```typescript 53 | // src/hooks/useAppUpdate.ts 54 | import { useState, useCallback, useEffect } from 'react'; 55 | import UpdateService from '../services/UpdateService'; 56 | import AsyncStorage from '@react-native-async-storage/async-storage'; 57 | 58 | interface VersionInfo { 59 | latestVersion: string; 60 | minimumVersion: string; 61 | releaseNotes?: string; 62 | } 63 | 64 | const useAppUpdate = (checkOnMount = true, updateCheckInterval = 60 * 60 * 1000) => { 65 | const [loading, setLoading] = useState(false); 66 | const [error, setError] = useState<string | null>(null); 67 | const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null); 68 | const [updateModalVisible, setUpdateModalVisible] = useState(false); 69 | 70 | // Check for updates 71 | const checkForUpdates = useCallback(async () => { 72 | try { 73 | setLoading(true); 74 | setError(null); 75 | 76 | const info = await UpdateService.checkForUpdates(); 77 | setVersionInfo(info); 78 | 79 | // Show update modal if needed 80 | if (info && UpdateService.isUpdateRequired(info)) { 81 | setUpdateModalVisible(true); 82 | } 83 | 84 | // Store last check time 85 | await AsyncStorage.setItem('last_update_check', Date.now().toString()); 86 | 87 | return info; 88 | } catch (err) { 89 | setError(err instanceof Error ? err.message : 'Unknown error'); 90 | return null; 91 | } finally { 92 | setLoading(false); 93 | } 94 | }, []); 95 | 96 | // Check for updates on mount if enabled 97 | useEffect(() => { 98 | if (checkOnMount) { 99 | checkForUpdates(); 100 | } 101 | }, [checkOnMount, checkForUpdates]); 102 | 103 | // Return state and functions 104 | return { 105 | loading, 106 | error, 107 | versionInfo, 108 | updateModalVisible, 109 | setUpdateModalVisible, 110 | checkForUpdates, 111 | }; 112 | }; 113 | 114 | export default useAppUpdate; 115 | ``` 116 | 117 | ### When to use custom hooks: 118 | 119 | - Reusable state logic across multiple components 120 | - Complex state management with multiple related states 121 | - API communication patterns 122 | - Feature-specific logic that combines multiple React hooks 123 | 124 | ## AsyncStorage for Persistence 125 | 126 | Use AsyncStorage for persisting data across app sessions: 127 | 128 | ```typescript 129 | // Example in authService.ts 130 | import AsyncStorage from '@react-native-async-storage/async-storage'; 131 | 132 | class AuthService { 133 | async login(email: string, password: string): Promise<LoginResponse> { 134 | try { 135 | const response = await axiosRequest.post<LoginResponse>( 136 | API.ENDPOINTS.LOGIN, 137 | { email, password } 138 | ); 139 | 140 | if (response?.loginInfo?.token) { 141 | // Store authentication data 142 | await AsyncStorage.setItem('userToken', response.loginInfo.token); 143 | await AsyncStorage.setItem('userData', JSON.stringify({ loginInfo: response.loginInfo })); 144 | } 145 | 146 | return response; 147 | } catch (error) { 148 | console.error('Login error:', error); 149 | throw error; 150 | } 151 | } 152 | 153 | async logout(): Promise<void> { 154 | try { 155 | // Remove stored data 156 | await AsyncStorage.multiRemove(['userToken', 'userData', 'rememberMe']); 157 | } catch (error) { 158 | console.error('Logout error:', error); 159 | throw error; 160 | } 161 | } 162 | 163 | async isLoggedIn(): Promise<boolean> { 164 | try { 165 | const token = await AsyncStorage.getItem('userToken'); 166 | return token !== null; 167 | } catch (error) { 168 | console.error('Error checking login status:', error); 169 | return false; 170 | } 171 | } 172 | } 173 | 174 | export default new AuthService(); 175 | ``` 176 | 177 | ### When to use AsyncStorage: 178 | 179 | - Authentication tokens 180 | - User preferences 181 | - App configuration 182 | - Cached data that should persist between app launches 183 | - Form data that should be saved if the app closes 184 | 185 | ## Context API (When Needed) 186 | 187 | For cases where state needs to be shared across multiple components, use the Context API: 188 | 189 | ```typescript 190 | // src/navigation/NavigationContext.tsx 191 | import React, { createContext, useState, useContext, ReactNode } from 'react'; 192 | 193 | interface NavigationContextType { 194 | currentScreen: string; 195 | setCurrentScreen: (screen: string) => void; 196 | previousScreen: string | null; 197 | navigateBack: () => void; 198 | } 199 | 200 | const NavigationContext = createContext<NavigationContextType | undefined>(undefined); 201 | 202 | export const NavigationProvider: React.FC<{children: ReactNode}> = ({ children }) => { 203 | const [currentScreen, setCurrentScreen] = useState<string>('Home'); 204 | const [previousScreen, setPreviousScreen] = useState<string | null>(null); 205 | 206 | const handleSetCurrentScreen = (screen: string) => { 207 | setPreviousScreen(currentScreen); 208 | setCurrentScreen(screen); 209 | }; 210 | 211 | const navigateBack = () => { 212 | if (previousScreen) { 213 | setCurrentScreen(previousScreen); 214 | setPreviousScreen(null); 215 | } 216 | }; 217 | 218 | return ( 219 | <NavigationContext.Provider 220 | value={{ 221 | currentScreen, 222 | setCurrentScreen: handleSetCurrentScreen, 223 | previousScreen, 224 | navigateBack 225 | }} 226 | > 227 | {children} 228 | </NavigationContext.Provider> 229 | ); 230 | }; 231 | 232 | export const useNavigation = () => { 233 | const context = useContext(NavigationContext); 234 | if (context === undefined) { 235 | throw new Error('useNavigation must be used within a NavigationProvider'); 236 | } 237 | return context; 238 | }; 239 | ``` 240 | 241 | ### When to use Context API: 242 | 243 | - Theme information 244 | - Authentication state (when needed across many components) 245 | - Navigation state 246 | - Localization data 247 | - Application-wide preferences 248 | 249 | ## Best Practices 250 | 251 | 1. **Keep state as local as possible** - Only lift state up when necessary 252 | 2. **Use TypeScript** - Define proper interfaces for all state 253 | 3. **Implement proper error handling** - Always handle errors in async operations 254 | 4. **Separate concerns** - Keep UI state separate from data state 255 | 5. **Optimize performance** - Use memoization techniques like `useMemo` and `useCallback` 256 | 6. **Consistent patterns** - Use similar patterns across the application 257 | 7. **Minimize state** - Only track what's necessary in state 258 | 8. **Document state management** - Add comments explaining complex state logic 259 | 260 | const selectUsers = (state) => state.users.data; 261 | const selectCurrentUserId = (state) => state.auth.user?.id; 262 | 263 | export const selectCurrentUser = createSelector( 264 | [selectUsers, selectCurrentUserId], 265 | (users, currentUserId) => { 266 | if (!users || !currentUserId) return null; 267 | return users.find(user => user.id === currentUserId); 268 | } 269 | ); 270 | 271 | export const selectActiveUsers = createSelector( 272 | [selectUsers], 273 | (users) => users.filter(user => user.isActive) 274 | ); 275 | ``` 276 | 277 | ## Persistence 278 | 279 | For persisting state across app restarts: 280 | 281 | ```javascript 282 | // src/store/index.js 283 | import { persistStore, persistReducer } from 'redux-persist'; 284 | import AsyncStorage from '@react-native-async-storage/async-storage'; 285 | import { configureStore } from '@reduxjs/toolkit'; 286 | import rootReducer from './reducers'; 287 | 288 | const persistConfig = { 289 | key: 'root', 290 | storage: AsyncStorage, 291 | whitelist: ['auth', 'userPreferences'], // only these reducers will be persisted 292 | }; 293 | 294 | const persistedReducer = persistReducer(persistConfig, rootReducer); 295 | 296 | export const store = configureStore({ 297 | reducer: persistedReducer, 298 | middleware: (getDefaultMiddleware) => 299 | getDefaultMiddleware({ 300 | serializableCheck: { 301 | ignoredActions: ['persist/PERSIST', 'persist/REHYDRATE'], 302 | }, 303 | }), 304 | }); 305 | 306 | export const persistor = persistStore(store); 307 | ``` 308 | 309 | ## State Management Best Practices 310 | 311 | 1. **Single Source of Truth**: Store overlapping data in only one place 312 | 2. **Immutable Updates**: Always use immutable patterns for updating state 313 | 3. **Normalize Complex Data**: Use normalized state structure for relational data 314 | 4. **Minimal State**: Only store essential application state 315 | 5. **Separate UI State**: Keep UI state separate from domain/data state 316 | 6. **Smart/Dumb Components**: Use container/presentational pattern 317 | 7. **Consistent Pattern**: Choose specific patterns for specific state needs 318 | 8. **Avoid Prop Drilling**: Use Context or Redux instead of passing props deeply 319 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/PostLogin/Calendar/CalendarScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | import { View, Text, ScrollView, TouchableOpacity } from 'react-native'; 3 | import { Calendar } from 'react-native-calendars'; 4 | import AsyncStorage from '@react-native-async-storage/async-storage'; 5 | import { styles } from './Styles'; 6 | import axiosRequest from '../../../helper/axiosRequest'; 7 | import { API } from '../../../helper/config'; 8 | import axios from 'axios'; 9 | import { colors } from '../../../theme/colors'; 10 | 11 | interface CalendarEvent { 12 | event_id: string; 13 | event_title: string; 14 | event_content: string; 15 | event_date: string; 16 | event_to_time: string; 17 | event_from_time: string; 18 | event_street_address: string; 19 | event_apt_suite: string; 20 | event_city: string; 21 | event_state: string; 22 | event_zip: string; 23 | event_longitude: string | null; 24 | event_latitude: string | null; 25 | event_price: string | null; 26 | } 27 | 28 | interface MarkedDates { 29 | [date: string]: { 30 | marked?: boolean; 31 | selected?: boolean; 32 | dotColor?: string; 33 | }; 34 | } 35 | 36 | const CalendarScreen = ({ navigation }: any) => { 37 | const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); 38 | const [events, setEvents] = useState<CalendarEvent[]>([]); 39 | const [markedDates, setMarkedDates] = useState<MarkedDates>({}); 40 | 41 | const fetchEvents = async () => { 42 | try { 43 | // Try to get token from both possible storage locations 44 | let token = await AsyncStorage.getItem('userToken'); 45 | if (!token) { 46 | const userDataStr = await AsyncStorage.getItem('userData'); 47 | if (userDataStr) { 48 | const userData = JSON.parse(userDataStr); 49 | token = userData?.loginInfo?.token; 50 | } 51 | } 52 | 53 | if (!token) { 54 | console.error('No token found in either storage location'); 55 | return; 56 | } 57 | 58 | console.log('Fetching events with token'); 59 | const response = await axiosRequest.get(API.ENDPOINTS.GET_EVENTS, { 60 | headers: { 61 | 'Accept': 'application/json', 62 | 'Content-Type': 'application/json', 63 | 'Authorization': `Bearer ${token}` 64 | }, 65 | params: { 66 | month: new Date(selectedDate).getMonth() + 1, // Adding 1 because getMonth() returns 0-11 67 | year: new Date(selectedDate).getFullYear() 68 | } 69 | }); 70 | 71 | console.log('Events API Response:', response); 72 | 73 | if (response?.data?.status === 'ok' && Array.isArray(response?.data?.listing)) { 74 | const eventsData = response.data.listing; 75 | console.log('Events found:', eventsData); 76 | setEvents(eventsData); 77 | 78 | // Mark dates that have events 79 | const marked: MarkedDates = {}; 80 | eventsData.forEach((event: CalendarEvent) => { 81 | const eventDate = event.event_date; 82 | marked[eventDate] = { 83 | marked: true, 84 | dotColor: colors.tertiary 85 | }; 86 | }); 87 | 88 | // Also mark the selected date 89 | const selectedDateKey = new Date(selectedDate).toISOString().split('T')[0]; 90 | marked[selectedDateKey] = { 91 | ...marked[selectedDateKey], 92 | selected: true, 93 | selectedColor: colors.primary // Add this to make selected date more visible 94 | }; 95 | 96 | setMarkedDates(marked); 97 | } else { 98 | console.warn('Unexpected response format:', response); 99 | setEvents([]); 100 | setMarkedDates({ 101 | [selectedDate]: { 102 | selected: true, 103 | selectedColor: colors.primary 104 | } 105 | }); 106 | } 107 | } catch (error) { 108 | console.error('Error fetching events:', error); 109 | if (axios.isAxiosError(error)) { 110 | console.error('Error details:', { 111 | status: error.response?.status, 112 | statusText: error.response?.statusText, 113 | url: error.config?.url, 114 | method: error.config?.method, 115 | headers: error.config?.headers, 116 | params: error.config?.params, 117 | response: error.response?.data 118 | }); 119 | } 120 | setEvents([]); 121 | setMarkedDates({ 122 | [selectedDate]: { 123 | selected: true 124 | } 125 | }); 126 | } 127 | }; 128 | 129 | useEffect(() => { 130 | fetchEvents(); 131 | }, [selectedDate]); // Re-fetch when selected date changes 132 | 133 | useEffect(() => { 134 | console.log('Selected date changed to:', selectedDate); 135 | console.log('Current events:', events); 136 | console.log('Filtered events:', filteredEvents); 137 | }, [selectedDate, events, filteredEvents]); 138 | 139 | const onDayPress = (day: any) => { 140 | console.log('Day pressed - raw data:', day); 141 | const newSelectedDate = day.dateString; 142 | console.log('Setting new selected date:', newSelectedDate); 143 | 144 | // Update marked dates to include the new selection 145 | const newMarkedDates = { ...markedDates }; 146 | 147 | // Remove selected state from previous date 148 | const previousSelectedDateKey = new Date(selectedDate).toISOString().split('T')[0]; 149 | if (markedDates[previousSelectedDateKey]) { 150 | newMarkedDates[previousSelectedDateKey] = { 151 | ...markedDates[previousSelectedDateKey], 152 | selected: false 153 | }; 154 | // If the date only had selected: true, remove it entirely 155 | if (!newMarkedDates[previousSelectedDateKey].marked) { 156 | delete newMarkedDates[previousSelectedDateKey]; 157 | } 158 | } 159 | 160 | // Add selected state to new date 161 | newMarkedDates[newSelectedDate] = { 162 | ...markedDates[newSelectedDate], 163 | selected: true 164 | }; 165 | 166 | // Update states 167 | setMarkedDates(newMarkedDates); 168 | setSelectedDate(newSelectedDate); 169 | }; 170 | 171 | // Filter events for selected date 172 | const filteredEvents = useMemo(() => { 173 | console.log('Filtering events for date:', selectedDate); 174 | console.log('Available events:', events); 175 | const filtered = events.filter(event => event.event_date === selectedDate); 176 | console.log('Filtered events for selected date:', filtered); 177 | return filtered; 178 | }, [events, selectedDate]); 179 | 180 | const formatDate = (dateString: string) => { 181 | console.log('Formatting date:', dateString); 182 | try { 183 | const [year, month, day] = dateString.split('-').map(num => parseInt(num, 10)); 184 | const date = new Date(year, month - 1, day); // month is 0-based in Date constructor 185 | console.log('Constructed date:', date); 186 | return date.toLocaleDateString('en-US', { 187 | weekday: 'long', 188 | year: 'numeric', 189 | month: 'long', 190 | day: 'numeric' 191 | }); 192 | } catch (error) { 193 | console.error('Error formatting date:', error); 194 | return dateString; 195 | } 196 | }; 197 | 198 | const formattedSelectedDate = useMemo(() => { 199 | return formatDate(selectedDate); 200 | }, [selectedDate]); 201 | 202 | useEffect(() => { 203 | console.log('Selected date state updated:', selectedDate); 204 | console.log('Formatted date:', formatDate(selectedDate)); 205 | }, [selectedDate]); 206 | 207 | const formatTime = (timeStr: string) => { 208 | const [hours, minutes] = timeStr.split(':'); 209 | const date = new Date(); 210 | date.setHours(parseInt(hours, 10)); 211 | date.setMinutes(parseInt(minutes, 10)); 212 | return date.toLocaleTimeString('en-US', { 213 | hour: 'numeric', 214 | minute: '2-digit', 215 | hour12: true 216 | }); 217 | }; 218 | 219 | return ( 220 | <View style={styles.container}> 221 | <Calendar 222 | style={styles.calendar} 223 | onDayPress={onDayPress} 224 | markedDates={markedDates} 225 | theme={{ 226 | selectedDayBackgroundColor: colors.secondary, 227 | selectedDayTextColor: colors.white, 228 | todayTextColor: colors.primary, 229 | dotColor: colors.danger, 230 | arrowColor: colors.dark, 231 | monthTextColor: colors.dark, 232 | textDayFontWeight: '300', 233 | textMonthFontWeight: 'bold', 234 | textDayHeaderFontWeight: '500', 235 | textDayColor: colors.tertiary, 236 | textDayHeaderColor: colors.secondary 237 | }} 238 | /> 239 | <View style={[styles.sectionHeader, { padding: 20, marginVertical: 15 }]}> 240 | <Text style={[styles.sectionTitle, { fontSize: 20, fontWeight: '600' }]}> 241 | Events for {formatDate(selectedDate)} 242 | </Text> 243 | </View> 244 | <ScrollView style={styles.eventsContainer}> 245 | {filteredEvents.length > 0 ? ( 246 | filteredEvents.map((event) => ( 247 | <TouchableOpacity 248 | key={event.event_id} 249 | style={styles.eventCard} 250 | onPress={() => navigation.navigate('EventDetails', { event })} 251 | > 252 | <Text style={styles.eventTitle}>{event.event_title}</Text> 253 | <Text style={styles.eventTime}> 254 | {formatTime(event.event_from_time)} - {formatTime(event.event_to_time)} 255 | </Text> 256 | <Text style={styles.eventLocation}> 257 | {event.event_street_address} 258 | {event.event_apt_suite ? `, ${event.event_apt_suite}` : ''} 259 | {event.event_city ? `, ${event.event_city}` : ''} 260 | {event.event_state ? `, ${event.event_state}` : ''} 261 | {event.event_zip ? ` ${event.event_zip}` : ''} 262 | </Text> 263 | </TouchableOpacity> 264 | )) 265 | ) : ( 266 | <Text style={styles.noEventsText}>No events on this date</Text> 267 | )} 268 | </ScrollView> 269 | </View> 270 | ); 271 | }; 272 | 273 | export default CalendarScreen; 274 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/navigation/DrawerNavigator.tsx: -------------------------------------------------------------------------------- ```typescript 1 | import React from 'react'; 2 | import { withNavigationWrapper } from './NavigationWrapper'; 3 | import { View, StyleSheet, Platform, TouchableOpacity } from 'react-native'; 4 | import { createDrawerNavigator } from '@react-navigation/drawer'; 5 | import { GestureHandlerRootView } from 'react-native-gesture-handler'; 6 | import Icon from 'react-native-vector-icons/Ionicons'; 7 | import authService from '../services/authService'; 8 | import TabNavigator from './TabNavigator'; 9 | import AboutUsScreen from '../screens/PostLogin/AboutUs/AboutUsScreen'; 10 | import CalendarScreen from '../screens/PostLogin/Calendar/CalendarScreen'; 11 | import EventDetails from '../screens/PostLogin/Calendar/EventDetails'; 12 | import MyProfileScreen from '../screens/PostLogin/MyProfile/MyProfileScreen'; 13 | import EditProfileScreen from '../screens/PostLogin/EditProfile/EditProfileScreen'; 14 | import ChangePasswordScreen from '../screens/PostLogin/ChangePassword/ChangePasswordScreen'; 15 | import BluestoneAppsAIScreen from '../screens/PostLogin/BluestoneAppsAI/BluestoneAppsAIScreen'; 16 | import HomeScreen from '../screens/PostLogin/Home/HomeScreen'; 17 | import ContactScreen from '../screens/PostLogin/Contact/ContactScreen'; 18 | import PostsScreen from '../screens/PostLogin/Posts/PostsScreen'; 19 | import PostScreen from '../screens/PostLogin/Posts/PostScreen'; 20 | import { colors } from '../theme/colors'; 21 | 22 | const Drawer = createDrawerNavigator(); 23 | 24 | // Create a wrapper component that combines screen content with TabNavigator 25 | const ScreenWrapper = ({ children, navigation }: { children: React.ReactNode; navigation: any }) => { 26 | return ( 27 | <View style={styles.container}> 28 | <View style={styles.contentWrapper}> 29 | {React.cloneElement(children as React.ReactElement, { navigation })} 30 | </View> 31 | <View style={styles.tabNavigator}> 32 | <TabNavigator /> 33 | </View> 34 | </View> 35 | ); 36 | }; 37 | 38 | // Create screen-specific wrappers 39 | const AboutUsWrapper = withNavigationWrapper(({ navigation }: any) => ( 40 | <ScreenWrapper navigation={navigation}> 41 | <AboutUsScreen /> 42 | </ScreenWrapper> 43 | )); 44 | 45 | const HomeWrapper = withNavigationWrapper(({ navigation }: any) => ( 46 | <ScreenWrapper navigation={navigation}> 47 | <HomeScreen /> 48 | </ScreenWrapper> 49 | )); 50 | 51 | const CalendarWrapper = withNavigationWrapper(({ navigation }: any) => ( 52 | <ScreenWrapper navigation={navigation}> 53 | <CalendarScreen /> 54 | </ScreenWrapper> 55 | )); 56 | 57 | const EventDetailsWrapper = withNavigationWrapper(({ navigation }: any) => ( 58 | <ScreenWrapper navigation={navigation}> 59 | <EventDetails /> 60 | </ScreenWrapper> 61 | )); 62 | 63 | const ProfileWrapper = withNavigationWrapper(({ navigation }: any) => ( 64 | <ScreenWrapper navigation={navigation}> 65 | <MyProfileScreen /> 66 | </ScreenWrapper> 67 | )); 68 | 69 | const EditProfileWrapper = withNavigationWrapper(({ navigation }: any) => ( 70 | <View style={styles.container}> 71 | <EditProfileScreen navigation={navigation} /> 72 | </View> 73 | )); 74 | 75 | const ChangePasswordWrapper = withNavigationWrapper(({ navigation }: any) => ( 76 | <View style={styles.container}> 77 | <ChangePasswordScreen navigation={navigation} /> 78 | </View> 79 | )); 80 | 81 | const AIWrapper = withNavigationWrapper(({ navigation }: any) => ( 82 | <ScreenWrapper navigation={navigation}> 83 | <BluestoneAppsAIScreen /> 84 | </ScreenWrapper> 85 | )); 86 | 87 | const ContactWrapper = withNavigationWrapper(({ navigation }: any) => ( 88 | <ScreenWrapper navigation={navigation}> 89 | <ContactScreen /> 90 | </ScreenWrapper> 91 | )); 92 | 93 | const PostWrapper = withNavigationWrapper(({ navigation }: any) => ( 94 | <ScreenWrapper navigation={navigation}> 95 | <PostScreen /> 96 | </ScreenWrapper> 97 | )); 98 | 99 | const DrawerNavigator = ({ navigation }: any) => { 100 | const handleLogout = async () => { 101 | try { 102 | // Call the logout service 103 | await authService.logout(); 104 | } catch (error) { 105 | console.error('Logout error:', error); 106 | } finally { 107 | // Always navigate to login screen, even if there was an error in logout 108 | navigation.reset({ 109 | index: 0, 110 | routes: [{ name: 'Login' }], 111 | }); 112 | } 113 | }; 114 | 115 | return ( 116 | <GestureHandlerRootView style={{ flex: 1 }}> 117 | <Drawer.Navigator 118 | screenOptions={{ 119 | headerShown: true, 120 | headerStyle: { 121 | backgroundColor: colors.headerBg, 122 | elevation: 0, 123 | shadowOpacity: 0, 124 | borderBottomWidth: 1, 125 | borderBottomColor: colors.light, 126 | }, 127 | headerTintColor: colors.headerFont, 128 | headerTitleStyle: { 129 | fontWeight: '600', 130 | color: colors.headerFont, 131 | }, 132 | drawerStyle: { 133 | backgroundColor: colors.white, 134 | width: 280, 135 | }, 136 | drawerActiveBackgroundColor: colors.footerBg, 137 | drawerActiveTintColor: colors.footerFont, 138 | drawerInactiveTintColor: colors.dark, 139 | }} 140 | > 141 | <Drawer.Screen 142 | name="Home" 143 | component={HomeWrapper} 144 | options={{ 145 | drawerIcon: ({ color }) => ( 146 | <Icon name="home-outline" size={24} color={color} /> 147 | ), 148 | }} 149 | /> 150 | <Drawer.Screen 151 | name="MyProfile" 152 | component={ProfileWrapper} 153 | options={{ 154 | drawerIcon: ({ color }) => ( 155 | <Icon name="person-outline" size={24} color={color} /> 156 | ), 157 | drawerLabel: 'My Profile', 158 | }} 159 | /> 160 | {/* Temporarily hidden Bluestone AI screen */} 161 | {/* <Drawer.Screen 162 | name="BluestoneAI" 163 | component={AIWrapper} 164 | options={{ 165 | drawerIcon: ({ color }) => ( 166 | <Icon name="bulb-outline" size={24} color={color} /> 167 | ), 168 | drawerLabel: 'Bluestone AI', 169 | }} 170 | /> */} 171 | <Drawer.Screen 172 | name="Calendar" 173 | component={CalendarWrapper} 174 | options={{ 175 | drawerIcon: ({ color }) => ( 176 | <Icon name="calendar-outline" size={24} color={color} /> 177 | ), 178 | drawerLabel: 'Calendar', 179 | }} 180 | /> 181 | <Drawer.Screen 182 | name="EventDetails" 183 | component={EventDetailsWrapper} 184 | options={{ 185 | drawerIcon: ({ color }) => ( 186 | <Icon name="calendar-outline" size={24} color={color} /> 187 | ), 188 | drawerLabel: () => null, 189 | drawerItemStyle: { display: 'none' }, 190 | headerLeft: () => ( 191 | <Icon 192 | name="chevron-back" 193 | size={28} 194 | color={colors.headerFont} 195 | style={{ marginLeft: 15 }} 196 | onPress={() => navigation.navigate('Calendar')} 197 | /> 198 | ), 199 | }} 200 | /> 201 | <Drawer.Screen 202 | name="About Us" 203 | component={AboutUsWrapper} 204 | options={{ 205 | drawerIcon: ({ focused, size, color }) => ( 206 | <Icon name={focused ? 'information-circle' : 'information-circle-outline'} size={size} color={focused ? color : '#666'} /> 207 | ), 208 | }} 209 | /> 210 | <Drawer.Screen 211 | name="Posts" 212 | component={PostsScreen} 213 | options={{ 214 | drawerIcon: ({ color }) => ( 215 | <Icon name="newspaper-outline" size={24} color={color} /> 216 | ), 217 | drawerLabel: 'Posts', 218 | }} 219 | /> 220 | <Drawer.Screen 221 | name="Post" 222 | component={PostWrapper} 223 | options={{ 224 | drawerItemStyle: { display: 'none' }, 225 | headerLeft: () => ( 226 | <Icon 227 | name="chevron-back" 228 | size={28} 229 | color={colors.headerFont} 230 | style={{ marginLeft: 15 }} 231 | onPress={() => navigation.navigate('Posts')} 232 | /> 233 | ), 234 | headerStyle: { 235 | backgroundColor: colors.headerBg, 236 | elevation: 0, 237 | shadowOpacity: 0, 238 | borderBottomWidth: 1, 239 | borderBottomColor: colors.light, 240 | }, 241 | headerTitleStyle: { 242 | color: colors.headerFont, 243 | fontSize: 18, 244 | }, 245 | }} 246 | /> 247 | <Drawer.Screen 248 | name="EditProfile" 249 | component={EditProfileWrapper} 250 | options={{ 251 | drawerItemStyle: { display: 'none' }, 252 | headerTitle: 'Edit Profile' 253 | }} 254 | /> 255 | <Drawer.Screen 256 | name="ChangePassword" 257 | component={ChangePasswordWrapper} 258 | options={{ 259 | drawerItemStyle: { display: 'none' }, 260 | headerTitle: 'Change Password' 261 | }} 262 | /> 263 | <Drawer.Screen 264 | name="Contact" 265 | component={ContactWrapper} 266 | options={{ 267 | drawerIcon: ({ color }) => ( 268 | <Icon name="mail-outline" size={24} color={color} /> 269 | ), 270 | drawerLabel: 'Contact Us', 271 | headerTitle: 'Contact Us', 272 | }} 273 | /> 274 | <Drawer.Screen 275 | name="Logout" 276 | component={EmptyComponent} 277 | options={{ 278 | drawerIcon: ({ color }) => ( 279 | <Icon name="log-out-outline" size={24} color={color} /> 280 | ), 281 | }} 282 | listeners={{ 283 | drawerItemPress: () => handleLogout(), 284 | }} 285 | /> 286 | </Drawer.Navigator> 287 | </GestureHandlerRootView> 288 | ); 289 | }; 290 | 291 | const styles = StyleSheet.create({ 292 | container: { 293 | flex: 1, 294 | backgroundColor: '#fff', 295 | }, 296 | contentWrapper: { 297 | flex: 1, 298 | marginBottom: Platform.select({ 299 | ios: 80, 300 | android: 60, 301 | }), 302 | }, 303 | content: { 304 | flex: 1, 305 | }, 306 | tabNavigator: { 307 | position: 'absolute', 308 | bottom: 0, 309 | left: 0, 310 | right: 0, 311 | backgroundColor: '#fff', 312 | borderTopWidth: 1, 313 | borderTopColor: '#e0e0e0', 314 | }, 315 | }); 316 | 317 | const EmptyComponent = () => null; 318 | 319 | export default DrawerNavigator; ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/HomeScreen.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * HomeScreen Component 3 | * 4 | * Main screen of the application displaying featured content, 5 | * recent activities, and navigation options. 6 | */ 7 | 8 | import React, { useState, useEffect } from 'react'; 9 | import { 10 | View, 11 | Text, 12 | StyleSheet, 13 | ScrollView, 14 | TouchableOpacity, 15 | Image, 16 | FlatList, 17 | RefreshControl, 18 | StatusBar, 19 | SafeAreaView 20 | } from 'react-native'; 21 | import { useNavigation } from '@react-navigation/native'; 22 | 23 | // Import components 24 | import Button from '../components/Button'; 25 | import Card from '../components/Card'; 26 | import FeatureCarousel from '../components/FeatureCarousel'; 27 | import LoadingIndicator from '../components/LoadingIndicator'; 28 | 29 | // Import services and utilities 30 | import ApiService from '../services/ApiService'; 31 | import { useAuth } from '../contexts/AuthContext'; 32 | import { formatDate } from '../utils/dateUtils'; 33 | import theme from '../theme/theme'; 34 | 35 | const HomeScreen = () => { 36 | const navigation = useNavigation(); 37 | const { user } = useAuth(); 38 | 39 | // State management 40 | const [featuredItems, setFeaturedItems] = useState([]); 41 | const [recentActivities, setRecentActivities] = useState([]); 42 | const [recommendations, setRecommendations] = useState([]); 43 | const [loading, setLoading] = useState(true); 44 | const [refreshing, setRefreshing] = useState(false); 45 | const [error, setError] = useState(null); 46 | 47 | // Load data on component mount 48 | useEffect(() => { 49 | fetchHomeData(); 50 | }, []); 51 | 52 | // Function to fetch all necessary data 53 | const fetchHomeData = async () => { 54 | try { 55 | setLoading(true); 56 | setError(null); 57 | 58 | // Fetch featured items 59 | const featuredData = await ApiService.get('/featured', {}, { 60 | withCache: true, 61 | cacheTTL: 10 * 60 * 1000 // 10 minutes 62 | }); 63 | 64 | // Fetch recent activity 65 | const activitiesData = await ApiService.get('/activities/recent'); 66 | 67 | // Fetch personalized recommendations if user is logged in 68 | let recommendationsData = []; 69 | if (user) { 70 | recommendationsData = await ApiService.get('/recommendations'); 71 | } 72 | 73 | // Update state with fetched data 74 | setFeaturedItems(featuredData.items || []); 75 | setRecentActivities(activitiesData.activities || []); 76 | setRecommendations(recommendationsData.items || []); 77 | } catch (err) { 78 | console.error('Error fetching home data:', err); 79 | setError('Unable to load content. Please try again later.'); 80 | } finally { 81 | setLoading(false); 82 | setRefreshing(false); 83 | } 84 | }; 85 | 86 | // Pull-to-refresh handler 87 | const onRefresh = () => { 88 | setRefreshing(true); 89 | fetchHomeData(); 90 | }; 91 | 92 | // Navigate to item details 93 | const handleItemPress = (item) => { 94 | navigation.navigate('ItemDetails', { itemId: item.id }); 95 | }; 96 | 97 | // Render activity item 98 | const renderActivityItem = ({ item }) => ( 99 | <TouchableOpacity 100 | style={styles.activityItem} 101 | onPress={() => navigation.navigate('ActivityDetails', { activityId: item.id })} 102 | > 103 | <Image 104 | source={{ uri: item.imageUrl }} 105 | style={styles.activityImage} 106 | /> 107 | <View style={styles.activityContent}> 108 | <Text style={styles.activityTitle} numberOfLines={1}> 109 | {item.title} 110 | </Text> 111 | <Text style={styles.activityMeta}> 112 | {formatDate(item.date)} • {item.category} 113 | </Text> 114 | </View> 115 | </TouchableOpacity> 116 | ); 117 | 118 | // Render recommendation item 119 | const renderRecommendationItem = ({ item }) => ( 120 | <Card 121 | style={styles.recommendationCard} 122 | onPress={() => handleItemPress(item)} 123 | > 124 | <Image 125 | source={{ uri: item.imageUrl }} 126 | style={styles.recommendationImage} 127 | /> 128 | <View style={styles.recommendationContent}> 129 | <Text style={styles.recommendationTitle} numberOfLines={2}> 130 | {item.title} 131 | </Text> 132 | <Text style={styles.recommendationDescription} numberOfLines={3}> 133 | {item.description} 134 | </Text> 135 | </View> 136 | </Card> 137 | ); 138 | 139 | // Loading state 140 | if (loading && !refreshing) { 141 | return <LoadingIndicator fullScreen />; 142 | } 143 | 144 | return ( 145 | <SafeAreaView style={styles.safeArea}> 146 | <StatusBar barStyle="dark-content" /> 147 | 148 | <ScrollView 149 | style={styles.container} 150 | refreshControl={ 151 | <RefreshControl 152 | refreshing={refreshing} 153 | onRefresh={onRefresh} 154 | colors={[theme.colors.primary]} 155 | /> 156 | } 157 | > 158 | {/* Welcome section */} 159 | <View style={styles.welcomeSection}> 160 | <Text style={styles.welcomeTitle}> 161 | {user ? `Welcome back, ${user.firstName}!` : 'Welcome to AppName'} 162 | </Text> 163 | <Text style={styles.welcomeSubtitle}> 164 | Discover what's new today 165 | </Text> 166 | </View> 167 | 168 | {/* Featured carousel */} 169 | {featuredItems.length > 0 ? ( 170 | <View style={styles.carouselContainer}> 171 | <FeatureCarousel 172 | items={featuredItems} 173 | onItemPress={handleItemPress} 174 | /> 175 | </View> 176 | ) : null} 177 | 178 | {/* Quick actions */} 179 | <View style={styles.quickActions}> 180 | <TouchableOpacity 181 | style={styles.actionButton} 182 | onPress={() => navigation.navigate('Search')} 183 | > 184 | <Image 185 | source={require('../assets/icons/search.png')} 186 | style={styles.actionIcon} 187 | /> 188 | <Text style={styles.actionText}>Search</Text> 189 | </TouchableOpacity> 190 | 191 | <TouchableOpacity 192 | style={styles.actionButton} 193 | onPress={() => navigation.navigate('Categories')} 194 | > 195 | <Image 196 | source={require('../assets/icons/categories.png')} 197 | style={styles.actionIcon} 198 | /> 199 | <Text style={styles.actionText}>Categories</Text> 200 | </TouchableOpacity> 201 | 202 | <TouchableOpacity 203 | style={styles.actionButton} 204 | onPress={() => navigation.navigate('Favorites')} 205 | > 206 | <Image 207 | source={require('../assets/icons/favorite.png')} 208 | style={styles.actionIcon} 209 | /> 210 | <Text style={styles.actionText}>Favorites</Text> 211 | </TouchableOpacity> 212 | 213 | <TouchableOpacity 214 | style={styles.actionButton} 215 | onPress={() => navigation.navigate('Notifications')} 216 | > 217 | <Image 218 | source={require('../assets/icons/notification.png')} 219 | style={styles.actionIcon} 220 | /> 221 | <Text style={styles.actionText}>Updates</Text> 222 | </TouchableOpacity> 223 | </View> 224 | 225 | {/* Recent activity section */} 226 | {recentActivities.length > 0 && ( 227 | <View style={styles.section}> 228 | <View style={styles.sectionHeader}> 229 | <Text style={styles.sectionTitle}>Recent Activity</Text> 230 | <TouchableOpacity onPress={() => navigation.navigate('AllActivities')}> 231 | <Text style={styles.seeAllText}>See All</Text> 232 | </TouchableOpacity> 233 | </View> 234 | 235 | <FlatList 236 | data={recentActivities} 237 | renderItem={renderActivityItem} 238 | keyExtractor={(item) => item.id.toString()} 239 | horizontal 240 | showsHorizontalScrollIndicator={false} 241 | contentContainerStyle={styles.activitiesList} 242 | /> 243 | </View> 244 | )} 245 | 246 | {/* Recommendations section (only for logged in users) */} 247 | {user && recommendations.length > 0 && ( 248 | <View style={styles.section}> 249 | <View style={styles.sectionHeader}> 250 | <Text style={styles.sectionTitle}>Recommended for You</Text> 251 | <TouchableOpacity onPress={() => navigation.navigate('Recommendations')}> 252 | <Text style={styles.seeAllText}>See All</Text> 253 | </TouchableOpacity> 254 | </View> 255 | 256 | <FlatList 257 | data={recommendations} 258 | renderItem={renderRecommendationItem} 259 | keyExtractor={(item) => item.id.toString()} 260 | horizontal 261 | showsHorizontalScrollIndicator={false} 262 | contentContainerStyle={styles.recommendationsList} 263 | /> 264 | </View> 265 | )} 266 | 267 | {/* Call to action */} 268 | <View style={styles.ctaContainer}> 269 | <Image 270 | source={require('../assets/images/cta-background.png')} 271 | style={styles.ctaBackground} 272 | /> 273 | <View style={styles.ctaContent}> 274 | <Text style={styles.ctaTitle}>Ready to get started?</Text> 275 | <Text style={styles.ctaDescription}> 276 | Join thousands of users and start exploring now. 277 | </Text> 278 | <Button 279 | title={user ? "Explore Premium" : "Sign Up Now"} 280 | onPress={() => navigation.navigate(user ? 'Subscription' : 'Signup')} 281 | variant="primary" 282 | style={styles.ctaButton} 283 | /> 284 | </View> 285 | </View> 286 | 287 | {/* Error message */} 288 | {error && ( 289 | <View style={styles.errorContainer}> 290 | <Text style={styles.errorText}>{error}</Text> 291 | <Button 292 | title="Try Again" 293 | onPress={fetchHomeData} 294 | variant="outline" 295 | size="small" 296 | style={styles.retryButton} 297 | /> 298 | </View> 299 | )} 300 | 301 | {/* Bottom padding */} 302 | <View style={styles.bottomPadding} /> 303 | </ScrollView> 304 | </SafeAreaView> 305 | ); 306 | }; 307 | 308 | const styles = StyleSheet.create({ 309 | safeArea: { 310 | flex: 1, 311 | backgroundColor: theme.colors.background, 312 | }, 313 | container: { 314 | flex: 1, 315 | }, 316 | welcomeSection: { 317 | padding: theme.spacing.lg, 318 | }, 319 | welcomeTitle: { 320 | ...theme.typography.h4, 321 | color: theme.colors.textPrimary, 322 | marginBottom: theme.spacing.xs, 323 | }, 324 | welcomeSubtitle: { 325 | ...theme.typography.body, 326 | color: theme.colors.textSecondary, 327 | }, 328 | carouselContainer: { 329 | marginBottom: theme.spacing.lg, 330 | }, 331 | quickActions: { 332 | flexDirection: 'row', 333 | justifyContent: 'space-between', 334 | paddingHorizontal: theme.spacing.lg, 335 | marginBottom: theme.spacing.xl, 336 | }, 337 | actionButton: { 338 | alignItems: 'center', 339 | width: 70, 340 | }, 341 | actionIcon: { 342 | width: 40, 343 | height: 40, 344 | marginBottom: theme.spacing.xs, 345 | }, 346 | actionText: { 347 | ...theme.typography.caption, 348 | color: theme.colors.textPrimary, 349 | }, 350 | section: { 351 | marginBottom: theme.spacing.xl, 352 | }, 353 | sectionHeader: { 354 | flexDirection: 'row', 355 | justifyContent: 'space-between', 356 | alignItems: 'center', 357 | paddingHorizontal: theme.spacing.lg, 358 | marginBottom: theme.spacing.md, 359 | }, 360 | sectionTitle: { 361 | ...theme.typography.h5, 362 | color: theme.colors.textPrimary, 363 | }, 364 | seeAllText: { 365 | ...theme.typography.body, 366 | color: theme.colors.primary, 367 | }, 368 | activitiesList: { 369 | paddingLeft: theme.spacing.lg, 370 | }, 371 | activityItem: { 372 | width: 280, 373 | marginRight: theme.spacing.md, 374 | borderRadius: theme.borderRadius.md, 375 | backgroundColor: theme.colors.white, 376 | ...theme.shadows.sm, 377 | overflow: 'hidden', 378 | }, 379 | activityImage: { 380 | width: '100%', 381 | height: 150, 382 | resizeMode: 'cover', 383 | }, 384 | activityContent: { 385 | padding: theme.spacing.md, 386 | }, 387 | activityTitle: { 388 | ...theme.typography.h6, 389 | marginBottom: theme.spacing.xs, 390 | }, 391 | activityMeta: { 392 | ...theme.typography.caption, 393 | color: theme.colors.textSecondary, 394 | }, 395 | recommendationsList: { 396 | paddingLeft: theme.spacing.lg, 397 | }, 398 | recommendationCard: { 399 | width: 200, 400 | marginRight: theme.spacing.md, 401 | overflow: 'hidden', 402 | }, 403 | recommendationImage: { 404 | width: '100%', 405 | height: 120, 406 | resizeMode: 'cover', 407 | }, 408 | recommendationContent: { 409 | padding: theme.spacing.md, 410 | }, 411 | recommendationTitle: { 412 | ...theme.typography.h6, 413 | fontSize: 15, 414 | marginBottom: theme.spacing.xs, 415 | }, 416 | recommendationDescription: { 417 | ...theme.typography.caption, 418 | color: theme.colors.textSecondary, 419 | }, 420 | ctaContainer: { 421 | marginHorizontal: theme.spacing.lg, 422 | marginBottom: theme.spacing.xl, 423 | borderRadius: theme.borderRadius.lg, 424 | overflow: 'hidden', 425 | position: 'relative', 426 | height: 180, 427 | }, 428 | ctaBackground: { 429 | position: 'absolute', 430 | width: '100%', 431 | height: '100%', 432 | resizeMode: 'cover', 433 | }, 434 | ctaContent: { 435 | padding: theme.spacing.lg, 436 | backgroundColor: 'rgba(0,0,0,0.5)', 437 | height: '100%', 438 | justifyContent: 'center', 439 | }, 440 | ctaTitle: { 441 | ...theme.typography.h4, 442 | color: theme.colors.white, 443 | marginBottom: theme.spacing.sm, 444 | }, 445 | ctaDescription: { 446 | ...theme.typography.body, 447 | color: theme.colors.white, 448 | marginBottom: theme.spacing.lg, 449 | }, 450 | ctaButton: { 451 | alignSelf: 'flex-start', 452 | }, 453 | errorContainer: { 454 | margin: theme.spacing.lg, 455 | padding: theme.spacing.lg, 456 | backgroundColor: theme.colors.errorLight, 457 | borderRadius: theme.borderRadius.md, 458 | alignItems: 'center', 459 | }, 460 | errorText: { 461 | ...theme.typography.body, 462 | color: theme.colors.error, 463 | marginBottom: theme.spacing.md, 464 | textAlign: 'center', 465 | }, 466 | retryButton: { 467 | marginTop: theme.spacing.sm, 468 | }, 469 | bottomPadding: { 470 | height: 40, 471 | }, 472 | }); 473 | 474 | export default HomeScreen; 475 | ``` -------------------------------------------------------------------------------- /resources/code-examples/react-native/screens/HomeScreen.tsx: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * HomeScreen Component 3 | * 4 | * Main screen of the application displaying featured content, 5 | * recent activities, and navigation options. 6 | */ 7 | 8 | import React, { useState, useEffect } from 'react'; 9 | import { 10 | View, 11 | Text, 12 | StyleSheet, 13 | ScrollView, 14 | TouchableOpacity, 15 | Image, 16 | FlatList, 17 | RefreshControl, 18 | StatusBar, 19 | SafeAreaView, 20 | ImageSourcePropType 21 | } from 'react-native'; 22 | import { useNavigation } from '@react-navigation/native'; 23 | 24 | // Import components 25 | import Button from '../components/Button'; 26 | import Card from '../components/Card'; 27 | import FeatureCarousel from '../components/FeatureCarousel'; 28 | import LoadingIndicator from '../components/LoadingIndicator'; 29 | 30 | // Import services and utilities 31 | import ApiService from '../services/ApiService'; 32 | import { useAuth } from '../contexts/AuthContext'; 33 | import { formatDate } from '../utils/dateUtils'; 34 | import theme from '../theme/theme'; 35 | 36 | // Define interfaces for data types 37 | interface FeaturedItem { 38 | id: string; 39 | title: string; 40 | description: string; 41 | imageUrl: string; 42 | } 43 | 44 | interface Activity { 45 | id: string; 46 | title: string; 47 | date: string; 48 | category: string; 49 | imageUrl: string; 50 | } 51 | 52 | interface Recommendation { 53 | id: string; 54 | title: string; 55 | description: string; 56 | imageUrl: string; 57 | } 58 | 59 | interface User { 60 | firstName: string; 61 | } 62 | 63 | const HomeScreen: React.FC = () => { 64 | const navigation = useNavigation(); 65 | const { user } = useAuth() as { user: User | null }; 66 | 67 | // State management 68 | const [featuredItems, setFeaturedItems] = useState<FeaturedItem[]>([]); 69 | const [recentActivities, setRecentActivities] = useState<Activity[]>([]); 70 | const [recommendations, setRecommendations] = useState<Recommendation[]>([]); 71 | const [loading, setLoading] = useState<boolean>(true); 72 | const [refreshing, setRefreshing] = useState<boolean>(false); 73 | const [error, setError] = useState<string | null>(null); 74 | 75 | // Load data on component mount 76 | useEffect(() => { 77 | fetchHomeData(); 78 | }, []); 79 | 80 | // Function to fetch all necessary data 81 | const fetchHomeData = async (): Promise<void> => { 82 | try { 83 | setLoading(true); 84 | setError(null); 85 | 86 | // Fetch featured items 87 | const featuredData = await ApiService.get('/featured', {}, { 88 | withCache: true, 89 | cacheTTL: 10 * 60 * 1000 // 10 minutes 90 | }); 91 | 92 | // Fetch recent activity 93 | const activitiesData = await ApiService.get('/activities/recent'); 94 | 95 | // Fetch personalized recommendations if user is logged in 96 | let recommendationsData = { items: [] }; 97 | if (user) { 98 | recommendationsData = await ApiService.get('/recommendations'); 99 | } 100 | 101 | // Update state with fetched data 102 | setFeaturedItems(featuredData.items || []); 103 | setRecentActivities(activitiesData.activities || []); 104 | setRecommendations(recommendationsData.items || []); 105 | } catch (err) { 106 | console.error('Error fetching home data:', err); 107 | setError('Unable to load content. Please try again later.'); 108 | } finally { 109 | setLoading(false); 110 | setRefreshing(false); 111 | } 112 | }; 113 | 114 | // Pull-to-refresh handler 115 | const onRefresh = (): void => { 116 | setRefreshing(true); 117 | fetchHomeData(); 118 | }; 119 | 120 | // Navigate to item details 121 | const handleItemPress = (item: FeaturedItem | Recommendation): void => { 122 | navigation.navigate('ItemDetails' as never, { itemId: item.id } as never); 123 | }; 124 | 125 | // Render activity item 126 | const renderActivityItem = ({ item }: { item: Activity }): React.ReactElement => ( 127 | <TouchableOpacity 128 | style={styles.activityItem} 129 | onPress={() => navigation.navigate('ActivityDetails' as never, { activityId: item.id } as never)} 130 | > 131 | <Image 132 | source={{ uri: item.imageUrl }} 133 | style={styles.activityImage} 134 | /> 135 | <View style={styles.activityContent}> 136 | <Text style={styles.activityTitle} numberOfLines={1}> 137 | {item.title} 138 | </Text> 139 | <Text style={styles.activityMeta}> 140 | {formatDate(item.date)} • {item.category} 141 | </Text> 142 | </View> 143 | </TouchableOpacity> 144 | ); 145 | 146 | // Render recommendation item 147 | const renderRecommendationItem = ({ item }: { item: Recommendation }): React.ReactElement => ( 148 | <Card 149 | style={styles.recommendationCard} 150 | onPress={() => handleItemPress(item)} 151 | > 152 | <Image 153 | source={{ uri: item.imageUrl }} 154 | style={styles.recommendationImage} 155 | /> 156 | <View style={styles.recommendationContent}> 157 | <Text style={styles.recommendationTitle} numberOfLines={2}> 158 | {item.title} 159 | </Text> 160 | <Text style={styles.recommendationDescription} numberOfLines={3}> 161 | {item.description} 162 | </Text> 163 | </View> 164 | </Card> 165 | ); 166 | 167 | // Loading state 168 | if (loading && !refreshing) { 169 | return <LoadingIndicator fullScreen />; 170 | } 171 | 172 | return ( 173 | <SafeAreaView style={styles.safeArea}> 174 | <StatusBar barStyle="dark-content" /> 175 | 176 | <ScrollView 177 | style={styles.container} 178 | refreshControl={ 179 | <RefreshControl 180 | refreshing={refreshing} 181 | onRefresh={onRefresh} 182 | colors={[theme.colors.primary]} 183 | /> 184 | } 185 | > 186 | {/* Welcome section */} 187 | <View style={styles.welcomeSection}> 188 | <Text style={styles.welcomeTitle}> 189 | {user ? `Welcome back, ${user.firstName}!` : 'Welcome to AppName'} 190 | </Text> 191 | <Text style={styles.welcomeSubtitle}> 192 | Discover what's new today 193 | </Text> 194 | </View> 195 | 196 | {/* Featured carousel */} 197 | {featuredItems.length > 0 ? ( 198 | <View style={styles.carouselContainer}> 199 | <FeatureCarousel 200 | items={featuredItems} 201 | onItemPress={handleItemPress} 202 | /> 203 | </View> 204 | ) : null} 205 | 206 | {/* Quick actions */} 207 | <View style={styles.quickActions}> 208 | <TouchableOpacity 209 | style={styles.actionButton} 210 | onPress={() => navigation.navigate('Search' as never)} 211 | > 212 | <Image 213 | source={require('../assets/icons/search.png') as ImageSourcePropType} 214 | style={styles.actionIcon} 215 | /> 216 | <Text style={styles.actionText}>Search</Text> 217 | </TouchableOpacity> 218 | 219 | <TouchableOpacity 220 | style={styles.actionButton} 221 | onPress={() => navigation.navigate('Categories' as never)} 222 | > 223 | <Image 224 | source={require('../assets/icons/categories.png') as ImageSourcePropType} 225 | style={styles.actionIcon} 226 | /> 227 | <Text style={styles.actionText}>Categories</Text> 228 | </TouchableOpacity> 229 | 230 | <TouchableOpacity 231 | style={styles.actionButton} 232 | onPress={() => navigation.navigate('Favorites' as never)} 233 | > 234 | <Image 235 | source={require('../assets/icons/favorite.png') as ImageSourcePropType} 236 | style={styles.actionIcon} 237 | /> 238 | <Text style={styles.actionText}>Favorites</Text> 239 | </TouchableOpacity> 240 | 241 | <TouchableOpacity 242 | style={styles.actionButton} 243 | onPress={() => navigation.navigate('Notifications' as never)} 244 | > 245 | <Image 246 | source={require('../assets/icons/notification.png') as ImageSourcePropType} 247 | style={styles.actionIcon} 248 | /> 249 | <Text style={styles.actionText}>Updates</Text> 250 | </TouchableOpacity> 251 | </View> 252 | 253 | {/* Recent activity section */} 254 | {recentActivities.length > 0 && ( 255 | <View style={styles.section}> 256 | <View style={styles.sectionHeader}> 257 | <Text style={styles.sectionTitle}>Recent Activity</Text> 258 | <TouchableOpacity onPress={() => navigation.navigate('AllActivities' as never)}> 259 | <Text style={styles.seeAllText}>See All</Text> 260 | </TouchableOpacity> 261 | </View> 262 | 263 | <FlatList 264 | data={recentActivities} 265 | renderItem={renderActivityItem} 266 | keyExtractor={(item) => item.id.toString()} 267 | horizontal 268 | showsHorizontalScrollIndicator={false} 269 | contentContainerStyle={styles.activitiesList} 270 | /> 271 | </View> 272 | )} 273 | 274 | {/* Recommendations section (only for logged in users) */} 275 | {user && recommendations.length > 0 && ( 276 | <View style={styles.section}> 277 | <View style={styles.sectionHeader}> 278 | <Text style={styles.sectionTitle}>Recommended for You</Text> 279 | <TouchableOpacity onPress={() => navigation.navigate('Recommendations' as never)}> 280 | <Text style={styles.seeAllText}>See All</Text> 281 | </TouchableOpacity> 282 | </View> 283 | 284 | <FlatList 285 | data={recommendations} 286 | renderItem={renderRecommendationItem} 287 | keyExtractor={(item) => item.id.toString()} 288 | horizontal 289 | showsHorizontalScrollIndicator={false} 290 | contentContainerStyle={styles.recommendationsList} 291 | /> 292 | </View> 293 | )} 294 | 295 | {/* Call to action */} 296 | <View style={styles.ctaContainer}> 297 | <Image 298 | source={require('../assets/images/cta-background.png') as ImageSourcePropType} 299 | style={styles.ctaBackground} 300 | /> 301 | <View style={styles.ctaContent}> 302 | <Text style={styles.ctaTitle}>Ready to get started?</Text> 303 | <Text style={styles.ctaDescription}> 304 | Join thousands of users and start exploring now. 305 | </Text> 306 | <Button 307 | title={user ? "Explore Premium" : "Sign Up Now"} 308 | onPress={() => navigation.navigate(user ? 'Subscription' as never : 'Signup' as never)} 309 | style={styles.ctaButton} 310 | /> 311 | </View> 312 | </View> 313 | 314 | {/* Error message */} 315 | {error && ( 316 | <View style={styles.errorContainer}> 317 | <Text style={styles.errorText}>{error}</Text> 318 | <Button 319 | title="Try Again" 320 | onPress={fetchHomeData} 321 | style={styles.retryButton} 322 | /> 323 | </View> 324 | )} 325 | 326 | {/* Bottom padding */} 327 | <View style={styles.bottomPadding} /> 328 | </ScrollView> 329 | </SafeAreaView> 330 | ); 331 | }; 332 | 333 | const styles = StyleSheet.create({ 334 | safeArea: { 335 | flex: 1, 336 | backgroundColor: theme.colors.background, 337 | }, 338 | container: { 339 | flex: 1, 340 | }, 341 | welcomeSection: { 342 | padding: theme.spacing.lg, 343 | }, 344 | welcomeTitle: { 345 | ...theme.typography.h4, 346 | color: theme.colors.textPrimary, 347 | marginBottom: theme.spacing.xs, 348 | }, 349 | welcomeSubtitle: { 350 | ...theme.typography.body, 351 | color: theme.colors.textSecondary, 352 | }, 353 | carouselContainer: { 354 | marginBottom: theme.spacing.lg, 355 | }, 356 | quickActions: { 357 | flexDirection: 'row', 358 | justifyContent: 'space-between', 359 | paddingHorizontal: theme.spacing.lg, 360 | marginBottom: theme.spacing.xl, 361 | }, 362 | actionButton: { 363 | alignItems: 'center', 364 | width: 70, 365 | }, 366 | actionIcon: { 367 | width: 40, 368 | height: 40, 369 | marginBottom: theme.spacing.xs, 370 | }, 371 | actionText: { 372 | ...theme.typography.caption, 373 | color: theme.colors.textPrimary, 374 | }, 375 | section: { 376 | marginBottom: theme.spacing.xl, 377 | }, 378 | sectionHeader: { 379 | flexDirection: 'row', 380 | justifyContent: 'space-between', 381 | alignItems: 'center', 382 | paddingHorizontal: theme.spacing.lg, 383 | marginBottom: theme.spacing.md, 384 | }, 385 | sectionTitle: { 386 | ...theme.typography.h5, 387 | color: theme.colors.textPrimary, 388 | }, 389 | seeAllText: { 390 | ...theme.typography.body, 391 | color: theme.colors.primary, 392 | }, 393 | activitiesList: { 394 | paddingLeft: theme.spacing.lg, 395 | }, 396 | activityItem: { 397 | width: 280, 398 | marginRight: theme.spacing.md, 399 | borderRadius: theme.borderRadius.md, 400 | backgroundColor: theme.colors.white, 401 | ...theme.shadows.sm, 402 | overflow: 'hidden', 403 | }, 404 | activityImage: { 405 | width: '100%', 406 | height: 150, 407 | resizeMode: 'cover', 408 | }, 409 | activityContent: { 410 | padding: theme.spacing.md, 411 | }, 412 | activityTitle: { 413 | ...theme.typography.h6, 414 | marginBottom: theme.spacing.xs, 415 | }, 416 | activityMeta: { 417 | ...theme.typography.caption, 418 | color: theme.colors.textSecondary, 419 | }, 420 | recommendationsList: { 421 | paddingLeft: theme.spacing.lg, 422 | }, 423 | recommendationCard: { 424 | width: 200, 425 | marginRight: theme.spacing.md, 426 | overflow: 'hidden', 427 | }, 428 | recommendationImage: { 429 | width: '100%', 430 | height: 120, 431 | resizeMode: 'cover', 432 | }, 433 | recommendationContent: { 434 | padding: theme.spacing.md, 435 | }, 436 | recommendationTitle: { 437 | ...theme.typography.h6, 438 | fontSize: 15, 439 | marginBottom: theme.spacing.xs, 440 | }, 441 | recommendationDescription: { 442 | ...theme.typography.caption, 443 | color: theme.colors.textSecondary, 444 | }, 445 | ctaContainer: { 446 | marginHorizontal: theme.spacing.lg, 447 | marginBottom: theme.spacing.xl, 448 | borderRadius: theme.borderRadius.lg, 449 | overflow: 'hidden', 450 | position: 'relative', 451 | height: 180, 452 | }, 453 | ctaBackground: { 454 | position: 'absolute', 455 | width: '100%', 456 | height: '100%', 457 | resizeMode: 'cover', 458 | }, 459 | ctaContent: { 460 | padding: theme.spacing.lg, 461 | backgroundColor: 'rgba(0,0,0,0.5)', 462 | height: '100%', 463 | justifyContent: 'center', 464 | }, 465 | ctaTitle: { 466 | ...theme.typography.h4, 467 | color: theme.colors.white, 468 | marginBottom: theme.spacing.sm, 469 | }, 470 | ctaDescription: { 471 | ...theme.typography.body, 472 | color: theme.colors.white, 473 | marginBottom: theme.spacing.lg, 474 | }, 475 | ctaButton: { 476 | alignSelf: 'flex-start', 477 | }, 478 | errorContainer: { 479 | margin: theme.spacing.lg, 480 | padding: theme.spacing.lg, 481 | backgroundColor: theme.colors.errorLight, 482 | borderRadius: theme.borderRadius.md, 483 | alignItems: 'center', 484 | }, 485 | errorText: { 486 | ...theme.typography.body, 487 | color: theme.colors.error, 488 | marginBottom: theme.spacing.md, 489 | textAlign: 'center', 490 | }, 491 | retryButton: { 492 | marginTop: theme.spacing.sm, 493 | }, 494 | bottomPadding: { 495 | height: 40, 496 | }, 497 | }); 498 | 499 | export default HomeScreen; 500 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { z } from "zod"; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | import { glob } from 'glob'; 8 | 9 | // Get the directory name of the current module 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | // Base directory for resources 14 | const BASE_DIR = path.resolve(__dirname, '..'); 15 | const RESOURCES_DIR = path.join(BASE_DIR, "resources"); 16 | const CODE_EXAMPLES_DIR = path.join(RESOURCES_DIR, "code-examples"); 17 | 18 | // Create server instance 19 | const server = new McpServer({ 20 | name: "bluestoneapps", 21 | version: "0.2.1", 22 | capabilities: { 23 | tools: {}, 24 | }, 25 | }); 26 | 27 | // Helper function to get standard content 28 | function getStandardContent(category: string, standardId: string): { content?: string; error?: string } { 29 | const standardPath = path.join(RESOURCES_DIR, category, `${standardId}.md`); 30 | 31 | if (!fs.existsSync(standardPath)) { 32 | return { error: `Standard ${standardId} not found` }; 33 | } 34 | 35 | try { 36 | const content = fs.readFileSync(standardPath, 'utf8'); 37 | return { content }; 38 | } catch (err) { 39 | console.error(`Error reading standard ${standardId}:`, err); 40 | return { error: `Error reading standard ${standardId}` }; 41 | } 42 | } 43 | 44 | // Helper function to find file in subdirectories 45 | function findFileInSubdirectories(baseDir: string, fileName: string, extensions: string[] = ['.js', '.jsx', '.ts', '.tsx']) { 46 | // First, try with exact filename match 47 | let files = glob.sync(`${baseDir}/**/${fileName}`); 48 | if (files.length > 0) { 49 | return files[0]; 50 | } 51 | 52 | // Then try with file name + extensions 53 | for (const ext of extensions) { 54 | const fileWithExt = `${fileName}${ext}`; 55 | files = glob.sync(`${baseDir}/**/${fileWithExt}`); 56 | if (files.length > 0) { 57 | return files[0]; 58 | } 59 | } 60 | 61 | return null; 62 | } 63 | 64 | // Helper function to get example content 65 | function getExampleContent(subcategory: string, filename: string): { content?: string[]; path?: string; error?: string } { 66 | const searchDir = path.join(CODE_EXAMPLES_DIR, "react-native", subcategory); 67 | 68 | const filePath = findFileInSubdirectories(searchDir, filename); 69 | 70 | if (!filePath || !fs.existsSync(filePath)) { 71 | return { error: `Example ${filename} not found` }; 72 | } 73 | 74 | try { 75 | const content = fs.readFileSync(filePath, 'utf8'); 76 | return { 77 | content: [content], 78 | path: path.relative(BASE_DIR, filePath) 79 | }; 80 | } catch (err) { 81 | console.error(`Error reading example ${filename}:`, err); 82 | return { error: `Error reading example ${filename}` }; 83 | } 84 | } 85 | 86 | // Find closest match implementation 87 | function findClosestMatch(directory: string, searchName: string, extensions: string[] = ['.js', '.jsx', '.ts', '.tsx']) { 88 | if (!fs.existsSync(directory)) return null; 89 | 90 | let closestMatch = null; 91 | 92 | for (const ext of extensions) { 93 | const files = glob.sync(`${directory}/**/*${ext}`); 94 | 95 | for (const filePath of files) { 96 | const fileName = path.basename(filePath); 97 | const fileNameNoExt = path.basename(fileName, path.extname(fileName)); 98 | 99 | if (fileNameNoExt.toLowerCase().includes(searchName.toLowerCase())) { 100 | closestMatch = fileNameNoExt; 101 | break; 102 | } 103 | } 104 | 105 | if (closestMatch) break; 106 | } 107 | 108 | return closestMatch; 109 | } 110 | 111 | // List all available examples 112 | function listAvailableExamples() { 113 | const examples: Record<string, string[]> = { 114 | components: [], 115 | hooks: [], 116 | services: [], 117 | screens: [], 118 | themes: [] 119 | }; 120 | 121 | const categories = [ 122 | { key: "components", dir: "components" }, 123 | { key: "hooks", dir: "hooks" }, 124 | { key: "services", dir: "services" }, 125 | { key: "screens", dir: "screens" }, 126 | { key: "themes", dir: "theme" } 127 | ]; 128 | 129 | const extensions = ['.js', '.jsx', '.ts', '.tsx']; 130 | 131 | for (const category of categories) { 132 | const dirPath = path.join(CODE_EXAMPLES_DIR, "react-native", category.dir); 133 | if (fs.existsSync(dirPath)) { 134 | for (const ext of extensions) { 135 | const files = glob.sync(`${dirPath}/**/*${ext}`); 136 | for (const filePath of files) { 137 | const fileName = path.basename(filePath); 138 | const fileNameNoExt = path.basename(fileName, path.extname(fileName)); 139 | examples[category.key].push(fileNameNoExt); 140 | } 141 | } 142 | } 143 | } 144 | 145 | return examples; 146 | } 147 | 148 | // Register tools 149 | // 1. Get project structure 150 | server.tool( 151 | "get_project_structure", 152 | "Get project structure standards for React Native development", 153 | {}, 154 | async () => { 155 | const result = getStandardContent("standards", "project_structure"); 156 | 157 | return { 158 | content: [ 159 | { 160 | type: "text", 161 | text: result.content ?? result.error ?? "Error: No content or error message available", 162 | }, 163 | ], 164 | }; 165 | }, 166 | ); 167 | 168 | // 2. Get API communication 169 | server.tool( 170 | "get_api_communication", 171 | "Get API communication standards for React Native development", 172 | {}, 173 | async () => { 174 | const result = getStandardContent("standards", "api_communication"); 175 | 176 | return { 177 | content: [ 178 | { 179 | type: "text", 180 | text: result.content ?? result.error ?? "Error: No content or error message available", 181 | }, 182 | ], 183 | }; 184 | }, 185 | ); 186 | 187 | // 3. Get component design 188 | server.tool( 189 | "get_component_design", 190 | "Get component design standards for React Native development", 191 | {}, 192 | async () => { 193 | const result = getStandardContent("standards", "component_design"); 194 | 195 | return { 196 | content: [ 197 | { 198 | type: "text", 199 | text: result.content ?? result.error ?? "Error: No content or error message available", 200 | }, 201 | ], 202 | }; 203 | }, 204 | ); 205 | 206 | // 4. Get state management 207 | server.tool( 208 | "get_state_management", 209 | "Get state management standards for React Native development", 210 | {}, 211 | async () => { 212 | const result = getStandardContent("standards", "state_management"); 213 | 214 | return { 215 | content: [ 216 | { 217 | type: "text", 218 | text: result.content ?? result.error ?? "Error: No content or error message available", 219 | }, 220 | ], 221 | }; 222 | }, 223 | ); 224 | 225 | // 5. Get component example 226 | server.tool( 227 | "get_component_example", 228 | "Get a React Native component example", 229 | { 230 | component_name: z.string().describe("Component Name"), 231 | }, 232 | async ({ component_name }) => { 233 | if (!component_name) { 234 | return { 235 | content: [ 236 | { 237 | type: "text", 238 | text: "Component name not specified", 239 | }, 240 | ], 241 | }; 242 | } 243 | 244 | try { 245 | // First try exact match 246 | const result = getExampleContent("components", component_name); 247 | 248 | if (result.error) { 249 | // Try to find by fuzzy match 250 | const componentsDir = path.join(CODE_EXAMPLES_DIR, "react-native", "components"); 251 | const closestMatch = findClosestMatch(componentsDir, component_name); 252 | 253 | if (closestMatch) { 254 | const fuzzyResult = getExampleContent("components", closestMatch); 255 | return { 256 | content: [ 257 | { 258 | type: "text", 259 | text: fuzzyResult.content?.[0] ?? fuzzyResult.error ?? "Error: No content available", 260 | }, 261 | ], 262 | }; 263 | } else { 264 | return { 265 | content: [ 266 | { 267 | type: "text", 268 | text: `Component ${component_name} not found`, 269 | }, 270 | ], 271 | }; 272 | } 273 | } 274 | 275 | return { 276 | content: [ 277 | { 278 | type: "text", 279 | text: result.content?.[0] ?? result.error ?? "Error: No content available", 280 | }, 281 | ], 282 | }; 283 | } catch (err) { 284 | console.error(`Error getting component example ${component_name}:`, err); 285 | return { 286 | content: [ 287 | { 288 | type: "text", 289 | text: `Error getting component example: ${err}`, 290 | }, 291 | ], 292 | }; 293 | } 294 | }, 295 | ); 296 | 297 | // 6. Get hook example 298 | server.tool( 299 | "get_hook_example", 300 | "Get a React Native hook example", 301 | { 302 | hook_name: z.string().describe("Hook Name"), 303 | }, 304 | async ({ hook_name }) => { 305 | if (!hook_name) { 306 | return { 307 | content: [ 308 | { 309 | type: "text", 310 | text: "Hook name not specified", 311 | }, 312 | ], 313 | }; 314 | } 315 | 316 | try { 317 | // First try exact match 318 | const result = getExampleContent("hooks", hook_name); 319 | 320 | if (result.error) { 321 | // Try to find by fuzzy match 322 | const hooksDir = path.join(CODE_EXAMPLES_DIR, "react-native", "hooks"); 323 | const closestMatch = findClosestMatch(hooksDir, hook_name); 324 | 325 | if (closestMatch) { 326 | const fuzzyResult = getExampleContent("hooks", closestMatch); 327 | return { 328 | content: [ 329 | { 330 | type: "text", 331 | text: fuzzyResult.content?.[0] ?? fuzzyResult.error ?? "Error: No content available", 332 | }, 333 | ], 334 | }; 335 | } else { 336 | return { 337 | content: [ 338 | { 339 | type: "text", 340 | text: `Hook ${hook_name} not found`, 341 | }, 342 | ], 343 | }; 344 | } 345 | } 346 | 347 | return { 348 | content: [ 349 | { 350 | type: "text", 351 | text: result.content?.[0] ?? result.error ?? "Error: No content available", 352 | }, 353 | ], 354 | }; 355 | } catch (err) { 356 | console.error(`Error getting hook example ${hook_name}:`, err); 357 | return { 358 | content: [ 359 | { 360 | type: "text", 361 | text: `Error getting hook example: ${err}`, 362 | }, 363 | ], 364 | }; 365 | } 366 | }, 367 | ); 368 | 369 | // 7. Get service example 370 | server.tool( 371 | "get_service_example", 372 | "Get a React Native service example", 373 | { 374 | service_name: z.string().describe("Service Name"), 375 | }, 376 | async ({ service_name }) => { 377 | if (!service_name) { 378 | return { 379 | content: [ 380 | { 381 | type: "text", 382 | text: "Service name not specified", 383 | }, 384 | ], 385 | }; 386 | } 387 | 388 | try { 389 | // First try exact match 390 | const result = getExampleContent("services", service_name); 391 | 392 | if (result.error) { 393 | // Try to find by fuzzy match 394 | const servicesDir = path.join(CODE_EXAMPLES_DIR, "react-native", "services"); 395 | const closestMatch = findClosestMatch(servicesDir, service_name); 396 | 397 | if (closestMatch) { 398 | const fuzzyResult = getExampleContent("helper", closestMatch); 399 | return { 400 | content: [ 401 | { 402 | type: "text", 403 | text: fuzzyResult.content?.[0] ?? fuzzyResult.error ?? "Error: No content available", 404 | }, 405 | ], 406 | }; 407 | } else { 408 | return { 409 | content: [ 410 | { 411 | type: "text", 412 | text: `Service ${service_name} not found`, 413 | }, 414 | ], 415 | }; 416 | } 417 | } 418 | 419 | return { 420 | content: [ 421 | { 422 | type: "text", 423 | text: result.content?.[0] ?? result.error ?? "Error: No content available", 424 | }, 425 | ], 426 | }; 427 | } catch (err) { 428 | console.error(`Error getting service example ${service_name}:`, err); 429 | return { 430 | content: [ 431 | { 432 | type: "text", 433 | text: `Error getting service example: ${err}`, 434 | }, 435 | ], 436 | }; 437 | } 438 | }, 439 | ); 440 | 441 | // 8. Get screen example 442 | server.tool( 443 | "get_screen_example", 444 | "Get a React Native screen example", 445 | { 446 | screen_name: z.string().describe("Screen Name"), 447 | }, 448 | async ({ screen_name }) => { 449 | if (!screen_name) { 450 | return { 451 | content: [ 452 | { 453 | type: "text", 454 | text: "Screen name not specified", 455 | }, 456 | ], 457 | }; 458 | } 459 | 460 | try { 461 | // First try exact match 462 | const result = getExampleContent("screens", screen_name); 463 | 464 | if (result.error) { 465 | // Try to find by fuzzy match 466 | const screensDir = path.join(CODE_EXAMPLES_DIR, "react-native", "screens"); 467 | const closestMatch = findClosestMatch(screensDir, screen_name); 468 | 469 | if (closestMatch) { 470 | const fuzzyResult = getExampleContent("screens", closestMatch); 471 | return { 472 | content: [ 473 | { 474 | type: "text", 475 | text: fuzzyResult.content?.[0] ?? fuzzyResult.error ?? "Error: No content available", 476 | }, 477 | ], 478 | }; 479 | } else { 480 | return { 481 | content: [ 482 | { 483 | type: "text", 484 | text: `Screen ${screen_name} not found`, 485 | }, 486 | ], 487 | }; 488 | } 489 | } 490 | 491 | return { 492 | content: [ 493 | { 494 | type: "text", 495 | text: result.content?.[0] ?? result.error ?? "Error: No content available", 496 | }, 497 | ], 498 | }; 499 | } catch (err) { 500 | console.error(`Error getting screen example ${screen_name}:`, err); 501 | return { 502 | content: [ 503 | { 504 | type: "text", 505 | text: `Error getting screen example: ${err}`, 506 | }, 507 | ], 508 | }; 509 | } 510 | }, 511 | ); 512 | 513 | // 9. Get theme example 514 | server.tool( 515 | "get_theme_example", 516 | "Get code for a React Native theme", 517 | { 518 | theme_name: z.string().describe("Theme Name"), 519 | }, 520 | async ({ theme_name }) => { 521 | if (!theme_name) { 522 | return { 523 | content: [ 524 | { 525 | type: "text", 526 | text: "Theme name not specified", 527 | }, 528 | ], 529 | }; 530 | } 531 | 532 | try { 533 | // First try exact match 534 | const result = getExampleContent("theme", theme_name); 535 | 536 | if (result.error) { 537 | // Try to find by fuzzy match 538 | const themesDir = path.join(CODE_EXAMPLES_DIR, "react-native", "theme"); 539 | const closestMatch = findClosestMatch(themesDir, theme_name); 540 | 541 | if (closestMatch) { 542 | const fuzzyResult = getExampleContent("theme", closestMatch); 543 | return { 544 | content: [ 545 | { 546 | type: "text", 547 | text: fuzzyResult.content?.[0] ?? fuzzyResult.error ?? "Error: No content available", 548 | }, 549 | ], 550 | }; 551 | } else { 552 | return { 553 | content: [ 554 | { 555 | type: "text", 556 | text: `Theme ${theme_name} not found`, 557 | }, 558 | ], 559 | }; 560 | } 561 | } 562 | 563 | return { 564 | content: [ 565 | { 566 | type: "text", 567 | text: result.content?.[0] ?? result.error ?? "Error: No content available", 568 | }, 569 | ], 570 | }; 571 | } catch (err) { 572 | console.error(`Error getting theme example ${theme_name}:`, err); 573 | return { 574 | content: [ 575 | { 576 | type: "text", 577 | text: `Error getting theme example: ${err}`, 578 | }, 579 | ], 580 | }; 581 | } 582 | }, 583 | ); 584 | 585 | // 10. List available examples 586 | server.tool( 587 | "list_available_examples", 588 | "List all available code examples by category", 589 | {}, 590 | async () => { 591 | try { 592 | const examples = listAvailableExamples(); 593 | 594 | return { 595 | content: [ 596 | { 597 | type: "text", 598 | text: JSON.stringify(examples, null, 2), 599 | }, 600 | ], 601 | }; 602 | } catch (err) { 603 | console.error("Error listing available examples:", err); 604 | return { 605 | content: [ 606 | { 607 | type: "text", 608 | text: `Error listing available examples: ${err}`, 609 | }, 610 | ], 611 | }; 612 | } 613 | }, 614 | ); 615 | 616 | // Run the server 617 | async function main() { 618 | const transport = new StdioServerTransport(); 619 | await server.connect(transport); 620 | console.error("BluestoneApps MCP Server running on stdio"); 621 | } 622 | 623 | main().catch((error) => { 624 | console.error("Fatal error in main():", error); 625 | process.exit(1); 626 | }); 627 | ```