diff --git a/App.tsx b/App.tsx index 8ab50a5..d1d9e46 100644 --- a/App.tsx +++ b/App.tsx @@ -17,6 +17,7 @@ import Onboarding from "./src/routes/Onboarding/Onboarding"; import Revalidation from "./src/routes/Revalidation/Revalidation"; import Activation from "./src/routes/Activation/Activation"; import { useState, useEffect } from "react"; +import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; const Drawer = createDrawerNavigator(); @@ -35,6 +36,8 @@ const linking = { }, }; +const queryClient = new QueryClient(); + export default function App() { const [initialRoute, setInitialRoute] = useState(null); useEffect(() => { @@ -50,20 +53,22 @@ export default function App() { }, [initialRoute]); return ( - - - - - - - - - - + + + + + + + + + + + + ); } diff --git a/package-lock.json b/package-lock.json index bc946b2..02e3d1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@react-navigation/native": "^6.1.7", "@react-navigation/native-stack": "^6.9.13", "@reduxjs/toolkit": "^1.9.5", + "@tanstack/react-query": "^4.29.19", "axios": "^1.4.0", "expo": "~48.0.18", "expo-linking": "~4.0.1", @@ -20,6 +21,7 @@ "moti": "^0.25.3", "react": "18.2.0", "react-native": "0.71.8", + "react-native-dropdown-picker": "^5.4.6", "react-native-gesture-handler": "~2.9.0", "react-native-reanimated": "~2.14.4", "react-native-safe-area-context": "4.5.0", @@ -5201,6 +5203,41 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tanstack/query-core": { + "version": "4.29.19", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.29.19.tgz", + "integrity": "sha512-uPe1DukeIpIHpQi6UzIgBcXsjjsDaLnc7hF+zLBKnaUlh7jFE/A+P8t4cU4VzKPMFB/C970n/9SxtpO5hmIRgw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "4.29.19", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.29.19.tgz", + "integrity": "sha512-XiTIOHHQ5Cw1WUlHaD4fmVUMhoWjuNJlAeJGq7eM4BraI5z7y8WkZO+NR8PSuRnQGblpuVdjClQbDFtwxTtTUw==", + "dependencies": { + "@tanstack/query-core": "4.29.19", + "use-sync-external-store": "^1.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/@types/hammerjs": { "version": "2.0.41", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz", @@ -12160,6 +12197,15 @@ "nullthrows": "^1.1.1" } }, + "node_modules/react-native-dropdown-picker": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/react-native-dropdown-picker/-/react-native-dropdown-picker-5.4.6.tgz", + "integrity": "sha512-T1XBHbE++M6aRU3wFYw3MvcOuabhWZ29RK/Ivdls2r1ZkZ62iEBZknLUPeVLMX3x6iUxj4Zgr3X2DGlEGXeHsA==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-gesture-handler": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz", diff --git a/package.json b/package.json index 5c2ccd0..3caa37d 100644 --- a/package.json +++ b/package.json @@ -14,20 +14,22 @@ "@react-navigation/native": "^6.1.7", "@react-navigation/native-stack": "^6.9.13", "@reduxjs/toolkit": "^1.9.5", + "@tanstack/react-query": "^4.29.19", "axios": "^1.4.0", "expo": "~48.0.18", + "expo-linking": "~4.0.1", "expo-status-bar": "~1.4.4", "moti": "^0.25.3", "react": "18.2.0", "react-native": "0.71.8", + "react-native-dropdown-picker": "^5.4.6", "react-native-gesture-handler": "~2.9.0", "react-native-reanimated": "~2.14.4", "react-native-safe-area-context": "4.5.0", "react-native-screens": "~3.20.0", "react-native-svg": "13.4.0", "react-redux": "^8.1.1", - "redux": "^4.2.1", - "expo-linking": "~4.0.1" + "redux": "^4.2.1" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 47d5bf7..5a5ddcb 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -3,6 +3,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { ActivationParams, LoginParams, + OnboardingParams, RegistrationParams, } from "../../interfaces/Interfaces"; @@ -51,7 +52,10 @@ export function UserRegister(register: RegistrationParams) { return [true, response.status]; }) .catch((error) => { - return [false, error.response.status, error.response.data]; + let error_message = ""; + if (error.response) error_message = error.response.data; + else error_message = "Unable to reach servers"; + return [false, error_message]; }); } @@ -70,8 +74,10 @@ export function UserLogin(user: LoginParams) { return [true]; }) .catch((error) => { - console.log("Login Failed:" + JSON.stringify(error.response.data)); - return [false, error.response.data]; + let error_message = ""; + if (error.response) error_message = error.response.data; + else error_message = "Unable to reach servers"; + return [false, error_message]; }); } @@ -88,11 +94,14 @@ export async function TokenRefresh() { "Token refresh success! New Access Token", response.data.access );*/ - return [true]; + return true; }) .catch((error) => { - console.log("Refresh Failed: " + JSON.stringify(error.response.data)); - return [false, error.response.data]; + let error_message = ""; + if (error.response) error_message = error.response.data; + else error_message = "Unable to reach servers"; + console.log("Token Refresh error:", error_message); + return false; }); } export async function UserInfo() { @@ -105,11 +114,13 @@ export async function UserInfo() { }) .then((response) => { // console.log(JSON.stringify(response.data)); - return response.data; + return [true, response.data]; }) .catch((error) => { - console.log("User Info Error", error.response.data); - return [false, error.response.data]; + let error_message = ""; + if (error.response) error_message = error.response.data; + else error_message = "Unable to reach servers"; + return [false, error_message]; }); } @@ -125,3 +136,86 @@ export function UserActivate(activation: ActivationParams) { } // App APIs + +export async function GetCourses() { + const accessToken = await getAccessToken(); + return instance + .get("/api/v1/courses/", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .then((response) => { + // console.log(JSON.stringify(response.data)); + return response.data; + }) + .catch((error) => { + let error_message = ""; + if (error.response) error_message = error.response.data; + else error_message = "Unable to reach servers"; + console.log("Error getting courses", error_message); + return false; + }); +} + +export async function GetSemesters() { + const accessToken = await getAccessToken(); + return instance + .get("/api/v1/semesters/", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .then((response) => { + // console.log(JSON.stringify(response.data)); + return response.data; + }) + .catch((error) => { + let error_message = ""; + if (error.response) error_message = error.response.data; + else error_message = "Unable to reach servers"; + console.log("Error getting semesters", error_message); + return false; + }); +} + +export async function GetYearLevels() { + const accessToken = await getAccessToken(); + return instance + .get("/api/v1/year_levels/", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }) + .then((response) => { + // console.log(JSON.stringify(response.data)); + return response.data; + }) + .catch((error) => { + let error_message = ""; + if (error.response) error_message = error.response.data; + else error_message = "Unable to reach servers"; + console.log("Error getting year levels", error_message); + return false; + }); +} + +export async function OnboardingUpdateStudentInfo(info: OnboardingParams) { + const accessToken = await getAccessToken(); + const headers = { + Authorization: `Bearer ${accessToken}`, + }; + return instance + .patch("/api/v1/accounts/users/me/", info, { headers }) + .then((response) => { + console.log(JSON.stringify(response.data)); + return [true, response.data]; + }) + .catch((error) => { + let error_message = ""; + if (error.response) error_message = error.response.data; + else error_message = "Unable to reach servers"; + console.log("Error updating onboarding info", error_message); + return [false, error_message]; + }); +} diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 6a843c7..83b9d7b 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -6,15 +6,25 @@ export interface props { children: React.ReactNode; onPress: (event: GestureResponderEvent) => void; color: string; + disabled?: boolean; } -export default function Button(props: props) { +export default function Button({ disabled = false, ...props }: props) { + const rgb = props.color.match(/\d+/g); return ( {props.children} diff --git a/src/components/DrawerSettings/CustomDrawerContent.tsx b/src/components/DrawerSettings/CustomDrawerContent.tsx index f453b46..74d5a4f 100644 --- a/src/components/DrawerSettings/CustomDrawerContent.tsx +++ b/src/components/DrawerSettings/CustomDrawerContent.tsx @@ -14,16 +14,40 @@ import DrawerButton from "../Button/DrawerButton"; import { useDispatch, useSelector } from "react-redux"; import { RootState } from "../../features/redux/Store/Store"; import LogoutIcon from "../../icons/LogoutIcon/LogoutIcon"; -import { clear } from "../../features/redux/slices/AuthSlice/AuthSlice"; +import { logout } from "../../features/redux/slices/StatusSlice/StatusSlice"; import AsyncStorage from "@react-native-async-storage/async-storage"; export default function CustomDrawerContent(props: {}) { const navigation = useNavigation(); - const logged_in = useSelector( - (state: RootState) => state.auth.creds.logged_in - ); + const status = useSelector((state: RootState) => state.status); const dispatch = useDispatch(); - if (logged_in) { + if (status.logged_in && status.onboarding) { + return ( + + + + Stud-E + + + { + dispatch(logout()); + await AsyncStorage.clear(); + navigation.navigate("Login"); + }} + > + + Logout + + + ); + } else if (status.logged_in) { return ( { - dispatch(await clear()); + dispatch(logout()); await AsyncStorage.clear(); navigation.navigate("Login"); }} @@ -69,15 +93,6 @@ export default function CustomDrawerContent(props: {}) { Stud-E - { - navigation.navigate("Home"); - }} - > - - Home - { @@ -90,7 +105,6 @@ export default function CustomDrawerContent(props: {}) { { - dispatch(clear()); navigation.navigate("Register"); }} > diff --git a/src/components/IsStringEmpty/IsStringEmpty.tsx b/src/components/IsStringEmpty/IsStringEmpty.tsx new file mode 100644 index 0000000..b1cd9d1 --- /dev/null +++ b/src/components/IsStringEmpty/IsStringEmpty.tsx @@ -0,0 +1,3 @@ +export default function isStringEmpty(str: string) { + return str === "" || str === null || str === undefined; +} diff --git a/src/features/redux/Store/Store.tsx b/src/features/redux/Store/Store.tsx index 55a6590..9fd7cd1 100644 --- a/src/features/redux/Store/Store.tsx +++ b/src/features/redux/Store/Store.tsx @@ -1,9 +1,11 @@ import { configureStore } from "@reduxjs/toolkit"; -import AuthReducer from "../slices/AuthSlice/AuthSlice"; +import StatusReducer from "../slices/StatusSlice/StatusSlice"; +import UserReducer from "../slices/UserSlice/UserSlice"; const store = configureStore({ reducer: { - auth: AuthReducer, + status: StatusReducer, + user: UserReducer, }, }); diff --git a/src/features/redux/slices/AuthSlice/AuthSlice.tsx b/src/features/redux/slices/AuthSlice/AuthSlice.tsx deleted file mode 100644 index b575579..0000000 --- a/src/features/redux/slices/AuthSlice/AuthSlice.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; - -export const AuthSlice = createSlice({ - name: "Auth", - initialState: { - creds: { - email: "", - uid: "", - username: "", - full_name: "", - logged_in: false, - }, - }, - reducers: { - login: (state) => { - state.creds.logged_in = true; - }, - setUser: (state, action) => { - state.creds = { - email: action.payload.email, - uid: action.payload.uid, - username: action.payload.username, - full_name: action.payload.full_name, - logged_in: true, - }; - }, - clear: (state) => { - state.creds = { - email: "", - uid: "", - username: "", - full_name: "", - logged_in: false, - }; - }, - }, -}); - -// Action creators are generated for each case reducer function -export const { login, setUser, clear } = AuthSlice.actions; - -export default AuthSlice.reducer; diff --git a/src/features/redux/slices/StatusSlice/StatusSlice.tsx b/src/features/redux/slices/StatusSlice/StatusSlice.tsx new file mode 100644 index 0000000..2baa661 --- /dev/null +++ b/src/features/redux/slices/StatusSlice/StatusSlice.tsx @@ -0,0 +1,29 @@ +import { createSlice } from "@reduxjs/toolkit"; + +export const StatusSlice = createSlice({ + name: "Status", + initialState: { + logged_in: false, + onboarding: false, + }, + reducers: { + login: (state) => { + state.logged_in = true; + }, + logout: (state) => { + state.logged_in = false; + }, + setOnboarding: (state) => { + state.onboarding = true; + }, + unsetOnboarding: (state) => { + state.onboarding = false; + }, + }, +}); + +// Action creators are generated for each case reducer function +export const { login, logout, setOnboarding, unsetOnboarding } = + StatusSlice.actions; + +export default StatusSlice.reducer; diff --git a/src/features/redux/slices/UserSlice/UserSlice.tsx b/src/features/redux/slices/UserSlice/UserSlice.tsx new file mode 100644 index 0000000..3927136 --- /dev/null +++ b/src/features/redux/slices/UserSlice/UserSlice.tsx @@ -0,0 +1,51 @@ +import { createSlice } from "@reduxjs/toolkit"; + +export const UserSlice = createSlice({ + name: "User", + initialState: { + user: { + email: "", + uid: "", + username: "", + first_name: "", + last_name: "", + full_name: "", + year_level: "", + semester: " ", + course: "", + }, + }, + reducers: { + setUser: (state, action) => { + state.user = { + email: action.payload.email, + uid: action.payload.uid, + username: action.payload.username, + first_name: action.payload.first_name, + last_name: action.payload.last_name, + full_name: action.payload.first_name + " " + action.payload.last_name, + year_level: action.payload.year_level, + semester: action.payload.semester, + course: action.payload.course, + }; + }, + clear: (state) => { + state.user = { + email: "", + uid: "", + username: "", + first_name: "", + last_name: "", + full_name: "", + year_level: "", + semester: " ", + course: "", + }; + }, + }, +}); + +// Action creators are generated for each case reducer function +export const { setUser, clear } = UserSlice.actions; + +export default UserSlice.reducer; diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index 6c6be0e..d9671ec 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -46,3 +46,27 @@ export interface ActivationParams { uid: string; token: string; } + +export interface SemesterParams { + id: string; + name: string; + shortname: string; +} + +export interface YearLevelParams { + id: string; + name: string; + shortname: string; +} + +export interface CourseParams { + id: string; + name: string; + shortname: string; +} + +export interface OnboardingParams { + year_level: string; + course: string; + semester: string; +} diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 944d131..96c6532 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -6,7 +6,7 @@ import { RootState } from "../../features/redux/Store/Store"; import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer"; export default function Home() { - const creds = useSelector((state: RootState) => state.auth.creds); + const creds = useSelector((state: RootState) => state.user.user); return ( diff --git a/src/routes/Login/Login.tsx b/src/routes/Login/Login.tsx index a0d1a23..8de2585 100644 --- a/src/routes/Login/Login.tsx +++ b/src/routes/Login/Login.tsx @@ -17,12 +17,17 @@ import { RootDrawerParamList } from "../../interfaces/Interfaces"; import { UserInfo, UserLogin } from "../../components/Api/Api"; import { ParseLoginError } from "../../components/ParseError/ParseError"; import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer"; -import { setUser as setStateUser } from "../../features/redux/slices/AuthSlice/AuthSlice"; +import { setUser } from "../../features/redux/slices/UserSlice/UserSlice"; +import { + login, + setOnboarding, + unsetOnboarding, +} from "../../features/redux/slices/StatusSlice/StatusSlice"; export default function Login() { const navigation = useNavigation(); const dispatch = useDispatch(); - const [user, setUser] = useState({ + const [creds, setCreds] = useState({ username: "", password: "", error: "", @@ -50,11 +55,11 @@ export default function Login() { placeholder="Username" placeholderTextColor="white" autoCapitalize="none" - value={user.username} + value={creds.username} onChange={( e: NativeSyntheticEvent ): void => { - setUser({ ...user, username: e.nativeEvent.text }); + setCreds({ ...creds, username: e.nativeEvent.text }); }} /> @@ -63,42 +68,43 @@ export default function Login() { placeholder="Password" placeholderTextColor="white" secureTextEntry={true} - value={user.password} + value={creds.password} onChange={( e: NativeSyntheticEvent ): void => { - setUser({ ...user, password: e.nativeEvent.text }); + setCreds({ ...creds, password: e.nativeEvent.text }); }} /> - {user.error} + {creds.error} diff --git a/src/routes/Register/Register.tsx b/src/routes/Register/Register.tsx index f484f11..ea2b90f 100644 --- a/src/routes/Register/Register.tsx +++ b/src/routes/Register/Register.tsx @@ -159,7 +159,7 @@ export default function Register() { } else { setUser({ ...user, - feedback: ParseError(JSON.stringify(result[2])), + feedback: ParseError(JSON.stringify(result[1])), }); } }); diff --git a/src/routes/Revalidation/Revalidation.tsx b/src/routes/Revalidation/Revalidation.tsx index bee1ca4..defd9fe 100644 --- a/src/routes/Revalidation/Revalidation.tsx +++ b/src/routes/Revalidation/Revalidation.tsx @@ -7,8 +7,13 @@ import { colors } from "../../styles"; import { useEffect, useState } from "react"; import { useNavigation } from "@react-navigation/native"; import { RootDrawerParamList } from "../../interfaces/Interfaces"; -import { setUser } from "../../features/redux/slices/AuthSlice/AuthSlice"; +import { + login, + unsetOnboarding, +} from "../../features/redux/slices/StatusSlice/StatusSlice"; import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer"; +import { setUser } from "../../features/redux/slices/UserSlice/UserSlice"; +import { setOnboarding } from "../../features/redux/slices/StatusSlice/StatusSlice"; export default function Revalidation() { const dispatch = useDispatch(); @@ -17,14 +22,23 @@ export default function Revalidation() { useEffect(() => { setState("Previous session found"); TokenRefresh().then(async (response) => { - if (response[0]) { - let user_info = await UserInfo(); - await dispatch(setUser(user_info)); - if (!(user_info.year_level || user_info.course || user_info.semester)) { + let user_info = await UserInfo(); + if (response && user_info[0]) { + dispatch(login()); + dispatch(setUser(user_info[1])); + if ( + !( + user_info[1].year_level || + user_info[1].course || + user_info[1].semester + ) + ) { + dispatch(setOnboarding()); await setTimeout(() => { navigation.navigate("Onboarding"); }, 700); } else { + dispatch(unsetOnboarding()); await setTimeout(() => { navigation.navigate("Home"); }, 700); diff --git a/src/styles.tsx b/src/styles.tsx index 7d80160..6700f9a 100644 --- a/src/styles.tsx +++ b/src/styles.tsx @@ -11,6 +11,7 @@ export const colors = { text_error: "#e32d1e", text_success: "green", icon_color: "white", + blue_disabled: "#C07624", }; export const font_sizes = { @@ -98,6 +99,11 @@ const styles = StyleSheet.create({ padding: 10, borderRadius: 8, }, + dropdown_template: { + borderRadius: 16, + width: "70%", + marginVertical: 6, + }, }); export default styles;