Compare commits

...

151 commits

Author SHA1 Message Date
0b3af716a5 limit studying to within 250m of the center of ustp 2023-12-10 22:50:53 +08:00
05cee78d31 Format with Prettier and turn off debug flag 2023-11-24 21:15:47 +08:00
cbd82a05f9 Added callout information for study groups when pressing on maps and improved responsiveness of the homepage map renderer 2023-11-24 21:14:33 +08:00
d2aecbd89c Turn off debug flags 2023-11-24 00:26:02 +08:00
51b7b24430 Show landmarks if it exists in homepage and in conversation page 2023-11-24 00:22:14 +08:00
c0a8a8efc8 Always refresh location when pressing refresh button 2023-11-23 23:29:27 +08:00
7cd549cad7 Refetch user location when refreshing and added code snippet to stop user from studying if they stray too far from where they set their studying locatio. Also allow modal to be shown even when not currently studying 2023-11-23 23:27:38 +08:00
Keannu Bernasol
0cad7458be
Merge pull request #16 from lemeow125/feature/qol-fixes
Feature/qol fixes
2023-10-28 09:42:34 +08:00
2603741aab Turn off debug flags and clear study group messages notification cache when switching study groups 2023-10-28 00:22:17 +08:00
5d7327ef26 Do not redirect to conversations page if leaving a group 2023-10-27 23:13:41 +08:00
856621fe06 Remove a redundant refresh on load as it prevented users from overriding their location 2023-10-27 22:16:23 +08:00
8a32d2b32c Fixed left and right caret icons 2023-10-27 22:06:52 +08:00
a65a3a84aa Added enter button to conversation page and fixed error not properly displaying when sending an invalid message. Also added refresh interval of 20 seconds to study group query in conversations page to automatically refresh students count 2023-10-27 22:03:46 +08:00
88d8ce05b8 Possible fix to race conditions between queries in homepage which resulted in laggy rerenders of map 2023-10-27 21:43:34 +08:00
bd42b5418e Allow register and create group page to be scrollable when onscreen keyboard is open 2023-10-27 21:16:43 +08:00
3891f12f5d Redirect to conversations page instead of homepage when joining or creating a study group 2023-10-27 20:52:49 +08:00
6564b52dc0 Bump expo version 2023-10-22 00:57:01 +08:00
946e455b83 Add spacing between entries in list view 2023-10-19 20:04:37 +08:00
e354335590 Move refresh button to the bottom alongside studying and group create buttons 2023-10-19 20:03:41 +08:00
a0d27aaa38 Convert onboarding dropdown menus into modals 2023-10-19 18:53:54 +08:00
f098db0dca Added create group button below start studying and increased refresh interval to 30 seconds 2023-10-19 18:46:54 +08:00
e501bc2c91 Hotfix on incorrect condition on login button 2023-10-15 12:57:44 +08:00
a11c9dff65 Homepage loading improvements 2023-10-15 12:35:53 +08:00
ecf62a1008 Homepage optimizations 2023-10-15 12:27:49 +08:00
b82b9d332f Prevent registers/logins multiple times when spamming buttons 2023-10-15 11:56:39 +08:00
be21689639 Added confirm password field 2023-10-15 11:54:48 +08:00
8867306bd0 Made button visually responsive on pressing 2023-10-15 11:44:31 +08:00
de33fe30fd Remvoe locationFetched variable as it seemed to cause performance issues with rerenders 2023-10-14 11:12:16 +08:00
22820e139e Move conditional statements in homepage 2023-10-14 10:51:28 +08:00
a11ff2ee6f Reverted query invalidation on mount in homepage as it was causing performance issues 2023-10-14 10:35:28 +08:00
4caf86c6be Fixed map elements not rendering on first open when on poor network conditions 2023-10-13 14:28:22 +08:00
8e5e0546df Align own user messages to the right and other user messages to the left 2023-10-13 14:06:04 +08:00
0c9f53b84d Added refresh button to homepage and made it possible to join and leave groups from list view 2023-10-13 14:01:11 +08:00
02eabd2b41 Fixed loading screen in start studying page 2023-10-13 13:56:05 +08:00
7b175c44df Added stop-gaps to help with refreshing queries on slower connections and increased refresh interval for homepage from 15 seconds to 10 seconds 2023-10-13 12:45:55 +08:00
e54fe893a0 Fixed UserInfo page dropdown menu clipping 2023-10-13 12:21:40 +08:00
963eaef628 Rounded edges for MapViews 2023-10-11 20:19:16 +08:00
4a406957b5 Added conversation icon 2023-10-11 20:05:01 +08:00
ec693a7bb6 Fixed loading indicator container size 2023-10-11 20:01:22 +08:00
f9c3a5c5d4 Added possible fix to allow permissions prompt being shown briefly in homepage 2023-10-11 19:50:02 +08:00
369a00a0b3 Added loading pages inidcators to subject and user info page and redirect login page to homepage if user is already logged in 2023-10-11 19:48:19 +08:00
6e63f86805 Added back button to create group and start studying pages and adding loading indicator for start studying 2023-10-11 19:43:59 +08:00
7ac6a6745f Added loading screen to homepage and proper rendering if location permission is denied 2023-10-11 19:33:02 +08:00
64cb7aabdd Fix google map overlay still rendering under open street map in homepage 2023-10-11 18:37:53 +08:00
64058cf2c8 Fixed missing url tile on homepage 2023-10-10 23:43:42 +08:00
3208c37a86 Fixed distance calculation rounding down to 0 and reduced distance threshhold to 1km 2023-10-10 22:54:29 +08:00
f53056f932 Bump react native version to latest 2023-10-10 22:15:54 +08:00
b3db2bb8e2 Turn off debug flags and fix student status deactivation on homepage 2023-10-01 21:30:04 +08:00
lemeow125
7c3eeda29e
Merge pull request #10 from lemeow125/feature/messaging
Feature/messaging
2023-10-01 16:54:20 +08:00
fa07743a90 Fixed group message notifications always triggering 2023-10-01 16:39:43 +08:00
2461f2c404 Fixed pullup menu wording on homepage and added message notifications 2023-10-01 16:29:36 +08:00
798c1a5e6b Added potential fix to homepage map elements sometimes not rendering on startup 2023-10-01 15:57:45 +08:00
b0345dc2b7 Code cleanup 2023-10-01 15:56:01 +08:00
a9d7188c67 Re-separate use effect for periodic checking and on load checking in homepage 2023-10-01 14:32:25 +08:00
d072ae456d Added background notifications 2023-10-01 14:28:03 +08:00
2cd770e5e1 Improved functionlity of messages page 2023-10-01 00:54:31 +08:00
63f863fa1e Revert fix to homepage and clean up code for messages page 2023-09-30 18:40:46 +08:00
97291a85cd Fixed homepage sometimes not rendering when not studying 2023-09-30 18:31:26 +08:00
bbffea76a3 Fix messages page button visibility 2023-09-30 17:48:48 +08:00
lemeow125
183b2b6e16
Merge pull request #9 from lemeow125/initial-frontend
added conversation/groupchat page
2023-09-30 17:40:25 +08:00
lemeow125
fab7491a8d
Merge branch 'master' into initial-frontend 2023-09-30 17:40:16 +08:00
cd9b8b91e3 Hotfix for rendering student statuses when student is not studying and show distance of study groups and students in map view 2023-09-30 17:27:29 +08:00
lemeow125
8f58dddbda
Merge pull request #8 from lemeow125/feature/student_status
Feature/student status
2023-09-30 17:14:55 +08:00
4a909a9236 Made pull up menu only operable if studying 2023-09-30 17:14:59 +08:00
4e1a73f6ed Fix some styling issues and improve pull up modal for groups/students list 2023-09-29 20:58:59 +08:00
8279665ab9 Code cleanup and add pull up menu for complete student status/study group list 2023-09-29 17:47:22 +08:00
fb8e948dfc Set student status to inactive if logging out and clear query cache 2023-09-29 16:32:31 +08:00
4de274edb4 Joining now ensures that you match the subject you are studying to the study group you are joining 2023-09-29 12:29:36 +08:00
c05c8d50f3 Improve wording for joining study groups 2023-09-29 12:26:42 +08:00
c2c589a3fe Code improvements, clear react query cache on logout and added joining/changing study group functionality on homepage 2023-09-29 12:23:44 +08:00
709125a344 Fixed bug in active student status if user leaves a study group 2023-09-29 11:33:25 +08:00
debaa544bc Added create group functionality 2023-09-28 21:03:04 +08:00
62cee96b94 Added study group creation 2023-09-26 20:29:21 +08:00
lemeow125
3c879b5cf2
Merge pull request #7 from lemeow125/feature/homepage_rendering
Feature/homepage rendering
2023-09-24 21:29:05 +08:00
ed19150d53 Added global study groups rendering 2023-09-24 21:26:15 +08:00
1bd07f9edd Optimized homepage rendering and removed overly complicated components 2023-09-24 21:02:34 +08:00
19d19c3dd5 Reflect user pin location change if user manually overrides location 2023-09-22 23:08:54 +08:00
7da7d0f217 Hidden unused console.log in api 2023-09-22 23:08:20 +08:00
c4413a185d Hidden unused console.log 2023-09-22 23:08:06 +08:00
81bead43ff Fixed filtering for study groups 2023-09-22 22:55:36 +08:00
68778cea7a Code cleanup for multiple pages and components 2023-09-20 21:16:54 +08:00
14e14b8bb6 Code cleanup 2023-09-20 19:53:25 +08:00
790574daee Separated distance calculation and far map renderer into own components 2023-09-20 19:36:06 +08:00
12e3d29822 Do not autocapitalize password fields 2023-09-20 17:44:58 +08:00
511f293ff1 Changed debug backend url 2023-09-20 17:42:37 +08:00
980d1e636e Added student status point rendering 2023-09-11 19:16:48 +08:00
00928ac947 Fixed and made changes to the Haversine distance formula calculation. We now point it to the user's location as intended 2023-09-11 19:02:11 +08:00
7b9d05f84b Added Haversine Formula calculation to get the radius of circles for study groups required for rendering 2023-09-09 20:45:29 +08:00
85e2a13071 Changed to circle rendering for student status 2023-09-08 21:41:46 +08:00
058b120ce9 Addded initial heatmap rendering 2023-09-06 18:13:43 +08:00
AngelV3rgs
f0c46f2fbe added conversation/groupchat page 2023-09-03 22:31:41 +08:00
15b14a32e8 Change map provider api to selfhosted one 2023-08-29 16:39:50 +08:00
040ffb622c Moved callouts to a separate component 2023-08-15 14:53:58 +08:00
146d80cc98 Added student status list query 2023-08-15 14:15:33 +08:00
cfd82d3c42 Refactored error handling in API functions and improved error feedback in pages 2023-08-15 00:41:42 +08:00
c4c11d1afe Updated login page 2023-08-14 23:31:00 +08:00
497e50f2a4 Refactored types for better readability 2023-08-14 23:29:53 +08:00
ce2bffe1cb Fixed duplicate modal in user info page 2023-08-14 22:01:20 +08:00
2ca1dd13ca Finalize changes to using modals for user feedback 2023-08-14 21:57:52 +08:00
ff114b496c Move to modals for user feedback 2023-08-14 21:13:46 +08:00
529a7a75fd Improved homepage 2023-08-10 17:23:12 +08:00
33ffcde6be Added map preview to start studying page 2023-08-07 15:03:53 +08:00
c95e3e2d79 Added start studying page 2023-08-07 14:55:44 +08:00
126223394d Remove bottomsheet library and added homepage improvements 2023-08-07 14:22:47 +08:00
029ef84671 Improved API, added loading page, initial changes to homepage, and fixed irregular field on user info page not being centered 2023-08-06 14:25:09 +08:00
16f3cda10d Allow user location to be manually adjusted 2023-08-05 14:43:29 +08:00
93aab046d8 Removed some console logs 2023-07-27 16:02:14 +08:00
e4d64f3656 Added avatar uploading 2023-07-27 16:00:31 +08:00
a3b3bd887f Add user feedback to user info and subjects page 2023-07-27 12:55:51 +08:00
1a46945d1e Code cleanup in api.tsx 2023-07-27 00:09:43 +08:00
e4278517bc Code cleanup in api.tsx 2023-07-27 00:06:32 +08:00
3331ccb974 Pray to the gods the duplicate subjects bug is fixed. Move irregular status toggle to user info page from subjects page 2023-07-27 00:01:44 +08:00
283c030b37 Fixed homepage distance bug 2023-07-26 12:52:24 +08:00
cd85852c9b Fixed condition in homepage 2023-07-26 10:04:15 +08:00
b43577870b Homepage improvements 2023-07-26 10:03:25 +08:00
22707b8372 Homepage and subjects page improvements 2023-07-25 23:26:30 +08:00
d87f61ec08 Some homepage improvements 2023-07-25 17:02:35 +08:00
eb08e02a16 Added subject selection for irregular students 2023-07-25 16:52:35 +08:00
473291646c Added code comments 2023-07-25 14:17:28 +08:00
40c52f1419 Homepage improvements 2023-07-24 16:53:20 +08:00
17b41cde65 Switch to open street view to hopefully fix production crashes with gps 2023-07-20 18:31:51 +08:00
60f2250cb8 Possible fix to gps crash 2023-07-19 22:21:19 +08:00
98bd8dcfb8 Potential fix to expo-location crash 2023-07-19 17:57:25 +08:00
334978d2fd Unified styling for buttons 2023-07-19 17:47:12 +08:00
lemeow125
8fb8d6b8b2
Merge pull request #6 from lemeow125/feature/UserInfo
Feature/user info
2023-07-19 17:21:03 +08:00
de97dca9d3 Improved homepage 2023-07-19 17:20:29 +08:00
258435cbdd Added needed build confs 2023-07-19 02:27:34 +08:00
72163d2c26 Swapped adaptive and regular icon 2023-07-19 01:27:29 +08:00
3b128e5d19 Increased max height for subjects dropdown menu to accomodate number of subjects 2023-07-18 21:42:55 +08:00
1ac68bee30 Finish up subjects page and user info page 2023-07-18 21:38:49 +08:00
ff973ccb0b Fixed subjectspage and userinfo page being swapped 2023-07-18 21:00:44 +08:00
d6df6d4f7a Prepare to separate subjects into own page 2023-07-18 20:51:43 +08:00
1990be3972 Improved subject selection in userinfo 2023-07-18 17:20:11 +08:00
ffde700a36 Made subjects menu functional in user info 2023-07-18 16:06:58 +08:00
1aed66150c Improvements to user info page. Made year level course and semester menus functional 2023-07-18 14:46:09 +08:00
160ffc5763 Overhauled dropdown menu designs for onboarding and userinfo page 2023-07-18 00:33:02 +08:00
0247afe553 Fixed buttons 2023-07-17 22:54:06 +08:00
e67485d247 Fixed onboarding page 2023-07-17 22:44:50 +08:00
8de8e67070 Allow editing of user info 2023-07-17 18:45:25 +08:00
f24b84139b Added app icons 2023-07-17 18:09:04 +08:00
33ecc0fdee Made status bar light 2023-07-17 17:52:41 +08:00
00c6974cdc Updated app icon 2023-07-17 17:50:16 +08:00
1403657412 Overhauled colors 2023-07-17 17:10:32 +08:00
dfd74cd06a Styling improvements 2023-07-17 16:34:09 +08:00
e6a92bfff2 Improved color scheme naming 2023-07-17 16:26:16 +08:00
a96fe668a8 Added medium_large text size 2023-07-17 16:00:06 +08:00
f503f060f2 Reworked the onboarding apis to follow standard format 2023-07-17 15:57:23 +08:00
df58613d4b Improved user info page 2023-07-17 15:48:51 +08:00
0a5617ff56 Improved user info page 2023-07-17 15:48:19 +08:00
a4d8309820 Improved avatar photo in user info 2023-07-17 15:36:18 +08:00
f9931a1fdd Cleaned up code for user info page 2023-07-17 15:10:44 +08:00
48 changed files with 5014 additions and 2364 deletions

