inital
This commit is contained in:
		
							
								
								
									
										81
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | node_modules/ | ||||||
|  | .expo/ | ||||||
|  | dist/ | ||||||
|  | npm-debug.* | ||||||
|  | *.jks | ||||||
|  | *.p8 | ||||||
|  | *.p12 | ||||||
|  | *.key | ||||||
|  | *.mobileprovision | ||||||
|  | *.orig.* | ||||||
|  | web-build/ | ||||||
|  |  | ||||||
|  | # macOS | ||||||
|  | .DS_Store | ||||||
|  |  | ||||||
|  | # Temporary files created by Metro to check the health of the file watcher | ||||||
|  | .metro-health-check* | ||||||
|  |  | ||||||
|  | # @generated expo-cli sync-647791c5bd841d5c91864afb91c302553d300921 | ||||||
|  | # The following patterns were generated by expo-cli | ||||||
|  |  | ||||||
|  | # OSX | ||||||
|  | # | ||||||
|  | .DS_Store | ||||||
|  |  | ||||||
|  | # Xcode | ||||||
|  | # | ||||||
|  | build/ | ||||||
|  | *.pbxuser | ||||||
|  | !default.pbxuser | ||||||
|  | *.mode1v3 | ||||||
|  | !default.mode1v3 | ||||||
|  | *.mode2v3 | ||||||
|  | !default.mode2v3 | ||||||
|  | *.perspectivev3 | ||||||
|  | !default.perspectivev3 | ||||||
|  | xcuserdata | ||||||
|  | *.xccheckout | ||||||
|  | *.moved-aside | ||||||
|  | DerivedData | ||||||
|  | *.hmap | ||||||
|  | *.ipa | ||||||
|  | *.xcuserstate | ||||||
|  | project.xcworkspace | ||||||
|  |  | ||||||
|  | # Android/IntelliJ | ||||||
|  | # | ||||||
|  | build/ | ||||||
|  | .idea | ||||||
|  | .gradle | ||||||
|  | local.properties | ||||||
|  | *.iml | ||||||
|  | *.hprof | ||||||
|  | .cxx/ | ||||||
|  | *.keystore | ||||||
|  | !debug.keystore | ||||||
|  |  | ||||||
|  | # node.js | ||||||
|  | # | ||||||
|  | node_modules/ | ||||||
|  | npm-debug.log | ||||||
|  | yarn-error.log | ||||||
|  |  | ||||||
|  | # Bundle artifacts | ||||||
|  | *.jsbundle | ||||||
|  |  | ||||||
|  | # CocoaPods | ||||||
|  | /ios/Pods/ | ||||||
|  |  | ||||||
|  | # Temporary files created by Metro to check the health of the file watcher | ||||||
|  | .metro-health-check* | ||||||
|  |  | ||||||
|  | # Expo | ||||||
|  | .expo/ | ||||||
|  | web-build/ | ||||||
|  | dist/ | ||||||
|  |  | ||||||
|  | # @end expo-cli | ||||||
|  | ios | ||||||
|  | android | ||||||
|  | out | ||||||
							
								
								
									
										159
									
								
								App.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								App.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,159 @@ | |||||||
