inital
This commit is contained in:
commit
91767b9d67
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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user