60
App.tsx
View file

@ -1,4 +1,5 @@
import "react-native-gesture-handler";
import styles from "./src/styles";
import { NavigationContainer } from "@react-navigation/native";
import { createDrawerNavigator } from "@react-navigation/drawer";
import { Provider } from "react-redux";
@ -16,9 +17,18 @@ import Register from "./src/routes/Register/Register";
import Onboarding from "./src/routes/Onboarding/Onboarding";
import Revalidation from "./src/routes/Revalidation/Revalidation";
import Activation from "./src/routes/Activation/Activation";
import UserInfo from "./src/routes/UserInfo/UserInfo";
import { useState, useEffect } from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { StatusBar } from "expo-status-bar";
import UserInfoPage from "./src/routes/UserInfoPage/UserInfoPage";
import SubjectsPage from "./src/routes/SubjectsPage/SubjectsPage";
import ConversationPage from "./src/routes/ConversationPage/ConversationPage";
import Loading from "./src/routes/Loading/Loading";
import StartStudying from "./src/routes/StartStudying/StartStudying";
import { ToastProvider } from "react-native-toast-notifications";
import InfoIcon from "./src/icons/InfoIcon/InfoIcon";
import CreateGroup from "./src/routes/CreateGroup/CreateGroup";
import BackgroundComponent from "./src/components/BackgroundTask/BackgroundTask";
const Drawer = createDrawerNavigator();
@ -26,7 +36,7 @@ const linking = {
prefixes: [Linking.makeUrl("/")],
config: {
screens: {
Home: "home",
Home: "",
Login: "login",
Register: "register",
Onboarding: "onboarding",
@ -53,24 +63,36 @@ export default function App() {
}
}, [initialRoute]);
return (
<Provider store={store}>
<ToastProvider
icon={<InfoIcon size={32} />}
textStyle={{ ...styles.text_white_tiny_bold }}
>
<BackgroundComponent />
<QueryClientProvider client={queryClient}>
<NavigationContainer linking={linking}>
<Drawer.Navigator
initialRouteName="Revalidation"
drawerContent={CustomDrawerContent}
screenOptions={DrawerScreenSettings}
>
<Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="Login" component={Login} />
<Drawer.Screen name="Register" component={Register} />
<Drawer.Screen name="Onboarding" component={Onboarding} />
<Drawer.Screen name="Revalidation" component={Revalidation} />
<Drawer.Screen name="Activation" component={Activation} />
<Drawer.Screen name="UserInfo" component={UserInfo} />
</Drawer.Navigator>
</NavigationContainer>
<Provider store={store}>
<StatusBar style="light" />
<NavigationContainer linking={linking} fallback={<Loading />}>
<Drawer.Navigator
initialRouteName="Revalidation"
drawerContent={CustomDrawerContent}
screenOptions={DrawerScreenSettings}
>
<Drawer.Screen name="Login" component={Login} />
<Drawer.Screen name="Register" component={Register} />
<Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="Onboarding" component={Onboarding} />
<Drawer.Screen name="Revalidation" component={Revalidation} />
<Drawer.Screen name="Activation" component={Activation} />
<Drawer.Screen name="User Info" component={UserInfoPage} />
<Drawer.Screen name="Subjects" component={SubjectsPage} />
<Drawer.Screen name="Start Studying" component={StartStudying} />
<Drawer.Screen name="Create Group" component={CreateGroup} />
<Drawer.Screen name="Conversation" component={ConversationPage} />
</Drawer.Navigator>
</NavigationContainer>
</Provider>
</QueryClientProvider>
</Provider>
</ToastProvider>
);
}

View file

@ -1,6 +1,6 @@
{
"expo": {
"name": "StudE_Frontend",
"name": "StudE",
"scheme": "stude",
"slug": "StudE_Frontend",
"version": "1.0.0",
@ -10,7 +10,7 @@
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
"backgroundColor": "#1C2C3F"
},
"assetBundlePatterns": [
"**/*"
@ -21,7 +21,18 @@
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
"backgroundColor": "#FFFF"
},
"package": "com.teamblackpink.stude",
"permissions": [
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.FOREGROUND_SERVICE"
],
"config":{
"googleMaps":{
"apiKey": "AIzaSyCw2Fhwe7nGUbGQATnjDfEYfoZFN6lXGeA"
}
}
},
"web": {
@ -31,9 +42,20 @@
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow Stud-E to use your location."
"locationAlwaysAndWhenInUsePermission": "Allow StudE to use your location."
}
]
],
[
"expo-image-picker",
{
"photosPermission": "Allow StudE to take and send photos for sharing in-app"
}
]
],
"extra": {
"eas": {
"projectId": "614fd93f-345c-4d72-a9e7-592f1ba0c6e8"
}
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 177 KiB

19
eas.json Normal file
View file

@ -0,0 +1,19 @@
{
"build": {
"preview": {
"android": {
"buildType": "apk"
}
},
"preview2": {
"android": {
"gradleCommand": ":app:assembleRelease"
}
},
"preview3": {
"developmentClient": true
},
"production": {}
}
}

2125
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,13 +17,17 @@
"@tanstack/react-query": "^4.29.25",
"axios": "^1.4.0",
"expo": "~48.0.18",
"expo-file-system": "~15.2.2",
"expo-image-picker": "~14.1.1",
"expo-intent-launcher": "~10.5.2",
"expo-linking": "~4.0.1",
"expo-location": "~15.1.1",
"expo-status-bar": "~1.4.4",
"moment": "^2.29.4",
"moti": "^0.25.3",
"react": "18.2.0",
"react-native": "0.71.8",
"react-native": "0.71.14",
"react-native-bouncy-checkbox": "^3.0.7",
"react-native-dropdown-picker": "^5.4.6",
"react-native-gesture-handler": "~2.9.0",
"react-native-image-picker": "^5.6.0",
@ -34,13 +38,18 @@
"react-native-screens": "~3.20.0",
"react-native-select-dropdown": "^3.3.4",
"react-native-svg": "13.4.0",
"react-native-toast-notifications": "^3.3.1",
"react-query": "^3.39.3",
"react-redux": "^8.1.1",
"redux": "^4.2.1"
"redux": "^4.2.1",
"expo-task-manager": "~11.1.1",
"expo-background-fetch": "~11.1.1",
"expo-notifications": "~0.18.1"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~18.0.14",
"@types/react-native-fetch-blob": "^0.10.7",
"typescript": "^4.9.4"
},
"private": true

View file