|  | import React, { useEffect, useState } from 'react'; | ||||||
|  | import { NavigationContainer } from '@react-navigation/native'; | ||||||
|  | import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; | ||||||
|  | import HomeScreen from './Home'; | ||||||
|  | import ProfileScreen from './Profile'; | ||||||
|  | import LoginScreen from './Login'; | ||||||
|  | import SignupScreen from './Signup'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import SwitScreen from './Swit'; | ||||||
|  | import UserProfileScreen from './UserProfile'; | ||||||
|  | import { createNativeStackNavigator } from '@react-navigation/native-stack' | ||||||
|  | import {TailwindProvider} from 'tailwind-rn'; | ||||||
|  | import utilities from './tailwind.json'; | ||||||
|  | import FollowingScreen from './Following'; | ||||||
|  | import SwitDetailScreen from './SwitDetail'; | ||||||
|  | import SettingsScreen from './Settings'; | ||||||
|  | import SearchScreen from './Search'; | ||||||
|  | import Toast from 'react-native-toast-message' | ||||||
|  | import Notifications from './Notification'; | ||||||
|  | import LikesScreen from './LikesScreen'; | ||||||
|  | import { socket } from './services/socket'; | ||||||
|  | import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; | ||||||
|  | import { SafeAreaView } from 'react-native-safe-area-context'; | ||||||
|  | import EditProfileScreen from './EditProfileScreen'; | ||||||
|  | import DirectMessagesScreen from './DirectMessagesScreen'; | ||||||
|  | import ConversationScreen from './ConversationScreen'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const Tab = createBottomTabNavigator(); | ||||||
|  | const HomeStack = createNativeStackNavigator(); | ||||||
|  | const HomeTopTab = createMaterialTopTabNavigator(); | ||||||
|  |  | ||||||
|  | function HomeTopTabNavigator() { | ||||||
|  |   return ( | ||||||
|  |     <SafeAreaView style={{ flex: 1 }}> | ||||||
|  |       <HomeTopTab.Navigator> | ||||||
|  |         <HomeTopTab.Screen name="Home" component={HomeScreen} /> | ||||||
|  |         <HomeTopTab.Screen name="Following" component={FollowingScreen} /> | ||||||
|  |       </HomeTopTab.Navigator> | ||||||
|  |     </SafeAreaView> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function HomeStackScreen() { | ||||||
|  |   return ( | ||||||
|  |     <HomeStack.Navigator> | ||||||
|  |       <HomeStack.Screen  | ||||||
|  |         name="HomeTab"  | ||||||
|  |         component={HomeTopTabNavigator}  | ||||||
|  |         options={{ headerShown: false }}  | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen  | ||||||
|  |         name="Swit"  | ||||||
|  |         component={SwitScreen}  | ||||||
|  |         options={{title:'New swit'}} | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen  | ||||||
|  |         name="Profile"  | ||||||
|  |         component={UserProfileScreen}  | ||||||
|  |         options={({ route }) => ({ title: route.params.username })} | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen  | ||||||
|  |         name="SwitDetail"  | ||||||
|  |         component={SwitDetailScreen}  | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen  | ||||||
|  |         name="Settings"  | ||||||
|  |         component={SettingsScreen}  | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen  | ||||||
|  |         name="Login"  | ||||||
|  |         children={() => <LoginScreen setUserToken={setUserToken} />}  | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen  | ||||||
|  |         name="Signup"  | ||||||
|  |         component={SignupScreen}  | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen  | ||||||
|  |         name="Likes"  | ||||||
|  |         component={LikesScreen}  | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen | ||||||
|  |         name="EditProfile" | ||||||
|  |         component={EditProfileScreen} | ||||||
|  |         options={{title: 'Edit Profile'}} | ||||||
|  |       /> | ||||||
|  |       <HomeStack.Screen | ||||||
|  |       name="Conversation" | ||||||
|  |       component={ConversationScreen} | ||||||
|  |       options={({ route }) => ({ title: route.params.username })} | ||||||
|  |       /> | ||||||
|  |  | ||||||
|  |     </HomeStack.Navigator> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export default function App() { | ||||||
|  |   const [userToken, setUserToken] = useState(null); | ||||||
|  |   // Listen for 'notification' messages | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   const checkUserToken = async () => { | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     setUserToken(token); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     checkUserToken(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const setDefaultAPI = async () => { | ||||||
|  |       const apiEndpoint = await AsyncStorage.getItem('apiEndpoint'); | ||||||
|  |       if (!apiEndpoint) { | ||||||
|  |         await AsyncStorage.setItem('apiEndpoint', 'http://staging.swifter.win'); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     setDefaultAPI(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |     <TailwindProvider utilities={utilities}> | ||||||
|  |     <NavigationContainer> | ||||||
|  |     {userToken ? ( | ||||||
|  |       <Tab.Navigator | ||||||
|  |       initialRouteName="Home" | ||||||
|  |       screenOptions={{ | ||||||
|  |         tabBarActiveTintColor: 'tomato', | ||||||
|  |         tabBarInactiveTintColor: 'gray', | ||||||
|  |       }} | ||||||
|  |       > | ||||||
|  |         <Tab.Screen name="Home" component={HomeStackScreen} options={{ headerShown: false}}/> | ||||||
|  |         <Tab.Screen name="Search" component={SearchScreen} /> | ||||||
|  |         <Tab.Screen name="Notifications" component={Notifications} /> | ||||||
|  |         <Tab.Screen name="DirectMessages" component={DirectMessagesScreen} options={{title:"DMs"}}/> | ||||||
|  |  | ||||||
|  |         <Tab.Screen name="Profile" component={ProfileScreen} /> | ||||||
|  |          | ||||||
|  |       </Tab.Navigator> | ||||||
|  |     ) : ( | ||||||
|  |       <Tab.Navigator initialRouteName="Login"> | ||||||
|  |         <Tab.Screen name="Login" children={() => <LoginScreen setUserToken={setUserToken} />} /> | ||||||
|  |         <Tab.Screen name="Signup" component={SignupScreen} /> | ||||||
|  |         <Tab.Screen name="Settings" component={SettingsScreen} /> | ||||||
|  |       </Tab.Navigator> | ||||||
|  |     )} | ||||||
|  |   </NavigationContainer> | ||||||
|  |   </TailwindProvider> | ||||||
|  |   <Toast /> | ||||||
|  |   </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										117
									
								
								ConversationScreen.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								ConversationScreen.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { View, Text, TextInput, Button, FlatList, ActivityIndicator } from 'react-native'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import io from 'socket.io-client' | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  |  | ||||||
|  | function ConversationScreen({ route, navigation }) { | ||||||
|  |   const { otherUserId } = route.params; | ||||||
|  |   const [directMessages, setDirectMessages] = useState([]); | ||||||
|  |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |   const [newMessage, setNewMessage] = useState(''); | ||||||
|  |     const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |   async function fetchDirectMessages() { | ||||||
|  |     setIsLoading(true); | ||||||
|  |     try { | ||||||
|  |       const token = await AsyncStorage.getItem('token'); | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/dms/${otherUserId}`, { | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error(`Failed to fetch direct messages: ${response.status}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const messages = await response.json(); | ||||||
|  |       setDirectMessages(messages); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error:', error); | ||||||
|  |       setErrorMessage(error.message); | ||||||
|  |     } finally { | ||||||
|  |       setIsLoading(false); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async function handleSendMessage() { | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const senderId = await AsyncStorage.getItem('userID'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |     const response = await fetch(`${apiUrl}/api/v1/app/dms/send`, { | ||||||
|  |       method: 'POST', | ||||||
|  |       headers: { | ||||||
|  |         'Authorization': token, | ||||||
|  |         'Content-Type': 'application/json', | ||||||
|  |       }, | ||||||
|  |       body: JSON.stringify({ recipientId: otherUserId, text: newMessage, senderId: senderId }), | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (!response.ok) { | ||||||
|  |       throw new Error(`Failed to send message: ${response.status}`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const message = await response.json(); | ||||||
|  |     setNewMessage(''); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     async function setupSocket() { | ||||||
|  |       const senderId = await AsyncStorage.getItem('userID'); | ||||||
|  |       console.log('Sender ID:', senderId); | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |       const socket = io(apiUrl); | ||||||
|  |       socket.emit('user connected', senderId); | ||||||
|  |       socket.on('new_dm', (newDm) => { | ||||||
|  |         setDirectMessages(prevMessages => [...prevMessages, newDm]); | ||||||
|  |     }); | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     async function setup() { | ||||||
|  |       await fetchDirectMessages(); | ||||||
|  |       await setupSocket(); | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     setup(); | ||||||
|  |   }, []); | ||||||
|  |    | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('flex-auto')}> | ||||||
|  |       {/* Display a loading indicator while the messages are being fetched */} | ||||||
|  |       {isLoading && <ActivityIndicator size="large" color="#0000ff" />} | ||||||
|  |       {errorMessage !== '' && <Text>{errorMessage}</Text>} | ||||||
|  |  | ||||||
|  |       {/* Render the list of direct messages */} | ||||||
|  |       <View style={tailwind('flex-1')}> | ||||||
|  |   <FlatList | ||||||
|  |     style={tailwind('flex-1')} | ||||||
|  |     contentContainerStyle={tailwind('pb-12')} | ||||||
|  |     data={directMessages} | ||||||
|  |     keyExtractor={(item) => item._id} | ||||||
|  |     renderItem={({ item }) => ( | ||||||
|  |       <Text> | ||||||
|  |         {item.sender.username}: {item.content} | ||||||
|  |       </Text> | ||||||
|  |     )} | ||||||
|  |   /> | ||||||
|  |         </View> | ||||||
|  |  | ||||||
|  |       {/* Render the text input and send button */} | ||||||
|  |       <View style={tailwind('flex-row items-center mb-3')}>  | ||||||
|  |         <TextInput | ||||||
|  |           style={tailwind('flex-1 border border-gray-500 mr-3 px-2')}  | ||||||
|  |           value={newMessage} | ||||||
|  |           onChangeText={setNewMessage} | ||||||
|  |           placeholder="Type a message" | ||||||
|  |         /> | ||||||
|  |         <Button title="Send" onPress={handleSendMessage} /> | ||||||
|  |       </View>  | ||||||
|  |     </View> | ||||||
|  | ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | export default ConversationScreen; | ||||||
							
								
								
									
										47
									
								
								DirectMessagesScreen.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								DirectMessagesScreen.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { View, Text, Button, FlatList, ActivityIndicator } from 'react-native'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  |  | ||||||
|  | function DirectMessagesScreen({ navigation }) { | ||||||
|  |   const [conversations, setConversations] = useState([]); | ||||||
|  |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const fetchConversations = async () => { | ||||||
|  |       const userId = await AsyncStorage.getItem('userID'); | ||||||
|  |       const token = await AsyncStorage.getItem('token'); | ||||||
|  |             const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |       const response = await fetch(`http://192.168.1.107:1989/api/v1/app/dms/`, { | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |       }); | ||||||
|  |    | ||||||
|  |       if (response.ok) { | ||||||
|  |         const data = await response.json(); | ||||||
|  |         setConversations(data); | ||||||
|  |       } else { | ||||||
|  |         console.error(`Failed to fetch conversations: ${response.status}`); | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  |    | ||||||
|  |     fetchConversations(); | ||||||
|  |     console.warn(conversations) | ||||||
|  |   }, []); | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <FlatList | ||||||
|  |   data={conversations} | ||||||
|  |   keyExtractor={(item) => item.userId} | ||||||
|  |   renderItem={({ item }) => ( | ||||||
|  |     <Text onPress={() => navigation.navigate('Conversation', { otherUserId: item.userId }, console.warn("item", item))}> | ||||||
|  |       {item.username}: {item.messages[0].content} | ||||||
|  |     </Text> | ||||||
|  |   )} | ||||||
|  | /> | ||||||
|  |  | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default DirectMessagesScreen; | ||||||
							
								
								
									
										119
									
								
								EditProfileScreen.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								EditProfileScreen.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | import React, { useState } from 'react'; | ||||||
|  | import { View, TextInput, Button, Image, ActivityIndicator } from 'react-native'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import * as ImagePicker from 'expo-image-picker'; | ||||||
|  |  | ||||||
|  | function EditProfileScreen({ route, navigation }) { | ||||||
|  |   const { user, currentUsername, currentBio, currentPronouns, currentName } = route.params; | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |    | ||||||
|  |   const [username, setUsername] = useState(user.username); | ||||||
|  |   const [profilePicture, setProfilePicture] = useState(user.profilePicture); | ||||||
|  |   const [bio, setBio] = useState(user.bio); | ||||||
|  |   const [pronouns, setPronouns] = useState(user.pronouns); | ||||||
|  |   const [name, setName] = useState(user.name); | ||||||
|  |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |  | ||||||
|  |   const pickImage = async () => { | ||||||
|  |     let result = await ImagePicker.launchImageLibraryAsync({ | ||||||
|  |       mediaTypes: ImagePicker.MediaTypeOptions.All, | ||||||
|  |       allowsEditing: true, | ||||||
|  |       aspect: [1, 1], | ||||||
|  |       quality: 1, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     console.log(result); | ||||||
|  |  | ||||||
|  |     if (!result.cancelled) { | ||||||
|  |       setProfilePicture(result.uri); | ||||||
|  |  | ||||||
|  |       // Get the user's token | ||||||
|  |       const token = await AsyncStorage.getItem('token'); | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |  | ||||||
|  |       // Create a new FormData object | ||||||
|  |       let formData = new FormData(); | ||||||
|  |       // Add the image to the form data | ||||||
|  |       let uriParts = result.uri.split('.'); | ||||||
|  |       let fileType = uriParts[uriParts.length - 1]; | ||||||
|  |       formData.append('profilePicture', { | ||||||
|  |         uri: result.uri, | ||||||
|  |         name: `photo.${fileType}`, | ||||||
|  |         type: `image/${fileType}`, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Upload the image | ||||||
|  |       try { | ||||||
|  |         let response = await fetch(`${apiUrl}/api/v1/app/user/profilePicture`, { | ||||||
|  |           method: 'POST', | ||||||
|  |           body: formData, | ||||||
|  |           headers: { | ||||||
|  |             'Content-Type': 'multipart/form-data', | ||||||
|  |             'Authorization': token, | ||||||
|  |           }, | ||||||
|  |         }); | ||||||
|  |         let responseJson = await response.json(); | ||||||
|  |         console.log(responseJson); | ||||||
|  |       } catch (error) { | ||||||
|  |         console.error(error); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   async function saveChanges() { | ||||||
|  |     setIsLoading(true); | ||||||
|  |     try { | ||||||
|  |       const token = await AsyncStorage.getItem('token'); | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/user/data/${user._id}/edit`, { | ||||||
|  |         method: 'PUT', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |           'Authorization': token, | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |           username, | ||||||
|  |           profilePicture, | ||||||
|  |           bio, | ||||||
|  |           pronouns, | ||||||
|  |           name, | ||||||
|  |         }), | ||||||
|  |       }); | ||||||
|  |    | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error(`Failed to save changes: ${response.status}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |    | ||||||
|  |       navigation.goBack();  // Go back to the profile screen | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error:', error); | ||||||
|  |       setErrorMessage(error.message); | ||||||
|  |     } finally { | ||||||
|  |       setIsLoading(false); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |       {isLoading && <ActivityIndicator size="large" color="#0000ff" />} | ||||||
|  |       {errorMessage !== '' && <Text style={tailwind('text-red-500')}>{errorMessage}</Text>} | ||||||
|  |       {profilePicture && <Image source={{ uri: profilePicture }} style={{ width: 200, height: 200 }} />} | ||||||
|  |        | ||||||
|  |       <Button title="Upload Profile Picture" onPress={pickImage} /> | ||||||
|  |       <TextInput value={username} onChangeText={setUsername} placeholder={currentUsername} /> | ||||||
|  |       <TextInput value={bio} onChangeText={setBio} placeholder={currentBio} /> | ||||||
|  |       <TextInput value={pronouns} onChangeText={setPronouns} placeholder={currentPronouns} /> | ||||||
|  |       <TextInput value={name} onChangeText={setName} placeholder={currentName} /> | ||||||
|  |        | ||||||
|  |       <Button title="Save Changes" onPress={saveChanges} /> | ||||||
|  |       <Button title="Cancel" onPress={() => navigation.goBack()} /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default EditProfileScreen; | ||||||
							
								
								
									
										113
									
								
								Following.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								Following.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { View, Text, Button, ScrollView, RefreshControl, ActivityIndicator } from 'react-native'; | ||||||
|  | import { useTailwind } from 'tailwind-rn' | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import Icon from 'react-native-vector-icons/FontAwesome' | ||||||
|  | import { TouchableOpacity } from 'react-native'; | ||||||
|  | import { HeaderButtons, Item } from 'react-navigation-header-buttons'; | ||||||
|  | import CreateSwitButton from './components/Create'; | ||||||
|  | // import date | ||||||
|  | import moment from 'moment'; | ||||||
|  | import { Swit } from './Home'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function HomeScreen({ navigation }) { | ||||||
|  |   const [swits, setSwits] = useState([]); | ||||||
|  |   const [refreshing, setRefreshing] = useState(false); | ||||||
|  |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |   const [userId, setUserId] = useState(''); | ||||||
|  |  | ||||||
|  |   async function fetchSwits() { | ||||||
|  |     setIsLoading(true) | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |     setErrorMessage(''); | ||||||
|  |     try { | ||||||
|  |       setErrorMessage(''); | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/swit/swits/following`, { | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!response.ok) { | ||||||
|  |         const errorText = await response.text(); | ||||||
|  |         setErrorMessage(errorText); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const swits = await response.json(); | ||||||
|  |       setSwits(swits); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.log(error); | ||||||
|  |       setErrorMessage('Something went wrong'); | ||||||
|  |     } | ||||||
|  |     setIsLoading(false) | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const unsubscribe = navigation.addListener('focus', fetchSwits); | ||||||
|  |     return unsubscribe; | ||||||
|  |   }, [navigation]); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     async function fetchUserId() { | ||||||
|  |       const id = await AsyncStorage.getItem('userID'); | ||||||
|  |       setUserId(id); | ||||||
|  |     } | ||||||
|  |     fetchUserId(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     navigation.setOptions({ | ||||||
|  |       headerRight: () => ( | ||||||
|  |         <HeaderButtons HeaderButtonComponent={CreateSwitButton}> | ||||||
|  |           <Item | ||||||
|  |             title="Add" | ||||||
|  |             iconName="ios-add" | ||||||
|  |             onPress={() => { | ||||||
|  |               navigation.navigate('Swit'); | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         </HeaderButtons> | ||||||
|  |       ), | ||||||
|  |     }); | ||||||
|  |   }, [navigation]); | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |   if (isLoading) { | ||||||
|  |     return ( | ||||||
|  |       <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}> | ||||||
|  |       <ActivityIndicator size="large" color="#0000ff" /> | ||||||
|  |     </View> | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |                   {errorMessage ? <Text style={tailwind('text-red-500 mb-2 text-center')}>{errorMessage}</Text> : null} | ||||||
|  |       <ScrollView | ||||||
|  |         refreshControl={ | ||||||
|  |           <RefreshControl | ||||||
|  |             refreshing={refreshing} | ||||||
|  |             onRefresh={async () => { | ||||||
|  |               setRefreshing(true); | ||||||
|  |               await fetchSwits(); | ||||||
|  |               setRefreshing(false); | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         } | ||||||
|  |       > | ||||||
|  |         {swits.map(swit => ( | ||||||
|  |           <Swit swit={swit} navigation={navigation} userId={userId} setSwits={setSwits} swits={swits}/> | ||||||
|  |           ))} | ||||||
|  |  | ||||||
|  |       </ScrollView> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  |    | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default HomeScreen; | ||||||
							
								
								
									
										262
									
								
								Home.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								Home.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,262 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { View, Text, Button, ScrollView, RefreshControl, ActivityIndicator } from 'react-native'; | ||||||
|  | import { useTailwind } from 'tailwind-rn' | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import Icon from 'react-native-vector-icons/FontAwesome' | ||||||
|  | import { TouchableOpacity } from 'react-native'; | ||||||
|  | import { HeaderButtons, Item } from 'react-navigation-header-buttons'; | ||||||
|  | import CreateSwitButton from './components/Create'; | ||||||
|  | // import date | ||||||
|  | import moment from 'moment'; | ||||||
|  | import { FAB } from 'react-native-paper'; | ||||||
|  |  | ||||||
|  | function Swit({ swit, navigation, userId, setSwits, swits }) { | ||||||
|  |   const [isLiked, setIsLiked] = useState(swit.likes.some(like => like === userId)); | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |   const [isReposted, setIsReposted] = useState(''); | ||||||
|  |   const [repostCount, setRepostCount] = useState(null); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   function formatTime(time) { | ||||||
|  |     const duration = moment.duration(moment().diff(time)); | ||||||
|  |    | ||||||
|  |     const years = duration.years(); | ||||||
|  |     if (years > 0) { | ||||||
|  |       return `${years}y`; | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     const months = duration.months(); | ||||||
|  |     if (months > 0) { | ||||||
|  |       return `${months}m`; | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     const days = duration.days(); | ||||||
|  |     if (days > 0) { | ||||||
|  |       return `${days}d`; | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     const hours = duration.hours(); | ||||||
|  |     if (hours > 0) { | ||||||
|  |       return `${hours}h`; | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     const minutes = duration.minutes(); | ||||||
|  |     if (minutes > 0) { | ||||||
|  |       return `${minutes}m`; | ||||||
|  |     } | ||||||
|  |    | ||||||
|  |     return 'Just now'; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   const handleLikePress = async (id) => { | ||||||
|  |     setIsLiked(!isLiked); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |     const url = `${apiUrl}/api/v1/app/swit/swits/${id}/${isLiked ? 'unlike' : 'like'}`; | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |    | ||||||
|  |     try { | ||||||
|  |       const response = await fetch(url, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { | ||||||
|  |           'Authorization': token, | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |    | ||||||
|  |       if (!response.ok) { | ||||||
|  |         setIsLiked(!isLiked); | ||||||
|  |         console.warn(response) | ||||||
|  |       } else { | ||||||
|  |         // Fetch the updated swit | ||||||
|  |  | ||||||
|  |         const switResponse = await fetch(`${apiUrl}/api/v1/app/swit/swits/${id}`, { | ||||||
|  |           headers: { 'Authorization': token }, | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         if (switResponse.ok) { | ||||||
|  |           const updatedSwit = await switResponse.json(); | ||||||
|  |           // Update the swit in the swits array | ||||||
|  |           setSwits(swits.map(s => s._id === id ? updatedSwit : s)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error:', error); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const handleRepostPress = async (id) => { | ||||||
|  |   setIsReposted(!isReposted); | ||||||
|  |   const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |   const url = `${apiUrl}/api/v1/app/swit/swits/${id}/${isReposted ? 'unrepost' : 'repost'}`; | ||||||
|  |   const token = await AsyncStorage.getItem('token'); | ||||||
|  |  | ||||||
|  |   try { | ||||||
|  |     const response = await fetch(url, { | ||||||
|  |       method: 'POST', | ||||||
|  |       headers: { | ||||||
|  |         'Authorization': token, | ||||||
|  |         'Content-Type': 'application/json', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (!response.ok) { | ||||||
|  |       setIsReposted(!isReposted); | ||||||
|  |       throw new Error('Network response was not ok'); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const data = response.json() | ||||||
|  |     setRepostCount(data.count) | ||||||
|  |  | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Error:', error); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | useEffect(() => { | ||||||
|  |   setIsLiked(swit.likes.some(like => like === userId)); | ||||||
|  | }, [swit, userId]); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <TouchableOpacity key={swit._id} onPress={() => navigation.navigate('SwitDetail', { switId: swit._id, userId: userId })}> | ||||||
|  |     <View key={swit._id} style={tailwind('border border-gray-500 p-2 mb-4')}> | ||||||
|  |       <Text | ||||||
|  |         style={tailwind('font-bold mb-2')} | ||||||
|  |         onPress={() => { | ||||||
|  |           navigation.navigate('Profile', { userId: swit.user }) | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|  |         {swit.username} - <Text style={tailwind('text-gray-400')}>{formatTime(swit.createdAt)}</Text> | ||||||
|  |       </Text> | ||||||
|  |  | ||||||
|  |       <Text>{swit.text}</Text> | ||||||
|  |       <Icon name="heart" size={24} color={isLiked ? 'red' : 'black'} onPress={() => handleLikePress(swit._id)} />  | ||||||
|  |       <Text>{swit.likes.length}</Text> | ||||||
|  |       <Icon name="rotate-right" size={24} color={isReposted ? 'green' : 'black'} onPress={() => handleRepostPress(swit._id)} />  | ||||||
|  |       <Text>{swit.reposts.length}</Text> | ||||||
|  |  | ||||||
|  |     </View> | ||||||
|  |     </TouchableOpacity> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function HomeScreen({ navigation }) { | ||||||
|  |   const [swits, setSwits] = useState([]); | ||||||
|  |   const [refreshing, setRefreshing] = useState(false); | ||||||
|  |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |   const [userId, setUserId] = useState(''); | ||||||
|  |  | ||||||
|  |   async function fetchSwits() { | ||||||
|  |     setIsLoading(true) | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |     setErrorMessage(''); | ||||||
|  |     try { | ||||||
|  |       setErrorMessage(''); | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/swit/swits/`, { | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!response.ok) { | ||||||
|  |         const errorText = await response.text(); | ||||||
|  |         setErrorMessage(errorText); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const swits = await response.json(); | ||||||
|  |       setSwits(swits); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.log(error); | ||||||
|  |       setErrorMessage('Something went wrong'); | ||||||
|  |     } | ||||||
|  |     setIsLoading(false) | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const unsubscribe = navigation.addListener('focus', fetchSwits); | ||||||
|  |     return unsubscribe; | ||||||
|  |   }, [navigation]); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     async function fetchUserId() { | ||||||
|  |       const id = await AsyncStorage.getItem('userID'); | ||||||
|  |       setUserId(id); | ||||||
|  |     } | ||||||
|  |     fetchUserId(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     navigation.setOptions({ | ||||||
|  |       headerRight: () => ( | ||||||
|  |         <HeaderButtons HeaderButtonComponent={CreateSwitButton}> | ||||||
|  |           <Item | ||||||
|  |             title="Add" | ||||||
|  |             iconName="ios-add" | ||||||
|  |             onPress={() => { | ||||||
|  |               navigation.navigate('Swit'); | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         </HeaderButtons> | ||||||
|  |       ), | ||||||
|  |     }); | ||||||
|  |   }, [navigation]); | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |   if (isLoading) { | ||||||
|  |     return ( | ||||||
|  |       <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}> | ||||||
|  |       <ActivityIndicator size="large" color="#0000ff" /> | ||||||
|  |     </View> | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |                   {errorMessage ? <Text style={tailwind('text-red-500 mb-2 text-center')}>{errorMessage}</Text> : null} | ||||||
|  |       <ScrollView | ||||||
|  |         refreshControl={ | ||||||
|  |           <RefreshControl | ||||||
|  |             refreshing={refreshing} | ||||||
|  |             onRefresh={async () => { | ||||||
|  |               setRefreshing(true); | ||||||
|  |               await fetchSwits(); | ||||||
|  |               setRefreshing(false); | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         } | ||||||
|  |       > | ||||||
|  |         {swits.map(swit => ( | ||||||
|  |           <Swit swit={swit} navigation={navigation} userId={userId} setSwits={setSwits} swits={swits}/> | ||||||
|  |         ))} | ||||||
|  |  | ||||||
|  |       </ScrollView> | ||||||
|  |  | ||||||
|  | <FAB | ||||||
|  |     style={{ | ||||||
|  |         position: 'absolute', | ||||||
|  |         margin: 16, | ||||||
|  |         right: 0, | ||||||
|  |         bottom: 0, | ||||||
|  |     }} | ||||||
|  |     icon="plus" | ||||||
|  |     onPress={() => navigation.navigate('Swit')} | ||||||
|  | /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  |    | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default HomeScreen; | ||||||
|  | export { Swit }; | ||||||
							
								
								
									
										39
									
								
								LikesScreen.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								LikesScreen.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | |||||||
|  | import React, { useEffect, useState } from 'react'; | ||||||
|  | import { View, Text, FlatList } from 'react-native'; | ||||||
|  | import { useAsyncStorage } from '@react-native-async-storage/async-storage'; | ||||||
|  |  | ||||||
|  | const LikesScreen = ({ route }) => { | ||||||
|  |   const { switId } = route.params; | ||||||
|  |   const [likes, setLikes] = useState([]); | ||||||
|  |    | ||||||
|  |  | ||||||
|  |    | ||||||
|  |   useEffect(() => { | ||||||
|  |     fetchLikes(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const fetchLikes = async () => { | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |     // replace with your server's address and correct route | ||||||
|  |     const response = await fetch(`${apiUrl}/api/v1/app/swit/swits/${switId}/likes`, { | ||||||
|  |       headers: { | ||||||
|  |         Authorization: 'Bearer ' + token, // replace 'token' with the actual token | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |     const data = await response.json(); | ||||||
|  |     setLikes(data); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View> | ||||||
|  |       <FlatList | ||||||
|  |         data={likes} | ||||||
|  |         keyExtractor={(item) => item._id} | ||||||
|  |         renderItem={({ item }) => <Text>{item.username}</Text>} | ||||||
|  |       /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default LikesScreen; | ||||||
							
								
								
									
										74
									
								
								Login.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								Login.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | import React, { useState } from 'react'; | ||||||
|  | import { View, TextInput, Button, Text } from 'react-native'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  | import socket from './services/socket'; | ||||||
|  |  | ||||||
|  | function LoginScreen({ navigation, setUserToken }) { | ||||||
|  |   const [username, setUsername] = useState(''); | ||||||
|  |   const [password, setPassword] = useState(''); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |   async function loginUser() { | ||||||
|  |     try { | ||||||
|  |       setErrorMessage(''); | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/auth/login`, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ username, password }), | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!response.ok) { | ||||||
|  |         const errorText = await response.text(); | ||||||
|  |         console.error('Failed to login:', response.status, errorText); | ||||||
|  |         setErrorMessage(errorText); | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const { token, userId } = await response.json(); | ||||||
|  |       await AsyncStorage.setItem('token', token); | ||||||
|  |       await AsyncStorage.setItem('userID', userId); | ||||||
|  |       setUserToken(token); | ||||||
|  |       setErrorMessage(''); | ||||||
|  |       navigation.navigate('Home'); | ||||||
|  |        | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Network error:', error); | ||||||
|  |       setErrorMessage('Network error. Please try again later.'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |    | ||||||
|  |    | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |             {errorMessage ? <Text style={tailwind('text-red-500 mb-2 text-center')}>{errorMessage}</Text> : null} | ||||||
|  |       <TextInput | ||||||
|  |         style={tailwind('border border-gray-500 p-2 mb-4')} | ||||||
|  |         value={username} | ||||||
|  |         onChangeText={setUsername} | ||||||
|  |         placeholder="Username" | ||||||
|  |       /> | ||||||
|  |       <TextInput | ||||||
|  |         style={tailwind('border border-gray-500 p-2 mb-4')} | ||||||
|  |         value={password} | ||||||
|  |         onChangeText={setPassword} | ||||||
|  |         placeholder="Password" | ||||||
|  |         secureTextEntry | ||||||
|  |         required | ||||||
|  |       /> | ||||||
|  |       <Button title="Log in" onPress={loginUser} /> | ||||||
|  |       <Button title="Sign up instead" onPress={() => navigation.navigate('Signup')} /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default LoginScreen; | ||||||
							
								
								
									
										89
									
								
								Notification.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								Notification.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | import React, { useEffect, useState } from 'react'; | ||||||
|  | import { FlatList, View, Text, Image, TouchableOpacity, ActivityIndicator } from 'react-native'; | ||||||
|  | import axios from 'axios'; | ||||||
|  | import { useNavigation } from '@react-navigation/native'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  |  | ||||||
|  | const Notifications = () => { | ||||||
|  |   const [notifications, setNotifications] = useState([]); | ||||||
|  |   const [page, setPage] = useState(1); | ||||||
|  |   const navigation = useNavigation(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |   // Fetch notifications from the server | ||||||
|  |   const fetchNotifications = async () => { | ||||||
|  |     try { | ||||||
|  |         const token = await AsyncStorage.getItem('token'); | ||||||
|  |         console.log(token) | ||||||
|  |         const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |  | ||||||
|  |       const res = await axios.get(`${apiUrl}/api/v1/app/notify/notifications?page=${page}`, { | ||||||
|  |         headers: {'Authorization': token,    } // replace token with your JWT token | ||||||
|  |       }); | ||||||
|  |       setNotifications([...notifications, ...res.data]); | ||||||
|  |       setPage(page + 1); | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error("fetch: ", err); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // Fetch notifications when the component mounts | ||||||
|  |   useEffect(() => { | ||||||
|  |     fetchNotifications(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   // Mark a notification as read and navigate to the relevant screen | ||||||
|  |   const handlePress = async (notification) => { | ||||||
|  |     try { | ||||||
|  |         const token = AsyncStorage.getItem('token'); | ||||||
|  |         const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |  | ||||||
|  |       await axios.put(`${apiUrl}/api/v1/app/notify/notifications/${notification._id}`, {}, { | ||||||
|  |         headers: { 'Authorization': token } // replace token with your JWT token | ||||||
|  |       }); | ||||||
|  |       if (notification.type === 'like') { | ||||||
|  |         navigation.navigate('Likes', { switId: notification.switId }); | ||||||
|  |       } else if (notification.type === 'comment') { | ||||||
|  |         navigation.navigate('Comments', { postId: notification.postId }); | ||||||
|  |       } else if (notification.type === 'follow') { | ||||||
|  |         navigation.navigate('Profile', { userId: notification.fromUser }); | ||||||
|  |       } | ||||||
|  |     } catch (err) { | ||||||
|  |       console.error("press: ", err); | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   // Render a single notification | ||||||
|  |   const renderNotification = ({ item }) => ( | ||||||
|  |     <View style={tailwind('border-b border-gray-200 p-2 flex-row items-center')}> | ||||||
|  |       <Image source={{ uri: item.fromUser.profilePicture }} style={tailwind('w-12 h-12 rounded-full')} /> | ||||||
|  |       <TouchableOpacity style={tailwind('ml-2 flex-grow')} onPress={() => handlePress(item)}> | ||||||
|  |         <Text style={tailwind('text-lg')}>{item.text}</Text> | ||||||
|  |         <Text style={tailwind('text-sm text-gray-500')}>{new Date(item.createdAt).toLocaleString()}</Text> | ||||||
|  |       </TouchableOpacity> | ||||||
|  |       <View style={tailwind('pr-2')}> | ||||||
|  |         <ActivityIndicator size="small" color="#0000ff" /> | ||||||
|  |       </View> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('flex-1')}> | ||||||
|  |       <FlatList | ||||||
|  |         data={notifications} | ||||||
|  |         renderItem={renderNotification} | ||||||
|  |         keyExtractor={item => item._id} | ||||||
|  |         onEndReached={fetchNotifications} | ||||||
|  |         onEndReachedThreshold={0.5} | ||||||
|  |       /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default Notifications; | ||||||
							
								
								
									
										131
									
								
								Profile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								Profile.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,131 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { View, Text, Button, Image, ActivityIndicator, RefreshControl, TextInput } from 'react-native'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import * as ImagePicker from 'expo-image-picker'; | ||||||
|  | import { Swit } from './Home'; | ||||||
|  | import { ScrollView } from 'react-native-gesture-handler'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function ProfileScreen({ navigation }) { | ||||||
|  |   const [username, setUsername] = useState(''); | ||||||
|  |   const [profilePicture, setProfilePicture] = useState(null); | ||||||
|  |   const [bio, setBio] = useState(''); | ||||||
|  |   const [pronouns, setPronouns] = useState(''); | ||||||
|  |   const [name, setName] = useState(''); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |   const [following, setFollowing] = useState([]); | ||||||
|  |   const [followers, setFollowers] = useState([]); | ||||||
|  |   const [isLoading, setIsLoading] = useState(false); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |   const [userSwits, setUserSwits] = useState([]); | ||||||
|  |   const [userId, setUserId] = useState(''); | ||||||
|  |   const [refreshing, setRefreshing] = useState(false); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   async function fetchUserData() { | ||||||
|  |     setIsLoading(true); | ||||||
|  |     try { | ||||||
|  |       const userId = await AsyncStorage.getItem('userID'); | ||||||
|  |       const token = await AsyncStorage.getItem('token'); | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/user/data/${userId}`, { | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |       }); | ||||||
|  |    | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error(`Failed to fetch user data: ${response.status}`); | ||||||
|  |       } | ||||||
|  |    | ||||||
|  |       const user = await response.json(); | ||||||
|  |       setUsername(user.username); | ||||||
|  |       setFollowing(user.following); | ||||||
|  |       setProfilePicture(user.profilePicture); | ||||||
|  |    | ||||||
|  |       const followersResponse = await fetch(`${apiUrl}/api/v1/app/user/followers/${userId}`, { | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |       }); | ||||||
|  |    | ||||||
|  |       if (!followersResponse.ok) { | ||||||
|  |         throw new Error(`Failed to fetch followers: ${followersResponse.status}`); | ||||||
|  |       } | ||||||
|  |    | ||||||
|  |       const followersData = await followersResponse.json(); | ||||||
|  |       setFollowers(followersData); | ||||||
|  |       setBio(user.bio); | ||||||
|  |       setPronouns(user.pronouns); | ||||||
|  |       setName(user.name); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |       const userPostedSwits = await fetch(`${apiUrl}/api/v1/app/swit/swits/user/${userId}`, { | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |         }); | ||||||
|  |         if (!userPostedSwits.ok) { | ||||||
|  |           throw new Error(`Failed to fetch swits: ${userPostedSwits.status}`); | ||||||
|  |         } | ||||||
|  |         const userPostedSwitsData = await userPostedSwits.json(); | ||||||
|  |         setUserSwits(userPostedSwitsData); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error:', error); | ||||||
|  |       setErrorMessage(error.message); | ||||||
|  |     } finally { | ||||||
|  |       setIsLoading(false); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     setUserId(AsyncStorage.getItem('userID')); | ||||||
|  |     fetchUserData(); | ||||||
|  |  | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |       {isLoading && <ActivityIndicator size="large" color="#0000ff" />} | ||||||
|  |       {errorMessage !== '' && <Text style={tailwind('text-red-500')}>{errorMessage}</Text>} | ||||||
|  |       {profilePicture && <Image source={{ uri: profilePicture }} style={{ width: 200, height: 200 }} />}   | ||||||
|  |           <Text style={tailwind('text-lg')}>Username: {username}</Text> | ||||||
|  |           <Text style={tailwind('text-lg')}>Bio: {bio}</Text> | ||||||
|  |           <Text style={tailwind('text-lg')}>Pronouns: {pronouns}</Text> | ||||||
|  |           <Text style={tailwind('text-lg')}>Name: {name}</Text> | ||||||
|  |           <Button title="Edit Profile" onPress={() => navigation.navigate('EditProfile', { user: userId, currentUsername: username, currentBio: bio, currentPronouns: pronouns, currentName: name })} /> | ||||||
|  |        | ||||||
|  |    | ||||||
|  |       <Text style={tailwind('text-lg')}>Following: {following.length}</Text> | ||||||
|  |        | ||||||
|  |       <Text style={tailwind('text-lg')}>Followers: {followers.length}</Text> | ||||||
|  |        | ||||||
|  |       <Button title="Settings" onPress={() => navigation.navigate('Settings')} /> | ||||||
|  |  | ||||||
|  |       <ScrollView | ||||||
|  |         refreshControl={ | ||||||
|  |           <RefreshControl | ||||||
|  |             refreshing={refreshing} | ||||||
|  |             onRefresh={async () => { | ||||||
|  |               setRefreshing(true); | ||||||
|  |               await fetchSwits(); | ||||||
|  |               setRefreshing(false); | ||||||
|  |             }} | ||||||
|  |           /> | ||||||
|  |         } | ||||||
|  |       > | ||||||
|  |         {userSwits.map(swit => ( | ||||||
|  |           <Swit swit={swit} navigation={navigation} userId={userId} setSwits={setUserSwits} swits={userSwits}/> | ||||||
|  |         ))} | ||||||
|  |  | ||||||
|  |       </ScrollView> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default ProfileScreen; | ||||||
							
								
								
									
										71
									
								
								Search.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								Search.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | |||||||
|  | import React, { useState } from 'react'; | ||||||
|  | import { View, Text, TextInput, Button, TouchableOpacity } from 'react-native'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  | import moment from 'moment'; | ||||||
|  | import Icon from 'react-native-vector-icons/FontAwesome' | ||||||
|  | import { Swit } from './Home'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | function SearchScreen({ navigation }) { | ||||||
|  |     const tailwind = useTailwind(); | ||||||
|  |     const [userId, setUserId] = useState(''); | ||||||
|  |      | ||||||
|  |   // query is the search term entered by the user | ||||||
|  |   const [query, setQuery] = useState(''); | ||||||
|  |   // userResults and switResults are the search results | ||||||
|  |   const [userResults, setUserResults] = useState([]); | ||||||
|  |   const [switResults, setSwitResults] = useState([]); | ||||||
|  |  | ||||||
|  |   async function search() { | ||||||
|  |     // Get the user's token | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |     const id = await AsyncStorage.getItem('userId'); | ||||||
|  |     setUserId(id); | ||||||
|  |  | ||||||
|  |     // Fetch user search results | ||||||
|  |     const userResponse = await fetch(`${apiUrl}/api/v1/app/user/search/?q=${query}`, { | ||||||
|  |       headers: { 'Authorization': token }, | ||||||
|  |     }); | ||||||
|  |     const userData = await userResponse.json(); | ||||||
|  |     console.warn(userData) | ||||||
|  |     setUserResults(userData); | ||||||
|  |  | ||||||
|  |     // Fetch swit search results | ||||||
|  |     const switResponse = await fetch(`${apiUrl}/api/v1/app/swit/search/?q=${query}`, { | ||||||
|  |       headers: { 'Authorization': token }, | ||||||
|  |     }); | ||||||
|  |     const switData = await switResponse.json(); | ||||||
|  |     console.warn(switData) | ||||||
|  |     setSwitResults(switData); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View > | ||||||
|  |       <TextInput value={query} onChangeText={text => setQuery(text)} /> | ||||||
|  |       <Button title="Search" onPress={search} /> | ||||||
|  |  | ||||||
|  |       <Text>User Results</Text> | ||||||
|  |       {userResults.slice(0, 2).map(user => ( | ||||||
|  |         // Display user result, which includes profile picture, display name, username, and bio | ||||||
|  |         // This is a placeholder and needs to be replaced with actual components | ||||||
|  |         <Text key={user._id}>{user.username}</Text> | ||||||
|  |       ))} | ||||||
|  |       {userResults.length > 2 && <Button title="Show More" onPress={() => navigation.navigate('UserSearchResults', { query })} />} | ||||||
|  |  | ||||||
|  |       <Text>Swit Results</Text> | ||||||
|  |       {switResults.slice(0, 10).map(swit => ( | ||||||
|  |         // Display swit result, which includes the swit content | ||||||
|  |         // This is a placeholder and needs to be replaced with actual components | ||||||
|  |         <View style={tailwind('p-4')}> | ||||||
|  |           <Swit swit={swit} navigation={navigation} userId={userId} setSwits={setSwitResults} swits={switResults}/> | ||||||
|  |         </View> | ||||||
|  |       ))} | ||||||
|  |       {switResults.length > 10 && <Button title="Show More" onPress={() => navigation.navigate('SwitSearchResults', { query })} />} | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default SearchScreen; | ||||||
							
								
								
									
										45
									
								
								Settings.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								Settings.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { View, Text, Button, TextInput } from 'react-native'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  |  | ||||||
|  | function SettingsScreen({ navigation }) { | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |   const [apiEndpoint, setApiEndpoint] = useState(''); | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     const getAPI = async () => { | ||||||
|  |       const api = await AsyncStorage.getItem('apiEndpoint'); | ||||||
|  |       setApiEndpoint(api || ''); | ||||||
|  |       console.log(api) | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     getAPI(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   const handleLogout = async () => { | ||||||
|  |     await AsyncStorage.removeItem('token'); | ||||||
|  |     await AsyncStorage.removeItem('userID'); | ||||||
|  |     navigation.navigate('Login'); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleSave = async () => { | ||||||
|  |     await AsyncStorage.setItem('apiEndpoint', apiEndpoint); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |       <Text style={tailwind('text-lg')}>Settings</Text> | ||||||
|  |       <TextInput | ||||||
|  |         style={tailwind('border border-gray-500 mb-2')} | ||||||
|  |         value={apiEndpoint} | ||||||
|  |         onChangeText={text => setApiEndpoint(text)} | ||||||
|  |         placeholder="API endpoint" | ||||||
|  |       /> | ||||||
|  |       <Button title="Save" onPress={handleSave} /> | ||||||
|  |       <Button title="Logout" onPress={handleLogout} /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default SettingsScreen; | ||||||
							
								
								
									
										66
									
								
								Signup.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Signup.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | import React, { useState } from 'react'; | ||||||
|  | import { View, TextInput, Button } from 'react-native'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  |  | ||||||
|  | function SignupScreen({ navigation }) { | ||||||
|  |   const [username, setUsername] = useState(''); | ||||||
|  |   const [password, setPassword] = useState(''); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |  | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async function signupUser() { | ||||||
|  |   try { | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |     setErrorMessage(null); // Reset the error message before starting a new request | ||||||
|  |     const response = await fetch(`${apiUrl}/api/v1/app/auth/register`, { | ||||||
|  |       method: 'POST', | ||||||
|  |       headers: { | ||||||
|  |         'Content-Type': 'application/json', | ||||||
|  |       }, | ||||||
|  |       body: JSON.stringify({ username, password }), | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (!response.ok) { | ||||||
|  |       throw new Error(`Failed to signup: ${response.status} ${await response.text()}`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { token, userId } = await response.json(); | ||||||
|  |     await AsyncStorage.setItem('token', token); | ||||||
|  |     await AsyncStorage.setItem('userID', userId); | ||||||
|  |     navigation.reset({ | ||||||
|  |       index: 0, | ||||||
|  |       routes: [{ name: 'Home' }], | ||||||
|  |     }); | ||||||
|  |   } catch (error) { | ||||||
|  |     console.error('Error:', error); | ||||||
|  |     setErrorMessage(error.message); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |       {errorMessage ? <Text style={tailwind('text-red-500 mb-2 text-center')}>{errorMessage}</Text> : null} | ||||||
|  |       <TextInput | ||||||
|  |         style={tailwind('border border-gray-500 p-2 mb-4')} | ||||||
|  |         value={username} | ||||||
|  |         onChangeText={setUsername} | ||||||
|  |         placeholder="Username" | ||||||
|  |       /> | ||||||
|  |       <TextInput | ||||||
|  |         style={tailwind('border border-gray-500 p-2 mb-4')} | ||||||
|  |         value={password} | ||||||
|  |         onChangeText={setPassword} | ||||||
|  |         placeholder="Password" | ||||||
|  |         secureTextEntry | ||||||
|  |       /> | ||||||
|  |       <Button title="Sign up" onPress={signupUser} /> | ||||||
|  |       <Button title="Log in instead" onPress={() => navigation.navigate('Login')} /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default SignupScreen; | ||||||
							
								
								
									
										63
									
								
								Swit.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								Swit.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | import React, { useState } from 'react'; | ||||||
|  | import { View, TextInput, Button, ActivityIndicator, Text, TouchableOpacity } from 'react-native'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  |  | ||||||
|  | function SwitScreen({ navigation }) { | ||||||
|  |   const [text, setText] = useState(''); | ||||||
|  |   const [loading, setLoading] = useState(false); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(''); | ||||||
|  |  | ||||||
|  |   async function postTweet() { | ||||||
|  |     try { | ||||||
|  |       setLoading(true); | ||||||
|  |       setErrorMessage(null); // Reset the error message before starting a new request | ||||||
|  |  | ||||||
|  |       const token = await AsyncStorage.getItem('token'); | ||||||
|  |       const userID = await AsyncStorage.getItem('userID'); | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/swit/swits`, { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: { | ||||||
|  |           'Content-Type': 'application/json', | ||||||
|  |           'Authorization': token, | ||||||
|  |         }, | ||||||
|  |         body: JSON.stringify({ text }), | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error(`Failed to post swit: ${response.status} ${await response.text()}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       navigation.goBack(); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error:', error); | ||||||
|  |       setErrorMessage(error.message); | ||||||
|  |     } finally { | ||||||
|  |       setLoading(false); // Put the setLoading in finally block so it will run whether there's an error or not | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |         {errorMessage ? <Text style={tailwind('text-red-500 mb-2 text-center')}>{errorMessage}</Text> : null} | ||||||
|  |       <TextInput | ||||||
|  |         style={tailwind('border border-gray-500 p-2 mb-4')} | ||||||
|  |         multiline | ||||||
|  |         value={text} | ||||||
|  |         onChangeText={setText} | ||||||
|  |       /> | ||||||
|  |       {/* if isLoading = true show activityindicator if not show below */} | ||||||
|  |       {loading && <ActivityIndicator size="large" color="#0000ff" />} | ||||||
|  |  | ||||||
|  |       <Button title="Post" onPress={postTweet} /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default SwitScreen; | ||||||
							
								
								
									
										155
									
								
								SwitDetail.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								SwitDetail.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { View, Text, TextInput, Button, ScrollView } from 'react-native'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  | import axios from 'axios'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  |  | ||||||
|  | function SwitDetailScreen({ route, navigation, userId }) { | ||||||
|  |   const [swit, setSwit] = useState(null); | ||||||
|  |   const [newComment, setNewComment] = useState(''); | ||||||
|  |   const { switId } = route.params; | ||||||
|  |   const [username, setUsername] = useState(''); | ||||||
|  |   const [errorMessage, setErrorMessage] = useState(null); // Add this state for error message | ||||||
|  |   const [uId, setUId] = useState(''); // Add this state for error message | ||||||
|  |  | ||||||
|  |   const tailwind = useTailwind(); | ||||||
|  |  | ||||||
|  |   async function fetchUserData() { | ||||||
|  |     try { | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |       const userId = await AsyncStorage.getItem('userID'); | ||||||
|  |       setUId(userId); | ||||||
|  |       const token = await AsyncStorage.getItem('token');   | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/user/data/${userId}`, { | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |       }); | ||||||
|  |    | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error(`Failed to fetch user data: ${response.status} ${await response.text()}`); | ||||||
|  |       } | ||||||
|  |    | ||||||
|  |       const user = await response.json(); | ||||||
|  |       setUsername(user.username); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error:', error); | ||||||
|  |       setErrorMessage(error.message); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const fetchSwit = async () => { | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |     const response = await axios.get(`${apiUrl}/api/v1/app/swit/swits/${switId}`, { | ||||||
|  |       headers: { 'Authorization': token }, | ||||||
|  |     }); | ||||||
|  |     console.warn(response.data) | ||||||
|  |     setSwit(response.data); | ||||||
|  |     console.warn(swit.comments) | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const postComment = async () => { | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |     await axios.post(`${apiUrl}/api/v1/app/swit/swits/${switId}/comments`, { | ||||||
|  |       text: newComment, | ||||||
|  |       username: username | ||||||
|  |     }, { | ||||||
|  |       headers: { 'Authorization': token }, | ||||||
|  |     }); | ||||||
|  |     setNewComment(''); | ||||||
|  |     fetchSwit(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const likeComment = async (commentId) => { | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |     await axios.post(`${apiUrl}/api/v1/app/swit/swits/${switId}/comments/${commentId}/like`, {}, { | ||||||
|  |       headers: { 'Authorization': token }, | ||||||
|  |     }); | ||||||
|  |     fetchSwit(); | ||||||
|  |   }; | ||||||
|  |    | ||||||
|  |   const unlikeComment = async (commentId) => { | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |     await axios.post(`${apiUrl}/api/v1/app/swit/swits/${switId}/comments/${commentId}/unlike`, {}, { | ||||||
|  |       headers: { 'Authorization': token }, | ||||||
|  |     }); | ||||||
|  |     fetchSwit(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const deleteComment = async (commentId) => { | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |     await axios.delete(`${apiUrl}/api/v1/app/swit/swits/${switId}/comments/${commentId}`, { | ||||||
|  |       headers: { 'Authorization': token }, | ||||||
|  |     }); | ||||||
|  |     fetchSwit(); | ||||||
|  |   }; | ||||||
|  |    | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   async function deleteSwit() { | ||||||
|  |     try { | ||||||
|  |       const token = await AsyncStorage.getItem('token'); | ||||||
|  |       const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |       const response = await fetch(`${apiUrl}/api/v1/app/swit/swits/${switId}`, { | ||||||
|  |         method: 'DELETE', | ||||||
|  |         headers: { 'Authorization': token }, | ||||||
|  |       }); | ||||||
|  |    | ||||||
|  |       if (!response.ok) { | ||||||
|  |         throw new Error(`Failed to delete swit: ${response.status} ${await response.text()}`); | ||||||
|  |       } | ||||||
|  |    | ||||||
|  |       // Redirect to the home screen after deleting | ||||||
|  |       navigation.goBack(); | ||||||
|  |     } catch (error) { | ||||||
|  |       console.error('Error:', error); | ||||||
|  |       setErrorMessage(error.message); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     fetchSwit(); | ||||||
|  |     fetchUserData(); | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|  |   if (!swit) { | ||||||
|  |     return <Text>Loading...</Text>; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View> | ||||||
|  |       {errorMessage ? <Text style={tailwind('text-red-500 mb-2 text-center')}>{errorMessage}</Text> : null} | ||||||
|  |       <Text>{swit.text}</Text> | ||||||
|  |       <ScrollView> | ||||||
|  |   {swit.comments.map((comment, index) => ( | ||||||
|  |     <> | ||||||
|  |       <Text key={index}>{comment.username}: {comment.text}</Text> | ||||||
|  |       <Button title="Like" onPress={() => likeComment(comment._id)} /> | ||||||
|  |       <Button title="Unlike" onPress={() => unlikeComment(comment._id)} /> | ||||||
|  |       {uId === comment.user && <Button title="Delete" onPress={() => deleteComment(comment._id)} />} | ||||||
|  |  | ||||||
|  |     </> | ||||||
|  |   ))} | ||||||
|  | </ScrollView> | ||||||
|  |  | ||||||
|  |       <TextInput | ||||||
|  |         value={newComment} | ||||||
|  |         onChangeText={text => setNewComment(text)} | ||||||
|  |         placeholder="Write a comment..." | ||||||
|  |       /> | ||||||
|  |       <Button title="Post comment" onPress={postComment} /> | ||||||
|  |       {swit.user === userId && <Button title="Delete" onPress={deleteSwit} />} | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default SwitDetailScreen; | ||||||
							
								
								
									
										90
									
								
								UserProfile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								UserProfile.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | import React, { useState, useEffect } from 'react'; | ||||||
|  | import { View, Text, Button } from 'react-native'; | ||||||
|  | import { useTailwind } from 'tailwind-rn'; | ||||||
|  | import AsyncStorage from '@react-native-async-storage/async-storage'; | ||||||
|  |  | ||||||
|  | function UserProfileScreen({ route, navigation }) { | ||||||
|  |     const { userId } = route.params; // get the user ID from the parameters | ||||||
|  |     const [username, setUsername] = useState(''); | ||||||
|  |     const [following, setFollowing] = useState([]); | ||||||
|  |     const tailwind = useTailwind(); | ||||||
|  |    | ||||||
|  |     console.log(userId) | ||||||
|  |  | ||||||
|  |     async function fetchUserData() { | ||||||
|  |         const token = await AsyncStorage.getItem('token'); | ||||||
|  |         const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |         const response = await fetch(`${apiUrl}/api/v1/app/user/data/${userId}`, { | ||||||
|  |           headers: { 'Authorization': token }, | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         if (!response.ok) { | ||||||
|  |             console.error(`Error: ${response.status}`); | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |         const user = await response.json(); | ||||||
|  |         console.warn("user: ", user) | ||||||
|  |         setUsername(user.username); | ||||||
|  |         setFollowing(user.following.count); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       useEffect(() => { | ||||||
|  |         fetchUserData(); | ||||||
|  |       }, []); | ||||||
|  |  | ||||||
|  |       async function followUser() { | ||||||
|  |         const token = await AsyncStorage.getItem('token'); | ||||||
|  |         const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |         console.log(userId) | ||||||
|  |         const response = await fetch(`${apiUrl}/api/v1/app/user/follow/${userId}`, { | ||||||
|  |           method: 'POST', | ||||||
|  |           headers: { 'Authorization': token }, | ||||||
|  |         }); | ||||||
|  |         const data = await response.json(); | ||||||
|  |         setFollowing(data.count); | ||||||
|  |         console.log('followed') | ||||||
|  |         // Update the user state to reflect the new following status | ||||||
|  |       } | ||||||
|  |        | ||||||
|  |  | ||||||
|  |   async function unfollowUser() { | ||||||
|  |     const token = await AsyncStorage.getItem('token'); | ||||||
|  |     const apiUrl = await AsyncStorage.getItem('apiEndpoint') | ||||||
|  |  | ||||||
|  |     const response = await fetch(`$[apiUrl}/api/v1/app/user/unfollow/${userId}`, { | ||||||
|  |       method: 'POST', | ||||||
|  |       headers: { 'Authorization': token }, | ||||||
|  |     }); | ||||||
|  |     if (response.ok) { | ||||||
|  |         const data = await response.json(); | ||||||
|  |         console.warn(data) | ||||||
|  |         setFollowing(data.count) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |      | ||||||
|  |     // Update the user state to reflect the new following status | ||||||
|  |      | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!username) return null; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <View style={tailwind('p-4')}> | ||||||
|  |       <Text style={tailwind('text-lg')}>@{username}</Text> | ||||||
|  |       {/* Display the user's swits here */} | ||||||
|  |       <Button title="Follow" onPress={followUser} /> | ||||||
|  |       <Button title="Unfollow" onPress={unfollowUser} /> | ||||||
|  |       <Text>Following: {following}</Text> | ||||||
|  |  | ||||||
|  |       <Button | ||||||
|  |         title="Message" | ||||||
|  |         onPress={() => navigation.navigate('Conversation', { userId })} | ||||||
|  |       /> | ||||||
|  |     </View> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default UserProfileScreen; | ||||||
							
								
								
									
										38
									
								
								app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								app.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | { | ||||||
|  |   "expo": { | ||||||
|  |     "name": "switter", | ||||||
|  |     "slug": "switter", | ||||||
|  |     "version": "1.0.1", | ||||||
|  |     "orientation": "portrait", | ||||||
|  |     "icon": "./assets/switterappicon.png", | ||||||
|  |     "userInterfaceStyle": "light", | ||||||
|  |     "splash": { | ||||||
|  |       "image": "./assets/splash.png", | ||||||
|  |       "resizeMode": "contain", | ||||||
|  |       "backgroundColor": "#ffffff" | ||||||
|  |     }, | ||||||
|  |     "assetBundlePatterns": [ | ||||||
|  |       "**/*" | ||||||
|  |     ], | ||||||
|  |     "ios": { | ||||||
|  |       "supportsTablet": true, | ||||||
|  |       "bundleIdentifier": "win.swifter.switter" | ||||||
|  |     }, | ||||||
|  |     "android": { | ||||||
|  |       "adaptiveIcon": { | ||||||
|  |         "foregroundImage": "./assets/swittertrans.png", | ||||||
|  |         "backgroundColor": "#ffffff" | ||||||
|  |       }, | ||||||
|  |       "package": "win.swifter.switter" | ||||||
|  |     }, | ||||||
|  |     "web": { | ||||||
|  |       "favicon": "./assets/swittertrans.png" | ||||||
|  |     }, | ||||||
|  |     "extra": { | ||||||
|  |       "eas": { | ||||||
|  |         "projectId": "30cf001f-264e-4127-bba3-f59c5290a254" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "owner": "switterapp" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								assets/adaptive-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/adaptive-icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 17 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/favicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/favicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/splash.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/splash.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 46 KiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/switterappicon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/switterappicon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 2.3 MiB | 
							
								
								
									
										
											BIN
										
									
								
								assets/swittertrans.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/swittertrans.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 414 KiB | 
							
								
								
									
										6
									
								
								babel.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								babel.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | module.exports = function(api) { | ||||||
|  |   api.cache(true); | ||||||
|  |   return { | ||||||
|  |     presets: ['babel-preset-expo'], | ||||||
|  |   }; | ||||||
|  | }; | ||||||
							
								
								
									
										17
									
								
								components/Create.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								components/Create.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import { HeaderButton } from 'react-navigation-header-buttons'; | ||||||
|  | import { Ionicons } from '@expo/vector-icons'; | ||||||
|  | import { Platform } from 'react-native'; | ||||||
|  |  | ||||||
|  | const CreateSwitButton = (props) => { | ||||||
|  |   return ( | ||||||
|  |     <HeaderButton | ||||||
|  |       {...props} | ||||||
|  |       IconComponent={Ionicons} | ||||||
|  |       iconSize={23} | ||||||
|  |       color={Platform.OS === 'android' ? 'white' : 'blue'} | ||||||
|  |     /> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default CreateSwitButton; | ||||||
							
								
								
									
										31
									
								
								eas.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								eas.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | { | ||||||
|  |   "cli": { | ||||||
|  |     "version": ">= 3.15.0" | ||||||
|  |   }, | ||||||
|  |   "build": { | ||||||
|  |     "development": { | ||||||
|  |       "developmentClient": true, | ||||||
|  |       "distribution": "internal" | ||||||
|  |     }, | ||||||
|  |     "preview": { | ||||||
|  |       "android": { | ||||||
|  |         "buildType": "apk" | ||||||
|  |       }, | ||||||
|  |       "ios": { | ||||||
|  |         "simulator": true | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "preview2": { | ||||||
|  |       "android": { | ||||||
|  |         "gradleCommand": ":app:assembleRelease" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   "preview3": { | ||||||
|  |     "developmentClient": true | ||||||
|  |   }, | ||||||
|  |     "production": {} | ||||||
|  |   }, | ||||||
|  |   "submit": { | ||||||
|  |     "production": {} | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import { registerRootComponent } from 'expo'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | import App from './App'; | ||||||
|  |  | ||||||
|  | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); | ||||||
|  | // It also ensures that whether you load the app in Expo Go or in a native build, | ||||||
|  | // the environment is set up appropriately | ||||||
|  | registerRootComponent(App); | ||||||
							
								
								
									
										3
									
								
								input.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								input.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | @tailwind utilities; | ||||||
|  | @tailwind base; | ||||||
|  | @tailwind components; | ||||||
							
								
								
									
										4
									
								
								metro.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								metro.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | // Learn more https://docs.expo.io/guides/customizing-metro | ||||||
|  | const { getDefaultConfig } = require('expo/metro-config'); | ||||||
|  |  | ||||||
|  | module.exports = getDefaultConfig(__dirname); | ||||||
							
								
								
									
										27280
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										27280
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										51
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | { | ||||||
|  |   "name": "frontend", | ||||||
|  |   "version": "1.0.0", | ||||||
|  |   "scripts": { | ||||||
|  |     "start": "expo start --dev-client", | ||||||
|  |     "android": "expo run:android", | ||||||
|  |     "ios": "expo run:ios", | ||||||
|  |     "web": "expo start --web", | ||||||
|  |     "build:tailwind": "tailwindcss --input input.css --output tailwind.css --no-autoprefixer && tailwind-rn", | ||||||
|  |     "dev:tailwind": "concurrently \"tailwindcss --input input.css --output tailwind.css --no-autoprefixer --watch\" \"tailwind-rn --watch\"" | ||||||
|  |   }, | ||||||
|  |   "dependencies": { | ||||||
|  |     "@expo/vector-icons": "^13.0.0", | ||||||
|  |     "@react-native-async-storage/async-storage": "1.17.11", | ||||||
|  |     "@react-navigation/bottom-tabs": "^6.5.8", | ||||||
|  |     "@react-navigation/material-top-tabs": "^6.6.3", | ||||||
|  |     "@react-navigation/native": "^6.1.7", | ||||||
|  |     "@react-navigation/native-stack": "^6.9.13", | ||||||
|  |     "@react-navigation/stack": "^6.3.17", | ||||||
|  |     "axios": "^1.4.0", | ||||||
|  |     "color-convert": "^2.0.1", | ||||||
|  |     "expo": "~48.0.18", | ||||||
|  |     "expo-image-picker": "~14.1.1", | ||||||
|  |     "expo-splash-screen": "~0.18.2", | ||||||
|  |     "expo-status-bar": "~1.4.4", | ||||||
|  |     "moment": "^2.29.4", | ||||||
|  |     "nativewind": "^2.0.11", | ||||||
|  |     "react": "18.2.0", | ||||||
|  |     "react-native": "0.71.8", | ||||||
|  |     "react-native-gesture-handler": "~2.9.0", | ||||||
|  |     "react-native-pager-view": "6.1.2", | ||||||
|  |     "react-native-paper": "^5.9.1", | ||||||
|  |     "react-native-reanimated": "~2.14.4", | ||||||
|  |     "react-native-safe-area-context": "^4.5.0", | ||||||
|  |     "react-native-screens": "~3.20.0", | ||||||
|  |     "react-native-tab-view": "^3.5.2", | ||||||
|  |     "react-native-toast-message": "^2.1.6", | ||||||
|  |     "react-native-vector-icons": "^9.2.0", | ||||||
|  |     "react-navigation-header-buttons": "^11.0.0", | ||||||
|  |     "socket.io-client": "^4.7.2", | ||||||
|  |     "tailwind-rn": "^4.2.0", | ||||||
|  |     "twrnc": "^3.6.1" | ||||||
|  |   }, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@babel/core": "^7.20.0", | ||||||
|  |     "concurrently": "^8.2.0", | ||||||
|  |     "postcss": "^8.4.25", | ||||||
|  |     "tailwindcss": "^3.3.2" | ||||||
|  |   }, | ||||||
|  |   "private": true | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								services/socket.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								services/socket.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import io from 'socket.io-client'; | ||||||
|  | import {Toast} from 'react-native-toast-message' | ||||||
|  |  | ||||||
|  | let socket; | ||||||
|  |  | ||||||
|  | export const connectSocket = (userId) => { | ||||||
|  |   socket = io('http://localhost:1989', { | ||||||
|  |     query: { userId }, | ||||||
|  |     transports: ['websocket'] | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   socket.on('notification', (notification) => { | ||||||
|  |     Toast.show({ | ||||||
|  |         type: 'info', | ||||||
|  |         text1: notification.message, | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const disconnectSocket = () => { | ||||||
|  |   if (socket) socket.disconnect(); | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								tailwind.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | module.exports = { | ||||||
|  |   content: ["./*.js"], | ||||||
|  |   theme: { | ||||||
|  |     extend: {}, | ||||||
|  |   }, | ||||||
|  |   plugins: [], | ||||||
|  |   corePlugins: require('tailwind-rn/unsupported-core-plugins'), | ||||||
|  | } | ||||||
							
								
								
									
										155
									
								
								tailwind.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								tailwind.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | |||||||
|  | .absolute { | ||||||
|  |   position: absolute | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mb-2 { | ||||||
|  |   margin-bottom: 0.5rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mb-3 { | ||||||
|  |   margin-bottom: 0.75rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mb-4 { | ||||||
|  |   margin-bottom: 1rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .ml-2 { | ||||||
|  |   margin-left: 0.5rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mr-3 { | ||||||
|  |   margin-right: 0.75rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .block { | ||||||
|  |   display: block | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flex { | ||||||
|  |   display: flex | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .h-12 { | ||||||
|  |   height: 3rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .w-12 { | ||||||
|  |   width: 3rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flex-1 { | ||||||
|  |   flex: 1 1 0% | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flex-auto { | ||||||
|  |   flex: 1 1 auto | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flex-grow { | ||||||
|  |   flex-grow: 1 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .flex-row { | ||||||
|  |   flex-direction: row | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .items-center { | ||||||
|  |   align-items: center | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .rounded-full { | ||||||
|  |   border-radius: 9999px | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .border { | ||||||
|  |   border-width: 1px | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .border-b { | ||||||
|  |   border-bottom-width: 1px | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .border-gray-200 { | ||||||
|  |   --tw-border-opacity: 1; | ||||||
|  |   border-color: rgb(229 231 235 / var(--tw-border-opacity)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .border-gray-500 { | ||||||
|  |   --tw-border-opacity: 1; | ||||||
|  |   border-color: rgb(107 114 128 / var(--tw-border-opacity)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .p-2 { | ||||||
|  |   padding: 0.5rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .p-4 { | ||||||
|  |   padding: 1rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .px-2 { | ||||||
|  |   padding-left: 0.5rem; | ||||||
|  |   padding-right: 0.5rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pb-12 { | ||||||
|  |   padding-bottom: 3rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .pr-2 { | ||||||
|  |   padding-right: 0.5rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .text-center { | ||||||
|  |   text-align: center | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .text-lg { | ||||||
|  |   font-size: 1.125rem; | ||||||
|  |   line-height: 1.75rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .text-sm { | ||||||
|  |   font-size: 0.875rem; | ||||||
|  |   line-height: 1.25rem | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .font-bold { | ||||||
|  |   font-weight: 700 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .text-gray-400 { | ||||||
|  |   --tw-text-opacity: 1; | ||||||
|  |   color: rgb(156 163 175 / var(--tw-text-opacity)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .text-gray-500 { | ||||||
|  |   --tw-text-opacity: 1; | ||||||
|  |   color: rgb(107 114 128 / var(--tw-text-opacity)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .text-red-500 { | ||||||
|  |   --tw-text-opacity: 1; | ||||||
|  |   color: rgb(239 68 68 / var(--tw-text-opacity)) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | *, ::before, ::after { | ||||||
|  |   --tw-border-spacing-x: 0; | ||||||
|  |   --tw-border-spacing-y: 0; | ||||||
|  |   --tw-ordinal:  ; | ||||||
|  |   --tw-slashed-zero:  ; | ||||||
|  |   --tw-numeric-figure:  ; | ||||||
|  |   --tw-numeric-spacing:  ; | ||||||
|  |   --tw-numeric-fraction:   | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ::backdrop { | ||||||
|  |   --tw-border-spacing-x: 0; | ||||||
|  |   --tw-border-spacing-y: 0; | ||||||
|  |   --tw-ordinal:  ; | ||||||
|  |   --tw-slashed-zero:  ; | ||||||
|  |   --tw-numeric-figure:  ; | ||||||
|  |   --tw-numeric-spacing:  ; | ||||||
|  |   --tw-numeric-fraction:   | ||||||
|  | } | ||||||
							
								
								
									
										236
									
								
								tailwind.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								tailwind.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | |||||||
|  | { | ||||||
|  | 	"absolute": { | ||||||
|  | 		"style": { | ||||||
|  | 			"position": "absolute" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"mb-2": { | ||||||
|  | 		"style": { | ||||||
|  | 			"marginBottom": 8 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"mb-3": { | ||||||
|  | 		"style": { | ||||||
|  | 			"marginBottom": 12 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"mb-4": { | ||||||
|  | 		"style": { | ||||||
|  | 			"marginBottom": 16 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"ml-2": { | ||||||
|  | 		"style": { | ||||||
|  | 			"marginLeft": 8 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"mr-3": { | ||||||
|  | 		"style": { | ||||||
|  | 			"marginRight": 12 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"block": { | ||||||
|  | 		"style": { | ||||||
|  | 			"display": "block" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"flex": { | ||||||
|  | 		"style": { | ||||||
|  | 			"display": "flex" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"h-12": { | ||||||
|  | 		"style": { | ||||||
|  | 			"height": 48 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"w-12": { | ||||||
|  | 		"style": { | ||||||
|  | 			"width": 48 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"flex-1": { | ||||||
|  | 		"style": { | ||||||
|  | 			"flexGrow": 1, | ||||||
|  | 			"flexShrink": 1, | ||||||
|  | 			"flexBasis": "0%" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"flex-auto": { | ||||||
|  | 		"style": { | ||||||
|  | 			"flexGrow": 1, | ||||||
|  | 			"flexShrink": 1, | ||||||
|  | 			"flexBasis": "auto" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"flex-grow": { | ||||||
|  | 		"style": { | ||||||
|  | 			"flexGrow": 1 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"flex-row": { | ||||||
|  | 		"style": { | ||||||
|  | 			"flexDirection": "row" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"items-center": { | ||||||
|  | 		"style": { | ||||||
|  | 			"alignItems": "center" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"rounded-full": { | ||||||
|  | 		"style": { | ||||||
|  | 			"borderTopLeftRadius": 9999, | ||||||
|  | 			"borderTopRightRadius": 9999, | ||||||
|  | 			"borderBottomRightRadius": 9999, | ||||||
|  | 			"borderBottomLeftRadius": 9999 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"border": { | ||||||
|  | 		"style": { | ||||||
|  | 			"borderTopWidth": 1, | ||||||
|  | 			"borderRightWidth": 1, | ||||||
|  | 			"borderBottomWidth": 1, | ||||||
|  | 			"borderLeftWidth": 1 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"border-b": { | ||||||
|  | 		"style": { | ||||||
|  | 			"borderBottomWidth": 1 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"border-gray-200": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-border-opacity": 1, | ||||||
|  | 			"borderTopColor": "rgb(229 231 235 / var(--tw-border-opacity))", | ||||||
|  | 			"borderRightColor": "rgb(229 231 235 / var(--tw-border-opacity))", | ||||||
|  | 			"borderBottomColor": "rgb(229 231 235 / var(--tw-border-opacity))", | ||||||
|  | 			"borderLeftColor": "rgb(229 231 235 / var(--tw-border-opacity))" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"border-gray-500": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-border-opacity": 1, | ||||||
|  | 			"borderTopColor": "rgb(107 114 128 / var(--tw-border-opacity))", | ||||||
|  | 			"borderRightColor": "rgb(107 114 128 / var(--tw-border-opacity))", | ||||||
|  | 			"borderBottomColor": "rgb(107 114 128 / var(--tw-border-opacity))", | ||||||
|  | 			"borderLeftColor": "rgb(107 114 128 / var(--tw-border-opacity))" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"p-2": { | ||||||
|  | 		"style": { | ||||||
|  | 			"paddingTop": 8, | ||||||
|  | 			"paddingRight": 8, | ||||||
|  | 			"paddingBottom": 8, | ||||||
|  | 			"paddingLeft": 8 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"p-4": { | ||||||
|  | 		"style": { | ||||||
|  | 			"paddingTop": 16, | ||||||
|  | 			"paddingRight": 16, | ||||||
|  | 			"paddingBottom": 16, | ||||||
|  | 			"paddingLeft": 16 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"px-2": { | ||||||
|  | 		"style": { | ||||||
|  | 			"paddingLeft": 8, | ||||||
|  | 			"paddingRight": 8 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"pb-12": { | ||||||
|  | 		"style": { | ||||||
|  | 			"paddingBottom": 48 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"pr-2": { | ||||||
|  | 		"style": { | ||||||
|  | 			"paddingRight": 8 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"text-center": { | ||||||
|  | 		"style": { | ||||||
|  | 			"textAlign": "center" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"text-lg": { | ||||||
|  | 		"style": { | ||||||
|  | 			"fontSize": 18, | ||||||
|  | 			"lineHeight": 28 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"text-sm": { | ||||||
|  | 		"style": { | ||||||
|  | 			"fontSize": 14, | ||||||
|  | 			"lineHeight": 20 | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"font-bold": { | ||||||
|  | 		"style": { | ||||||
|  | 			"fontWeight": "700" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"text-gray-400": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-text-opacity": 1, | ||||||
|  | 			"color": "rgb(156 163 175 / var(--tw-text-opacity))" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"text-gray-500": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-text-opacity": 1, | ||||||
|  | 			"color": "rgb(107 114 128 / var(--tw-text-opacity))" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"text-red-500": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-text-opacity": 1, | ||||||
|  | 			"color": "rgb(239 68 68 / var(--tw-text-opacity))" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"*": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-border-spacing-x": 0, | ||||||
|  | 			"--tw-border-spacing-y": 0, | ||||||
|  | 			"--tw-ordinal": "", | ||||||
|  | 			"--tw-slashed-zero": "", | ||||||
|  | 			"--tw-numeric-figure": "", | ||||||
|  | 			"--tw-numeric-spacing": "", | ||||||
|  | 			"--tw-numeric-fraction": "" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"::before": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-border-spacing-x": 0, | ||||||
|  | 			"--tw-border-spacing-y": 0, | ||||||
|  | 			"--tw-ordinal": "", | ||||||
|  | 			"--tw-slashed-zero": "", | ||||||
|  | 			"--tw-numeric-figure": "", | ||||||
|  | 			"--tw-numeric-spacing": "", | ||||||
|  | 			"--tw-numeric-fraction": "" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"::after": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-border-spacing-x": 0, | ||||||
|  | 			"--tw-border-spacing-y": 0, | ||||||
|  | 			"--tw-ordinal": "", | ||||||
|  | 			"--tw-slashed-zero": "", | ||||||
|  | 			"--tw-numeric-figure": "", | ||||||
|  | 			"--tw-numeric-spacing": "", | ||||||
|  | 			"--tw-numeric-fraction": "" | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	"::backdrop": { | ||||||
|  | 		"style": { | ||||||
|  | 			"--tw-border-spacing-x": 0, | ||||||
|  | 			"--tw-border-spacing-y": 0, | ||||||
|  | 			"--tw-ordinal": "", | ||||||
|  | 			"--tw-slashed-zero": "", | ||||||
|  | 			"--tw-numeric-figure": "", | ||||||
|  | 			"--tw-numeric-spacing": "", | ||||||
|  | 			"--tw-numeric-fraction": "" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user