Merge branch 'master' into initial-frontend

This commit is contained in:
Keannu Bernasol 2023-07-17 13:55:02 +08:00
commit 22d4aa4a29
21 changed files with 762 additions and 166 deletions

33
App.tsx
View file

@ -18,6 +18,7 @@ import Revalidation from "./src/routes/Revalidation/Revalidation";
import Activation from "./src/routes/Activation/Activation";
import UserInfo from "./src/routes/UserInfo/UserInfo";
import { useState, useEffect } from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
const Drawer = createDrawerNavigator();
@ -36,6 +37,8 @@ const linking = {
},
};
const queryClient = new QueryClient();
export default function App() {
const [initialRoute, setInitialRoute] = useState<string | null>(null);
useEffect(() => {
@ -51,21 +54,23 @@ export default function App() {
}, [initialRoute]);
return (
<Provider store={store}>
<NavigationContainer linking={linking}>
<Drawer.Navigator
initialRouteName="Revalidation"
drawerContent={CustomDrawerContent}
screenOptions={DrawerScreenSettings}
>
<Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="Login" component={Login} />
<Drawer.Screen name="Register" component={Register} />
<Drawer.Screen name="Onboarding" component={Onboarding} />
<Drawer.Screen name="Revalidation" component={Revalidation} />
<Drawer.Screen name="Activation" component={Activation} />
<QueryClientProvider client={queryClient}>
<NavigationContainer linking={linking}>
<Drawer.Navigator
initialRouteName="Revalidation"
drawerContent={CustomDrawerContent}
screenOptions={DrawerScreenSettings}
>
<Drawer.Screen name="Home" component={Home} />
<Drawer.Screen name="Login" component={Login} />
<Drawer.Screen name="Register" component={Register} />
<Drawer.Screen name="Onboarding" component={Onboarding} />
<Drawer.Screen name="Revalidation" component={Revalidation} />
<Drawer.Screen name="Activation" component={Activation} />
<Drawer.Screen name="UserInfo" component={UserInfo} />
</Drawer.Navigator>
</NavigationContainer>
</Drawer.Navigator>
</NavigationContainer>
</QueryClientProvider>
</Provider>
);
}

View file

@ -26,6 +26,14 @@
},
"web": {
"favicon": "./assets/favicon.png"
}
},
"plugins": [
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow Stud-E to use your location."
}
]
]
}
}

76
package-lock.json generated
View file

