Compare commits

...

115 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
33 changed files with 4171 additions and 2221 deletions

61
App.tsx
View file

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

View file

@ -42,9 +42,15 @@
[ [
"expo-location", "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": { "extra": {
"eas": { "eas": {

2115
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,21 +1,29 @@
import axios from "axios"; import axios from "axios";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import { import {
ActivationParams, ActivationType,
LoginParams, LocationType,
OnboardingParams, LoginType,
PatchStudentData, MessagePostType,
RegistrationParams, OnboardingType,
StudentData, PatchUserInfoType,
RegistrationType,
StudentStatusPatchType,
StudentStatusType,
StudyGroupCreateType,
StudyGroupType,
} from "../../interfaces/Interfaces"; } from "../../interfaces/Interfaces";
export let backendURL = ""; export let backendURL = "https://stude.keannu1.duckdns.org";
export let backendURLWebsocket = ""; 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; let use_production = true;
if (__DEV__ || !use_production) { if (__DEV__ && use_production) {
backendURL = "http://10.0.10.8:8000";
backendURLWebsocket = "ws://10.0.10.8:8000";
} else {
backendURL = "https://stude.keannu1.duckdns.org"; backendURL = "https://stude.keannu1.duckdns.org";
backendURLWebsocket = "ws://stude.keannu1.duckdns.org"; backendURLWebsocket = "ws://stude.keannu1.duckdns.org";
} }
@ -25,8 +33,29 @@ const instance = axios.create({
timeout: 1000, 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 // 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 // Token Handling
export async function getAccessToken() { export async function getAccessToken() {
const accessToken = await AsyncStorage.getItem("access_token"); const accessToken = await AsyncStorage.getItem("access_token");
@ -59,40 +88,28 @@ export async function GetConfig() {
} }
// User APIs // User APIs
export function UserRegister(register: RegistrationParams) { export function UserRegister(register: RegistrationType) {
console.log(JSON.stringify(register));
return instance return instance
.post("/api/v1/accounts/users/", register) .post("/api/v1/accounts/users/", register)
.then(async (response) => { .then(async (response) => {
return [true, response.status]; return [true, response.status];
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
return [false, error_message]; return [false, error_message];
}); });
} }
export function UserLogin(user: LoginParams) { export function UserLogin(user: LoginType) {
return instance return instance
.post("/api/v1/accounts/jwt/create/", user) .post("/api/v1/accounts/jwt/create/", user)
.then(async (response) => { .then(async (response) => {
/*console.log(
"Access Token:",
response.data.access,
"\nRefresh Token:",
response.data.refresh
);*/
setAccessToken(response.data.access); setAccessToken(response.data.access);
setRefreshToken(response.data.refresh); setRefreshToken(response.data.refresh);
return [true]; return [true];
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
// console.log(error_message);
return [false, error_message]; return [false, error_message];
}); });
} }
@ -106,21 +123,14 @@ export async function TokenRefresh() {
}) })
.then(async (response) => { .then(async (response) => {
setAccessToken(response.data.access); setAccessToken(response.data.access);
/*console.log(
"Token refresh success! New Access Token",
response.data.access
);*/
return true; return true;
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
console.log("Token Refresh error:", error_message);
return false; return false;
}); });
} }
export async function UserInfo() { export async function GetUserInfo() {
const config = await GetConfig(); const config = await GetConfig();
return instance return instance
.get("/api/v1/accounts/users/me/", config) .get("/api/v1/accounts/users/me/", config)
@ -129,37 +139,31 @@ export async function UserInfo() {
return [true, response.data]; return [true, response.data];
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
return [false, error_message]; return [false, error_message];
}); });
} }
export async function PatchUserInfo(info: PatchStudentData) { export async function PatchUserInfo(info: PatchUserInfoType) {
const config = await GetConfig(); const config = await GetConfig();
return instance return instance
.patch("/api/v1/accounts/users/me/", info, config) .patch("/api/v1/accounts/users/me/", info, config)
.then((response) => { .then((response) => {
console.log(JSON.stringify(response.data));
return [true, response.data]; return [true, response.data];
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
// console.log(error_message);
return [false, error_message]; return [false, error_message];
}); });
} }
export function UserActivate(activation: ActivationParams) { export function UserActivate(activation: ActivationType) {
return instance return instance
.post("/api/v1/accounts/users/activation/", activation) .post("/api/v1/accounts/users/activation/", activation)
.then(async (response) => { .then(() => {
return true; return true;
}) })
.catch((error) => { .catch(() => {
return false; return false;
}); });
} }
@ -179,9 +183,7 @@ export async function GetCourses() {
return [true, response.data]; return [true, response.data];
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
return [false, error_message]; return [false, error_message];
}); });
} }
@ -199,9 +201,7 @@ export async function GetSemesters() {
return [true, response.data]; return [true, response.data];
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
return [false, error_message]; return [false, error_message];
}); });
} }
@ -215,69 +215,187 @@ export async function GetYearLevels() {
return [true, response.data]; return [true, response.data];
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data;
else error_message = "Unable to reach servers";
return [false, error_message]; return [false, error_message];
}); });
} }
export async function GetSubjects( export async function GetSubjects() {
byCourseOnly: boolean,
course: string,
year_level?: string,
semester?: string
) {
const config = await GetConfig();
console.log("by course only?", byCourseOnly);
// If year level and semester specified,
if (!byCourseOnly && year_level && semester) {
return instance
.get(
"/api/v1/subjects/" + course + "/" + year_level + "/" + semester,
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";
return [false, error_message];
});
}
// If only course is specified
else {
return instance
.get("/api/v1/subjects/" + course, 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";
return [false, error_message];
});
}
}
export async function OnboardingUpdateStudentInfo(info: OnboardingParams) {
const config = await GetConfig(); const config = await GetConfig();
return instance return instance
.patch("/api/v1/accounts/users/me/", info, config) .get("/api/v1/subjects/", config)
.then((response) => { .then((response) => {
console.log(JSON.stringify(response.data));
return [true, response.data]; return [true, response.data];
}) })
.catch((error) => { .catch((error) => {
let error_message = ""; let error_message = ParseError(error);
if (error.response) error_message = error.response.data; return [false, error_message];
else error_message = "Unable to reach servers"; });
console.log("Error updating onboarding info", 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]; 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

@ -17,7 +17,10 @@ export default function Button({ disabled = false, ...props }: props) {
<Pressable <Pressable
disabled={disabled} disabled={disabled}
onPress={props.onPress} onPress={props.onPress}
style={{ ...styles.button_template, ...{ backgroundColor: props.color } }} style={({ pressed }) => [
styles.button_template,
{ backgroundColor: pressed ? colors.primary_2 : props.color },
]}
> >
{props.children} {props.children}
</Pressable> </Pressable>

View file

@ -5,7 +5,10 @@ import { Text, View } from "react-native";
import { colors } from "../../styles"; import { colors } from "../../styles";
import styles from "../../styles"; import styles from "../../styles";
import { RootDrawerParamList } from "../../interfaces/Interfaces"; import {
RootDrawerParamList,
StudentStatusPatchType,
} from "../../interfaces/Interfaces";
import AppIcon from "../../icons/AppIcon/AppIcon"; import AppIcon from "../../icons/AppIcon/AppIcon";
import HomeIcon from "../../icons/HomeIcon/HomeIcon"; import HomeIcon from "../../icons/HomeIcon/HomeIcon";
import LoginIcon from "../../icons/LoginIcon/LoginIcon"; import LoginIcon from "../../icons/LoginIcon/LoginIcon";
@ -18,11 +21,48 @@ import { logout } from "../../features/redux/slices/StatusSlice/StatusSlice";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import UserIcon from "../../icons/UserIcon/UserIcon"; import UserIcon from "../../icons/UserIcon/UserIcon";
import SubjectIcon from "../../icons/SubjectIcon/SubjectIcon"; 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: {}) { export default function CustomDrawerContent(props: {}) {
const debug = false;
const navigation = useNavigation<RootDrawerParamList>(); const navigation = useNavigation<RootDrawerParamList>();
const status = useSelector((state: RootState) => state.status); const status = useSelector((state: RootState) => state.status);
const dispatch = useDispatch(); 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) { if (status.logged_in && status.onboarding) {
return ( return (
<DrawerContentScrollView {...props}> <DrawerContentScrollView {...props}>
@ -38,9 +78,17 @@ export default function CustomDrawerContent(props: {}) {
<DrawerButton <DrawerButton
onPress={async () => { onPress={async () => {
dispatch(logout()); // We don't clear student statuses when logging out on debug
await AsyncStorage.clear(); if (!debug) {
navigation.navigate("Login"); queryClient.clear();
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
} else {
stop_studying_logout.mutate({
active: false,
});
}
}} }}
> >
<LogoutIcon size={32} /> <LogoutIcon size={32} />
@ -84,11 +132,27 @@ export default function CustomDrawerContent(props: {}) {
<SubjectIcon size={32} /> <SubjectIcon size={32} />
<Text style={styles.text_white_medium}>Subjects</Text> <Text style={styles.text_white_medium}>Subjects</Text>
</DrawerButton> </DrawerButton>
<DrawerButton
onPress={() => {
navigation.navigate("Conversation");
}}
>
<MessageIcon size={32} />
<Text style={styles.text_white_medium}>Conversation</Text>
</DrawerButton>
<DrawerButton <DrawerButton
onPress={async () => { onPress={async () => {
dispatch(logout()); // We don't clear student statuses when logging out on debug
await AsyncStorage.clear(); if (debug) {
navigation.navigate("Login"); queryClient.clear();
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
} else {
stop_studying_logout.mutate({
active: false,
});
}
}} }}
> >
<LogoutIcon size={32} /> <LogoutIcon size={32} />
@ -124,7 +188,6 @@ export default function CustomDrawerContent(props: {}) {
<SignupIcon size={32} /> <SignupIcon size={32} />
<Text style={styles.text_white_medium}>Register</Text> <Text style={styles.text_white_medium}>Register</Text>
</DrawerButton> </DrawerButton>
{/* {/*
Debug buttons for accessing revalidation and activation page Debug buttons for accessing revalidation and activation page
<DrawerButton <DrawerButton

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

@ -3,7 +3,7 @@ import { IconProps } from "../../interfaces/Interfaces";
import { Svg, Path } from "react-native-svg"; import { Svg, Path } from "react-native-svg";
import { colors } from "../../styles"; import { colors } from "../../styles";
export default function DropdownIcon(props: IconProps) { export default function CaretDownIcon(props: IconProps) {
return ( return (
<> <>
<Svg <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

@ -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 { export interface IconProps {
size: number; size: number;
} }
@ -9,6 +13,7 @@ export interface ResponsiveIconProps {
export interface RootDrawerParamList { export interface RootDrawerParamList {
navigate: any; navigate: any;
replace: any;
} }
// Redux Interfaces // Redux Interfaces
@ -28,7 +33,7 @@ export interface LoggedInUserState {
// API Interfaces // API Interfaces
export interface RegistrationParams { export interface RegistrationType {
email: string; email: string;
username: string; username: string;
password: string; password: string;
@ -37,12 +42,12 @@ export interface RegistrationParams {
student_id_number: string; student_id_number: string;
} }
export interface LoginParams { export interface LoginType {
username: string; username: string;
password: string; password: string;
} }
export interface ActivationParams { export interface ActivationType {
uid: string; uid: string;
token: string; token: string;
} }
@ -53,80 +58,183 @@ export interface OptionType {
} }
// Semester // Semester
export interface Semester { export interface SemesterType {
id: string; id: string;
name: string; name: string;
shortname: string; shortname: string;
} }
export type Semesters = Array<Semester>; export type SemestersType = Array<SemesterType>;
export type SemesterParams = [boolean, Semesters]; export type SemesterReturnType = [boolean, SemestersType];
// Year Level // Year Level
export interface YearLevel { export interface YearLevelType {
id: string; id: string;
name: string; name: string;
shortname: string; shortname: string;
} }
export type YearLevels = Array<YearLevel>; export type YearLevelsType = Array<YearLevelType>;
export type YearLevelParams = [boolean, YearLevels]; export type YearLevelReturnType = [boolean, YearLevelsType];
// Course // Course
export interface Course { export interface CourseType {
id: string; id: string;
name: string; name: string;
shortname: string; shortname: string;
} }
export type Courses = Array<Course>; export type CoursesType = Array<CourseType>;
export type CourseParams = [boolean, Courses]; export type CourseReturnType = [boolean, CoursesType];
// Subject // Subject
export interface Subject { export interface SubjectType {
id: number;
name: string; name: string;
code: string; code: string;
// courses: any[]; // To-do course: string;
// year_levels: any[]; // To-do year_level: string;
// semesters: any[]; // To-do semester: string;
} }
export type Subjects = Array<Subject>; export type SubjectsType = Array<SubjectType>;
export type SubjectParams = [boolean, Subjects]; export type SubjectsReturnType = [boolean, SubjectsType];
export type AvatarType = {
uri: string;
type: string;
name: string;
};
// For dropdown menu // For dropdown menu
export interface OnboardingParams { export interface OnboardingType {
year_level: string; year_level: string;
course: string; course: string;
semester: string; semester: string;
} }
export interface PatchStudentData { export interface PatchUserInfoType {
course?: string | null; course?: string;
first_name?: string | null; first_name?: string;
last_name?: string | null; last_name?: string;
semester?: string | null; semester?: string;
subjects?: any[] | null; // To-do, replace 'any' with your actual type subjects?: string[];
year_level?: string | null; year_level?: string;
irregular?: boolean;
avatar?: string;
} }
export interface StudentData { 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; first_name: string;
last_name: string; last_name: string;
email: string; email: string;
avatar: string; avatar: string;
student_id_number: string; student_id_number: string;
is_banned: boolean; irregular: boolean;
semester: string; semester: string;
semester_shortname: string; semester_shortname: string;
course: string; course: string;
course_shortname: string; course_shortname: string;
year_level: string; year_level: string;
yearlevel_shortname: string; yearlevel_shortname: string;
subjects: any[]; // To-do subjects: string[];
username: string; username: string;
} }
export type UserInfoParams = [boolean, StudentData]; 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 { useEffect, useState } from "react";
import { UserActivate } from "../../components/Api/Api"; import { UserActivate } from "../../components/Api/Api";
import { RootDrawerParamList } from "../../interfaces/Interfaces"; import { RootDrawerParamList } from "../../interfaces/Interfaces";
import { useToast } from "react-native-toast-notifications";
interface ActivationRouteParams { interface ActivationRouteParams {
uid?: string; uid?: string;
@ -16,9 +17,7 @@ export default function Activation() {
const route = useRoute(); const route = useRoute();
const { uid, token } = (route.params as ActivationRouteParams) || ""; const { uid, token } = (route.params as ActivationRouteParams) || "";
const navigation = useNavigation<RootDrawerParamList>(); const navigation = useNavigation<RootDrawerParamList>();
const [state, setState] = useState( const toast = useToast();
"Activating with UID " + uid + " and Token " + token
);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@ -28,16 +27,22 @@ export default function Activation() {
token: String(token), token: String(token),
}); });
if (result) { if (result) {
setTimeout(() => { toast.show("Activation successful", {
setState("Activation successful!"); type: "success",
}, 1000); placement: "top",
duration: 4000,
animationType: "slide-in",
});
setTimeout(() => { setTimeout(() => {
navigation.navigate("Login"); navigation.navigate("Login");
}, 2000); }, 2000);
} else { } else {
setTimeout(() => { toast.show("Activation unsuccessful. Please contact support", {
setState("Activation unsuccessful\nPlease contact support"); type: "warning",
}, 1000); placement: "top",
duration: 4000,
animationType: "slide-in",
});
} }
setLoading(false); setLoading(false);
} }
@ -63,8 +68,9 @@ export default function Activation() {
size={96} size={96}
color={colors.secondary_1} color={colors.secondary_1}
/> />
<Text style={styles.text_white_medium}>{state}</Text> <Text style={styles.text_white_medium}>
<Text style={styles.text_white_tiny}>{uid + "\n" + token}</Text> {"Activating with UID: " + uid + "\nToken: " + token}
</Text>
</AnimatedContainer> </AnimatedContainer>
</View> </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, NativeSyntheticEvent,
TextInputChangeEventData, TextInputChangeEventData,
} from "react-native"; } from "react-native";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { colors } from "../../styles"; import { useEffect, useState } from "react";
import { useState } from "react";
import LoginIcon from "../../icons/LoginIcon/LoginIcon"; import LoginIcon from "../../icons/LoginIcon/LoginIcon";
import Button from "../../components/Button/Button"; import Button from "../../components/Button/Button";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import { RootDrawerParamList } from "../../interfaces/Interfaces"; import { RootDrawerParamList } from "../../interfaces/Interfaces";
import { UserInfo, UserLogin } from "../../components/Api/Api"; import { GetUserInfo, UserLogin } from "../../components/Api/Api";
import { ParseLoginError } from "../../components/ParseError/ParseError";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer"; import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import { setUser } from "../../features/redux/slices/UserSlice/UserSlice"; import { setUser } from "../../features/redux/slices/UserSlice/UserSlice";
import { import {
@ -23,15 +21,24 @@ import {
setOnboarding, setOnboarding,
unsetOnboarding, unsetOnboarding,
} from "../../features/redux/slices/StatusSlice/StatusSlice"; } from "../../features/redux/slices/StatusSlice/StatusSlice";
import { useToast } from "react-native-toast-notifications";
import { RootState } from "../../features/redux/Store/Store";
export default function Login() { export default function Login() {
const navigation = useNavigation<RootDrawerParamList>(); const navigation = useNavigation<RootDrawerParamList>();
const status = useSelector((state: RootState) => state.status);
const [logging_in, setLoggingIn] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [creds, setCreds] = useState({ const [creds, setCreds] = useState({
username: "", username: "",
password: "", password: "",
}); });
const [error, setError] = useState(""); const toast = useToast();
useEffect(() => {
if (status.logged_in) {
navigation.navigate("Home");
}
}, []);
return ( return (
<View style={styles.background}> <View style={styles.background}>
<AnimatedContainer> <AnimatedContainer>
@ -60,44 +67,62 @@ export default function Login() {
placeholderTextColor="white" placeholderTextColor="white"
secureTextEntry={true} secureTextEntry={true}
value={creds.password} value={creds.password}
autoCapitalize={"none"}
onChange={( onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData> e: NativeSyntheticEvent<TextInputChangeEventData>
): void => { ): void => {
setCreds({ ...creds, password: e.nativeEvent.text }); setCreds({ ...creds, password: e.nativeEvent.text });
}} }}
/> />
<View style={{ paddingVertical: 2 }} />
<Text style={styles.text_white_small}>{error}</Text>
<View style={{ paddingVertical: 4 }} /> <View style={{ paddingVertical: 4 }} />
<Button <Button
onPress={async () => { onPress={async () => {
await UserLogin({ if (!logging_in) {
username: creds.username, await UserLogin({
password: creds.password, username: creds.username,
}).then(async (result) => { password: creds.password,
if (result[0]) { }).then(async (result) => {
setUser({ ...creds, username: "", password: "", error: "" }); if (result[0]) {
let user_info = await UserInfo(); setUser({ ...creds, username: "", password: "", error: "" });
dispatch(login()); let user_info = await GetUserInfo();
dispatch(setUser(user_info[1])); dispatch(login());
// Redirect to onboarding if no year level, course, or semester specified dispatch(setUser(user_info[1]));
if ( // Redirect to onboarding if no year level, course, or semester specified
user_info[1].year_level == null || if (
user_info[1].course == null || user_info[1].year_level == null ||
user_info[1].semester == null user_info[1].course == null ||
) { user_info[1].semester == null
dispatch(setOnboarding()); ) {
navigation.navigate("Onboarding"); 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 { } else {
dispatch(unsetOnboarding()); toast.show(JSON.stringify(result[1]), {
navigation.navigate("Home"); type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
} }
console.log(JSON.stringify(user_info)); setLoggingIn(false);
} else { });
console.log("heh", ParseLoginError(JSON.stringify(result[1]))); }
setError(ParseLoginError(JSON.stringify(result[1])));
}
});
}} }}
> >
<Text style={styles.text_white_small}>Login</Text> <Text style={styles.text_white_small}>Login</Text>

View file

@ -3,10 +3,10 @@ import styles from "../../styles";
import { View, Text } from "react-native"; import { View, Text } from "react-native";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import { import {
Course,
RootDrawerParamList, RootDrawerParamList,
Semester, CourseType,
YearLevel, SemesterType,
YearLevelType,
} from "../../interfaces/Interfaces"; } from "../../interfaces/Interfaces";
import { colors } from "../../styles"; import { colors } from "../../styles";
import { MotiView } from "moti"; import { MotiView } from "moti";
@ -18,18 +18,18 @@ import {
GetCourses, GetCourses,
GetSemesters, GetSemesters,
GetYearLevels, GetYearLevels,
OnboardingUpdateStudentInfo, PatchUserInfo,
} from "../../components/Api/Api"; } from "../../components/Api/Api";
import { useDispatch } from "react-redux"; import { useDispatch } from "react-redux";
import { unsetOnboarding } from "../../features/redux/slices/StatusSlice/StatusSlice"; import { unsetOnboarding } from "../../features/redux/slices/StatusSlice/StatusSlice";
import { setUser } from "../../features/redux/slices/UserSlice/UserSlice"; import { setUser } from "../../features/redux/slices/UserSlice/UserSlice";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll"; import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll";
import { useToast } from "react-native-toast-notifications";
export default function Onboarding() { export default function Onboarding() {
const navigation = useNavigation<RootDrawerParamList>(); const navigation = useNavigation<RootDrawerParamList>();
const dispatch = useDispatch(); const dispatch = useDispatch();
// const creds = useSelector((state: RootState) => state.auth.creds); // const creds = useSelector((state: RootState) => state.auth.creds);
const [error, setError] = useState(""); const toast = useToast();
// Semesters // Semesters
const [selected_semester, setSelectedSemester] = useState(""); const [selected_semester, setSelectedSemester] = useState("");
const [semesterOpen, setSemesterOpen] = useState(false); const [semesterOpen, setSemesterOpen] = useState(false);
@ -39,14 +39,28 @@ export default function Onboarding() {
]); ]);
const semester_query = useQuery({ const semester_query = useQuery({
queryKey: ["semesters"], 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) => { onSuccess: (data) => {
let semesters = data[1].map((item: Semester) => ({ let semesters = data[1].map((item: SemesterType) => ({
label: item.name, label: item.name,
value: item.name, value: item.name,
})); }));
setSemesters(semesters); setSemesters(semesters);
}, },
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
}); });
// Year Level // Year Level
const [selected_yearlevel, setSelectedYearLevel] = useState(""); const [selected_yearlevel, setSelectedYearLevel] = useState("");
@ -57,14 +71,28 @@ export default function Onboarding() {
]); ]);
const yearlevel_query = useQuery({ const yearlevel_query = useQuery({
queryKey: ["year_levels"], 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) => { onSuccess: (data) => {
let year_levels = data[1].map((item: YearLevel) => ({ let year_levels = data[1].map((item: YearLevelType) => ({
label: item.name, label: item.name,
value: item.name, value: item.name,
})); }));
setYearLevels(year_levels); setYearLevels(year_levels);
}, },
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
}); });
// Course // Course
const [selected_course, setSelectedCourse] = useState(""); const [selected_course, setSelectedCourse] = useState("");
@ -78,14 +106,28 @@ export default function Onboarding() {
]); ]);
const course_query = useQuery({ const course_query = useQuery({
queryKey: ["courses"], 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) => { onSuccess: (data) => {
let courses = data[1].map((item: Course) => ({ let courses = data[1].map((item: CourseType) => ({
label: item.name, label: item.name,
value: item.name, value: item.name,
})); }));
setCourses(courses); 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) { if (yearlevel_query.error || semester_query.error || course_query.error) {
return ( return (
@ -153,7 +195,13 @@ export default function Onboarding() {
...styles.text_white_small_bold, ...styles.text_white_small_bold,
...{ textAlign: "center" }, ...{ textAlign: "center" },
}} }}
dropDownContainerStyle={{ backgroundColor: colors.primary_2 }} modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/> />
<DropDownPicker <DropDownPicker
zIndex={2000} zIndex={2000}
@ -173,7 +221,13 @@ export default function Onboarding() {
...styles.text_white_small_bold, ...styles.text_white_small_bold,
...{ textAlign: "center" }, ...{ textAlign: "center" },
}} }}
dropDownContainerStyle={{ backgroundColor: colors.primary_2 }} modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/> />
<DropDownPicker <DropDownPicker
zIndex={1000} zIndex={1000}
@ -193,10 +247,15 @@ export default function Onboarding() {
...styles.text_white_small_bold, ...styles.text_white_small_bold,
...{ textAlign: "center" }, ...{ textAlign: "center" },
}} }}
dropDownContainerStyle={{ backgroundColor: colors.primary_2 }} modalContentContainerStyle={{
backgroundColor: colors.primary_2,
borderWidth: 0,
zIndex: 1000,
}}
dropDownDirection="BOTTOM"
listMode="MODAL"
/> />
</MotiView> </MotiView>
<Text style={styles.text_white_small}>{error}</Text>
<MotiView <MotiView
from={{ from={{
opacity: 0, opacity: 0,
@ -217,7 +276,7 @@ export default function Onboarding() {
!selected_yearlevel || !selected_course || !selected_semester !selected_yearlevel || !selected_course || !selected_semester
} }
onPress={async () => { onPress={async () => {
let result = await OnboardingUpdateStudentInfo({ let result = await PatchUserInfo({
semester: selected_semester, semester: selected_semester,
course: selected_course, course: selected_course,
year_level: selected_yearlevel, year_level: selected_yearlevel,
@ -227,11 +286,25 @@ export default function Onboarding() {
setSelectedCourse(""); setSelectedCourse("");
setSelectedYearLevel(""); setSelectedYearLevel("");
setSelectedSemester(""); setSelectedSemester("");
setError("Success!");
dispatch(setUser(result[1])); dispatch(setUser(result[1]));
toast.show("Changes applied successfully", {
type: "success",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
navigation.navigate("Home"); navigation.navigate("Home");
} else { } 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",
}
);
} }
}} }}
> >

View file

@ -15,13 +15,16 @@ import { RootDrawerParamList } from "../../interfaces/Interfaces";
import SignupIcon from "../../icons/SignupIcon/SignupIcon"; import SignupIcon from "../../icons/SignupIcon/SignupIcon";
import { UserRegister } from "../../components/Api/Api"; import { UserRegister } from "../../components/Api/Api";
import IsNumber from "../../components/IsNumber/IsNumber"; import IsNumber from "../../components/IsNumber/IsNumber";
import ParseError from "../../components/ParseError/ParseError";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer"; import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import { useToast } from "react-native-toast-notifications";
import { ScrollView } from "react-native-gesture-handler";
export default function Register() { export default function Register() {
const navigation = useNavigation<RootDrawerParamList>(); const navigation = useNavigation<RootDrawerParamList>();
const toast = useToast();
// const dispatch = useDispatch(); // const dispatch = useDispatch();
// const creds = useSelector((state: RootState) => state.auth.creds); // const creds = useSelector((state: RootState) => state.auth.creds);
const [registering, setRegistering] = useState(false);
const [user, setUser] = useState({ const [user, setUser] = useState({
first_name: "", first_name: "",
last_name: "", last_name: "",
@ -29,7 +32,7 @@ export default function Register() {
username: "", username: "",
email: "", email: "",
password: "", password: "",
feedback: "", confirm_password: "",
}); });
return ( return (
<View style={styles.background}> <View style={styles.background}>
@ -113,6 +116,7 @@ export default function Register() {
placeholderTextColor={colors.text_default} placeholderTextColor={colors.text_default}
secureTextEntry={true} secureTextEntry={true}
value={user.password} value={user.password}
autoCapitalize={"none"}
onChange={( onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData> e: NativeSyntheticEvent<TextInputChangeEventData>
): void => { ): void => {
@ -120,42 +124,77 @@ export default function Register() {
}} }}
/> />
<View style={{ paddingVertical: 4 }} /> <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 }} /> <View style={{ paddingVertical: 4 }} />
<Button <Button
onPress={async () => { onPress={async () => {
await UserRegister({ if (!registering) {
username: user.username, if (user.password === user.confirm_password) {
email: user.email, setRegistering(true);
password: user.password, await UserRegister({
student_id_number: user.student_id_number, username: user.username,
first_name: user.first_name, email: user.email,
last_name: user.last_name, password: user.password,
}).then((result) => { student_id_number: user.student_id_number,
console.log(result); first_name: user.first_name,
if (result[0]) { last_name: user.last_name,
setUser({ }).then((result: any) => {
...user, console.log(result);
first_name: "", if (result[0]) {
last_name: "", setUser({
student_id_number: "", ...user,
username: "", first_name: "",
email: "", last_name: "",
password: "", student_id_number: "",
feedback: username: "",
"Success! An email has been sent to activate your account", 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 { } else {
setUser({ toast.show(
...user, "Password does not match confirm password. Please try again"
feedback: ParseError(JSON.stringify(result[1])), ),
}); {
type: "warning",
placement: "top",
duration: 6000,
animationType: "slide-in",
};
} }
});
{
} }
}} }}
> >

View file

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

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

@ -1,42 +1,63 @@
import * as React from "react"; import * as React from "react";
import styles from "../../styles"; import styles from "../../styles";
import { View, Text, ActivityIndicator } from "react-native";
import { useState } from "react";
import { import {
View, UserInfoReturnType,
Text, SubjectsReturnType,
TextInput, SubjectType,
NativeSyntheticEvent,
TextInputChangeEventData,
} from "react-native";
import { useEffect, useState } from "react";
import {
SemesterParams,
UserInfoParams,
Semester,
SubjectParams,
Subject,
YearLevel,
Course,
OptionType, OptionType,
Subjects, StudentStatusType,
PatchUserInfoType,
StudentStatusPatchType,
} from "../../interfaces/Interfaces"; } from "../../interfaces/Interfaces";
import Button from "../../components/Button/Button"; import Button from "../../components/Button/Button";
import { Image } from "react-native"; import { Image } from "react-native";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
GetCourses,
GetSemesters,
GetSubjects, GetSubjects,
GetYearLevels,
PatchUserInfo, PatchUserInfo,
UserInfo, GetUserInfo,
PatchStudentStatus,
} from "../../components/Api/Api"; } from "../../components/Api/Api";
import { colors } from "../../styles"; import { colors } from "../../styles";
import DropDownPicker from "react-native-dropdown-picker"; import DropDownPicker from "react-native-dropdown-picker";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll"; 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 { 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() { export default function SubjectsPage() {
const logged_in_user = useSelector((state: RootState) => state.user.user);
const queryClient = useQueryClient(); 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 // User Info
const [user, setUser] = useState({ const [user, setUser] = useState({
first_name: "", first_name: "",
@ -49,17 +70,18 @@ export default function SubjectsPage() {
course_shortname: "", course_shortname: "",
avatar: "", avatar: "",
student_id_number: "", student_id_number: "",
subjects: [] as Subjects, subjects: [] as string[],
});
const [displayName, setDisplayName] = useState({
first_name: "",
last_name: "",
}); });
const StudentInfo = useQuery({ const StudentInfo = useQuery({
queryKey: ["user"], queryKey: ["user"],
queryFn: UserInfo, queryFn: async () => {
onSuccess: (data: UserInfoParams) => { const data = await GetUserInfo();
// console.log(data[1]); if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data: UserInfoReturnType) => {
setUser({ setUser({
...user, ...user,
first_name: data[1].first_name, first_name: data[1].first_name,
@ -71,31 +93,50 @@ export default function SubjectsPage() {
student_id_number: data[1].student_id_number, student_id_number: data[1].student_id_number,
subjects: data[1].subjects, subjects: data[1].subjects,
}); });
setDisplayName({ setSelectedSubjects(data[1].subjects);
first_name: data[1].first_name, },
last_name: data[1].last_name, onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
}); });
setSelectedSubjects(user.subjects);
}, },
}); });
const mutation = useMutation({ const mutation = useMutation({
mutationFn: PatchUserInfo, mutationFn: async (info: PatchUserInfoType) => {
const data = await PatchUserInfo(info);
if (data[0] != true) {
return Promise.reject(new Error());
}
return data;
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["subjects", viewAll] }); queryClient.invalidateQueries({ queryKey: ["subjects"] });
setSelectedSubjects([]); 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",
});
}, },
}); });
// View all Subjects or only view those under current course, year level, and semester
// This is for irregular students
const [viewAll, setViewAll] = useState(false);
// If viewing all subjects, refresh the choices
useEffect(() => {
queryClient.invalidateQueries({ queryKey: ["subjects", viewAll] });
}, [viewAll]);
// Subjects // Subjects
const [selected_subjects, setSelectedSubjects] = useState<any>([]); const [selected_subjects, setSelectedSubjects] = useState<any>([]);
@ -104,42 +145,31 @@ export default function SubjectsPage() {
const Subjects = useQuery({ const Subjects = useQuery({
enabled: StudentInfo.isFetched, enabled: StudentInfo.isFetched,
queryKey: ["subjects", viewAll], queryKey: ["subjects"],
queryFn: async () => { queryFn: async () => {
let data; const data = await GetSubjects();
if ( if (data[0] == false) {
StudentInfo.data && return Promise.reject(new Error(JSON.stringify(data[1])));
StudentInfo.data[1].course_shortname &&
StudentInfo.data[1].yearlevel_shortname &&
StudentInfo.data[1].semester_shortname
) {
data = await GetSubjects(
viewAll,
StudentInfo.data[1].course_shortname,
StudentInfo.data[1].yearlevel_shortname,
StudentInfo.data[1].semester_shortname
);
console.log(JSON.stringify(data));
}
if (data) {
if (!data[0]) {
throw new Error("Error with query" + data[1]);
}
if (!data[1]) {
throw new Error("User has no course, year level, or semester!");
}
// console.log("Subjects available:", data[1]);
} }
return data; return data;
}, },
onSuccess: (data: SubjectParams) => { onSuccess: (data: SubjectsReturnType) => {
let subjectsData = data[1].map((subject: Subject) => ({ if (data[1]) {
label: subject.name, let subjects = data[1].map((subject: SubjectType) => ({
value: subject.name, label: subject.name,
})); value: subject.name,
// Update the 'subjects' state }));
setSelectedSubjects(user.subjects);
setSubjects(subjectsData); setSubjects(subjects);
}
},
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
}, },
}); });
@ -156,16 +186,18 @@ export default function SubjectsPage() {
); );
} }
} }
if (StudentInfo.isLoading || Subjects.isLoading) {
return <LoadingFeedback />;
}
return ( return (
<View style={styles.background}> <View style={styles.background}>
<AnimatedContainerNoScroll> <AnimatedContainerNoScroll>
<View style={styles.flex_row}> <View style={styles.flex_row}>
<Avatar /> <Avatar />
<Text style={{ ...styles.text_white_small, ...{ marginLeft: 16 } }}> <Text style={{ ...styles.text_white_small, ...{ marginLeft: 16 } }}>
{(displayName.first_name || "Undefined") + {(logged_in_user.first_name || "Undefined") +
" " + " " +
(displayName.last_name || "User") + (logged_in_user.last_name || "User") +
"\n" + "\n" +
user.student_id_number} user.student_id_number}
</Text> </Text>
@ -211,29 +243,17 @@ export default function SubjectsPage() {
</View> </View>
</View> </View>
<View style={{ zIndex: -1 }}> <View style={{ zIndex: -1 }}>
<View style={styles.padding} />
<View style={styles.flex_row}>
<BouncyCheckbox
onPress={() => {
setViewAll(!viewAll);
setSubjectsOpen(false);
}}
fillColor={colors.secondary_3}
/>
<Text style={styles.text_white_small}>Irregular </Text>
</View>
<View style={styles.padding} /> <View style={styles.padding} />
<Button <Button
onPress={() => { onPress={() => {
setSelectedSubjects([]);
setSubjectsOpen(!subjectsOpen);
mutation.mutate({ mutation.mutate({
subjects: selected_subjects, subjects: selected_subjects,
}); });
}} }}
> >
<Text style={styles.text_white_small}>Save Change</Text> <Text style={styles.text_white_small}>Save Changes</Text>
</Button> </Button>
<View style={styles.padding} />
</View> </View>
</AnimatedContainerNoScroll> </AnimatedContainerNoScroll>
</View> </View>

View file

@ -6,18 +6,20 @@ import {
TextInput, TextInput,
NativeSyntheticEvent, NativeSyntheticEvent,
TextInputChangeEventData, TextInputChangeEventData,
Pressable,
ActivityIndicator,
} from "react-native"; } from "react-native";
import { useState } from "react"; import { useState } from "react";
import { import {
SemesterParams, SemesterReturnType,
UserInfoParams, UserInfoReturnType,
Semester, SemesterType,
SubjectParams, YearLevelType,
Subject, CourseType,
YearLevel,
Course,
OptionType, OptionType,
Subjects, StudentStatusType,
PatchUserInfoType,
StudentStatusPatchType,
} from "../../interfaces/Interfaces"; } from "../../interfaces/Interfaces";
import Button from "../../components/Button/Button"; import Button from "../../components/Button/Button";
import { Image } from "react-native"; import { Image } from "react-native";
@ -25,18 +27,55 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { import {
GetCourses, GetCourses,
GetSemesters, GetSemesters,
GetSubjects,
GetYearLevels, GetYearLevels,
PatchStudentStatus,
PatchUserInfo, PatchUserInfo,
UserInfo, GetUserInfo,
} from "../../components/Api/Api"; } from "../../components/Api/Api";
import { colors } from "../../styles"; import { colors } from "../../styles";
import DropDownPicker from "react-native-dropdown-picker"; import DropDownPicker from "react-native-dropdown-picker";
import { ValueType } from "react-native-dropdown-picker";
import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll"; 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() { export default function UserInfoPage() {
const logged_in_user = useSelector((state: RootState) => state.user.user);
const dispatch = useDispatch();
const queryClient = useQueryClient(); 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 // User Info
const [user, setUser] = useState({ const [user, setUser] = useState({
first_name: "", first_name: "",
@ -49,15 +88,18 @@ export default function UserInfoPage() {
course_shortname: "", course_shortname: "",
avatar: "", avatar: "",
student_id_number: "", student_id_number: "",
}); irregular: false,
const [displayName, setDisplayName] = useState({
first_name: "",
last_name: "",
}); });
const StudentInfo = useQuery({ const StudentInfo = useQuery({
queryKey: ["user"], queryKey: ["user"],
queryFn: UserInfo, queryFn: async () => {
onSuccess: (data: UserInfoParams) => { const data = await GetUserInfo();
if (data[0] == false) {
return Promise.reject(new Error(data[1]));
}
return data;
},
onSuccess: (data: UserInfoReturnType) => {
// console.log(data[1]); // console.log(data[1]);
setUser({ setUser({
...user, ...user,
@ -66,23 +108,55 @@ export default function UserInfoPage() {
year_level: data[1].year_level, year_level: data[1].year_level,
semester: data[1].semester, semester: data[1].semester,
course: data[1].course, course: data[1].course,
avatar: data[1].avatar,
student_id_number: data[1].student_id_number, student_id_number: data[1].student_id_number,
}); irregular: data[1].irregular,
setDisplayName({ avatar: data[1].avatar,
first_name: data[1].first_name,
last_name: data[1].last_name,
}); });
setSelectedCourse(data[1].course); setSelectedCourse(data[1].course);
setSelectedSemester(data[1].semester); setSelectedSemester(data[1].semester);
setSelectedYearLevel(data[1].year_level); 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({ const mutation = useMutation({
mutationFn: PatchUserInfo, mutationFn: async (info: PatchUserInfoType) => {
const data = await PatchUserInfo(info);
if (data[0] == false) {
return Promise.reject(new Error());
}
return data;
},
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["user"] });
queryClient.invalidateQueries({ queryKey: ["subjects"] }); 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",
});
}, },
}); });
@ -92,9 +166,15 @@ export default function UserInfoPage() {
const [semesters, setSemesters] = useState<OptionType[]>([]); const [semesters, setSemesters] = useState<OptionType[]>([]);
const Semesters = useQuery({ const Semesters = useQuery({
queryKey: ["semesters"], queryKey: ["semesters"],
queryFn: GetSemesters, queryFn: async () => {
onSuccess: (data: SemesterParams) => { const data = await GetSemesters();
let semestersData = data[1].map((semester: Semester) => ({ 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, label: semester.name,
value: semester.name, value: semester.name,
shortname: semester.shortname, shortname: semester.shortname,
@ -102,6 +182,14 @@ export default function UserInfoPage() {
// Update the 'semesters' state // Update the 'semesters' state
setSemesters(semestersData); setSemesters(semestersData);
}, },
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
}); });
// Year Level // Year Level
@ -110,14 +198,28 @@ export default function UserInfoPage() {
const [year_levels, setYearLevels] = useState<OptionType[]>([]); const [year_levels, setYearLevels] = useState<OptionType[]>([]);
const yearlevel_query = useQuery({ const yearlevel_query = useQuery({
queryKey: ["year_levels"], 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) => { onSuccess: (data) => {
let year_levels = data[1].map((yearlevel: YearLevel) => ({ let year_levels = data[1].map((yearlevel: YearLevelType) => ({
label: yearlevel.name, label: yearlevel.name,
value: yearlevel.name, value: yearlevel.name,
})); }));
setYearLevels(year_levels); setYearLevels(year_levels);
}, },
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
}); });
// Course // Course
@ -126,40 +228,84 @@ export default function UserInfoPage() {
const [courses, setCourses] = useState<OptionType[]>([]); const [courses, setCourses] = useState<OptionType[]>([]);
const course_query = useQuery({ const course_query = useQuery({
queryKey: ["courses"], 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) => { onSuccess: (data) => {
let courses = data[1].map((course: Course) => ({ let courses = data[1].map((course: CourseType) => ({
label: course.name, label: course.name,
value: course.name, value: course.name,
})); }));
setCourses(courses); setCourses(courses);
}, },
onError: (error: Error) => {
toast.show(String(error), {
type: "warning",
placement: "top",
duration: 2000,
animationType: "slide-in",
});
},
}); });
// Toggle editing of profile
const [isEditable, setIsEditable] = useState(false);
// Profile photo // 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() { function Avatar() {
if (user.avatar) { if (user.avatar) {
return <Image source={{ uri: user.avatar }} style={styles.profile} />; return (
<Pressable onPress={pickImage}>
<Image source={{ uri: user.avatar }} style={styles.profile} />
</Pressable>
);
} else { } else {
return ( return (
<Image <Pressable onPress={pickImage}>
source={require("../../img/user_profile_placeholder.png")} <Image
style={{ ...styles.profile, ...{ marginRight: 48 } }} 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 ( return (
<View style={styles.background}> <View style={styles.background}>
<AnimatedContainerNoScroll> <AnimatedContainerNoScroll>
<View style={styles.flex_row}> <View style={styles.flex_row}>
<Avatar /> <Avatar />
<Text style={{ ...styles.text_white_small, ...{ marginLeft: 16 } }}> <Text style={{ ...styles.text_white_small, ...{ marginLeft: 16 } }}>
{(displayName.first_name || "Undefined") + {(logged_in_user.first_name || "Undefined") +
" " + " " +
(displayName.last_name || "User") + (logged_in_user.last_name || "User") +
"\n" + "\n" +
user.student_id_number} user.student_id_number}
</Text> </Text>
@ -173,7 +319,6 @@ export default function UserInfoPage() {
<View style={{ flex: 3 }}> <View style={{ flex: 3 }}>
<TextInput <TextInput
style={styles.input} style={styles.input}
editable={isEditable}
onChange={( onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData> e: NativeSyntheticEvent<TextInputChangeEventData>
): void => { ): void => {
@ -190,7 +335,6 @@ export default function UserInfoPage() {
<View style={{ flex: 3 }}> <View style={{ flex: 3 }}>
<TextInput <TextInput
style={styles.input} style={styles.input}
editable={isEditable}
onChange={( onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData> e: NativeSyntheticEvent<TextInputChangeEventData>
): void => { ): void => {
@ -206,8 +350,7 @@ export default function UserInfoPage() {
</View> </View>
<View style={{ flex: 3 }}> <View style={{ flex: 3 }}>
<DropDownPicker <DropDownPicker
disabled={!isEditable} zIndex={1000}
zIndex={4000}
open={yearLevelOpen} open={yearLevelOpen}
value={selected_yearlevel} value={selected_yearlevel}
items={year_levels} items={year_levels}
@ -229,7 +372,7 @@ export default function UserInfoPage() {
}} }}
dropDownContainerStyle={{ dropDownContainerStyle={{
backgroundColor: colors.primary_2, backgroundColor: colors.primary_2,
zIndex: 4000, zIndex: 1000,
borderWidth: 0, borderWidth: 0,
}} }}
dropDownDirection="TOP" dropDownDirection="TOP"
@ -242,8 +385,7 @@ export default function UserInfoPage() {
</View> </View>
<View style={{ flex: 3 }}> <View style={{ flex: 3 }}>
<DropDownPicker <DropDownPicker
disabled={!isEditable} zIndex={2000}
zIndex={3000}
open={semesterOpen} open={semesterOpen}
value={selected_semester} value={selected_semester}
items={semesters} items={semesters}
@ -265,7 +407,7 @@ export default function UserInfoPage() {
}} }}
dropDownContainerStyle={{ dropDownContainerStyle={{
backgroundColor: colors.primary_2, backgroundColor: colors.primary_2,
zIndex: 3000, zIndex: 2000,
borderWidth: 0, borderWidth: 0,
}} }}
dropDownDirection="TOP" dropDownDirection="TOP"
@ -278,8 +420,7 @@ export default function UserInfoPage() {
</View> </View>
<View style={{ flex: 3 }}> <View style={{ flex: 3 }}>
<DropDownPicker <DropDownPicker
disabled={!isEditable} zIndex={3000}
zIndex={2000}
open={courseOpen} open={courseOpen}
value={selected_course} value={selected_course}
items={courses} items={courses}
@ -301,7 +442,7 @@ export default function UserInfoPage() {
}} }}
dropDownContainerStyle={{ dropDownContainerStyle={{
backgroundColor: colors.primary_2, backgroundColor: colors.primary_2,
zIndex: 2000, zIndex: 3000,
borderWidth: 0, borderWidth: 0,
}} }}
dropDownDirection="TOP" dropDownDirection="TOP"
@ -310,27 +451,37 @@ export default function UserInfoPage() {
</View> </View>
<View style={styles.padding} /> <View style={styles.padding} />
<View style={{ zIndex: -1 }}> <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 <Button
onPress={() => { onPress={() => {
if (isEditable) { setYearLevelOpen(false);
setYearLevelOpen(false); setSemesterOpen(false);
setSemesterOpen(false); setCourseOpen(false);
setCourseOpen(false); mutation.mutate({
mutation.mutate({ first_name: user.first_name,
first_name: user.first_name, last_name: user.last_name,
last_name: user.last_name, course: selected_course,
course: selected_course, semester: selected_semester,
semester: selected_semester, year_level: selected_yearlevel,
year_level: selected_yearlevel, });
});
}
setIsEditable(!isEditable);
}} }}
> >
<Text style={styles.text_white_small}> <Text style={styles.text_white_small}>Save Changes</Text>
{isEditable && StudentInfo.isSuccess ? "Save" : "Edit Profile"}
</Text>
</Button> </Button>
<View style={styles.padding} />
</View> </View>
</AnimatedContainerNoScroll> </AnimatedContainerNoScroll>
</View> </View>

View file

@ -44,8 +44,9 @@ const styles = StyleSheet.create({
justifyContent: "center", justifyContent: "center",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
flex: 1, flexGrow: 1,
paddingHorizontal: 4, paddingHorizontal: 4,
paddingVertical: 32,
}, },
flex_row: { flex_row: {
display: "flex", display: "flex",
@ -171,7 +172,8 @@ const styles = StyleSheet.create({
width: "70%", width: "70%",
}, },
map: { map: {
height: Viewport.height * 0.8, marginVertical: 4,
height: Viewport.height * 0.7,
width: Viewport.width * 0.8, width: Viewport.width * 0.8,
alignSelf: "center", alignSelf: "center",
}, },
@ -182,6 +184,18 @@ const styles = StyleSheet.create({
borderRadius: 150 / 2, borderRadius: 150 / 2,
overflow: "hidden", overflow: "hidden",
padding: 0, 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: { input: {
paddingHorizontal: 8, paddingHorizontal: 8,
@ -200,5 +214,34 @@ const styles = StyleSheet.create({
padding: 0, padding: 0,
border: 0, border: 0,
}, },
chatbox: {
paddingHorizontal: 8,
height: 50,
marginVertical: 10,
borderWidth: 1,
color: colors.text_default,
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,
borderRadius: 20,
},
badge: {
height: 16,
width: 16,
justifyContent: "center",
borderRadius: 10,
marginLeft: 4,
marginRight: 4,
},
}); });
export default styles; export default styles;