@ -13,21 +13,23 @@ export default function AnimatedContainer(props: props) {
contentContainerStyle={styles.container}
from={{
borderRadius: 0,
backgroundColor: colors.orange_2,
opacity: 0,
backgroundColor: colors.secondary_2,
paddingTop: 4,
paddingBottom: 4,
marginHorizontal: "4%",
marginVertical: "5%",
marginVertical: "10%",
}}
animate={{
borderRadius: 15,
backgroundColor: colors.blue_2,
opacity: 1,
backgroundColor: colors.secondary_2,
paddingTop: 16,
paddingBottom: 16,
marginHorizontal: "4%",
marginVertical: "5%",
}}
transition={{ type: "timing", duration: 300 }}
transition={{ type: "timing", duration: 700 }}
>
{props.children}
</MotiScrollView>

View file

@ -0,0 +1,37 @@
import * as React from "react";
import { View, Text, ScrollView } from "react-native";
import styles from "../../styles";
import { colors } from "../../styles";
import { MotiView, MotiScrollView } from "moti";
export interface props {
children: React.ReactNode;
}
export default function AnimatedContainerNoScroll(props: props) {
return (
<MotiView
style={styles.container}
from={{
borderRadius: 0,
opacity: 0,
backgroundColor: colors.secondary_2,
paddingTop: 4,
paddingBottom: 4,
marginHorizontal: "4%",
marginVertical: "10%",
}}
animate={{
borderRadius: 15,
opacity: 1,
backgroundColor: colors.secondary_2,
paddingTop: 16,
paddingBottom: 16,
marginHorizontal: "4%",
marginVertical: "5%",
}}
transition={{ type: "timing", duration: 700 }}
>
{props.children}
</MotiView>
);
}

View file

@ -1,18 +1,31 @@
import axios from "axios";
import AsyncStorage from "@react-native-async-storage/async-storage";
import {
ActivationParams,
LoginParams,
OnboardingParams,
RegistrationParams,
ActivationType,
LocationType,
LoginType,
MessagePostType,
OnboardingType,
PatchUserInfoType,
RegistrationType,
StudentStatusPatchType,
StudentStatusType,
StudyGroupCreateType,
StudyGroupType,
} from "../../interfaces/Interfaces";
let debug = true;
export let backendURL = "";
if (debug) {
backendURL = "http://10.0.10.8:8000";
} else {
backendURL = "https://keannu125.pythonanywhere.com";
export let backendURL = "https://stude.keannu1.duckdns.org";
export let backendURLWebsocket = "ws://stude.keannu1.duckdns.org";
if (__DEV__) {
backendURL = "http://10.0.10.8:8083";
backendURLWebsocket = "ws://10.0.10.8:8083";
}
// Switch this on if you wanna run production URLs while in development
let use_production = true;
if (__DEV__ && use_production) {
backendURL = "https://stude.keannu1.duckdns.org";
backendURLWebsocket = "ws://stude.keannu1.duckdns.org";
}
const instance = axios.create({
@ -20,8 +33,29 @@ const instance = axios.create({
timeout: 1000,
});
console.log("Using backend API:", backendURL);
// 3rd Party APIs
export const urlProvider =
"https://openstreetmap.keannu1.duckdns.org/tile/{z}/{x}/{y}.png?";
// App APIs
// Error Handling
export function ParseError(error: any) {
if (error.response && error.response.data) {
return JSON.stringify(error.response.data)
.replaceAll(/[{}()"]/g, " ")
.replaceAll(/,/g, "\n")
.replaceAll("[", "")
.replaceAll("]", "")
.replaceAll(".", "")
.replaceAll(/"/g, "")
.replaceAll("non_field_errors", "")
.trim();
}
return "Unable to reach server";
}
// Token Handling
export async function getAccessToken() {
const accessToken = await AsyncStorage.getItem("access_token");
@ -43,40 +77,39 @@ export async function setRefreshToken(refresh: string) {
return true;
}
// Header Config Template for REST
export async function GetConfig() {
const accessToken = await getAccessToken();
return {
headers: {
Authorization: `Bearer ${accessToken}`,
},
};
}
// User APIs
export function UserRegister(register: RegistrationParams) {
console.log(JSON.stringify(register));
export function UserRegister(register: RegistrationType) {
return instance
.post("/api/v1/accounts/users/", register)
.then(async (response) => {
return [true, response.status];
})
.catch((error) => {
let error_message = "";
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
let error_message = ParseError(error);
return [false, error_message];
});
}
export function UserLogin(user: LoginParams) {
export function UserLogin(user: LoginType) {
return instance
.post("/api/v1/accounts/jwt/create/", user)
.then(async (response) => {
/*console.log(
"Access Token:",
response.data.access,
"\nRefresh Token:",
response.data.refresh
);*/
setAccessToken(response.data.access);
setRefreshToken(response.data.refresh);
return [true];
})
.catch((error) => {
let error_message = "";
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
let error_message = ParseError(error);
return [false, error_message];
});
}
@ -90,47 +123,47 @@ export async function TokenRefresh() {
})
.then(async (response) => {
setAccessToken(response.data.access);
/*console.log(
"Token refresh success! New Access Token",
response.data.access
);*/
return true;
})
.catch((error) => {
let error_message = "";
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
console.log("Token Refresh error:", error_message);
let error_message = ParseError(error);
return false;
});
}
export async function UserInfo() {
const accessToken = await getAccessToken();
export async function GetUserInfo() {
const config = await GetConfig();
return instance
.get("/api/v1/accounts/users/me/", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.get("/api/v1/accounts/users/me/", config)
.then((response) => {
// console.log(JSON.stringify(response.data));
return [true, response.data];
})
.catch((error) => {
let error_message = "";
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
let error_message = ParseError(error);
return [false, error_message];
});
}
export function UserActivate(activation: ActivationParams) {
export async function PatchUserInfo(info: PatchUserInfoType) {
const config = await GetConfig();
return instance
.post("/api/v1/accounts/users/activation/", activation)
.then(async (response) => {
return true;
.patch("/api/v1/accounts/users/me/", info, config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export function UserActivate(activation: ActivationType) {
return instance
.post("/api/v1/accounts/users/activation/", activation)
.then(() => {
return true;
})
.catch(() => {
return false;
});
}
@ -147,14 +180,11 @@ export async function GetCourses() {
})
.then((response) => {
// console.log(JSON.stringify(response.data));
return response.data;
return [true, response.data];
})
.catch((error) => {
let error_message = "";
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
console.log("Error getting courses", error_message);
return false;
let error_message = ParseError(error);
return [false, error_message];
});
}
@ -168,54 +198,204 @@ export async function GetSemesters() {
})
.then((response) => {
// console.log(JSON.stringify(response.data));
return response.data;
return [true, response.data];
})
.catch((error) => {
let error_message = "";
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
console.log("Error getting semesters", error_message);
return false;
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetYearLevels() {
const accessToken = await getAccessToken();
const config = await GetConfig();
return instance
.get("/api/v1/year_levels/", {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.get("/api/v1/year_levels/", config)
.then((response) => {
// console.log(JSON.stringify(response.data));
return response.data;
})
.catch((error) => {
let error_message = "";
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
console.log("Error getting year levels", error_message);
return false;
});
}
export async function OnboardingUpdateStudentInfo(info: OnboardingParams) {
const accessToken = await getAccessToken();
const headers = {
Authorization: `Bearer ${accessToken}`,
};
return instance
.patch("/api/v1/accounts/users/me/", info, { headers })
.then((response) => {
console.log(JSON.stringify(response.data));
return [true, response.data];
})
.catch((error) => {
let error_message = "";
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
console.log("Error updating onboarding info", error_message);
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetSubjects() {
const config = await GetConfig();
return instance
.get("/api/v1/subjects/", config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetStudentStatus() {
const config = await GetConfig();
return instance
.get("/api/v1/student_status/self/", config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function PatchStudentStatus(info: StudentStatusPatchType) {
const config = await GetConfig();
return instance
.patch("/api/v1/student_status/self/", info, config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetStudentStatusList() {
const config = await GetConfig();
return instance
.get("/api/v1/student_status/list/", config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetStudentStatusListNear() {
const config = await GetConfig();
return instance
.get("/api/v1/student_status/near/", config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
// To-do
export async function GetStudentStatusListFilteredCurrentLocation(
location: LocationType
) {
const config = await GetConfig();
return instance
.post(
"/api/v1/student_status/near_current_location/",
{
location: location,
},
config
)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetStudyGroupListFiltered() {
const config = await GetConfig();
return instance
.get("/api/v1/study_groups/near/", config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetStudyGroupList() {
const config = await GetConfig();
return instance
.get("/api/v1/study_groups/", config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function CreateStudyGroup(info: StudyGroupCreateType) {
const config = await GetConfig();
// console.log("Creating study group:", info);
return instance
.post("/api/v1/study_groups/create/", info, config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetStudyGroup(name: string) {
const config = await GetConfig();
return instance
.get(`/api/v1/study_groups/${name}`, config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetStudyGroupMessages() {
const config = await GetConfig();
return instance
.get(`/api/v1/messages/`, config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function GetStudyGroupMemberAvatars() {
const config = await GetConfig();
return instance
.get(`/api/v1/study_groups/member_avatars`, config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
let error_message = ParseError(error);
return [false, error_message];
});
}
export async function PostMessage(info: MessagePostType) {
const config = await GetConfig();
return instance
.post(`/api/v1/messages/`, info, config)
.then((response) => {
return [true, response.data];
})
.catch((error) => {
console.log("Error:", error.response.data);
let error_message = ParseError(error);
return [false, error_message];
});
}

View file

@ -0,0 +1,148 @@
import React, { useEffect } from "react";
import { View } from "react-native";
import * as BackgroundFetch from "expo-background-fetch";
import * as TaskManager from "expo-task-manager";
import * as Notifications from "expo-notifications";
import {
GetStudentStatus,
GetStudyGroupListFiltered,
GetStudyGroupMessages,
} from "../Api/Api";
import { StudyGroupType } from "../../interfaces/Interfaces";
import AsyncStorage from "@react-native-async-storage/async-storage";
const FETCH_STUDENT_STATUS = "STUDENT_STATUS_TASK";
const FETCH_GROUP_MESSAGES = "GROUP_MESSAGES_TASK";
TaskManager.defineTask(FETCH_GROUP_MESSAGES, async () => {
const data = await GetStudyGroupMessages();
if (data[0] && data[1]) {
let messages_prev = await JSON.parse(
(await AsyncStorage.getItem("messages")) || "[]"
);
await AsyncStorage.setItem("messages", JSON.stringify(data[1]));
let message_curr = data[1];
let difference: Array<any> = messages_prev
.filter(
(x: any) =>
!message_curr.some(
(y: any) => JSON.stringify(y) === JSON.stringify(x)
)
)
.concat(
message_curr.filter(
(x: any) =>
!messages_prev.some(
(y: any) => JSON.stringify(y) === JSON.stringify(x)
)
)
);
if (difference.length > 0) {
console.log(`${difference.length} unread messages`);
Notifications.scheduleNotificationAsync({
content: {
title: `${difference.length} unread messages`,
body: `${difference[0].user}: ${difference[0].message_content}`,
},
trigger: {
seconds: 1,
},
});
}
} else {
console.log(data[1].response.data);
}
return BackgroundFetch.BackgroundFetchResult.NewData;
});
TaskManager.defineTask(FETCH_STUDENT_STATUS, async () => {
const data = await GetStudyGroupListFiltered();
const student_status_data = await GetStudentStatus();
if (data[0] && data[1]) {
console.log("Fetching nearby study groups...");
const entryWithLeastDistance = data[1].reduce(
(prev: StudyGroupType, curr: StudyGroupType) => {
return prev.distance < curr.distance ? prev : curr;
}
);
// Only display a notification if a student isn't in a study group yet
if (
student_status_data[1].study_group == null ||
student_status_data[1].study_group == ""
) {
console.log(
"User has no study group yet. Found nearby groups, pushing notification"
);
Notifications.scheduleNotificationAsync({
content: {
title: "Students are studying nearby",
body: `Nearest study group is ${Math.round(
entryWithLeastDistance.distance * 1000
)}m away`,
},
trigger: {
seconds: 1,
},
});
}
} else {
console.log(data[1].response.data);
}
return BackgroundFetch.BackgroundFetchResult.NewData;
});
const BackgroundComponent = () => {
const notification_debug = false;
const [Task1_isRegistered, Task1_setIsRegistered] = React.useState(false);
const [Task2_isRegistered, Task2_setIsRegistered] = React.useState(false);
const [status, setStatus] = React.useState<any>();
const checkStatusAsync = async () => {
let status = await BackgroundFetch.getStatusAsync();
setStatus(status);
let Task1_isRegistered = await TaskManager.isTaskRegisteredAsync(
FETCH_STUDENT_STATUS
);
let Task2_isRegistered = await TaskManager.isTaskRegisteredAsync(
FETCH_GROUP_MESSAGES
);
Task1_setIsRegistered(Task1_isRegistered);
Task2_setIsRegistered(Task2_isRegistered);
};
useEffect(() => {
const registerTasks = async () => {
try {
await checkStatusAsync();
// Nearby students task
if (!Task1_isRegistered) {
await BackgroundFetch.registerTaskAsync(FETCH_STUDENT_STATUS, {
minimumInterval: notification_debug ? 5 : 60 * 3, // Check every 5 seconds in dev & every 3 minutes in production builds
});
console.log("Task for nearby students check registered");
} else {
console.log("Task for nearby students check already registered");
}
// Message Checking Task
if (!Task2_isRegistered) {
await BackgroundFetch.registerTaskAsync(FETCH_GROUP_MESSAGES, {
minimumInterval: notification_debug ? 5 : 30, // Check every 5 seconds in dev & every 30 seconds in production builds
});
console.log("Task for group messages check registered");
} else {
console.log("Task for group messages check already registered");
}
} catch (err) {
console.log("Task Register failed:", err);
}
};
registerTasks();
}, []);
return <View />;
};
export default BackgroundComponent;

View file

@ -1,30 +1,26 @@
import * as React from "react";
import { Pressable, GestureResponderEvent } from "react-native";
import styles from "../../styles";
import { colors } from "../../styles";
export interface props {
children: React.ReactNode;
onPress: (event: GestureResponderEvent) => void;
color: string;
color?: string;
disabled?: boolean;
}
export default function Button({ disabled = false, ...props }: props) {
const rgb = props.color.match(/\d+/g);
if (!props.color) {
props.color = colors.secondary_3;
}
return (
<Pressable
disabled={disabled}
onPress={props.onPress}
style={{
...styles.button_template,
...{
backgroundColor: disabled
? rgb
? `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.3)`
: "rgba(0, 0, 0, 0)"
: props.color,
},
}}
style={({ pressed }) => [
styles.button_template,
{ backgroundColor: pressed ? colors.primary_2 : props.color },
]}
>
{props.children}
</Pressable>

View file

@ -1,21 +1,16 @@
import * as React from "react";
import { Text, Pressable, GestureResponderEvent } from "react-native";
import { Pressable } from "react-native";
import styles from "../../styles";
import { colors } from "../../styles";
export interface props {
children: React.ReactNode;
onPress: (event: GestureResponderEvent) => void;
color: string;
}
export default function DrawerButton(props: props) {
export default function DrawerButton({ color = colors.secondary_3, ...props }) {
return (
<Pressable
onPress={props.onPress}
style={{
...styles.button_template,
...{
backgroundColor: props.color,
backgroundColor: color,
width: "95%",
justifyContent: "flex-start",
},

View file

@ -5,7 +5,10 @@ import { Text, View } from "react-native";
import { colors } from "../../styles";
import styles from "../../styles";
import { RootDrawerParamList } from "../../interfaces/Interfaces";
import {
RootDrawerParamList,
StudentStatusPatchType,
} from "../../interfaces/Interfaces";
import AppIcon from "../../icons/AppIcon/AppIcon";
import HomeIcon from "../../icons/HomeIcon/HomeIcon";
import LoginIcon from "../../icons/LoginIcon/LoginIcon";
@ -17,11 +20,49 @@ import LogoutIcon from "../../icons/LogoutIcon/LogoutIcon";
import { logout } from "../../features/redux/slices/StatusSlice/StatusSlice";
import AsyncStorage from "@react-native-async-storage/async-storage";
import UserIcon from "../../icons/UserIcon/UserIcon";
import SubjectIcon from "../../icons/SubjectIcon/SubjectIcon";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { PatchStudentStatus } from "../Api/Api";
import { useToast } from "react-native-toast-notifications";
import MessageIcon from "../../icons/MessageIcon/MessageIcon";
export default function CustomDrawerContent(props: {}) {
const debug = false;
const navigation = useNavigation<RootDrawerParamList>();
const status = useSelector((state: RootState) => state.status);
const dispatch = useDispatch();
const queryClient = useQueryClient();
const toast = useToast();
const stop_studying_logout = useMutation({
mutationFn: async (info: StudentStatusPatchType) => {
const data = await PatchStudentStatus(info);
if (data[0] != true) {
return Promise.reject(new Error());
}
console.log("DEBUG", data);
return data;
},
onSuccess: async () => {
toast.show("Logged out. Stopped studying", {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
queryClient.clear();
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
if (status.logged_in && status.onboarding) {
return (
<DrawerContentScrollView {...props}>
@ -31,16 +72,23 @@ export default function CustomDrawerContent(props: {}) {
...{ justifyContent: "center" },
}}
>
<AppIcon size={32} />
<AppIcon size={96} />
<Text style={styles.text_white_medium}>Stud-E</Text>
</View>
<DrawerButton
color={colors.blue_2}
onPress={async () => {
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
// We don't clear student statuses when logging out on debug
if (!debug) {
queryClient.clear();
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
} else {
stop_studying_logout.mutate({
active: false,
});
}
}}
>
<LogoutIcon size={32} />
@ -57,11 +105,10 @@ export default function CustomDrawerContent(props: {}) {
...{ justifyContent: "center" },
}}
>
<AppIcon size={32} />
<AppIcon size={96} />
<Text style={styles.text_white_medium}>Stud-E</Text>
</View>
<DrawerButton
color={colors.blue_3}
onPress={() => {
navigation.navigate("Home");
}}
@ -70,20 +117,42 @@ export default function CustomDrawerContent(props: {}) {
<Text style={styles.text_white_medium}>Home</Text>
</DrawerButton>
<DrawerButton
color={colors.blue_2}
onPress={() => {
navigation.navigate("UserInfo");
navigation.navigate("User Info");
}}
>
<UserIcon size={32} />
<Text style={styles.text_white_medium}>UserInfo</Text>
<Text style={styles.text_white_medium}>User Info</Text>
</DrawerButton>
<DrawerButton
onPress={() => {
navigation.navigate("Subjects");
}}
>
<SubjectIcon size={32} />
<Text style={styles.text_white_medium}>Subjects</Text>
</DrawerButton>
<DrawerButton
onPress={() => {
navigation.navigate("Conversation");
}}
>
<MessageIcon size={32} />
<Text style={styles.text_white_medium}>Conversation</Text>
</DrawerButton>
<DrawerButton
color={colors.blue_2}
onPress={async () => {
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
// We don't clear student statuses when logging out on debug
if (debug) {
queryClient.clear();
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
} else {
stop_studying_logout.mutate({
active: false,
});
}
}}
>
<LogoutIcon size={32} />
@ -100,11 +169,10 @@ export default function CustomDrawerContent(props: {}) {
...{ justifyContent: "center" },
}}
>
<AppIcon size={32} />
<AppIcon size={96} />
<Text style={styles.text_white_medium}>Stud-E</Text>
</View>
<DrawerButton
color={colors.blue_2}
onPress={() => {
navigation.navigate("Login");
}}
@ -113,7 +181,6 @@ export default function CustomDrawerContent(props: {}) {
<Text style={styles.text_white_medium}>Login</Text>
</DrawerButton>
<DrawerButton
color={colors.blue_2}
onPress={() => {
navigation.navigate("Register");
}}
@ -121,11 +188,10 @@ export default function CustomDrawerContent(props: {}) {
<SignupIcon size={32} />
<Text style={styles.text_white_medium}>Register</Text>
</DrawerButton>
{/*
Debug buttons for accessing revalidation and activation page
<DrawerButton
color={colors.blue_2}
color={colors.secondary_2}
onPress={() => {
navigation.navigate("Revalidation");
}}
@ -133,7 +199,7 @@ export default function CustomDrawerContent(props: {}) {
<Text style={styles.text_white_medium}>Revalidation</Text>
</DrawerButton>
<DrawerButton
color={colors.blue_2}
color={colors.secondary_2}
onPress={() => {
navigation.navigate("Activation");
}}

View file

@ -10,21 +10,19 @@ const DrawerScreenSettings: DrawerNavigationOptions = {
fontSize: font_sizes.medium,
},
unmountOnBlur: true,
headerStyle: { backgroundColor: colors.login_color},
headerStyle: { backgroundColor: colors.primary_1 },
headerTintColor: colors.text_default,
drawerType: "slide",
drawerLabelStyle: {
color: colors.text_default,
},
drawerStyle: {
backgroundColor: colors.login_color,
backgroundColor: colors.primary_1,
width: 260,
},
headerRight: () => (
<View
style={{ flexDirection: "row", marginRight: 16, alignItems: "center" }}
>
<AppIcon size={32} />
<View style={{ flexDirection: "row", alignItems: "center" }}>
<AppIcon size={96} />
</View>
),
};

View file

@ -0,0 +1,20 @@
import { LocationType } from "../../interfaces/Interfaces";
import GetDistance from "./GetDistance";
export default function GetDistanceFromUSTP(location: LocationType) {
const ustpCoords = {
latitude: 8.4857,
longitude: 124.6565,
latitudeDelta: 0.000235,
longitudeDelta: 0.000067,
};
let dist = GetDistance(
location.latitude,
location.longitude,
ustpCoords.latitude,
ustpCoords.longitude
);
dist = Math.round(dist * 100) / 100;
return dist;
}

View file

@ -0,0 +1,17 @@
import * as React from "react";
import styles from "../../styles";
import { View, Text, ActivityIndicator } from "react-native";
import { colors } from "../../styles";
import AnimatedContainer from "../AnimatedContainer/AnimatedContainer";
export default function LoadingFeedback() {
return (
<View style={styles.background}>
<AnimatedContainer>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={128} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</AnimatedContainer>
</View>
);
}

View file

@ -0,0 +1,79 @@
import * as React from "react";
import { View, Text } from "react-native";
import MapView, { UrlTile, Callout, Marker } from "react-native-maps";
import styles, { Viewport, colors } from "../../styles";
import { urlProvider } from "../Api/Api";
import { LocationType, RawLocationType } from "../../interfaces/Interfaces";
import GetDistance from "../../components/GetDistance/GetDistance";
type props = {
location: LocationType;
dist: any;
};
export default function MapRendererFar(props: props) {
return (
<>
<Text style={styles.text_white_medium}>
You are too far from USTP {"\n"}
Get closer to use Stud-E
</Text>
<MapView
style={{
height: Viewport.height * 0.5,
width: Viewport.width * 0.8,
alignSelf: "center",
}}
customMapStyle={[
{
featureType: "poi",
stylers: [
{
visibility: "off",
},
],
},
]}
mapType="none"
scrollEnabled={false}
zoomEnabled={false}
toolbarEnabled={false}
rotateEnabled={false}
minZoomLevel={18}
initialRegion={{
latitude: props.location.latitude,
longitude: props.location.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
loadingBackgroundColor={colors.secondary_2}
>
<UrlTile
urlTemplate={urlProvider}
shouldReplaceMapContent={true}
maximumZ={19}
flipY={false}
zIndex={1}
/>
<Marker
coordinate={{
latitude: props.location.latitude,
longitude: props.location.longitude,
}}
pinColor={colors.primary_1}
>
<Callout>
<Text style={styles.text_black_tiny}>
You are here {"\n"}
X: {Math.round(props.location.longitude) + "\n"}
Z: {Math.round(props.location.latitude)}
</Text>
</Callout>
</Marker>
</MapView>
<Text style={styles.text_white_small}>
{props.dist}km away from USTP {"\n"}
</Text>
</>
);
}

View file

@ -1,24 +0,0 @@
export default function ParseError(text: string) {
if (text) {
return text
.replaceAll(/[{}()"]/g, " ")
.replaceAll(/,/g, "\n")
.replaceAll("[", "")
.replaceAll("]", "")
.replaceAll(".", "");
}
return "";
}
export function ParseLoginError(text: string) {
if (text) {
return text
.replaceAll(/[{}()"]/g, " ")
.replaceAll(/,/g, "\n")
.replaceAll("[", "")
.replaceAll("]", "")
.replaceAll(".", "")
.replaceAll("non_field_errors", "");
}
return "";
}

View file

@ -1,20 +1,13 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
import { Image } from "react-native";
export default function AppIcon(props: IconProps) {
return (
<>
<Svg
fill={colors.icon_color}
height={props.size + "px"}
width={props.size + "px"}
viewBox="0 0 375.775 375.775"
>
<Path d="M187.886,375.775c-16.216,0-29.409-13.193-29.409-29.411s13.193-29.412,29.409-29.412c16.218,0,29.412,13.194,29.412,29.412 S204.104,375.775,187.886,375.775z M187.886,332.952c-7.394,0-13.409,6.017-13.409,13.412c0,7.395,6.015,13.411,13.409,13.411 c7.395,0,13.412-6.017,13.412-13.411C201.298,338.969,195.281,332.952,187.886,332.952z M244.851,342.665 c-3.207,0-6.233-1.941-7.461-5.11c-1.597-4.12,0.449-8.754,4.568-10.351c40.127-15.554,71.73-47.612,86.706-87.957 c1.537-4.142,6.142-6.253,10.284-4.716s6.253,6.142,4.716,10.284c-16.568,44.633-51.531,80.101-95.924,97.307 C246.79,342.49,245.812,342.665,244.851,342.665z M130.671,342.665c-0.961,0-1.939-0.174-2.889-0.543 c-44.393-17.206-79.356-52.674-95.925-97.307c-1.538-4.143,0.574-8.746,4.716-10.284c4.144-1.538,8.746,0.573,10.284,4.716 c14.977,40.345,46.58,72.403,86.707,87.957c4.12,1.597,6.165,6.23,4.568,10.351C136.904,340.724,133.877,342.665,130.671,342.665z M196.589,305.682h-17.403c-25.12,0-36.188-20.112-42.33-35.241l-2.277-5.612c-1.661-4.095,0.312-8.76,4.406-10.421 c4.095-1.662,8.759,0.312,10.42,4.405l2.276,5.612c3.44,8.475,7.034,14.579,11.16,18.661l18.583-25.466 c1.506-2.063,3.907-3.284,6.462-3.284c2.555,0,4.956,1.22,6.462,3.284l18.585,25.465c4.126-4.082,7.719-10.187,11.159-18.662 l2.277-5.611c1.661-4.095,6.327-6.064,10.421-4.404c4.094,1.662,6.066,6.327,4.404,10.422l-2.277,5.611 C232.778,285.568,221.71,305.682,196.589,305.682z M177.857,289.652c0.438,0.02,0.88,0.029,1.329,0.029h17.402 c0.45,0,0.892-0.01,1.331-0.029l-10.031-13.745L177.857,289.652z M113.951,241.785c-1.002,0-2.02-0.189-3.005-0.589l-5.611-2.277 c-15.129-6.142-35.242-17.21-35.242-42.33v-17.401c0-25.12,20.113-36.189,35.241-42.33l5.612-2.278 c4.092-1.661,8.759,0.311,10.421,4.404c1.662,4.095-0.31,8.76-4.404,10.422l-5.611,2.277c-8.475,3.439-14.579,7.032-18.662,11.158 l25.467,18.585c2.064,1.506,3.284,3.907,3.284,6.462s-1.22,4.956-3.284,6.462l-25.467,18.584c4.082,4.126,10.187,7.72,18.662,11.159 l5.611,2.277c4.094,1.662,6.066,6.327,4.404,10.422C120.105,239.9,117.111,241.785,113.951,241.785z M86.123,177.855 c-0.02,0.438-0.03,0.881-0.03,1.33v17.403c0,0.449,0.01,0.893,0.03,1.33l13.747-10.031L86.123,177.855z M261.823,241.785 c-3.161,0-6.154-1.885-7.416-4.994c-1.661-4.094,0.311-8.76,4.405-10.421l5.612-2.277c8.475-3.44,14.579-7.033,18.661-11.159 l-25.467-18.584c-2.064-1.506-3.284-3.907-3.284-6.462s1.22-4.956,3.284-6.462l25.466-18.585 c-4.082-4.125-10.187-7.719-18.661-11.158l-5.611-2.277c-4.094-1.661-6.066-6.327-4.405-10.421s6.328-6.064,10.421-4.405 l5.612,2.277c15.129,6.142,35.242,17.21,35.242,42.329v17.403c0,25.12-20.112,36.188-35.241,42.329l-5.613,2.278 C263.843,241.596,262.825,241.785,261.823,241.785z M275.906,187.888l13.746,10.031c0.02-0.438,0.03-0.881,0.03-1.33v-17.401 c0-0.449-0.01-0.894-0.03-1.332L275.906,187.888z M346.363,217.299c-16.217,0-29.411-13.193-29.411-29.41 c0-16.218,13.194-29.412,29.411-29.412c16.218,0,29.412,13.194,29.412,29.412C375.775,204.105,362.581,217.299,346.363,217.299z M346.363,174.477c-7.395,0-13.411,6.017-13.411,13.412c0,7.395,6.016,13.41,13.411,13.41c7.395,0,13.412-6.016,13.412-13.41 C359.775,180.493,353.758,174.477,346.363,174.477z M29.411,217.299c-16.217,0-29.41-13.193-29.41-29.41 c0-16.218,13.193-29.412,29.41-29.412c16.217,0,29.411,13.194,29.411,29.412C58.822,204.105,45.628,217.299,29.411,217.299z M29.411,174.477c-7.395,0-13.41,6.017-13.41,13.412c0,7.395,6.016,13.41,13.41,13.41c7.395,0,13.411-6.016,13.411-13.41 C42.822,180.493,36.806,174.477,29.411,174.477z M336.165,140.768c-3.249,0-6.304-1.993-7.501-5.218 c-14.976-40.345-46.579-72.403-86.706-87.957c-4.12-1.597-6.165-6.23-4.568-10.351c1.596-4.119,6.231-6.168,10.351-4.567 c44.393,17.206,79.355,52.673,95.924,97.307c1.538,4.143-0.574,8.747-4.716,10.284C338.03,140.606,337.089,140.768,336.165,140.768z M39.356,140.768c-0.925,0-1.865-0.161-2.783-0.502c-4.142-1.538-6.253-6.143-4.716-10.284 c16.569-44.634,51.532-80.101,95.925-97.307c4.119-1.596,8.753,0.448,10.351,4.567c1.597,4.12-0.449,8.754-4.568,10.351 c-40.126,15.554-71.73,47.612-86.707,87.957C45.66,138.773,42.604,140.768,39.356,140.768z M141.99,121.957 c-1.001,0-2.02-0.189-3.004-0.589c-4.094-1.661-6.067-6.326-4.406-10.421l2.276-5.611c6.142-15.13,17.211-35.243,42.33-35.243 h17.402c25.121,0,36.189,20.113,42.33,35.242l2.277,5.611c1.662,4.095-0.31,8.76-4.404,10.422c-4.096,1.659-8.76-0.312-10.421-4.404 l-2.277-5.611c-3.44-8.476-7.033-14.58-11.159-18.663l-18.585,25.466c-1.506,2.063-3.907,3.284-6.462,3.284l0,0 c-2.555,0-4.957-1.221-6.462-3.284l-18.583-25.466c-4.126,4.082-7.72,10.188-11.16,18.663l-2.276,5.61 C148.145,120.071,145.15,121.957,141.99,121.957z M177.857,86.122l10.031,13.746l10.031-13.746c-0.438-0.02-0.881-0.029-1.33-0.029 h-17.403C178.737,86.093,178.294,86.103,177.857,86.122z M187.886,58.822c-16.216,0-29.409-13.193-29.409-29.411S171.67,0,187.886,0 c16.218,0,29.412,13.193,29.412,29.411S204.104,58.822,187.886,58.822z M187.886,16c-7.394,0-13.409,6.017-13.409,13.411 s6.015,13.411,13.409,13.411c7.395,0,13.412-6.017,13.412-13.411S195.281,16,187.886,16z"></Path>
</Svg>
<Image
style={{ height: props.size, width: props.size, marginHorizontal: -16 }}
source={require("../../img/app_icon_light.png")}
/>
</>
);
}

View file

@ -1,11 +1,9 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
import finalPropsSelectorFactory from "react-redux/es/connect/selectorFactory";
export default function DropdownIcon(props: IconProps) {
export default function CaretDownIcon(props: IconProps) {
return (
<>
<Svg

View file

@ -0,0 +1,28 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function CaretRightIcon(props: IconProps) {
return (
<>
<Svg
height={props.size + "px"}
width={props.size + "px"}
viewBox="0 0 24 24"
stroke-width="2"
stroke={colors.icon_color}
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path
d="M13.883 5.007l.058 -.005h.118l.058 .005l.06 .009l.052 .01l.108 .032l.067 .027l.132 .07l.09 .065l.081 .073l.083 .094l.054 .077l.054 .096l.017 .036l.027 .067l.032 .108l.01 .053l.01 .06l.004 .057l.002 .059v12c0 .852 -.986 1.297 -1.623 .783l-.084 -.076l-6 -6a1 1 0 0 1 -.083 -1.32l.083 -.094l6 -6l.094 -.083l.077 -.054l.096 -.054l.036 -.017l.067 -.027l.108 -.032l.053 -.01l.06 -.01z"
stroke-width="0"
fill={colors.icon_color}
></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,28 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function CaretRightIcon(props: IconProps) {
return (
<>
<Svg
height={props.size + "px"}
width={props.size + "px"}
viewBox="0 0 24 24"
stroke-width="2"
stroke={colors.icon_color}
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path
d="M13.883 5.007l.058 -.005h.118l.058 .005l.06 .009l.052 .01l.108 .032l.067 .027l.132 .07l.09 .065l.081 .073l.083 .094l.054 .077l.054 .096l.017 .036l.027 .067l.032 .108l.01 .053l.01 .06l.004 .057l.002 .059v12c0 .852 -.986 1.297 -1.623 .783l-.084 -.076l-6 -6a1 1 0 0 1 -.083 -1.32l.083 -.094l6 -6l.094 -.083l.077 -.054l.096 -.054l.036 -.017l.067 -.027l.108 -.032l.053 -.01l.06 -.01z"
stroke-width="0"
fill={colors.icon_color}
></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,24 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function CaretUpIcon(props: IconProps) {
return (
<>
<Svg
height={props.size + "px"}
width={props.size + "px"}
viewBox="0 0 24 24"
stroke-width="2"
stroke={colors.icon_color}
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path d="M18 14l-6 -6l-6 6h12"></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,27 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function InfoIcon(props: IconProps) {
return (
<>
<Svg
width={props.size}
height={props.size}
viewBox="0 0 24 24"
strokeWidth="2"
stroke={colors.icon_color}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path d="M12 9h.01"></Path>
<Path d="M11 12h1v4h1"></Path>
<Path d="M12 3c7.2 0 9 1.8 9 9s-1.8 9 -9 9s-9 -1.8 -9 -9s1.8 -9 9 -9z"></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,29 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function MessageIcon(props: IconProps) {
return (
<>
<Svg
width={props.size}
height={props.size}
viewBox="0 0 24 24"
strokeWidth="2"
stroke={colors.icon_color}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path d="M8 9h8"></Path>
<Path d="M8 13h6"></Path>
<Path d="M12.5 20.5l-.5 .5l-3 -3h-3a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v4"></Path>
<Path d="M21.121 20.121a3 3 0 1 0 -4.242 0c.418 .419 1.125 1.045 2.121 1.879c1.051 -.89 1.759 -1.516 2.121 -1.879z"></Path>
<Path d="M19 18v.01"></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,26 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function RefreshIcon(props: IconProps) {
return (
<>
<Svg
width={props.size}
height={props.size}
viewBox="0 0 24 24"
strokeWidth="2"
stroke={colors.icon_color}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></Path>
<Path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></Path>
</Svg>
</>
);
}

View file

@ -0,0 +1,27 @@
import * as React from "react";
import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles";
export default function SubjectIcon(props: IconProps) {
return (
<>
<Svg
width={props.size}
height={props.size}
viewBox="0 0 24 24"
strokeWidth="2"
stroke={colors.icon_color}
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<Path stroke="none" d="M0 0h24v24H0z" fill="none"></Path>
<Path d="M19 4v16h-12a2 2 0 0 1 -2 -2v-12a2 2 0 0 1 2 -2h12z"></Path>
<Path d="M19 16h-12a2 2 0 0 0 -2 2"></Path>
<Path d="M9 8h6"></Path>
</Svg>
</>
);
}

BIN
src/img/app_icon_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/img/app_icon_light.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View file

@ -1,3 +1,7 @@
import * as Location from "expo-location";
import { GetStudentStatus } from "../components/Api/Api";
import { Float } from "react-native/Libraries/Types/CodegenTypes";
export interface IconProps {
size: number;
}
@ -9,6 +13,7 @@ export interface ResponsiveIconProps {
export interface RootDrawerParamList {
navigate: any;
replace: any;
}
// Redux Interfaces
@ -28,7 +33,7 @@ export interface LoggedInUserState {
// API Interfaces
export interface RegistrationParams {
export interface RegistrationType {
email: string;
username: string;
password: string;
@ -37,36 +42,199 @@ export interface RegistrationParams {
student_id_number: string;
}
export interface LoginParams {
export interface LoginType {
username: string;
password: string;
}
export interface ActivationParams {
export interface ActivationType {
uid: string;
token: string;
}
export interface SemesterParams {
export interface OptionType {
label: string;
value: string;
}
// Semester
export interface SemesterType {
id: string;
name: string;
shortname: string;
}
export interface YearLevelParams {
export type SemestersType = Array<SemesterType>;
export type SemesterReturnType = [boolean, SemestersType];
// Year Level
export interface YearLevelType {
id: string;
name: string;
shortname: string;
}
export interface CourseParams {
export type YearLevelsType = Array<YearLevelType>;
export type YearLevelReturnType = [boolean, YearLevelsType];
// Course
export interface CourseType {
id: string;
name: string;
shortname: string;
}
export type CoursesType = Array<CourseType>;
export type CourseReturnType = [boolean, CoursesType];
export interface OnboardingParams {
// Subject
export interface SubjectType {
id: number;
name: string;
code: string;
course: string;
year_level: string;
semester: string;
}
export type SubjectsType = Array<SubjectType>;
export type SubjectsReturnType = [boolean, SubjectsType];
export type AvatarType = {
uri: string;
type: string;
name: string;
};
// For dropdown menu
export interface OnboardingType {
year_level: string;
course: string;
semester: string;
}
export interface PatchUserInfoType {
course?: string;
first_name?: string;
last_name?: string;
semester?: string;
subjects?: string[];
year_level?: string;
irregular?: boolean;
avatar?: string;
}
export interface LocationType {
latitude: Float;
longitude: Float;
}
export interface StudentStatusType {
subject: string;
location: LocationType;
landmark: string | null;
active: boolean;
study_group: string;
}
export interface StudentStatusPatchType {
subject?: string;
location?: LocationType;
landmark?: string | null;
active?: boolean;
study_group?: string;
}
export interface StudentStatusFilterType {
active: boolean;
distance: number;
landmark: string | null;
location: LocationType;
study_group?: string;
subject: string;
user: string;
weight?: number;
}
export interface StudentStatusFilterTypeFlattened {
active: boolean;
distance: number;
landmark: string | null;
latitude: Float;
longitude: Float;
study_group?: string;
subject: string;
user: string;
weight?: number;
}
export interface StudyGroupType {
name: string;
students: string[];
distance: number;
landmark: string | null;
location: LocationType;
subject: string;
radius: number;
}
export interface StudyGroupCreateType {
name: string;
location: LocationType;
subject: string;
}
export interface MessageType {
id: number;
user: string;
study_group: string;
message_content: string;
timestamp: string;
}
export interface MessagePostType {
message_content: string;
}
export interface GroupMessageAvatarType {
username: string;
avatar: string;
}
export type GroupMessageAvatarListType = GroupMessageAvatarType[];
export type GroupMessageAvatarReturnType = [boolean, GroupMessageAvatarType[]];
export type MessageReturnType = [boolean, MessageType[]];
export type StudyGroupDetailReturnType = [boolean, StudyGroupType];
export type StudyGroupReturnType = [boolean, StudyGroupType[]];
export type StudentStatusReturnType = [boolean, StudentStatusType];
export type StudentStatusListType = Array<StudentStatusFilterType>;
export type StudentStatusListReturnType = [boolean, StudentStatusListType];
export type RawLocationType = Location.LocationObject;
export interface UserInfoType {
first_name: string;
last_name: string;
email: string;
avatar: string;
student_id_number: string;
irregular: boolean;
semester: string;
semester_shortname: string;
course: string;
course_shortname: string;
year_level: string;
yearlevel_shortname: string;
subjects: string[];
username: string;
}
export type UserInfoReturnType = [boolean, UserInfoType];
export type subjectUserMapType = {
subject: string;
users: string[];
latitude: Float;
longitude: Float;
radius: Float;
};

View file

@ -6,6 +6,7 @@ import { useNavigation, useRoute } from "@react-navigation/native";
import { useEffect, useState } from "react";
import { UserActivate } from "../../components/Api/Api";
import { RootDrawerParamList } from "../../interfaces/Interfaces";
import { useToast } from "react-native-toast-notifications";
interface ActivationRouteParams {
uid?: string;
@ -16,9 +17,7 @@ export default function Activation() {
const route = useRoute();
const { uid, token } = (route.params as ActivationRouteParams) || "";
const navigation = useNavigation<RootDrawerParamList>();
const [state, setState] = useState(
"Activating with UID " + uid + " and Token " + token
);
const toast = useToast();
const [loading, setLoading] = useState(true);
useEffect(() => {
@ -28,16 +27,22 @@ export default function Activation() {
token: String(token),
});
if (result) {
setTimeout(() => {
setState("Activation successful!");
}, 1000);
toast.show("Activation successful", {
type: "success",
placement: "top",
duration: 4000,
animationType: "slide-in",
});
setTimeout(() => {
navigation.navigate("Login");
}, 2000);
} else {
setTimeout(() => {
setState("Activation unsuccessful\nPlease contact support");
}, 1000);
toast.show("Activation unsuccessful. Please contact support", {
type: "warning",
placement: "top",
duration: 4000,
animationType: "slide-in",
});
}
setLoading(false);
}
@ -53,7 +58,7 @@ export default function Activation() {
marginBottom: 16,
borderRadius: 4,
width: "90%",
backgroundColor: colors.blue_1,
backgroundColor: colors.secondary_1,
}}
/>
<Text style={styles.text_white_large}>Activation</Text>
@ -61,10 +66,11 @@ export default function Activation() {
<ActivityIndicator
animating={loading}
size={96}
color={colors.blue_1}
color={colors.secondary_1}
/>
<Text style={styles.text_white_medium}>{state}</Text>
<Text style={styles.text_white_tiny}>{uid + "\n" + token}</Text>
<Text style={styles.text_white_medium}>
{"Activating with UID: " + uid + "\nToken: " + token}
</Text>
</AnimatedContainer>
</View>
);

View file

@ -0,0 +1,365 @@
import * as React from "react";
import { ActivityIndicator, Image, Pressable } from "react-native";
import styles from "../../styles";
import {
View,
Text,
TextInput,
ScrollView,
NativeSyntheticEvent,
TextInputChangeEventData,
} from "react-native";
import { colors } from "../../styles";
import { useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
GetStudentStatus,
GetStudyGroup,
GetStudyGroupMemberAvatars,
GetStudyGroupMessages,
PostMessage,
} from "../../components/Api/Api";
import {
StudentStatusType,
StudentStatusReturnType,
StudyGroupType,
StudyGroupDetailReturnType,
MessageType,
MessageReturnType,
MessagePostType,
GroupMessageAvatarType,
GroupMessageAvatarReturnType,
} from "../../interfaces/Interfaces";
import { useToast } from "react-native-toast-notifications";
import { useQueryClient } from "@tanstack/react-query";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useSelector } from "react-redux";
import { RootState } from "../../features/redux/Store/Store";
import CaretRightIcon from "../../icons/CaretLeftIcon/CaretLeftIcon";
export default function ConversationPage() {
const toast = useToast();
const user = useSelector((state: RootState) => state.user);
// Student Status
const [student_status, setStudentStatus] = useState<StudentStatusType>();
const StudentStatusQuery = useQuery({
queryKey: ["user_status"],
queryFn: async () => {
const data = await GetStudentStatus();
if (data[0] == false) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: (data: StudentStatusReturnType) => {
setStudentStatus(data[1]);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Study Group Detail
const [studygroup, setStudyGroup] = useState<StudyGroupType>();
const StudyGroupQuery = useQuery({
enabled:
student_status?.study_group != "" && student_status?.study_group != null,
queryKey: ["study_group"],
refetchInterval: 10000,
queryFn: async () => {
const data = await GetStudyGroup(student_status?.study_group || "");
if (data[0] == false) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: (data: StudyGroupDetailReturnType) => {
if (data[1]) {
setStudyGroup(data[1]);
}
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Study Group Messages
const [messages, setMessages] = useState<MessageType[]>([]);
const MessageQuery = useQuery({
refetchInterval: 3000,
enabled:
!StudentStatusQuery.isLoading &&
(student_status?.study_group != null ||
student_status?.study_group != ""),
queryKey: ["study_group_messages"],
queryFn: async () => {
const data = await GetStudyGroupMessages();
if (data[0] == false) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: async (data: MessageReturnType) => {
if (data[1]) {
await AsyncStorage.setItem("messages", JSON.stringify(data[1]));
setMessages(data[1]);
}
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Avatar List
const [users, setUsers] = useState<GroupMessageAvatarType[]>([]);
const AvatarsQuery = useQuery({
refetchInterval: 10000,
enabled:
student_status?.study_group != null ||
(student_status?.study_group != "" &&
studygroup != null &&
studygroup.students != null),
queryKey: ["study_group_avatars"],
queryFn: async () => {
const data = await GetStudyGroupMemberAvatars();
if (data[0] == false) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: (data: GroupMessageAvatarReturnType) => {
if (data[1]) {
setUsers(data[1]);
}
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
const scrollViewRef = useRef<ScrollView>(null);
const queryClient = useQueryClient();
const [message, setMessage] = useState("");
const send_message = useMutation({
mutationFn: async (info: MessagePostType) => {
const data = await PostMessage(info);
if (data[0] != true) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["study_group_messages"] });
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
if (
!StudyGroupQuery.isLoading &&
!AvatarsQuery.isLoading &&
!MessageQuery.isLoading &&
student_status &&
studygroup &&
studygroup.students
) {
return (
<View style={styles.background}>
<AnimatedContainer>
<View
style={{
padding: 15,
alignSelf: "flex-start",
}}
>
<View style={styles.flex_row}>
<Text style={{ ...styles.text_white_medium }}>
{`Group: ${studygroup?.name ? studygroup.name : "Loading..."}`}
</Text>
</View>
{studygroup.landmark ? (
<Text style={{...styles.text_white_tiny_bold,...{textAlign:'left'}}}>
{studygroup.landmark}
</Text>
) : (
<></>
)}
<View style={{ ...styles.flex_row }}>
<Text
style={{
...styles.text_white_small,
textAlign: "left",
paddingRight: 4,
}}
>
{!StudyGroupQuery.isFetching
? studygroup.students.length + " studying"
: "Loading"}
</Text>
{users.map((user: GroupMessageAvatarType, index: number) => {
if (index > 6) {
return <React.Fragment key={index} />;
}
return (
<React.Fragment key={index}>
{user.avatar != null && user.avatar != "" ? (
<Image
source={{ uri: user.avatar }}
style={styles.profile_mini}
/>
) : (
<Image
source={require("../../img/user_profile_placeholder.png")}
style={styles.profile_mini}
/>
)}
</React.Fragment>
);
})}
</View>
</View>
<ScrollView
style={{ width: 320 }}
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd()}
ref={scrollViewRef}
>
{messages.length > 0 ? (
messages.map((message: MessageType, index: number) => {
let avatar = "";
users.filter((user: GroupMessageAvatarType) => {
if (user.username == message.user) {
avatar = user.avatar;
}
});
return (
<View
key={message.id}
style={{
...styles.message_contentContainer,
alignItems:
message.user === user.user.username
? "flex-end"
: "flex-start",
}}
>
<View style={styles.flex_row}>
{avatar != null && avatar != "" ? (
<Image
source={{ uri: avatar }}
style={styles.profile_mini}
/>
) : (
<Image
source={require("../../img/user_profile_placeholder.png")}
style={styles.profile_mini}
/>
)}
<Text style={styles.text_white_small}>
{message.user}
</Text>
<Text
style={{
...styles.text_white_tiny,
...{ marginLeft: 4, alignContent: "center" },
}}
>
{message.timestamp}
</Text>
</View>
<Text style={styles.text_white_small}>
{message.message_content}
</Text>
</View>
);
})
) : (
<Text style={styles.text_white_small}>There are no messages</Text>
)}
</ScrollView>
<View style={styles.flex_row}>
<TextInput
style={styles.chatbox}
placeholder="Send a message..."
placeholderTextColor="white"
value={message}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setMessage(e.nativeEvent.text);
}}
onSubmitEditing={() => {
send_message.mutate({
message_content: message,
});
setMessage("");
}}
/>
<Pressable
style={{
backgroundColor: colors.secondary_3,
borderRadius: 16,
alignSelf: "center",
marginLeft: 16,
}}
onPress={() => {
send_message.mutate({
message_content: message,
});
setMessage("");
}}
>
<CaretRightIcon size={48} />
</Pressable>
</View>
</AnimatedContainer>
</View>
);
} else if (!student_status?.study_group) {
return (
<View style={styles.background}>
<AnimatedContainer>
<Text style={styles.text_white_medium}>
You are not in a study group. Join one to start a conversation!
</Text>
</AnimatedContainer>
</View>
);
}
return (
<View style={styles.background}>
<AnimatedContainer>
<ActivityIndicator size={96} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</AnimatedContainer>
</View>
);
}

View file

@ -0,0 +1,192 @@
import * as React from "react";
import styles, { Viewport } from "../../styles";
import {
View,
Text,
TextInput,
NativeSyntheticEvent,
TextInputChangeEventData,
Pressable,
} from "react-native";
import { useState } from "react";
import {
RootDrawerParamList,
StudentStatusPatchType,
StudyGroupCreateType,
} from "../../interfaces/Interfaces";
import Button from "../../components/Button/Button";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { PatchStudentStatus, CreateStudyGroup } from "../../components/Api/Api";
import { colors } from "../../styles";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll";
import { urlProvider } from "../../components/Api/Api";
import MapView, { UrlTile, Marker } from "react-native-maps";
import { useNavigation } from "@react-navigation/native";
import { useToast } from "react-native-toast-notifications";
import CaretLeftIcon from "../../icons/CaretLeftIcon/CaretLeftIcon";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
export default function CreateGroup({ route }: any) {
const { location, subject } = route.params;
const queryClient = useQueryClient();
const navigation = useNavigation<RootDrawerParamList>();
const toast = useToast();
const [name, setName] = useState("");
const study_group_create = useMutation({
mutationFn: async (info: StudyGroupCreateType) => {
const data = await CreateStudyGroup(info);
if (data[0] != true) {
return Promise.reject(new Error());
}
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["user_status"] });
student_status_patch.mutate({
study_group: name,
});
toast.show("Created successfully", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
const student_status_patch = useMutation({
mutationFn: async (info: StudentStatusPatchType) => {
const data = await PatchStudentStatus(info);
if (data[0] != true) {
return Promise.reject(new Error());
}
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["user_status"] });
queryClient.invalidateQueries({ queryKey: ["user_status_list"] });
queryClient.invalidateQueries({ queryKey: ["study_group_list"] });
toast.show(`Joined group ${name} successfully`, {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
// Set a delay before going back to conversation page to hopefully let the queries refresh in time
setTimeout(() => {
navigation.navigate("Conversation");
}, 200);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
if (location) {
return (
<View style={styles.background}>
<AnimatedContainer>
<View style={{ zIndex: -1 }}>
<View style={styles.padding} />
<View style={{ borderRadius: 16, overflow: "hidden" }}>
<MapView
style={{
height: Viewport.height * 0.4,
width: Viewport.width * 0.8,
alignSelf: "center",
}}
customMapStyle={[
{
featureType: "poi",
stylers: [
{
visibility: "off",
},
],
},
]}
mapType="none"
scrollEnabled={false}
zoomEnabled={false}
toolbarEnabled={false}
rotateEnabled={false}
minZoomLevel={18}
initialRegion={{
latitude: location.latitude,
longitude: location.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
loadingBackgroundColor={colors.secondary_2}
>
<UrlTile
urlTemplate={urlProvider}
shouldReplaceMapContent={true}
maximumZ={19}
flipY={false}
zIndex={1}
/>
<Marker
coordinate={{
latitude: location.latitude,
longitude: location.longitude,
}}
pinColor={colors.primary_1}
/>
</MapView>
</View>
<View style={styles.padding} />
</View>
<TextInput
style={styles.text_input}
placeholder="Group Name"
placeholderTextColor="white"
autoCapitalize="none"
value={name}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setName(e.nativeEvent.text);
}}
/>
<View style={styles.padding} />
<View style={styles.flex_row}>
<Pressable onPress={() => navigation.navigate("Home")}>
<CaretLeftIcon size={32} />
</Pressable>
<Button
onPress={() => {
study_group_create.mutate({
name: name,
location: location,
subject: subject,
});
}}
>
<Text style={styles.text_white_small}>Start Studying</Text>
</Button>
</View>
<View style={styles.padding} />
</AnimatedContainer>
</View>
);
}
return <View />;
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
import * as React from "react";
import styles from "../../styles";
import { View, Text, ActivityIndicator } from "react-native";
import { colors } from "../../styles";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
export default function Loading() {
return (
<View style={styles.background}>
<AnimatedContainer>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={128} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading StudE...</Text>
</AnimatedContainer>
</View>
);
}

View file

@ -7,15 +7,13 @@ import {
NativeSyntheticEvent,
TextInputChangeEventData,
} from "react-native";
import { useDispatch } from "react-redux";
import { colors } from "../../styles";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useEffect, useState } from "react";
import LoginIcon from "../../icons/LoginIcon/LoginIcon";
import Button from "../../components/Button/Button";
import { useNavigation } from "@react-navigation/native";
import { RootDrawerParamList } from "../../interfaces/Interfaces";
import { UserInfo, UserLogin } from "../../components/Api/Api";
import { ParseLoginError } from "../../components/ParseError/ParseError";
import { GetUserInfo, UserLogin } from "../../components/Api/Api";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import { setUser } from "../../features/redux/slices/UserSlice/UserSlice";
import {
@ -23,32 +21,33 @@ import {
setOnboarding,
unsetOnboarding,
} from "../../features/redux/slices/StatusSlice/StatusSlice";
import { useToast } from "react-native-toast-notifications";
import { RootState } from "../../features/redux/Store/Store";
export default function Login() {
const navigation = useNavigation<RootDrawerParamList>();
const status = useSelector((state: RootState) => state.status);
const [logging_in, setLoggingIn] = useState(false);
const dispatch = useDispatch();
const [creds, setCreds] = useState({
username: "",
password: "",
error: "",
});
const toast = useToast();
useEffect(() => {
if (status.logged_in) {
navigation.navigate("Home");
}
}, []);
return (
<View style={styles.background}>
<AnimatedContainer>
<View style={styles.flex_row}>
<View style={styles.flex_row}>
<LoginIcon size={32} />
<Text style={styles.text_white_large}>Student Login</Text>
</View>
<View style={{ paddingVertical: 8 }} />
<View
style={{
paddingVertical: 4,
marginBottom: 16,
borderRadius: 4,
width: "90%",
backgroundColor: colors.head,
}}
/>
<View style={styles.padding} />
<TextInput
style={styles.text_input}
placeholder="Username"
@ -68,55 +67,67 @@ export default function Login() {
placeholderTextColor="white"
secureTextEntry={true}
value={creds.password}
autoCapitalize={"none"}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setCreds({ ...creds, password: e.nativeEvent.text });
}}
/>
<View style={{ paddingVertical: 2 }} />
<Text style={styles.text_white_small}>{creds.error}</Text>
<View style={{ paddingVertical: 4 }} />
<Button
onPress={async () => {
await UserLogin({
username: creds.username,
password: creds.password,
}).then(async (result) => {
if (result[0]) {
setUser({ ...creds, username: "", password: "", error: "" });
let user_info = await UserInfo();
dispatch(login());
dispatch(setUser(user_info[1]));
// Redirect to onboarding if no year level, course, or semester specified
if (
user_info[1].year_level == null ||
user_info[1].course == null ||
user_info[1].semester == null
) {
dispatch(setOnboarding());
navigation.navigate("Onboarding");
if (!logging_in) {
await UserLogin({
username: creds.username,
password: creds.password,
}).then(async (result) => {
if (result[0]) {
setUser({ ...creds, username: "", password: "", error: "" });
let user_info = await GetUserInfo();
dispatch(login());
dispatch(setUser(user_info[1]));
// Redirect to onboarding if no year level, course, or semester specified
if (
user_info[1].year_level == null ||
user_info[1].course == null ||
user_info[1].semester == null
) {
dispatch(setOnboarding());
navigation.navigate("Onboarding");
toast.show("Successfully logged in", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
} else {
dispatch(unsetOnboarding());
toast.show("Successfully logged in", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
navigation.navigate("Home");
}
console.log(JSON.stringify(user_info));
} else {
dispatch(unsetOnboarding());
navigation.navigate("Home");
toast.show(JSON.stringify(result[1]), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
}
console.log(JSON.stringify(user_info));
} else {
setUser({
...creds,
error: ParseLoginError(JSON.stringify(result[1])),
});
}
});
setLoggingIn(false);
});
}
}}
color={colors.login_color}
>
<Text style={styles.text_white_small}>Login</Text>
</Button>
<Button
onPress={() => navigation.navigate("Register")}
color={colors.reg_color}
>
<Button onPress={() => navigation.navigate("Register")}>
<Text style={styles.text_white_small}>Register</Text>
</Button>
</AnimatedContainer>

View file

@ -3,32 +3,33 @@ import styles from "../../styles";
import { View, Text } from "react-native";
import { useNavigation } from "@react-navigation/native";
import {
CourseParams,
RootDrawerParamList,
SemesterParams,
YearLevelParams,
CourseType,
SemesterType,
YearLevelType,
} from "../../interfaces/Interfaces";
import { colors } from "../../styles";
import { AnimatePresence, MotiView } from "moti";
import { useEffect, useState } from "react";
import { MotiView } from "moti";
import { useState } from "react";
import Button from "../../components/Button/Button";
import DropDownPicker from "react-native-dropdown-picker";
import isStringEmpty from "../../components/IsStringEmpty/IsStringEmpty";
import { useQuery } from "@tanstack/react-query";
import {
GetCourses,
GetSemesters,
GetYearLevels,
OnboardingUpdateStudentInfo,
PatchUserInfo,
} from "../../components/Api/Api";
import { useDispatch } from "react-redux";
import { unsetOnboarding } from "../../features/redux/slices/StatusSlice/StatusSlice";
import { setUser } from "../../features/redux/slices/UserSlice/UserSlice";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll";
import { useToast } from "react-native-toast-notifications";
export default function Onboarding() {
const navigation = useNavigation<RootDrawerParamList>();
const dispatch = useDispatch();
// const creds = useSelector((state: RootState) => state.auth.creds);
const [error, setError] = useState("");
const toast = useToast();
// Semesters
const [selected_semester, setSelectedSemester] = useState("");
const [semesterOpen, setSemesterOpen] = useState(false);
@ -38,14 +39,28 @@ export default function Onboarding() {
]);
const semester_query = useQuery({
queryKey: ["semesters"],
queryFn: GetSemesters,
queryFn: async () => {
const data = await GetSemesters();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data) => {
let semesters = data.map((item: SemesterParams) => ({
let semesters = data[1].map((item: SemesterType) => ({
label: item.name,
value: item.name,
}));
setSemesters(semesters);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Year Level
const [selected_yearlevel, setSelectedYearLevel] = useState("");
@ -56,14 +71,28 @@ export default function Onboarding() {
]);
const yearlevel_query = useQuery({
queryKey: ["year_levels"],
queryFn: GetYearLevels,
queryFn: async () => {
const data = await GetYearLevels();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data) => {
let year_levels = data.map((item: YearLevelParams) => ({
let year_levels = data[1].map((item: YearLevelType) => ({
label: item.name,
value: item.name,
}));
setYearLevels(year_levels);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Course
const [selected_course, setSelectedCourse] = useState("");
@ -77,14 +106,28 @@ export default function Onboarding() {
]);
const course_query = useQuery({
queryKey: ["courses"],
queryFn: GetCourses,
queryFn: async () => {
const data = await GetCourses();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data) => {
let courses = data.map((item: CourseParams) => ({
let courses = data[1].map((item: CourseType) => ({
label: item.name,
value: item.name,
}));
setCourses(courses);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
if (yearlevel_query.error || semester_query.error || course_query.error) {
return (
@ -97,7 +140,7 @@ export default function Onboarding() {
}
return (
<View style={styles.background}>
<View style={styles.container}>
<AnimatedContainerNoScroll>
<MotiView
from={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -112,7 +155,7 @@ export default function Onboarding() {
marginBottom: 16,
borderRadius: 4,
width: "90%",
backgroundColor: colors.blue_1,
backgroundColor: colors.secondary_1,
}}
/>
<View style={{ paddingVertical: 4 }} />
@ -144,34 +187,24 @@ export default function Onboarding() {
setSemesterOpen(false);
setYearLevelOpen(false);
}}
style={styles.input}
setValue={setSelectedCourse}
placeholder="Choose your course"
containerStyle={{
...styles.dropdown_template,
...{ zIndex: 3000 },
containerStyle={{ zIndex: 3000 }}
textStyle={{
...styles.text_white_small_bold,
...{ textAlign: "center" },
}}
dropDownContainerStyle={{ backgroundColor: "white" }}
modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/>
<DropDownPicker
zIndex={2000}
open={semesterOpen}
value={selected_semester}
items={semesters}
setOpen={(open) => {
setSemesterOpen(open);
setCourseOpen(false);
setYearLevelOpen(false);
}}
setValue={setSelectedSemester}
placeholder="Current semester"
containerStyle={{
...styles.dropdown_template,
...{ zIndex: 2000 },
}}
dropDownContainerStyle={{ backgroundColor: "white" }}
/>
<DropDownPicker
zIndex={1000}
open={yearLevelOpen}
value={selected_yearlevel}
items={year_levels}
@ -180,28 +213,70 @@ export default function Onboarding() {
setSemesterOpen(false);
setCourseOpen(false);
}}
style={styles.input}
setValue={setSelectedYearLevel}
placeholder="Your Year Level"
containerStyle={{
...styles.dropdown_template,
...{ zIndex: 1000 },
containerStyle={{ zIndex: 2000 }}
textStyle={{
...styles.text_white_small_bold,
...{ textAlign: "center" },
}}
dropDownContainerStyle={{ backgroundColor: "white" }}
modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/>
<DropDownPicker
zIndex={1000}
open={semesterOpen}
value={selected_semester}
items={semesters}
setOpen={(open) => {
setSemesterOpen(open);
setCourseOpen(false);
setYearLevelOpen(false);
}}
style={styles.input}
setValue={setSelectedSemester}
placeholder="Current semester"
containerStyle={{ zIndex: 1000 }}
textStyle={{
...styles.text_white_small_bold,
...{ textAlign: "center" },
}}
modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/>
</MotiView>
<MotiView
from={{ opacity: 0 }}
animate={{ opacity: 1, zIndex: -1 }}
from={{
opacity: 0,
zIndex: -1,
}}
animate={{
opacity: 1,
zIndex: -1,
}}
transition={{ type: "timing", duration: 400, delay: 1700 }}
style={styles.button_template}
style={{
...styles.button_template,
...{ padding: 0, backgroundColor: colors.secondary_3 },
}}
>
<Text style={styles.text_white_small}>{error}</Text>
<Button
disabled={
!selected_yearlevel || !selected_course || !selected_semester
}
onPress={async () => {
let result = await OnboardingUpdateStudentInfo({
let result = await PatchUserInfo({
semester: selected_semester,
course: selected_course,
year_level: selected_yearlevel,
@ -211,19 +286,32 @@ export default function Onboarding() {
setSelectedCourse("");
setSelectedYearLevel("");
setSelectedSemester("");
setError("Success!");
dispatch(setUser(result[1]));
toast.show("Changes applied successfully", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
navigation.navigate("Home");
} else {
setError(result[1]);
dispatch(setUser(result[1]));
toast.show(
"An error has occured\nChanges have not been saved",
{
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
}
);
}
}}
color={colors.blue_3}
>
<Text style={styles.text_white_small}>Proceed</Text>
</Button>
</MotiView>
</View>
</AnimatedContainerNoScroll>
</View>
);
}

View file

@ -15,13 +15,16 @@ import { RootDrawerParamList } from "../../interfaces/Interfaces";
import SignupIcon from "../../icons/SignupIcon/SignupIcon";
import { UserRegister } from "../../components/Api/Api";
import IsNumber from "../../components/IsNumber/IsNumber";
import ParseError from "../../components/ParseError/ParseError";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import { useToast } from "react-native-toast-notifications";
import { ScrollView } from "react-native-gesture-handler";
export default function Register() {
const navigation = useNavigation<RootDrawerParamList>();
const toast = useToast();
// const dispatch = useDispatch();
// const creds = useSelector((state: RootState) => state.auth.creds);
const [registering, setRegistering] = useState(false);
const [user, setUser] = useState({
first_name: "",
last_name: "",
@ -29,30 +32,21 @@ export default function Register() {
username: "",
email: "",
password: "",
feedback: "",
confirm_password: "",
});
return (
<View style={styles.background}>
<AnimatedContainer>
<View style={styles.flex_row}>
<View style={styles.flex_row}>
<SignupIcon size={32} />
<Text style={styles.text_white_large}>Student Signup</Text>
<View style={{ paddingVertical: 8, }} />
<View style={{ paddingVertical: 8 }} />
</View>
<View
style={{
paddingVertical: 4,
marginBottom: 16,
marginTop: 8,
borderRadius: 4,
width: "90%",
backgroundColor: colors.head,
}}
/>
<View style={styles.padding} />
<TextInput
style={styles.text_input}
placeholder="First Name"
placeholderTextColor="white"
placeholderTextColor={colors.text_default}
value={user.first_name}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
@ -64,7 +58,7 @@ export default function Register() {
<TextInput
style={styles.text_input}
placeholder="Last Name"
placeholderTextColor="white"
placeholderTextColor={colors.text_default}
value={user.last_name}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
@ -76,7 +70,7 @@ export default function Register() {
<TextInput
style={styles.text_input}
placeholder="USTP ID Number"
placeholderTextColor="white"
placeholderTextColor={colors.text_default}
value={user.student_id_number}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
@ -93,7 +87,7 @@ export default function Register() {
<TextInput
style={styles.text_input}
placeholder="Username"
placeholderTextColor="white"
placeholderTextColor={colors.text_default}
autoCapitalize={"none"}
value={user.username}
onChange={(
@ -106,7 +100,7 @@ export default function Register() {
<TextInput
style={styles.text_input}
placeholder="Email"
placeholderTextColor="white"
placeholderTextColor={colors.text_default}
autoCapitalize={"none"}
value={user.email}
onChange={(
@ -119,9 +113,10 @@ export default function Register() {
<TextInput
style={styles.text_input}
placeholder="Password"
placeholderTextColor="white"
placeholderTextColor={colors.text_default}
secureTextEntry={true}
value={user.password}
autoCapitalize={"none"}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
@ -129,45 +124,79 @@ export default function Register() {
}}
/>
<View style={{ paddingVertical: 4 }} />
<Text style={styles.text_white_small}>{user.feedback}</Text>
<TextInput
style={styles.text_input}
placeholder="Confirm Password"
placeholderTextColor={colors.text_default}
secureTextEntry={true}
value={user.confirm_password}
autoCapitalize={"none"}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, confirm_password: e.nativeEvent.text });
}}
/>
<View style={{ paddingVertical: 4 }} />
<Button
onPress={async () => {
await UserRegister({
username: user.username,
email: user.email,
password: user.password,
student_id_number: user.student_id_number,
first_name: user.first_name,
last_name: user.last_name,
}).then((result) => {
console.log(result);
if (result[0]) {
setUser({
...user,
first_name: "",
last_name: "",
student_id_number: "",
username: "",
email: "",
password: "",
feedback:
"Success! An email has been sent to activate your account",
if (!registering) {
if (user.password === user.confirm_password) {
setRegistering(true);
await UserRegister({
username: user.username,
email: user.email,
password: user.password,
student_id_number: user.student_id_number,
first_name: user.first_name,
last_name: user.last_name,
}).then((result: any) => {
console.log(result);
if (result[0]) {
setUser({
...user,
first_name: "",
last_name: "",
student_id_number: "",
username: "",
email: "",
password: "",
});
toast.show(
"Success! An email has been sent to activate your account",
{
type: "success",
placement: "top",
duration: 6000,
animationType: "slide-in",
}
);
setTimeout(() => {
navigation.navigate("Login");
}, 10000);
} else {
toast.show(JSON.parse(JSON.stringify(result[1])), {
type: "warning",
placement: "top",
duration: 6000,
animationType: "slide-in",
});
}
setRegistering(false);
});
setTimeout(() => {
navigation.navigate("Login");
}, 10000);
} else {
setUser({
...user,
feedback: ParseError(JSON.stringify(result[1])),
});
toast.show(
"Password does not match confirm password. Please try again"
),
{
type: "warning",
placement: "top",
duration: 6000,
animationType: "slide-in",
};
}
});
{
}
}}
color={colors.reg_color}
>
<Text style={styles.text_white_small}>Register</Text>
</Button>

View file

@ -1,7 +1,7 @@
import * as React from "react";
import styles from "../../styles";
import { View, Text, ActivityIndicator } from "react-native";
import { TokenRefresh, UserInfo } from "../../components/Api/Api";
import { TokenRefresh, GetUserInfo } from "../../components/Api/Api";
import { useDispatch } from "react-redux";
import { colors } from "../../styles";
import { useEffect, useState } from "react";
@ -15,37 +15,55 @@ import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContai
import { setUser } from "../../features/redux/slices/UserSlice/UserSlice";
import { setOnboarding } from "../../features/redux/slices/StatusSlice/StatusSlice";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useToast } from "react-native-toast-notifications";
export default function Revalidation() {
const dispatch = useDispatch();
const navigation = useNavigation<RootDrawerParamList>();
const [state, setState] = useState("Checking for existing session");
const toast = useToast();
useEffect(() => {
setState("Previous session found");
TokenRefresh().then(async (response) => {
let user_info = await UserInfo();
let user_info = await GetUserInfo();
if (response && user_info[0]) {
dispatch(login());
dispatch(setUser(user_info[1]));
if (
!(
user_info[1].year_level ||
user_info[1].course ||
user_info[1].semester
)
!user_info[1].year_level ||
!user_info[1].course ||
!user_info[1].semester
) {
dispatch(setOnboarding());
toast.show("Previous session restored", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
await setTimeout(() => {
navigation.navigate("Onboarding");
}, 700);
} else {
dispatch(unsetOnboarding());
toast.show("Previous session restored", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
await setTimeout(() => {
navigation.navigate("Home");
}, 700);
}
} else {
await setState("Session expired");
toast.show("Session expired. Please login again", {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
await setTimeout(() => {
AsyncStorage.clear();
navigation.navigate("Login");
@ -58,7 +76,7 @@ export default function Revalidation() {
<View style={styles.background}>
<AnimatedContainer>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={96} color={colors.blue_1} />
<ActivityIndicator size={96} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>{state}</Text>
</AnimatedContainer>
</View>

View file

@ -0,0 +1,234 @@
import * as React from "react";
import styles, { Viewport } from "../../styles";
import {
View,
Text,
ToastAndroid,
Pressable,
ActivityIndicator,
} from "react-native";
import { useState } from "react";
import {
UserInfoReturnType,
OptionType,
RootDrawerParamList,
StudentStatusType,
StudentStatusReturnType,
StudentStatusPatchType,
} from "../../interfaces/Interfaces";
import Button from "../../components/Button/Button";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
PatchStudentStatus,
GetUserInfo,
ParseError,
} from "../../components/Api/Api";
import { colors } from "../../styles";
import DropDownPicker from "react-native-dropdown-picker";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll";
import { urlProvider } from "../../components/Api/Api";
import MapView, { UrlTile, Marker } from "react-native-maps";
import { useNavigation } from "@react-navigation/native";
import { useToast } from "react-native-toast-notifications";
import CaretLeftIcon from "../../icons/CaretLeftIcon/CaretLeftIcon";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
export default function StartStudying({ route }: any) {
const { location } = route.params;
const queryClient = useQueryClient();
const navigation = useNavigation<RootDrawerParamList>();
const toast = useToast();
// Subject choices
const [selected_subject, setSelectedSubject] = useState("");
const [subjectsOpen, setSubjectsOpen] = useState(false);
const [subjects, setSubjects] = useState<OptionType[]>([]);
const StudentInfo = useQuery({
queryKey: ["user"],
queryFn: async () => {
const data = await GetUserInfo();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data: UserInfoReturnType) => {
let subjects = data[1].subjects.map((subject: string) => ({
label: subject,
value: subject,
}));
setSubjects(subjects);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
const mutation = useMutation({
mutationFn: async (info: StudentStatusPatchType) => {
const data = await PatchStudentStatus(info);
if (data[0] == false) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["user_status"] });
queryClient.invalidateQueries({ queryKey: ["user_status_list"] });
queryClient.invalidateQueries({ queryKey: ["study_group_list"] });
toast.show("You are now studying \n" + selected_subject, {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
// Set a delay before going back to homepage to hopefully let the queries refresh in time
setTimeout(() => {
navigation.navigate("Home");
}, 200);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
if (StudentInfo.isLoading) {
return (
<View style={styles.background}>
<AnimatedContainer>
<View style={{ paddingVertical: 8 }} />
<ActivityIndicator size={96} color={colors.secondary_1} />
<Text style={styles.text_white_medium}>Loading...</Text>
</AnimatedContainer>
</View>
);
}
if (location && location.coords) {
return (
<View style={styles.background}>
<AnimatedContainerNoScroll>
<View style={{ zIndex: -1 }}>
<View style={styles.padding} />
<View style={{ borderRadius: 16, overflow: "hidden" }}>
<MapView
style={{
height: Viewport.height * 0.4,
width: Viewport.width * 0.8,
alignSelf: "center",
}}
customMapStyle={[
{
featureType: "poi",
stylers: [
{
visibility: "off",
},
],
},
]}
mapType="none"
scrollEnabled={false}
zoomEnabled={false}
toolbarEnabled={false}
rotateEnabled={false}
minZoomLevel={18}
initialRegion={{
latitude: location.coords.latitude,
longitude: location.coords.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
loadingBackgroundColor={colors.secondary_2}
>
<UrlTile
urlTemplate={urlProvider}
shouldReplaceMapContent={true}
maximumZ={19}
flipY={false}
zIndex={1}
/>
<Marker
coordinate={{
latitude: location.coords.latitude,
longitude: location.coords.longitude,
}}
pinColor={colors.primary_1}
/>
</MapView>
</View>
<View style={styles.padding} />
</View>
<DropDownPicker
zIndex={1000}
max={16}
open={subjectsOpen}
value={selected_subject}
items={subjects}
setOpen={(open) => {
setSubjectsOpen(open);
}}
setValue={setSelectedSubject}
placeholderStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
placeholder="Select subject"
multipleText="Select subject"
style={styles.input}
textStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
autoScroll
dropDownDirection="BOTTOM"
listMode="MODAL"
/>
<View style={styles.padding} />
<View style={styles.flex_row}>
<Pressable onPress={() => navigation.navigate("Home")}>
<CaretLeftIcon size={32} />
</Pressable>
<Button
onPress={() => {
console.log({
subject: selected_subject,
location: {
latitude: location.coords.latitude,
longitude: location.coords.longitude,
},
});
mutation.mutate({
active: true,
subject: selected_subject,
location: {
latitude: location.coords.latitude,
longitude: location.coords.longitude,
},
});
}}
>
<Text style={styles.text_white_small}>Start Studying</Text>
</Button>
</View>
</AnimatedContainerNoScroll>
</View>
);
}
return <View />;
}

View file

@ -0,0 +1,261 @@
import * as React from "react";
import styles from "../../styles";
import { View, Text, ActivityIndicator } from "react-native";
import { useState } from "react";
import {
UserInfoReturnType,
SubjectsReturnType,
SubjectType,
OptionType,
StudentStatusType,
PatchUserInfoType,
StudentStatusPatchType,
} from "../../interfaces/Interfaces";
import Button from "../../components/Button/Button";
import { Image } from "react-native";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
GetSubjects,
PatchUserInfo,
GetUserInfo,
PatchStudentStatus,
} from "../../components/Api/Api";
import { colors } from "../../styles";
import DropDownPicker from "react-native-dropdown-picker";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll";
import { useSelector } from "react-redux";
import { RootState } from "../../features/redux/Store/Store";
import { useToast } from "react-native-toast-notifications";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import Loading from "../Loading/Loading";
import LoadingFeedback from "../../components/LoadingFeedback/LoadingFeedback";
export default function SubjectsPage() {
const logged_in_user = useSelector((state: RootState) => state.user.user);
const queryClient = useQueryClient();
const toast = useToast();
// Student Status
const studentstatus_mutation = useMutation({
mutationFn: async (info: StudentStatusPatchType) => {
const data = await PatchStudentStatus(info);
if (data[0] != true) {
return Promise.reject(new Error());
}
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["user_status"] });
},
onError: () => {
toast.show("An error has occured\nChanges have not been saved", {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// User Info
const [user, setUser] = useState({
first_name: "",
last_name: "",
year_level: "",
yearlevel_shortname: "",
semester: "",
semester_shortname: "",
course: "",
course_shortname: "",
avatar: "",
student_id_number: "",
subjects: [] as string[],
});
const StudentInfo = useQuery({
queryKey: ["user"],
queryFn: async () => {
const data = await GetUserInfo();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data: UserInfoReturnType) => {
setUser({
...user,
first_name: data[1].first_name,
last_name: data[1].last_name,
year_level: data[1].year_level,
semester: data[1].semester,
course: data[1].course,
avatar: data[1].avatar,
student_id_number: data[1].student_id_number,
subjects: data[1].subjects,
});
setSelectedSubjects(data[1].subjects);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
const mutation = useMutation({
mutationFn: async (info: PatchUserInfoType) => {
const data = await PatchUserInfo(info);
if (data[0] != true) {
return Promise.reject(new Error());
}
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["subjects"] });
setSelectedSubjects([]);
// Reset student status when changing user info to prevent bugs
studentstatus_mutation.mutate({
active: false,
});
toast.show("Changes applied successfully.\nStudent status reset", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Subjects
const [selected_subjects, setSelectedSubjects] = useState<any>([]);
const [subjectsOpen, setSubjectsOpen] = useState(false);
const [subjects, setSubjects] = useState<OptionType[]>([]);
const Subjects = useQuery({
enabled: StudentInfo.isFetched,
queryKey: ["subjects"],
queryFn: async () => {
const data = await GetSubjects();
if (data[0] == false) {
return Promise.reject(new Error(JSON.stringify(data[1])));
}
return data;
},
onSuccess: (data: SubjectsReturnType) => {
if (data[1]) {
let subjects = data[1].map((subject: SubjectType) => ({
label: subject.name,
value: subject.name,
}));
setSubjects(subjects);
}
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Profile photo
function Avatar() {
if (user.avatar) {
return <Image source={{ uri: user.avatar }} style={styles.profile} />;
} else {
return (
<Image
source={require("../../img/user_profile_placeholder.png")}
style={{ ...styles.profile, ...{ marginRight: 48 } }}
/>
);
}
}
if (StudentInfo.isLoading || Subjects.isLoading) {
return <LoadingFeedback />;
}
return (
<View style={styles.background}>
<AnimatedContainerNoScroll>
<View style={styles.flex_row}>
<Avatar />
<Text style={{ ...styles.text_white_small, ...{ marginLeft: 16 } }}>
{(logged_in_user.first_name || "Undefined") +
" " +
(logged_in_user.last_name || "User") +
"\n" +
user.student_id_number}
</Text>
</View>
<View style={styles.padding} />
<View style={styles.flex_row}>
<View style={{ flex: 1 }}>
<Text style={styles.text_white_small_bold}>Subjects</Text>
</View>
<View style={{ flex: 3 }}>
<DropDownPicker
zIndex={1000}
multiple={true}
max={16}
open={subjectsOpen}
value={selected_subjects}
items={subjects}
setOpen={(open) => {
setSubjectsOpen(open);
}}
setValue={setSelectedSubjects}
placeholderStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
placeholder="Select subjects"
multipleText="Select subjects"
style={styles.input}
textStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
autoScroll
dropDownDirection="BOTTOM"
listMode="MODAL"
/>
</View>
</View>
<View style={{ zIndex: -1 }}>
<View style={styles.padding} />
<Button
onPress={() => {
mutation.mutate({
subjects: selected_subjects,
});
}}
>
<Text style={styles.text_white_small}>Save Changes</Text>
</Button>
<View style={styles.padding} />
</View>
</AnimatedContainerNoScroll>
</View>
);
}

View file

@ -1,216 +0,0 @@
import * as React from "react";
import styles from "../../styles";
import {
View,
Text,
TextInput,
NativeSyntheticEvent,
TextInputChangeEventData,
} from "react-native";
import { colors } from "../../styles";
import { useState, useEffect } from "react";
import Button from "../../components/Button/Button";
import { useNavigation } from "@react-navigation/native";
import { RootDrawerParamList } from "../../interfaces/Interfaces";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import { TouchableOpacity, Image } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
import SelectDropdown from "react-native-select-dropdown";
import DropdownIcon from "../../icons/DropdownIcon/DropdownIcon";
import { useQuery } from "react-query";
import { UserInfo as GetUserInfo } from "../../components/Api/Api";
import { err } from "react-native-svg/lib/typescript/xml";
export default function UserInfo() {
const navigation = useNavigation<RootDrawerParamList>();
const [isEditable, setIsEditable] = useState(false);
const options = ["", "", "", ""];
const [isActive, setIsActive] = useState(false);
const toggleUserActive = () => {
setIsActive(!isActive);
};
//const dispatch = useDispatch();
// const creds = useSelector((state: RootState) => state.auth.creds);
const [user, setUser] = useState({
first_name: "",
last_name: "",
year_level: "",
semester: "",
course: "",
});
const { data, isLoading, error } = useQuery("user", UserInfo, {
retry: 0,
onSuccess: (data) => console.log(data),
});
if (!isLoading && !error) {
return (
<ScrollView style={styles.background}>
<AnimatedContainer>
<Text style={{ ...styles.text_white_medium, ...{ fontSize: 32 } }}>
Kurt Toledo
</Text>
<View>
<Image
source={require("./image/3135715.png")}
style={styles.profile}
/>
<TouchableOpacity onPress={toggleUserActive} style={styles.button}>
<Text
style={[
styles.text,
isActive ? styles.activeText : styles.inactiveText,
]}
>
Student {isActive ? "Active" : "Inactive"}
</Text>
</TouchableOpacity>
</View>
<View
style={{
paddingVertical: 4,
marginBottom: 16,
marginTop: 8,
borderRadius: 4,
width: "90%",
backgroundColor: colors.head,
}}
/>
<View style={styles.formGroup}>
<View style={{ width: 70 }}>
<Text style={styles.text}>First Name</Text>
</View>
<View style={{ flex: 1 }}>
<TextInput
style={[styles.input, !isEditable && styles.input]}
editable={isEditable}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, first_name: e.nativeEvent.text });
}}
/>
</View>
</View>
<View style={styles.formGroup}>
<View style={{ width: 70 }}>
<Text style={styles.text}>Last Name</Text>
</View>
<View style={{ flex: 1 }}>
<TextInput
style={[styles.input, !isEditable && styles.input]}
editable={isEditable}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, first_name: e.nativeEvent.text });
}}
/>
</View>
</View>
<View
style={{
paddingVertical: 4,
marginBottom: 16,
marginTop: 8,
borderRadius: 4,
width: "90%",
backgroundColor: colors.head,
}}
/>
<View style={styles.formGroup}>
<View style={{ width: 70 }}>
<Text style={styles.text}>Year Level</Text>
</View>
<View style={{ flex: 1 }}>
<TextInput
style={[styles.input, !isEditable && styles.input]}
editable={isEditable}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, first_name: e.nativeEvent.text });
}}
/>
</View>
</View>
<View style={styles.formGroup}>
<View style={{ width: 70 }}>
<Text style={styles.text}>Semester</Text>
</View>
<View style={{ flex: 1 }}>
<TextInput
style={[styles.input, !isEditable && styles.input]}
editable={isEditable}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, first_name: e.nativeEvent.text });
}}
/>
</View>
</View>
<View style={styles.formGroup}>
<View style={{ width: 70 }}>
<Text style={styles.text}>Course</Text>
</View>
<View style={{ flex: 1 }}>
<TextInput
style={[styles.input, !isEditable && styles.input]}
editable={isEditable}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, first_name: e.nativeEvent.text });
}}
/>
</View>
</View>
<View
style={{
paddingVertical: 4,
marginBottom: 16,
marginTop: 8,
borderRadius: 4,
width: "90%",
backgroundColor: colors.head,
}}
/>
<View style={styles.formGroup}>
<View style={{ width: 80 }}>
<Text style={styles.text}>Subject</Text>
</View>
<View style={{ flex: 1 }}>
<SelectDropdown
onSelect={(selectedItem, index) => {
console.log(selectedItem, index);
}}
renderDropdownIcon={() => <DropdownIcon size={32} />}
buttonTextStyle={{
color: "white",
}}
dropdownStyle={{
backgroundColor: "#E3963E",
}}
data={options}
buttonStyle={{
width: "90%",
marginLeft: 10,
backgroundColor: "#E3963E",
borderRadius: 8,
}}
/>
</View>
</View>
<TouchableOpacity
style={styles.button_template}
onPress={() => setIsEditable(!isEditable)}
>
<Text style={styles.text_white_small}>
{isEditable ? "Save" : "Edit Profile"}
</Text>
</TouchableOpacity>
</AnimatedContainer>
</ScrollView>
);
}
}

View file

@ -0,0 +1,489 @@
import * as React from "react";
import styles from "../../styles";
import {
View,
Text,
TextInput,
NativeSyntheticEvent,
TextInputChangeEventData,
Pressable,
ActivityIndicator,
} from "react-native";
import { useState } from "react";
import {
SemesterReturnType,
UserInfoReturnType,
SemesterType,
YearLevelType,
CourseType,
OptionType,
StudentStatusType,
PatchUserInfoType,
StudentStatusPatchType,
} from "../../interfaces/Interfaces";
import Button from "../../components/Button/Button";
import { Image } from "react-native";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
GetCourses,
GetSemesters,
GetYearLevels,
PatchStudentStatus,
PatchUserInfo,
GetUserInfo,
} from "../../components/Api/Api";
import { colors } from "../../styles";
import DropDownPicker from "react-native-dropdown-picker";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll";
import BouncyCheckbox from "react-native-bouncy-checkbox";
import { useSelector } from "react-redux";
import { RootState } from "../../features/redux/Store/Store";
import { useDispatch } from "react-redux";
import { setUser as setUserinState } from "../../features/redux/slices/UserSlice/UserSlice";
import * as ImagePicker from "expo-image-picker";
import * as FileSystem from "expo-file-system";
import { useToast } from "react-native-toast-notifications";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import Loading from "../Loading/Loading";
import LoadingFeedback from "../../components/LoadingFeedback/LoadingFeedback";
export default function UserInfoPage() {
const logged_in_user = useSelector((state: RootState) => state.user.user);
const dispatch = useDispatch();
const queryClient = useQueryClient();
const toast = useToast();
// Student Status
const studentstatus_mutation = useMutation({
mutationFn: async (info: StudentStatusPatchType) => {
const data = await PatchStudentStatus(info);
if (data[0] != true) {
return Promise.reject(new Error());
}
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["user_status"] });
},
onError: () => {
toast.show("An error has occured\nChanges have not been saved", {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// User Info
const [user, setUser] = useState({
first_name: "",
last_name: "",
year_level: "",
yearlevel_shortname: "",
semester: "",
semester_shortname: "",
course: "",
course_shortname: "",
avatar: "",
student_id_number: "",
irregular: false,
});
const StudentInfo = useQuery({
queryKey: ["user"],
queryFn: async () => {
const data = await GetUserInfo();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data: UserInfoReturnType) => {
// console.log(data[1]);
setUser({
...user,
first_name: data[1].first_name,
last_name: data[1].last_name,
year_level: data[1].year_level,
semester: data[1].semester,
course: data[1].course,
student_id_number: data[1].student_id_number,
irregular: data[1].irregular,
avatar: data[1].avatar,
});
setSelectedCourse(data[1].course);
setSelectedSemester(data[1].semester);
setSelectedYearLevel(data[1].year_level);
dispatch(setUserinState(data[1]));
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
const mutation = useMutation({
mutationFn: async (info: PatchUserInfoType) => {
const data = await PatchUserInfo(info);
if (data[0] == false) {
return Promise.reject(new Error());
}
return data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["subjects"] });
// Reset student status when changing user info to prevent bugs
studentstatus_mutation.mutate({
active: false,
});
toast.show("Changes applied successfully.\nStudent status reset", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
dispatch(setUserinState(user));
},
onError: () => {
toast.show("An error has occured\nChanges have not been saved", {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Semester
const [selected_semester, setSelectedSemester] = useState("");
const [semesterOpen, setSemesterOpen] = useState(false);
const [semesters, setSemesters] = useState<OptionType[]>([]);
const Semesters = useQuery({
queryKey: ["semesters"],
queryFn: async () => {
const data = await GetSemesters();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data: SemesterReturnType) => {
let semestersData = data[1].map((semester: SemesterType) => ({
label: semester.name,
value: semester.name,
shortname: semester.shortname,
}));
// Update the 'semesters' state
setSemesters(semestersData);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Year Level
const [selected_yearlevel, setSelectedYearLevel] = useState("");
const [yearLevelOpen, setYearLevelOpen] = useState(false);
const [year_levels, setYearLevels] = useState<OptionType[]>([]);
const yearlevel_query = useQuery({
queryKey: ["year_levels"],
queryFn: async () => {
const data = await GetYearLevels();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data) => {
let year_levels = data[1].map((yearlevel: YearLevelType) => ({
label: yearlevel.name,
value: yearlevel.name,
}));
setYearLevels(year_levels);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Course
const [selected_course, setSelectedCourse] = useState("");
const [courseOpen, setCourseOpen] = useState(false);
const [courses, setCourses] = useState<OptionType[]>([]);
const course_query = useQuery({
queryKey: ["courses"],
queryFn: async () => {
const data = await GetCourses();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data) => {
let courses = data[1].map((course: CourseType) => ({
label: course.name,
value: course.name,
}));
setCourses(courses);
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
});
// Profile photo
const pickImage = async () => {
// No permissions request is necessary for launching the image library
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled) {
const encodedImage = await FileSystem.readAsStringAsync(
result.assets[0].uri,
{ encoding: "base64" }
);
mutation.mutate({
avatar: encodedImage,
});
}
};
function Avatar() {
if (user.avatar) {
return (
<Pressable onPress={pickImage}>
<Image source={{ uri: user.avatar }} style={styles.profile} />
</Pressable>
);
} else {
return (
<Pressable onPress={pickImage}>
<Image
source={require("../../img/user_profile_placeholder.png")}
style={{ ...styles.profile, ...{ marginRight: 48 } }}
/>
</Pressable>
);
}
}
if (
StudentInfo.isLoading ||
Semesters.isLoading ||
yearlevel_query.isLoading ||
course_query.isLoading
) {
return <LoadingFeedback />;
}
return (
<View style={styles.background}>
<AnimatedContainerNoScroll>
<View style={styles.flex_row}>
<Avatar />
<Text style={{ ...styles.text_white_small, ...{ marginLeft: 16 } }}>
{(logged_in_user.first_name || "Undefined") +
" " +
(logged_in_user.last_name || "User") +
"\n" +
user.student_id_number}
</Text>
</View>
<View style={styles.padding} />
<View style={styles.flex_row}>
<View style={{ flex: 1 }}>
<Text style={styles.text_white_small_bold}>First Name</Text>
</View>
<View style={{ flex: 3 }}>
<TextInput
style={styles.input}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, first_name: e.nativeEvent.text });
}}
value={user.first_name}
/>
</View>
</View>
<View style={styles.flex_row}>
<View style={{ flex: 1 }}>
<Text style={styles.text_white_small_bold}>Last Name</Text>
</View>
<View style={{ flex: 3 }}>
<TextInput
style={styles.input}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, last_name: e.nativeEvent.text });
}}
value={user.last_name}
/>
</View>
</View>
<View style={styles.flex_row}>
<View style={{ flex: 1 }}>
<Text style={styles.text_white_small_bold}>Year Level</Text>
</View>
<View style={{ flex: 3 }}>
<DropDownPicker
zIndex={1000}
open={yearLevelOpen}
value={selected_yearlevel}
items={year_levels}
setOpen={(open) => {
setYearLevelOpen(open);
setSemesterOpen(false);
setCourseOpen(false);
}}
setValue={setSelectedYearLevel}
placeholder={user.year_level}
placeholderStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
style={styles.input}
textStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
dropDownContainerStyle={{
backgroundColor: colors.primary_2,
zIndex: 1000,
borderWidth: 0,
}}
dropDownDirection="TOP"
/>
</View>
</View>
<View style={styles.flex_row}>
<View style={{ flex: 1 }}>
<Text style={styles.text_white_small_bold}>Semester</Text>
</View>
<View style={{ flex: 3 }}>
<DropDownPicker
zIndex={2000}
open={semesterOpen}
value={selected_semester}
items={semesters}
setOpen={(open) => {
setYearLevelOpen(false);
setSemesterOpen(open);
setCourseOpen(false);
}}
setValue={setSelectedSemester}
placeholder={user.semester}
placeholderStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
style={styles.input}
textStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
dropDownContainerStyle={{
backgroundColor: colors.primary_2,
zIndex: 2000,
borderWidth: 0,
}}
dropDownDirection="TOP"
/>
</View>
</View>
<View style={styles.flex_row}>
<View style={{ flex: 1 }}>
<Text style={styles.text_white_small_bold}>Course</Text>
</View>
<View style={{ flex: 3 }}>
<DropDownPicker
zIndex={3000}
open={courseOpen}
value={selected_course}
items={courses}
setOpen={(open) => {
setYearLevelOpen(false);
setSemesterOpen(false);
setCourseOpen(open);
}}
setValue={setSelectedCourse}
placeholder={user.course}
placeholderStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
style={styles.input}
textStyle={{
...styles.text_white_tiny_bold,
...{ textAlign: "left" },
}}
dropDownContainerStyle={{
backgroundColor: colors.primary_2,
zIndex: 3000,
borderWidth: 0,
}}
dropDownDirection="TOP"
/>
</View>
</View>
<View style={styles.padding} />
<View style={{ zIndex: -1 }}>
<View style={{ ...styles.flex_row, ...{ alignSelf: "center" } }}>
<BouncyCheckbox
onPress={() => {
mutation.mutate({
irregular: !user.irregular,
});
setUser({ ...user, irregular: !user.irregular });
}}
isChecked={user.irregular}
disableBuiltInState
fillColor={colors.secondary_3}
/>
<Text style={styles.text_white_small}>Irregular </Text>
</View>
<Button
onPress={() => {
setYearLevelOpen(false);
setSemesterOpen(false);
setCourseOpen(false);
mutation.mutate({
first_name: user.first_name,
last_name: user.last_name,
course: selected_course,
semester: selected_semester,
year_level: selected_yearlevel,
});
}}
>
<Text style={styles.text_white_small}>Save Changes</Text>
</Button>
<View style={styles.padding} />
</View>
</AnimatedContainerNoScroll>
</View>
);
}

View file

@ -1,39 +1,40 @@
import { StyleSheet, Dimensions } from "react-native";
const width = Dimensions.get("window").width;
const height = Dimensions.get("window").height;
const containerWidth = width - width * 0.08;
const containerHeight = height - height * 0.01;
export const Viewport = {
width: Dimensions.get("window").width,
height: Dimensions.get("window").height,
};
export const colors = {
orange_1: "#FFDEAD",
orange_2: "#FFE2C1",
orange_3: "#C07624",
blue_1: "#E3963E",
blue_2: "#FFAC1C",
blue_3: "#FFAC1C",
text_default: "white",
primary_1: "#1C2C3F",
primary_2: "#445467",
primary_3: "#606F81",
primary_4: "#b4d0f3",
secondary_1: "#1E1F3D",
secondary_2: "#626297",
secondary_3: "#7a7abd",
secondary_4: "#FFE9CE",
secondary_5: "#FFF5E9",
text_default: "#FFFF",
text_dark: "black",
text_error: "#e32d1e",
text_success: "green",
icon_color: "white",
login_color: "#0047AB",
reg_color: "#0096FF",
head: "white",
blue_disabled: "#C07624",
text_success: "#2ecc71",
icon_color: "#FFFF",
head: "#FFFF",
};
export const font_sizes = {
tiny: 12,
small: 16,
medium: 24,
medium_large: 30,
large: 36,
xl: 48,
};
const styles = StyleSheet.create({
background: {
backgroundColor: colors.orange_1,
backgroundColor: colors.secondary_1,
height: "100%",
width: "100%",
},
@ -43,8 +44,9 @@ const styles = StyleSheet.create({
justifyContent: "center",
display: "flex",
flexDirection: "column",
flex: 1,
flexGrow: 1,
paddingHorizontal: 4,
paddingVertical: 32,
},
flex_row: {
display: "flex",
@ -75,6 +77,12 @@ const styles = StyleSheet.create({
fontWeight: "bold",
textAlign: "center",
},
text_white_medium_large: {
color: colors.text_default,
fontSize: font_sizes.medium_large,
fontWeight: "bold",
textAlign: "center",
},
text_white_large: {
color: colors.text_default,
fontSize: font_sizes.large,
@ -87,6 +95,54 @@ const styles = StyleSheet.create({
fontWeight: "bold",
textAlign: "center",
},
text_white_small_bold: {
color: colors.text_default,
fontSize: font_sizes.small,
fontWeight: "bold",
textAlign: "center",
},
text_white_tiny_bold: {
color: colors.text_default,
fontSize: font_sizes.tiny,
fontWeight: "bold",
textAlign: "center",
},
text_black_tiny: {
color: colors.text_dark,
fontSize: font_sizes.tiny,
fontWeight: "bold",
textAlign: "center",
},
text_black_small: {
color: colors.text_dark,
fontSize: font_sizes.small,
fontWeight: "bold",
textAlign: "center",
},
text_black_medium: {
color: colors.text_dark,
fontSize: font_sizes.medium,
fontWeight: "bold",
textAlign: "center",
},
text_black_medium_large: {
color: colors.text_dark,
fontSize: font_sizes.medium_large,
fontWeight: "bold",
textAlign: "center",
},
text_black_large: {
color: colors.text_dark,
fontSize: font_sizes.large,
fontWeight: "bold",
textAlign: "center",
},
text_black_xl: {
color: colors.text_dark,
fontSize: font_sizes.xl,
fontWeight: "bold",
textAlign: "center",
},
button_template: {
justifyContent: "center",
alignSelf: "center",
@ -98,63 +154,94 @@ const styles = StyleSheet.create({
marginHorizontal: 8,
padding: 8,
borderRadius: 16,
width: width * 0.4,
width: Viewport.width * 0.4,
},
text_input: {
color: colors.text_default,
backgroundColor: colors.blue_1,
backgroundColor: colors.primary_2,
borderColor: colors.primary_4,
borderWidth: 1,
padding: 10,
borderRadius: 8,
width: width * 0.5,
width: Viewport.width * 0.5,
},
dropdown_template: {
borderRadius: 16,
backgroundColor: colors.primary_2,
containerStyle: colors.primary_2,
borderRadius: 8,
width: "70%",
marginVertical: 6,
},
map: {
flex: 1,
height: containerHeight,
width: containerWidth,
marginVertical: 4,
height: Viewport.height * 0.7,
width: Viewport.width * 0.8,
alignSelf: "center",
},
profile: {
height: 80,
width: 80,
alignSelf: 'center',
},
height: 64,
width: 64,
alignSelf: "center",
borderRadius: 150 / 2,
overflow: "hidden",
padding: 0,
borderColor: colors.primary_2,
borderWidth: 3,
},
profile_mini: {
height: 32,
width: 32,
alignSelf: "center",
borderRadius: 150 / 2,
overflow: "hidden",
padding: 0,
borderColor: colors.primary_2,
borderWidth: 3,
},
input: {
height: 40,
margin: 12,
marginRight: 30,
paddingHorizontal: 8,
marginVertical: 2,
borderWidth: 1,
color: colors.text_default,
backgroundColor: colors.blue_1,
backgroundColor: colors.primary_2,
borderRadius: 8,
borderColor: '#FFAC1C',
padding: 8,
borderColor: colors.primary_3,
},
formGroup: {
display: "flex",
flexDirection: "row",
alignItems: "center",
padding: {
paddingVertical: 8,
},
text: {
marginLeft: 5,
calloutContainer: {
backgroundColor: colors.secondary_1,
padding: 0,
border: 0,
},
chatbox: {
paddingHorizontal: 8,
height: 50,
marginVertical: 10,
borderWidth: 1,
color: colors.text_default,
fontSize: font_sizes.small,
fontWeight: "bold",
},
button: {
backgroundColor: colors.primary_2,
borderRadius: 20,
borderColor: colors.primary_3,
width: 256,
},
messageScrollViewContainer: {
backgroundColor: colors.secondary_1,
padding: 15,
},
message_contentContainer: {
backgroundColor: "#00000038",
margin: 5,
padding: 10,
backgroundColor: colors.blue_2,
borderRadius: 5,
borderRadius: 20,
},
activeText: {
color: 'green',
},
inactiveText: {
color: 'white',
badge: {
height: 16,
width: 16,
justifyContent: "center",
borderRadius: 10,
marginLeft: 4,
marginRight: 4,
},
});
export default styles;