@ -13,9 +13,12 @@
"@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-intent-launcher": "~10.5.2",
"expo-linking": "~4.0.1",
"expo-location": "~15.1.1",
"expo-status-bar": "~1.4.4",
"moti": "^0.25.3",
"react": "18.2.0",
@ -24,6 +27,7 @@
"react-native-gesture-handler": "~2.9.0",
"react-native-image-picker": "^5.6.0",
"react-native-modal": "^13.0.1",
"react-native-maps": "1.3.2",
"react-native-reanimated": "~2.14.4",
"react-native-safe-area-context": "4.5.0",
"react-native-screens": "~3.20.0",
@ -5206,6 +5210,46 @@
"@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/geojson": {
"version": "7946.0.10",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
},
"node_modules/@types/hammerjs": {
"version": "2.0.41",
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.41.tgz",
@ -7413,6 +7457,14 @@
"expo": "*"
}
},
"node_modules/expo-intent-launcher": {
"version": "10.5.2",
"resolved": "https://registry.npmjs.org/expo-intent-launcher/-/expo-intent-launcher-10.5.2.tgz",
"integrity": "sha512-qFIanCkLlTvqYTtJQJocZuZesi6b8lAdY9xF3oLFsdaTXKIMrfQfWI67zwBJvaNqgiV9MbbYnBHsFoOAzhBTKA==",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-keep-awake": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-12.0.1.tgz",
@ -7433,6 +7485,14 @@
"url-parse": "^1.5.9"
}
},
"node_modules/expo-location": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-15.1.1.tgz",
"integrity": "sha512-hoKRlmi6Ya+NeZ72Zt385SDcSsIDpJI60TCBVO+Hc9xfKA9Hyminyyo5WiwI8J03igmPTCl8Y37MxBNKY9AWkg==",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-modules-autolinking": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.2.0.tgz",
@ -12289,6 +12349,22 @@
"peerDependencies": {
"react": "*",
"react-native": ">=0.65.0"
"node_modules/react-native-maps": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.3.2.tgz",
"integrity": "sha512-NB7HGRZOgxxXCWzrhIVucx/bsrEWANvk3DLci1ov4P9MQnEVQYQCCkTxsnaEvO191GeBOCRDyYn6jckqbfMtmg==",
"dependencies": {
"@types/geojson": "^7946.0.8"
},
"peerDependencies": {
"react": ">= 17.0.1",
"react-native": ">= 0.64.3",
"react-native-web": ">= 0.11"
},
"peerDependenciesMeta": {
"react-native-web": {
"optional": true
}
}
},
"node_modules/react-native-reanimated": {

View file

@ -14,9 +14,12 @@
"@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-intent-launcher": "~10.5.2",
"expo-location": "~15.1.1",
"expo-status-bar": "~1.4.4",
"moti": "^0.25.3",
"react": "18.2.0",
@ -32,7 +35,8 @@
"react-native-svg": "13.4.0",
"react-query": "^3.39.3",
"react-redux": "^8.1.1",
"redux": "^4.2.1"
"redux": "^4.2.1",
"react-native-maps": "1.3.2"
},
"devDependencies": {
"@babel/core": "^7.20.0",

View file

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

View file

@ -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];
});
}

View file

@ -6,15 +6,24 @@ 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 (
<Pressable
disabled={disabled}
onPress={props.onPress}
style={{
...styles.button_template,
...{ backgroundColor: props.color, width: "50%" },
...{
backgroundColor: disabled
? rgb
? `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.3)`
: "rgba(0, 0, 0, 0)"
: props.color,
},
}}
>
{props.children}

View file

@ -14,17 +14,41 @@ 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";
import UserIcon from "../../icons/UserIcon/UserIcon";
export default function CustomDrawerContent(props: {}) {
const navigation = useNavigation<RootDrawerParamList>();
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 (
<DrawerContentScrollView {...props}>
<View
style={{
...styles.flex_row,
...{ justifyContent: "center" },
}}
>
<AppIcon size={32} />
<Text style={styles.text_white_medium}>Stud-E</Text>
</View>
<DrawerButton
color={colors.blue_2}
onPress={async () => {
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
}}
>
<LogoutIcon size={32} />
<Text style={styles.text_white_medium}>Logout</Text>
</DrawerButton>
</DrawerContentScrollView>
);
} else if (status.logged_in) {
return (
<DrawerContentScrollView {...props}>
<View
@ -57,7 +81,7 @@ export default function CustomDrawerContent(props: {}) {
<DrawerButton
color={colors.blue_2}
onPress={async () => {
dispatch(await clear());
dispatch(logout());
await AsyncStorage.clear();
navigation.navigate("Login");
}}
@ -79,15 +103,6 @@ export default function CustomDrawerContent(props: {}) {
<AppIcon size={32} />
<Text style={styles.text_white_medium}>Stud-E</Text>
</View>
<DrawerButton
color={colors.blue_2}
onPress={() => {
navigation.navigate("Home");
}}
>
<HomeIcon size={32} />
<Text style={styles.text_white_medium}>Home</Text>
</DrawerButton>
<DrawerButton
color={colors.blue_2}
onPress={() => {
@ -100,7 +115,6 @@ export default function CustomDrawerContent(props: {}) {
<DrawerButton
color={colors.blue_2}
onPress={() => {
dispatch(clear());
navigation.navigate("Register");
}}
>

View file

@ -0,0 +1,23 @@
export default function GetDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
) {
var R = 6371; // km
var dLat = toRad(lat2 - lat1);
var dLon = toRad(lon2 - lon1);
var lat1 = toRad(lat1);
var lat2 = toRad(lat2);
var a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
var distance = R * c;
return distance;
}
export function toRad(value: number) {
return (value * Math.PI) / 180;
}

View file

@ -0,0 +1,3 @@
export default function isStringEmpty(str: string) {
return str === "" || str === null || str === undefined;
}

View file

@ -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,
},
});

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -1,17 +1,112 @@
import * as React from "react";
import styles from "../../styles";
import { View, Text } from "react-native";
import { useSelector } from "react-redux";
import { RootState } from "../../features/redux/Store/Store";
import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer";
import { useState, useEffect } from "react";
import MapView, { Marker, UrlTile } from "react-native-maps";
import * as Location from "expo-location";
import GetDistance from "../../components/GetDistance/GetDistance";
import Button from "../../components/Button/Button";
import { colors } from "../../styles";
import { startActivityAsync, ActivityAction } from "expo-intent-launcher";
type LocationType = Location.LocationObject;
export default function Home() {
const creds = useSelector((state: RootState) => state.auth.creds);
const [location, setLocation] = useState<LocationType | null>(null);
const [dist, setDist] = useState<number | null>(null);
const [feedback, setFeedback] = useState(
"To continue, please allow Stud-E permission to location services"
);
async function requestLocation() {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status === "granted") {
getLocation();
return;
} else if (status === "denied") {
setFeedback("Stud-E requires location services to function");
setTimeout(() => {
startActivityAsync(ActivityAction.LOCATION_SOURCE_SETTINGS);
}, 3000);
console.log("Location Permission denied");
}
}
async function getLocation() {
let location = await Location.getCurrentPositionAsync({});
setLocation(location);
let dist = GetDistance(
location.coords.latitude,
location.coords.longitude,
8.4857,
124.6565
);
setDist(Math.round(dist));
}
useEffect(() => {
requestLocation();
}, []);
const ustpCoords = {
latitude: 8.4857,
longitude: 124.6565,
latitudeDelta: 0.000235,
longitudeDelta: 0.000067,
};
function CustomMap() {
if (dist !== null && location !== null) {
if (dist <= 1.5) {
// Just switch this condition for map debugging
return <MapView style={styles.map} initialRegion={ustpCoords} />;
} else {
return (
<View>
<Text style={styles.text_white_medium}>
You are too far from USTP {"\n"}
Get closer to use Stud-E
</Text>
<MapView
style={{
height: 256,
width: 256,
alignSelf: "center",
}}
showsUserLocation={true}
scrollEnabled={false}
zoomEnabled={false}
rotateEnabled={false}
followsUserLocation={true}
minZoomLevel={15}
initialRegion={{
latitude: location.coords.latitude,
longitude: location.coords.longitude,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
/>
<Text style={styles.text_white_small}>
{dist}km away from USTP {"\n"}
</Text>
</View>
);
}
} else {
return (
<AnimatedContainer>
<Text style={styles.text_white_medium}>{feedback}</Text>
<Button onPress={() => requestLocation()} color={colors.blue_3}>
<Text style={styles.text_white_small}>Allow Access</Text>
</Button>
</AnimatedContainer>
);
}
}
const creds = useSelector((state: RootState) => state.user.user);
return (
<View style={styles.background}>
<AnimatedContainer>
<Text style={styles.text_white_large}>Template Homepage</Text>
<Text style={styles.text_white_tiny}>{JSON.stringify(creds)}</Text>
<CustomMap />
</AnimatedContainer>
</View>
);

View file

@ -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<RootDrawerParamList>();
const dispatch = useDispatch();
const [user, setUser] = useState({
const [creds, setCreds] = useState({
username: "",
password: "",
error: "",
@ -49,11 +54,11 @@ export default function Login() {
placeholder="Username"
placeholderTextColor="white"
autoCapitalize="none"
value={user.username}
value={creds.username}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, username: e.nativeEvent.text });
setCreds({ ...creds, username: e.nativeEvent.text });
}}
/>
<View style={{ paddingVertical: 4 }} />
@ -62,42 +67,43 @@ export default function Login() {
placeholder="Password"
placeholderTextColor="white"
secureTextEntry={true}
value={user.password}
value={creds.password}
onChange={(
e: NativeSyntheticEvent<TextInputChangeEventData>
): void => {
setUser({ ...user, password: e.nativeEvent.text });
setCreds({ ...creds, password: e.nativeEvent.text });
}}
/>
<View style={{ paddingVertical: 2 }} />
<Text style={styles.text_white_small}>{user.error}</Text>
<Text style={styles.text_white_small}>{creds.error}</Text>
<View style={{ paddingVertical: 4 }} />
<Button
onPress={async () => {
await UserLogin({
username: user.username,
password: user.password,
username: creds.username,
password: creds.password,
}).then(async (result) => {
if (result[0]) {
setUser({ ...user, username: "", password: "", error: "" });
setUser({ ...creds, username: "", password: "", error: "" });
let user_info = await UserInfo();
dispatch(setStateUser(user_info));
dispatch(login());
dispatch(setUser(user_info[1]));
// Redirect to onboarding if no year level, course, or semester specified
if (
!(
user_info.year_level ||
user_info.course ||
user_info.semester
)
user_info[1].year_level == null ||
user_info[1].course == null ||
user_info[1].semester == null
) {
dispatch(setOnboarding());
navigation.navigate("Onboarding");
} else {
dispatch(unsetOnboarding());
navigation.navigate("Home");
}
console.log(JSON.stringify(user_info));
} else {
setUser({
...user,
...creds,
error: ParseLoginError(JSON.stringify(result[1])),
});
}

View file

@ -2,46 +2,99 @@ import * as React from "react";
import styles from "../../styles";
import { View, Text } from "react-native";
import { useNavigation } from "@react-navigation/native";
import { RootDrawerParamList } from "../../interfaces/Interfaces";
import {
CourseParams,
RootDrawerParamList,
SemesterParams,
YearLevelParams,
} from "../../interfaces/Interfaces";
import { colors } from "../../styles";
import { AnimatePresence, MotiView } from "moti";
import { useEffect, useState } from "react";
import Button from "../../components/Button/Button";
import DropDownPicker from "react-native-dropdown-picker";
import isStringEmpty from "../../components/IsStringEmpty/IsStringEmpty";
import { useQuery } from "@tanstack/react-query";
import {
GetCourses,
GetSemesters,
GetYearLevels,
OnboardingUpdateStudentInfo,
} from "../../components/Api/Api";
import { useDispatch } from "react-redux";
import { unsetOnboarding } from "../../features/redux/slices/StatusSlice/StatusSlice";
import { setUser } from "../../features/redux/slices/UserSlice/UserSlice";
export default function Onboarding() {
const navigation = useNavigation<RootDrawerParamList>();
// const dispatch = useDispatch();
const dispatch = useDispatch();
// const creds = useSelector((state: RootState) => state.auth.creds);
const [student_info, setStudentInfo] = useState({
year_level: "",
course: "",
semester: "",
const [error, setError] = useState("");
// Semesters
const [selected_semester, setSelectedSemester] = useState("");
const [semesterOpen, setSemesterOpen] = useState(false);
const [semesters, setSemesters] = useState([
{ label: "1st Semester", value: "1st Sem" },
{ label: "2nd Semester", value: "2nd Sem" },
]);
const semester_query = useQuery({
queryKey: ["semesters"],
queryFn: GetSemesters,
onSuccess: (data) => {
let semesters = data.map((item: SemesterParams) => ({
label: item.name,
value: item.name,
}));
setSemesters(semesters);
},
});
function Introduction() {
const [shown, setShown] = useState(true);
useEffect(() => {
setTimeout(() => {
setShown(false);
}, 5000);
}, []);
// Year Level
const [selected_yearlevel, setSelectedYearLevel] = useState("");
const [yearLevelOpen, setYearLevelOpen] = useState(false);
const [year_levels, setYearLevels] = useState([
{ label: "1st Year", value: "1st Year" },
{ label: "2nd Year", value: "2nd Year" },
]);
const yearlevel_query = useQuery({
queryKey: ["year_levels"],
queryFn: GetYearLevels,
onSuccess: (data) => {
let year_levels = data.map((item: YearLevelParams) => ({
label: item.name,
value: item.name,
}));
setYearLevels(year_levels);
},
});
// Course
const [selected_course, setSelectedCourse] = useState("");
const [courseOpen, setCourseOpen] = useState(false);
const [courses, setCourses] = useState([
{
label: "Bachelor of Science in Information Technology",
value: "BSIT",
},
{ label: "Bachelor of Science in Computer Science", value: "BSCS" },
]);
const course_query = useQuery({
queryKey: ["courses"],
queryFn: GetCourses,
onSuccess: (data) => {
let courses = data.map((item: CourseParams) => ({
label: item.name,
value: item.name,
}));
setCourses(courses);
},
});
if (yearlevel_query.error || semester_query.error || course_query.error) {
return (
<AnimatePresence>
{shown && (
<MotiView
from={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
exitTransition={{ type: "timing", duration: 1200, delay: 600 }}
transition={{ type: "timing", duration: 1200, delay: 600 }}
>
<Text style={styles.text_white_small}>
We're glad to have you on board {"\n"}
Just a few more things before we get started
</Text>
</MotiView>
)}
</AnimatePresence>
<View style={styles.background}>
<View style={styles.container}>
<Text style={styles.text_white_medium}>Error loading details</Text>
</View>
</View>
);
}
return (
<View style={styles.background}>
<View style={styles.container}>
@ -63,14 +116,112 @@ export default function Onboarding() {
}}
/>
<View style={{ paddingVertical: 4 }} />
<Introduction />
<View style={{ paddingVertical: 8 }} />
<MotiView
from={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ type: "timing", duration: 900, delay: 2000 }}
transition={{ type: "timing", duration: 1200, delay: 600 }}
>
<Text style={styles.text_white_small}>
We're glad to have you on board {"\n"}
Just a few more things before we get started
</Text>
</MotiView>
<View style={{ paddingVertical: 8 }} />
<MotiView
from={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ type: "timing", duration: 400, delay: 1700 }}
>
<Text style={styles.text_white_medium}>Academic Info</Text>
<DropDownPicker
zIndex={3000}
open={courseOpen}
value={selected_course}
items={courses}
setOpen={(open) => {
setCourseOpen(open);
setSemesterOpen(false);
setYearLevelOpen(false);
}}
setValue={setSelectedCourse}
placeholder="Choose your course"
containerStyle={{
...styles.dropdown_template,
...{ zIndex: 3000 },
}}
dropDownContainerStyle={{ backgroundColor: "white" }}
/>
<DropDownPicker
zIndex={2000}
open={semesterOpen}
value={selected_semester}
items={semesters}
setOpen={(open) => {
setSemesterOpen(open);
setCourseOpen(false);
setYearLevelOpen(false);
}}
setValue={setSelectedSemester}
placeholder="Current semester"
containerStyle={{
...styles.dropdown_template,
...{ zIndex: 2000 },
}}
dropDownContainerStyle={{ backgroundColor: "white" }}
/>
<DropDownPicker
zIndex={1000}
open={yearLevelOpen}
value={selected_yearlevel}
items={year_levels}
setOpen={(open) => {
setYearLevelOpen(open);
setSemesterOpen(false);
setCourseOpen(false);
}}
setValue={setSelectedYearLevel}
placeholder="Your Year Level"
containerStyle={{
...styles.dropdown_template,
...{ zIndex: 1000 },
}}
dropDownContainerStyle={{ backgroundColor: "white" }}
/>
</MotiView>
<MotiView
from={{ opacity: 0 }}
animate={{ opacity: 1, zIndex: -1 }}
transition={{ type: "timing", duration: 400, delay: 1700 }}
style={styles.button_template}
>
<Text style={styles.text_white_small}>{error}</Text>
<Button
disabled={
!selected_yearlevel || !selected_course || !selected_semester
}
onPress={async () => {
let result = await OnboardingUpdateStudentInfo({
semester: selected_semester,
course: selected_course,
year_level: selected_yearlevel,
});
if (result[0]) {
dispatch(unsetOnboarding());
setSelectedCourse("");
setSelectedYearLevel("");
setSelectedSemester("");
setError("Success!");
dispatch(setUser(result[1]));
navigation.navigate("Home");
} else {
setError(result[1]);
}
}}
color={colors.blue_3}
>
<Text style={styles.text_white_small}>Proceed</Text>
</Button>
</MotiView>
</View>
</View>

View file

@ -160,7 +160,7 @@ export default function Register() {
} else {
setUser({
...user,
feedback: ParseError(JSON.stringify(result[2])),
feedback: ParseError(JSON.stringify(result[1])),
});
}
});

View file

@ -7,8 +7,14 @@ 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";
import AsyncStorage from "@react-native-async-storage/async-storage";
export default function Revalidation() {
const dispatch = useDispatch();
@ -17,14 +23,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);
@ -32,6 +47,7 @@ export default function Revalidation() {
} else {
await setState("Session expired");
await setTimeout(() => {
AsyncStorage.clear();
navigation.navigate("Login");
}, 700);
}

View file

@ -1,5 +1,10 @@
import { createErrorHandler } from "expo/build/errors/ExpoErrorManager";
import { StyleSheet } from "react-native";
import { StyleSheet, Dimensions } from "react-native";
const width = Dimensions.get("window").width;
const height = Dimensions.get("window").height;
const containerWidth = width - width * 0.08;
const containerHeight = height - height * 0.01;
export const colors = {
orange_1: "#FFDEAD",
@ -15,6 +20,7 @@ export const colors = {
login_color: "#0047AB",
reg_color: "#0096FF",
head: "white"
blue_disabled: "#C07624",
};
export const font_sizes = {
@ -32,17 +38,13 @@ const styles = StyleSheet.create({
width: "100%",
},
container: {
marginTop: "5%",
width: "92%",
borderRadius: 15,
backgroundColor: colors.blue_2,
alignItems: "center",
alignSelf: "center",
paddingTop: 32,
paddingBottom: 32,
justifyContent: "flex-start",
justifyContent: "center",
display: "flex",
flexDirection: "column",
flex: 1,
paddingHorizontal: 4,
},
flex_row: {
display: "flex",
@ -53,6 +55,7 @@ const styles = StyleSheet.create({
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},
text_white_tiny: {
color: colors.text_default,
@ -88,19 +91,32 @@ const styles = StyleSheet.create({
justifyContent: "center",
alignSelf: "center",
alignItems: "center",
textAlign: "center",
display: "flex",
flexDirection: "row",
marginVertical: 4,
marginHorizontal: 8,
padding: 8,
borderRadius: 16,
width: width * 0.4,
},
text_input: {
color: colors.text_default,
backgroundColor: colors.blue_1,
width: "50%",
padding: 10,
borderRadius: 8,
width: width * 0.5,
},
dropdown_template: {
borderRadius: 16,
width: "70%",
marginVertical: 6,
},
map: {
flex: 1,
height: containerHeight,
width: containerWidth,
alignSelf: "center",
},
profile: {
height: 80,
@ -140,7 +156,5 @@ const styles = StyleSheet.create({
inactiveText: {
color: 'white',
},
});
export default styles;