From b43577870bb6b11a71b21a18d2ae2406a697b61b Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Wed, 26 Jul 2023 10:03:25 +0800 Subject: [PATCH 01/51] Homepage improvements --- src/components/Api/Api.tsx | 4 +++- src/routes/Home/Home.tsx | 48 +++++++++++++++++++++++++++----------- src/routes/Login/Login.tsx | 2 +- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 20a3151..e1eb813 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -12,7 +12,7 @@ import { export let backendURL = ""; export let backendURLWebsocket = ""; 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 { @@ -25,6 +25,8 @@ const instance = axios.create({ timeout: 1000, }); +console.log(backendURL); + // App APIs // Token Handling diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index f80af2e..f18c804 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -19,8 +19,8 @@ export default function Home() { const ustpCoords = { latitude: 8.4857, longitude: 124.6565, - latitudeDelta: 0.000235, - longitudeDelta: 0.000067, + latitudeDelta: 0.4, + longitudeDelta: 0.4, }; async function requestLocation() { const { status } = await Location.requestForegroundPermissionsAsync(); @@ -62,27 +62,19 @@ export default function Home() { let dist = GetDistance( location.coords.latitude, location.coords.longitude, - 8.4857, // LatitudeDelta - 124.6565 // LongitudeDelta + 0.4, + 0.4 ); setDist(Math.round(dist)); } function CustomMap() { if (dist && location) { - if (dist <= 1.5) { + if (dist >= 2) { // Just switch this condition for map debugging return ( + onPress={() => console.log(location)} + pinColor={colors.primary_1} + > + + + You are here {"\n"} + X: {Math.round(location.coords.longitude) + "\n"} + Z: {Math.round(location.coords.latitude)} + + + ); } else { @@ -145,6 +163,7 @@ export default function Home() { latitudeDelta: 0.0922, longitudeDelta: 0.0421, }} + loadingBackgroundColor={colors.secondary_2} > + You are here {"\n"} X: {Math.round(location.coords.longitude) + "\n"} Z: {Math.round(location.coords.latitude)} diff --git a/src/routes/Login/Login.tsx b/src/routes/Login/Login.tsx index 34a3d6f..8789fad 100644 --- a/src/routes/Login/Login.tsx +++ b/src/routes/Login/Login.tsx @@ -94,7 +94,7 @@ export default function Login() { } console.log(JSON.stringify(user_info)); } else { - console.log("heh", ParseLoginError(JSON.stringify(result[1]))); + console.log(ParseLoginError(JSON.stringify(result[1]))); setError(ParseLoginError(JSON.stringify(result[1]))); } }); From cd85852c9b3b4f2100a68530e20a0e11196675b3 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Wed, 26 Jul 2023 10:04:15 +0800 Subject: [PATCH 02/51] Fixed condition in homepage --- src/routes/Home/Home.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index f18c804..7f146cd 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -70,7 +70,7 @@ export default function Home() { function CustomMap() { if (dist && location) { - if (dist >= 2) { + if (dist <= 2) { // Just switch this condition for map debugging return ( Date: Wed, 26 Jul 2023 12:52:24 +0800 Subject: [PATCH 03/51] Fixed homepage distance bug --- src/routes/Home/Home.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 7f146cd..bbe1b6e 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -19,8 +19,8 @@ export default function Home() { const ustpCoords = { latitude: 8.4857, longitude: 124.6565, - latitudeDelta: 0.4, - longitudeDelta: 0.4, + latitudeDelta: 0.000235, + longitudeDelta: 0.000067, }; async function requestLocation() { const { status } = await Location.requestForegroundPermissionsAsync(); @@ -62,8 +62,8 @@ export default function Home() { let dist = GetDistance( location.coords.latitude, location.coords.longitude, - 0.4, - 0.4 + 8.4857, // LatitudeDelta + 124.6565 // LongitudeDelta ); setDist(Math.round(dist)); } From 3331ccb9745b52d2fea815991c4c988f6967e9e6 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 27 Jul 2023 00:01:44 +0800 Subject: [PATCH 04/51] Pray to the gods the duplicate subjects bug is fixed. Move irregular status toggle to user info page from subjects page --- src/components/Api/Api.tsx | 54 ++++----------- src/interfaces/Interfaces.tsx | 12 ++-- src/routes/Revalidation/Revalidation.tsx | 8 +-- src/routes/SubjectsPage/SubjectsPage.tsx | 88 +++++------------------- src/routes/UserInfoPage/UserInfoPage.tsx | 72 ++++++++++--------- 5 files changed, 79 insertions(+), 155 deletions(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index e1eb813..58c3804 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -11,7 +11,7 @@ import { export let backendURL = ""; export let backendURLWebsocket = ""; -let use_production = true; +let use_production = false; if (__DEV__ && !use_production) { backendURL = "http://10.0.10.8:8000"; backendURLWebsocket = "ws://10.0.10.8:8000"; @@ -224,47 +224,19 @@ export async function GetYearLevels() { }); } -export async function GetSubjects( - byCourseOnly: boolean, - course: string, - year_level?: string, - semester?: string -) { +export async function GetSubjects() { 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]; - }); - } + return instance + .get("/api/v1/subjects/", config) + .then((response) => { + 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) { diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index 2b6e526..7c83a9b 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -85,11 +85,12 @@ export type CourseParams = [boolean, Courses]; // Subject export interface Subject { + id: number; name: string; code: string; - // courses: any[]; // To-do - // year_levels: any[]; // To-do - // semesters: any[]; // To-do + course: string; + year_level: string; + semester: string; } export type Subjects = Array; @@ -110,6 +111,7 @@ export interface PatchStudentData { semester?: string | null; subjects?: any[] | null; // To-do, replace 'any' with your actual type year_level?: string | null; + irregular?: boolean | null; } export interface StudentData { @@ -118,14 +120,14 @@ export interface StudentData { email: string; avatar: string; student_id_number: string; - is_banned: boolean; + irregular: boolean; semester: string; semester_shortname: string; course: string; course_shortname: string; year_level: string; yearlevel_shortname: string; - subjects: any[]; // To-do + subjects: Subject[]; // To-do username: string; } diff --git a/src/routes/Revalidation/Revalidation.tsx b/src/routes/Revalidation/Revalidation.tsx index ce5b1dc..c7d9869 100644 --- a/src/routes/Revalidation/Revalidation.tsx +++ b/src/routes/Revalidation/Revalidation.tsx @@ -28,11 +28,9 @@ export default function Revalidation() { dispatch(login()); dispatch(setUser(user_info[1])); if ( - !( - user_info[1].year_level || - user_info[1].course || - user_info[1].semester - ) + !user_info[1].year_level || + !user_info[1].course || + !user_info[1].semester ) { dispatch(setOnboarding()); await setTimeout(() => { diff --git a/src/routes/SubjectsPage/SubjectsPage.tsx b/src/routes/SubjectsPage/SubjectsPage.tsx index c95815b..1790663 100644 --- a/src/routes/SubjectsPage/SubjectsPage.tsx +++ b/src/routes/SubjectsPage/SubjectsPage.tsx @@ -33,9 +33,11 @@ import { import { colors } from "../../styles"; import DropDownPicker from "react-native-dropdown-picker"; import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll"; -import BouncyCheckbox from "react-native-bouncy-checkbox"; +import { useSelector } from "react-redux"; +import { RootState } from "../../features/redux/Store/Store"; export default function SubjectsPage() { + const logged_in_user = useSelector((state: RootState) => state.user.user); const queryClient = useQueryClient(); // User Info const [user, setUser] = useState({ @@ -51,15 +53,10 @@ export default function SubjectsPage() { student_id_number: "", subjects: [] as Subjects, }); - const [displayName, setDisplayName] = useState({ - first_name: "", - last_name: "", - }); const StudentInfo = useQuery({ queryKey: ["user"], queryFn: UserInfo, onSuccess: (data: UserInfoParams) => { - // console.log(data[1]); setUser({ ...user, first_name: data[1].first_name, @@ -71,31 +68,18 @@ export default function SubjectsPage() { student_id_number: data[1].student_id_number, subjects: data[1].subjects, }); - setDisplayName({ - first_name: data[1].first_name, - last_name: data[1].last_name, - }); - setSelectedSubjects(user.subjects); + setSelectedSubjects(data[1].subjects); }, }); const mutation = useMutation({ mutationFn: PatchUserInfo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["user"] }); - queryClient.invalidateQueries({ queryKey: ["subjects", viewAll] }); + queryClient.invalidateQueries({ queryKey: ["subjects"] }); setSelectedSubjects([]); }, }); - // 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 const [selected_subjects, setSelectedSubjects] = useState([]); @@ -104,42 +88,17 @@ export default function SubjectsPage() { const Subjects = useQuery({ enabled: StudentInfo.isFetched, - queryKey: ["subjects", viewAll], - queryFn: async () => { - let data; - if ( - StudentInfo.data && - 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; - }, + queryKey: ["subjects"], + queryFn: GetSubjects, onSuccess: (data: SubjectParams) => { - let subjectsData = data[1].map((subject: Subject) => ({ - label: subject.name, - value: subject.name, - })); - // Update the 'subjects' state - setSelectedSubjects(user.subjects); - setSubjects(subjectsData); + if (data[1]) { + let subjects = data[1].map((subject: Subject) => ({ + label: subject.name, + value: subject.name, + })); + + setSubjects(subjects); + } }, }); @@ -163,9 +122,9 @@ export default function SubjectsPage() { - {(displayName.first_name || "Undefined") + + {(logged_in_user.first_name || "Undefined") + " " + - (displayName.last_name || "User") + + (logged_in_user.last_name || "User") + "\n" + user.student_id_number} @@ -211,22 +170,9 @@ export default function SubjectsPage() { - - - { - setViewAll(!viewAll); - setSubjectsOpen(false); - }} - fillColor={colors.secondary_3} - /> - Irregular - From e4278517bc59a18f4fcd0cd661654ab4df15cc20 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 27 Jul 2023 00:06:32 +0800 Subject: [PATCH 05/51] Code cleanup in api.tsx --- src/components/Api/Api.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 58c3804..eaa4b18 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -9,15 +9,11 @@ import { StudentData, } from "../../interfaces/Interfaces"; -export let backendURL = ""; -export let backendURLWebsocket = ""; -let use_production = false; -if (__DEV__ && !use_production) { +export let backendURL = "https://stude.keannu1.duckdns.org"; +export let backendURLWebsocket = "ws://stude.keannu1.duckdns.org"; +if (__DEV__) { backendURL = "http://10.0.10.8:8000"; backendURLWebsocket = "ws://10.0.10.8:8000"; -} else { - backendURL = "https://stude.keannu1.duckdns.org"; - backendURLWebsocket = "ws://stude.keannu1.duckdns.org"; } const instance = axios.create({ From 1a46945d1e4ad8d6ae59250e8fcc2bfb2e0e7094 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 27 Jul 2023 00:09:43 +0800 Subject: [PATCH 06/51] Code cleanup in api.tsx --- src/components/Api/Api.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index eaa4b18..44f6e7d 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -16,6 +16,13 @@ if (__DEV__) { backendURLWebsocket = "ws://10.0.10.8:8000"; } +// Switch this on if you wanna run production URLs while in development +let use_production = false; +if (use_production) { + backendURL = "https://stude.keannu1.duckdns.org"; + backendURLWebsocket = "ws://stude.keannu1.duckdns.org"; +} + const instance = axios.create({ baseURL: backendURL, timeout: 1000, From a3b3bd887f43b06583811b1efcbb8fc2545fa344 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 27 Jul 2023 12:55:51 +0800 Subject: [PATCH 07/51] Add user feedback to user info and subjects page --- src/components/Api/Api.tsx | 2 +- src/routes/SubjectsPage/SubjectsPage.tsx | 12 +++++++++- src/routes/UserInfoPage/UserInfoPage.tsx | 30 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 44f6e7d..45701b9 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -17,7 +17,7 @@ if (__DEV__) { } // Switch this on if you wanna run production URLs while in development -let use_production = false; +let use_production = true; if (use_production) { backendURL = "https://stude.keannu1.duckdns.org"; backendURLWebsocket = "ws://stude.keannu1.duckdns.org"; diff --git a/src/routes/SubjectsPage/SubjectsPage.tsx b/src/routes/SubjectsPage/SubjectsPage.tsx index 1790663..b0429e0 100644 --- a/src/routes/SubjectsPage/SubjectsPage.tsx +++ b/src/routes/SubjectsPage/SubjectsPage.tsx @@ -39,6 +39,7 @@ import { RootState } from "../../features/redux/Store/Store"; export default function SubjectsPage() { const logged_in_user = useSelector((state: RootState) => state.user.user); const queryClient = useQueryClient(); + const [feedback, setFeedback] = useState(""); // User Info const [user, setUser] = useState({ first_name: "", @@ -70,6 +71,9 @@ export default function SubjectsPage() { }); setSelectedSubjects(data[1].subjects); }, + onError: () => { + setFeedback("Unable to query user info"); + }, }); const mutation = useMutation({ mutationFn: PatchUserInfo, @@ -77,6 +81,7 @@ export default function SubjectsPage() { queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["subjects"] }); setSelectedSubjects([]); + setFeedback("Changes applied successfully"); }, }); @@ -100,6 +105,9 @@ export default function SubjectsPage() { setSubjects(subjects); } }, + onError: () => { + setFeedback("Unable to query subject info"); + }, }); // Profile photo @@ -178,8 +186,10 @@ export default function SubjectsPage() { }); }} > - Save Change + Save Changes + + {feedback} diff --git a/src/routes/UserInfoPage/UserInfoPage.tsx b/src/routes/UserInfoPage/UserInfoPage.tsx index 6c16306..3ef6a8a 100644 --- a/src/routes/UserInfoPage/UserInfoPage.tsx +++ b/src/routes/UserInfoPage/UserInfoPage.tsx @@ -44,6 +44,7 @@ export default function UserInfoPage() { const logged_in_user = useSelector((state: RootState) => state.user.user); const dispatch = useDispatch(); const queryClient = useQueryClient(); + const [feedback, setFeedback] = useState(""); // User Info const [user, setUser] = useState({ first_name: "", @@ -79,14 +80,21 @@ export default function UserInfoPage() { setSelectedYearLevel(data[1].year_level); dispatch(setUserinState(data[1])); }, + onError: () => { + setFeedback("Unable to query user info"); + }, }); const mutation = useMutation({ mutationFn: PatchUserInfo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["subjects"] }); + setFeedback("Changes applied successfully"); dispatch(setUserinState(user)); }, + onError: () => { + setFeedback("An error has occured\nChanges have not been saved"); + }, }); // Semester @@ -105,6 +113,9 @@ export default function UserInfoPage() { // Update the 'semesters' state setSemesters(semestersData); }, + onError: () => { + setFeedback("Unable to query semester info"); + }, }); // Year Level @@ -121,6 +132,9 @@ export default function UserInfoPage() { })); setYearLevels(year_levels); }, + onError: () => { + setFeedback("Unable to query year level info"); + }, }); // Course @@ -137,6 +151,9 @@ export default function UserInfoPage() { })); setCourses(courses); }, + onError: () => { + setFeedback("Unable to query course info"); + }, }); // Profile photo @@ -179,6 +196,7 @@ export default function UserInfoPage() { e: NativeSyntheticEvent ): void => { setUser({ ...user, first_name: e.nativeEvent.text }); + setFeedback(""); }} value={user.first_name} /> @@ -195,6 +213,7 @@ export default function UserInfoPage() { e: NativeSyntheticEvent ): void => { setUser({ ...user, last_name: e.nativeEvent.text }); + setFeedback(""); }} value={user.last_name} /> @@ -216,6 +235,9 @@ export default function UserInfoPage() { setCourseOpen(false); }} setValue={setSelectedYearLevel} + onChangeValue={() => { + setFeedback(""); + }} placeholder={user.year_level} placeholderStyle={{ ...styles.text_white_tiny_bold, @@ -251,6 +273,9 @@ export default function UserInfoPage() { setCourseOpen(false); }} setValue={setSelectedSemester} + onChangeValue={() => { + setFeedback(""); + }} placeholder={user.semester} placeholderStyle={{ ...styles.text_white_tiny_bold, @@ -286,6 +311,9 @@ export default function UserInfoPage() { setCourseOpen(open); }} setValue={setSelectedCourse} + onChangeValue={() => { + setFeedback(""); + }} placeholder={user.course} placeholderStyle={{ ...styles.text_white_tiny_bold, @@ -337,6 +365,8 @@ export default function UserInfoPage() { > Save Changes + + {feedback} From e4d64f365631410dc2dee0b8c7efcd2ab4aa30a5 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 27 Jul 2023 16:00:31 +0800 Subject: [PATCH 08/51] Added avatar uploading --- app.json | 10 ++++-- package-lock.json | 28 ++++++++++++++++ package.json | 3 ++ src/components/Api/Api.tsx | 5 +-- src/interfaces/Interfaces.tsx | 6 ++++ src/routes/UserInfoPage/UserInfoPage.tsx | 42 +++++++++++++++++++----- src/styles.tsx | 2 ++ 7 files changed, 84 insertions(+), 12 deletions(-) diff --git a/app.json b/app.json index 893a6ba..2456397 100644 --- a/app.json +++ b/app.json @@ -42,9 +42,15 @@ [ "expo-location", { - "locationAlwaysAndWhenInUsePermission": "Allow Stud-E to use your location." + "locationAlwaysAndWhenInUsePermission": "Allow StudE to use your location." } - ] + ], + [ + "expo-image-picker", + { + "photosPermission": "Allow StudE to take and send photos for sharing in-app" + } + ] ], "extra": { "eas": { diff --git a/package-lock.json b/package-lock.json index 2c02127..14006b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@tanstack/react-query": "^4.29.25", "axios": "^1.4.0", "expo": "~48.0.18", + "expo-file-system": "~15.2.2", + "expo-image-picker": "~14.1.1", "expo-intent-launcher": "~10.5.2", "expo-linking": "~4.0.1", "expo-location": "~15.1.1", @@ -41,6 +43,7 @@ "devDependencies": { "@babel/core": "^7.20.0", "@types/react": "~18.0.14", + "@types/react-native-fetch-blob": "^0.10.7", "typescript": "^4.9.4" } }, @@ -5311,6 +5314,12 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-native-fetch-blob": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@types/react-native-fetch-blob/-/react-native-fetch-blob-0.10.7.tgz", + "integrity": "sha512-9UTvmUvArimShiENeR3xnRO71NcZjpTi7AcFAIbhdTIfqQOO2OK/I/DpUPXcZF/erffLxOoRkoXrZOxyBBWKRQ==", + "dev": true + }, "node_modules/@types/scheduler": { "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", @@ -7458,6 +7467,25 @@ "expo": "*" } }, + "node_modules/expo-image-loader": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-4.1.1.tgz", + "integrity": "sha512-ciEHVokU0f6w0eTxdRxLCio6tskMsjxWIoV92+/ZD37qePUJYMfEphPhu1sruyvMBNR8/j5iyOvPFVGTfO8oxA==", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-image-picker": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-14.1.1.tgz", + "integrity": "sha512-SvWtnkLW7jp5Ntvk3lVcRQmhFYja8psmiR7O6P/+7S6f4llt3vaFwb4I3+pUXqJxxpi7BHc2+95qOLf0SFOIag==", + "dependencies": { + "expo-image-loader": "~4.1.0" + }, + "peerDependencies": { + "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", diff --git a/package.json b/package.json index 5d4609b..5af9ec0 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "@tanstack/react-query": "^4.29.25", "axios": "^1.4.0", "expo": "~48.0.18", + "expo-file-system": "~15.2.2", + "expo-image-picker": "~14.1.1", "expo-intent-launcher": "~10.5.2", "expo-linking": "~4.0.1", "expo-location": "~15.1.1", @@ -42,6 +44,7 @@ "devDependencies": { "@babel/core": "^7.20.0", "@types/react": "~18.0.14", + "@types/react-native-fetch-blob": "^0.10.7", "typescript": "^4.9.4" }, "private": true diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 45701b9..0f4e758 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -17,7 +17,7 @@ if (__DEV__) { } // Switch this on if you wanna run production URLs while in development -let use_production = true; +let use_production = false; if (use_production) { backendURL = "https://stude.keannu1.duckdns.org"; backendURLWebsocket = "ws://stude.keannu1.duckdns.org"; @@ -142,6 +142,7 @@ export async function UserInfo() { } export async function PatchUserInfo(info: PatchStudentData) { + console.log("API", JSON.stringify(info)); const config = await GetConfig(); return instance .patch("/api/v1/accounts/users/me/", info, config) @@ -153,7 +154,7 @@ export async function PatchUserInfo(info: PatchStudentData) { let error_message = ""; if (error.response) error_message = error.response.data; else error_message = "Unable to reach servers"; - // console.log(error_message); + console.log(error_message); return [false, error_message]; }); } diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index 7c83a9b..40b0348 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -96,6 +96,11 @@ export interface Subject { export type Subjects = Array; export type SubjectParams = [boolean, Subjects]; +export type avatar = { + uri: string; + type: string; + name: string; +}; // For dropdown menu export interface OnboardingParams { @@ -112,6 +117,7 @@ export interface PatchStudentData { subjects?: any[] | null; // To-do, replace 'any' with your actual type year_level?: string | null; irregular?: boolean | null; + avatar?: string | null; } export interface StudentData { diff --git a/src/routes/UserInfoPage/UserInfoPage.tsx b/src/routes/UserInfoPage/UserInfoPage.tsx index 3ef6a8a..0453bd6 100644 --- a/src/routes/UserInfoPage/UserInfoPage.tsx +++ b/src/routes/UserInfoPage/UserInfoPage.tsx @@ -6,6 +6,7 @@ import { TextInput, NativeSyntheticEvent, TextInputChangeEventData, + Pressable, } from "react-native"; import { useState } from "react"; import { @@ -32,13 +33,14 @@ import { } from "../../components/Api/Api"; import { colors } from "../../styles"; import DropDownPicker from "react-native-dropdown-picker"; -import { ValueType } from "react-native-dropdown-picker"; import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll"; import BouncyCheckbox from "react-native-bouncy-checkbox"; import { useSelector } from "react-redux"; import { RootState } from "../../features/redux/Store/Store"; import { useDispatch } from "react-redux"; import { setUser as setUserinState } from "../../features/redux/slices/UserSlice/UserSlice"; +import * as ImagePicker from "expo-image-picker"; +import * as FileSystem from "expo-file-system"; export default function UserInfoPage() { const logged_in_user = useSelector((state: RootState) => state.user.user); @@ -71,9 +73,9 @@ export default function UserInfoPage() { year_level: data[1].year_level, semester: data[1].semester, course: data[1].course, - avatar: data[1].avatar, student_id_number: data[1].student_id_number, irregular: data[1].irregular, + avatar: data[1].avatar, }); setSelectedCourse(data[1].course); setSelectedSemester(data[1].semester); @@ -157,15 +159,39 @@ export default function UserInfoPage() { }); // Profile photo + const pickImage = async () => { + // No permissions request is necessary for launching the image library + let result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions.All, + allowsEditing: true, + aspect: [4, 3], + quality: 1, + }); + if (!result.canceled) { + const encodedImage = await FileSystem.readAsStringAsync( + result.assets[0].uri, + { encoding: "base64" } + ); + mutation.mutate({ + avatar: encodedImage, + }); + } + }; function Avatar() { if (user.avatar) { - return ; + return ( + + + + ); } else { return ( - + + + ); } } @@ -350,7 +376,7 @@ export default function UserInfoPage() { Irregular + ); } else { return ( diff --git a/src/routes/Loading/Loading.tsx b/src/routes/Loading/Loading.tsx new file mode 100644 index 0000000..9fb7c7c --- /dev/null +++ b/src/routes/Loading/Loading.tsx @@ -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 ( + + + + + Loading StudE... + + + ); +} diff --git a/src/routes/UserInfoPage/UserInfoPage.tsx b/src/routes/UserInfoPage/UserInfoPage.tsx index 0453bd6..d713a2f 100644 --- a/src/routes/UserInfoPage/UserInfoPage.tsx +++ b/src/routes/UserInfoPage/UserInfoPage.tsx @@ -361,7 +361,7 @@ export default function UserInfoPage() { - + { mutation.mutate({ diff --git a/src/styles.tsx b/src/styles.tsx index c0fb9e6..546847e 100644 --- a/src/styles.tsx +++ b/src/styles.tsx @@ -171,7 +171,8 @@ const styles = StyleSheet.create({ width: "70%", }, map: { - height: Viewport.height * 0.8, + marginVertical: 4, + height: Viewport.height * 0.7, width: Viewport.width * 0.8, alignSelf: "center", }, From 126223394dad11694fd9c8dd12c68d8821ce0f17 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Mon, 7 Aug 2023 14:22:47 +0800 Subject: [PATCH 12/51] Remove bottomsheet library and added homepage improvements --- package-lock.json | 38 ----------------------------------- package.json | 1 - src/components/Api/Api.tsx | 2 +- src/interfaces/Interfaces.tsx | 5 +++-- src/routes/Home/Home.tsx | 6 +++--- 5 files changed, 7 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 078f13d..14006b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "stude_frontend", "version": "1.0.0", "dependencies": { - "@gorhom/bottom-sheet": "^4.4.7", "@react-native-async-storage/async-storage": "1.17.11", "@react-navigation/drawer": "^6.6.3", "@react-navigation/native": "^6.1.7", @@ -3112,43 +3111,6 @@ "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==" }, - "node_modules/@gorhom/bottom-sheet": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-4.4.7.tgz", - "integrity": "sha512-ukTuTqDQi2heo68hAJsBpUQeEkdqP9REBcn47OpuvPKhdPuO1RBOOADjqXJNCnZZRcY+HqbnGPMSLFVc31zylQ==", - "dependencies": { - "@gorhom/portal": "1.0.14", - "invariant": "^2.2.4" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-native": "*", - "react": "*", - "react-native": "*", - "react-native-gesture-handler": ">=1.10.1", - "react-native-reanimated": ">=2.2.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-native": { - "optional": true - } - } - }, - "node_modules/@gorhom/portal": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@gorhom/portal/-/portal-1.0.14.tgz", - "integrity": "sha512-MXyL4xvCjmgaORr/rtryDNFy3kU4qUbKlwtQqqsygd0xX3mhKjOLn6mQK8wfu0RkoE0pBE0nAasRoHua+/QZ7A==", - "dependencies": { - "nanoid": "^3.3.1" - }, - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/@graphql-typed-document-node/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", diff --git a/package.json b/package.json index 37610e6..5af9ec0 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "web": "expo start --web" }, "dependencies": { - "@gorhom/bottom-sheet": "^4.4.7", "@react-native-async-storage/async-storage": "1.17.11", "@react-navigation/drawer": "^6.6.3", "@react-navigation/native": "^6.1.7", diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index e7df9e3..b8a8ace 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -262,7 +262,7 @@ export async function PostStudentStatus(info: StudentStatusParams) { const config = await GetConfig(); console.log(info); return instance - .post("/api/v1/student_status/self/", info, config) + .patch("/api/v1/student_status/self/", info, config) .then((response) => { console.log("heh1"); return [true, response.data]; diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index 2eeb8d0..d613b7c 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -145,6 +145,7 @@ interface Location { } export interface StudentStatusParams { - subject: string; - location: Location; + subject?: string; + location?: Location; + active?: boolean; } diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 89b88c8..8672b7a 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -56,7 +56,7 @@ export default function Home() { return () => clearInterval(interval); }); - // Run when screen loads + // Refresh when screen loads useEffect(() => { requestLocation(); }, []); @@ -65,8 +65,8 @@ export default function Home() { let dist = GetDistance( location.coords.latitude, location.coords.longitude, - 8.4857, // LatitudeDelta - 124.6565 // LongitudeDelta + ustpCoords.latitude, + ustpCoords.longitude ); setDist(Math.round(dist)); } From c95e3e2d7953e8e30e8b6d480a56cd2a28a15297 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Mon, 7 Aug 2023 14:55:44 +0800 Subject: [PATCH 13/51] Added start studying page --- App.tsx | 2 + src/interfaces/Interfaces.tsx | 6 +- src/routes/Home/Home.tsx | 20 ++--- src/routes/StartStudying/StartStudying.tsx | 99 ++++++++++++++++++++++ 4 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 src/routes/StartStudying/StartStudying.tsx diff --git a/App.tsx b/App.tsx index 86b7055..f381c9b 100644 --- a/App.tsx +++ b/App.tsx @@ -23,6 +23,7 @@ import { StatusBar } from "expo-status-bar"; import UserInfoPage from "./src/routes/UserInfoPage/UserInfoPage"; import SubjectsPage from "./src/routes/SubjectsPage/SubjectsPage"; import Loading from "./src/routes/Loading/Loading"; +import StartStudying from "./src/routes/StartStudying/StartStudying"; const Drawer = createDrawerNavigator(); @@ -75,6 +76,7 @@ export default function App() { + diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index d613b7c..76c7c8c 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -1,3 +1,5 @@ +import * as Location from "expo-location"; + export interface IconProps { size: number; } @@ -133,7 +135,7 @@ export interface StudentData { course_shortname: string; year_level: string; yearlevel_shortname: string; - subjects: Subject[]; // To-do + subjects: string[]; // To-do username: string; } @@ -149,3 +151,5 @@ export interface StudentStatusParams { location?: Location; active?: boolean; } + +export type LocationType = Location.LocationObject; diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 8672b7a..a213472 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -7,9 +7,15 @@ import * as Location from "expo-location"; import GetDistance from "../../components/GetDistance/GetDistance"; import Button from "../../components/Button/Button"; import { PostStudentStatus } from "../../components/Api/Api"; -import { StudentStatusParams } from "../../interfaces/Interfaces"; -type LocationType = Location.LocationObject; +import { + RootDrawerParamList, + StudentStatusParams, +} from "../../interfaces/Interfaces"; +import { LocationType } from "../../interfaces/Interfaces"; +import { useNavigation } from "@react-navigation/native"; + export default function Home() { + const navigation = useNavigation(); const [location, setLocation] = useState(null); const [dist, setDist] = useState(null); const [feedback, setFeedback] = useState( @@ -158,15 +164,7 @@ export default function Home() { + + {feedback} + + + + ); + } + return ; +} From 33ffcde6bee91fbb75007660e340f7ea9ebf0aa0 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Mon, 7 Aug 2023 15:03:53 +0800 Subject: [PATCH 14/51] Added map preview to start studying page --- src/components/Api/Api.tsx | 3 + src/routes/Home/Home.tsx | 10 +-- src/routes/StartStudying/StartStudying.tsx | 78 ++++++++++++++++------ 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index b8a8ace..55b410d 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -30,6 +30,9 @@ const instance = axios.create({ console.log(backendURL); +// 3rd Party APIs +export const urlProvider = + "https://tile.thunderforest.com/atlas/{z}/{x}/{y}.png?apikey=0f5cb5930d7642a8a921daea650754d9"; // App APIs // Token Handling diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index a213472..73eb82a 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -6,13 +6,10 @@ import MapView, { Callout, 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 { PostStudentStatus } from "../../components/Api/Api"; -import { - RootDrawerParamList, - StudentStatusParams, -} from "../../interfaces/Interfaces"; +import { RootDrawerParamList } from "../../interfaces/Interfaces"; import { LocationType } from "../../interfaces/Interfaces"; import { useNavigation } from "@react-navigation/native"; +import { urlProvider } from "../../components/Api/Api"; export default function Home() { const navigation = useNavigation(); @@ -21,8 +18,7 @@ export default function Home() { const [feedback, setFeedback] = useState( "To continue, please allow Stud-E permission to location services" ); - const urlProvider = - "https://tile.thunderforest.com/atlas/{z}/{x}/{y}.png?apikey=0f5cb5930d7642a8a921daea650754d9"; + const ustpCoords = { latitude: 8.4857, longitude: 124.6565, diff --git a/src/routes/StartStudying/StartStudying.tsx b/src/routes/StartStudying/StartStudying.tsx index 56b9b37..4fadf35 100644 --- a/src/routes/StartStudying/StartStudying.tsx +++ b/src/routes/StartStudying/StartStudying.tsx @@ -1,20 +1,16 @@ import * as React from "react"; -import styles from "../../styles"; +import styles, { Viewport } from "../../styles"; import { View, Text } from "react-native"; import { useState } from "react"; -import { - UserInfoParams, - Subject, - OptionType, -} from "../../interfaces/Interfaces"; +import { UserInfoParams, OptionType } from "../../interfaces/Interfaces"; import Button from "../../components/Button/Button"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { UserInfo } from "../../components/Api/Api"; import { colors } from "../../styles"; import DropDownPicker from "react-native-dropdown-picker"; import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll"; -import { useSelector } from "react-redux"; -import { RootState } from "../../features/redux/Store/Store"; +import { urlProvider } from "../../components/Api/Api"; +import MapView, { UrlTile, Marker } from "react-native-maps"; export default function StartStudying({ route }: any) { const { location } = route.params; @@ -43,6 +39,57 @@ export default function StartStudying({ route }: any) { return ( + + + + + + + + + {feedback} + Start Studying @@ -80,17 +127,10 @@ export default function StartStudying({ route }: any) { /> - - - - {location.coords.longitude + "\n" + location.coords.latitude} - - - - {feedback} - + + ); From 529a7a75fd3cdb7d73a3aa316c14f4056234ee8b Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Thu, 10 Aug 2023 17:23:12 +0800 Subject: [PATCH 15/51] Improved homepage --- src/components/Api/Api.tsx | 27 ++++- src/interfaces/Interfaces.tsx | 32 +++--- src/routes/Home/Home.tsx | 119 ++++++++++++++++--- src/routes/StartStudying/StartStudying.tsx | 128 +++++++++++++-------- src/routes/SubjectsPage/SubjectsPage.tsx | 2 +- src/routes/UserInfoPage/UserInfoPage.tsx | 20 ++++ 6 files changed, 246 insertions(+), 82 deletions(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 55b410d..54fb465 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -6,7 +6,7 @@ import { OnboardingParams, PatchStudentData, RegistrationParams, - StudentStatusParams, + StudentStatus, } from "../../interfaces/Interfaces"; export let backendURL = "https://stude.keannu1.duckdns.org"; @@ -261,20 +261,35 @@ export async function OnboardingUpdateStudentInfo(info: OnboardingParams) { }); } -export async function PostStudentStatus(info: StudentStatusParams) { +export async function GetStudentStatus() { const config = await GetConfig(); - console.log(info); return instance - .patch("/api/v1/student_status/self/", info, config) + .get("/api/v1/student_status/self/", config) .then((response) => { - console.log("heh1"); 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("heh2", error); + console.log(error_message); + return [false, error_message]; + }); +} + +export async function PatchStudentStatus(info: StudentStatus) { + const config = await GetConfig(); + console.log(info); + return instance + .patch("/api/v1/student_status/self/", info, config) + .then((response) => { + 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_message); return [false, error_message]; }); } diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index 76c7c8c..eb6486c 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -122,6 +122,23 @@ export interface PatchStudentData { avatar?: string | null; } +interface Location { + latitude: number; + longitude: number; +} + +export interface StudentStatus { + user?: string; + subject?: string; + location?: Location; + landmark?: string | null; + active?: boolean; +} + +export type StudentStatusParams = [boolean, StudentStatus]; + +export type LocationType = Location.LocationObject; + export interface StudentData { first_name: string; last_name: string; @@ -135,21 +152,8 @@ export interface StudentData { course_shortname: string; year_level: string; yearlevel_shortname: string; - subjects: string[]; // To-do + subjects: string[]; username: string; } export type UserInfoParams = [boolean, StudentData]; - -interface Location { - latitude: number; - longtitude: number; -} - -export interface StudentStatusParams { - subject?: string; - location?: Location; - active?: boolean; -} - -export type LocationType = Location.LocationObject; diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 73eb82a..0b79bae 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -1,23 +1,34 @@ import styles, { Viewport, colors } from "../../styles"; -import { View, Text } from "react-native"; +import { View, Text, ToastAndroid } from "react-native"; import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer"; import { useState, useEffect } from "react"; import MapView, { Callout, 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 { RootDrawerParamList } from "../../interfaces/Interfaces"; +import { + RootDrawerParamList, + StudentStatusParams, +} from "../../interfaces/Interfaces"; import { LocationType } from "../../interfaces/Interfaces"; import { useNavigation } from "@react-navigation/native"; -import { urlProvider } from "../../components/Api/Api"; +import { + GetStudentStatus, + PatchStudentStatus, + urlProvider, +} from "../../components/Api/Api"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; export default function Home() { + // Switch this condition to see the main map when debugging + const map_debug = true; const navigation = useNavigation(); const [location, setLocation] = useState(null); const [dist, setDist] = useState(null); const [feedback, setFeedback] = useState( "To continue, please allow Stud-E permission to location services" ); + const queryClient = useQueryClient(); const ustpCoords = { latitude: 8.4857, @@ -28,8 +39,10 @@ export default function Home() { async function requestLocation() { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== "granted") { - setFeedback( - "Permission to access location was denied. Please allow permission" + setFeedback("Allow location permissions to continue"); + ToastAndroid.show( + "Location permission was denied. Please allow in order to use StudE", + ToastAndroid.SHORT ); return; } @@ -71,12 +84,86 @@ export default function Home() { ustpCoords.longitude ); setDist(Math.round(dist)); + // Deactivate student status if too far away + if (dist >= 2 && !map_debug) + mutation.mutate({ + active: false, + }); } + // Student Status + const [studying, setStudying] = useState(false); + const [subject, setSubject] = useState(""); + const [buttonLabel, setButtonLabel] = useState("Start studying"); + const StudentStatus = useQuery({ + queryKey: ["user_status"], + queryFn: GetStudentStatus, + onSuccess: (data: StudentStatusParams) => { + if (data[1].active !== undefined) { + setStudying(data[1].active); + } + if (data[1].subject !== undefined) { + setSubject(data[1].subject); + } + if (data[1].active == true) { + setButtonLabel("Stop Studying"); + } else if (data[1].active == false) { + setButtonLabel("Start Studying"); + } + console.log(data[1]); + }, + onError: () => { + setFeedback("Unable to query available subjects"); + }, + }); + + const mutation = useMutation({ + mutationFn: PatchStudentStatus, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user"] }); + queryClient.invalidateQueries({ queryKey: ["user_status"] }); + ToastAndroid.show( + "You are no longer studying " + subject, + ToastAndroid.SHORT + ); + }, + onError: () => { + ToastAndroid.show( + "Server error. Unable to update student status", + ToastAndroid.SHORT + ); + }, + }); + function CustomCallout() { + if (location && location.coords) { + if (studying) { + return ( + + + You are here {"\n"} + X: {Math.round(location.coords.longitude) + "\n"} + Z: {Math.round(location.coords.latitude) + "\n"} + Studying: {subject} + + + ); + } else { + return ( + + + You are here {"\n"} + X: {Math.round(location.coords.longitude) + "\n"} + Z: {Math.round(location.coords.latitude)} + + + ); + } + } + return <>; + } function CustomMap() { if (dist && location) { - if (dist >= 2) { - // Just switch this condition for map debugging + if (dist <= 2 || map_debug) { return ( - - - You are here {"\n"} - X: {Math.round(location.coords.longitude) + "\n"} - Z: {Math.round(location.coords.latitude)} - - + ); diff --git a/src/routes/StartStudying/StartStudying.tsx b/src/routes/StartStudying/StartStudying.tsx index 4fadf35..562762d 100644 --- a/src/routes/StartStudying/StartStudying.tsx +++ b/src/routes/StartStudying/StartStudying.tsx @@ -1,21 +1,26 @@ import * as React from "react"; import styles, { Viewport } from "../../styles"; -import { View, Text } from "react-native"; +import { View, Text, ToastAndroid } from "react-native"; import { useState } from "react"; -import { UserInfoParams, OptionType } from "../../interfaces/Interfaces"; +import { + UserInfoParams, + OptionType, + RootDrawerParamList, +} from "../../interfaces/Interfaces"; import Button from "../../components/Button/Button"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { UserInfo } from "../../components/Api/Api"; +import { PatchStudentStatus, UserInfo } 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"; export default function StartStudying({ route }: any) { const { location } = route.params; const queryClient = useQueryClient(); - const [feedback, setFeedback] = useState(""); + const navigation = useNavigation(); // Subject choices const [selected_subject, setSelectedSubject] = useState(""); @@ -32,9 +37,32 @@ export default function StartStudying({ route }: any) { setSubjects(subjects); }, onError: () => { - setFeedback("Unable to query available subjects"); + ToastAndroid.show( + "Server error: Unable to query available subjects", + ToastAndroid.SHORT + ); }, }); + + const mutation = useMutation({ + mutationFn: PatchStudentStatus, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user"] }); + queryClient.invalidateQueries({ queryKey: ["user_status"] }); + ToastAndroid.show( + "You are now studying " + selected_subject, + ToastAndroid.SHORT + ); + navigation.navigate("Home"); + }, + onError: () => { + ToastAndroid.show( + "A server error has occured. Please try again", + ToastAndroid.SHORT + ); + }, + }); + if (location && location.coords) { return ( @@ -87,50 +115,60 @@ export default function StartStudying({ route }: any) { /> - - {feedback} - - - - Start Studying - - - { - 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" - /> - + { + 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" + /> - + ); diff --git a/src/routes/SubjectsPage/SubjectsPage.tsx b/src/routes/SubjectsPage/SubjectsPage.tsx index b0429e0..2db5e3b 100644 --- a/src/routes/SubjectsPage/SubjectsPage.tsx +++ b/src/routes/SubjectsPage/SubjectsPage.tsx @@ -52,7 +52,7 @@ export default function SubjectsPage() { course_shortname: "", avatar: "", student_id_number: "", - subjects: [] as Subjects, + subjects: [] as string[], }); const StudentInfo = useQuery({ queryKey: ["user"], diff --git a/src/routes/UserInfoPage/UserInfoPage.tsx b/src/routes/UserInfoPage/UserInfoPage.tsx index d713a2f..63d1b39 100644 --- a/src/routes/UserInfoPage/UserInfoPage.tsx +++ b/src/routes/UserInfoPage/UserInfoPage.tsx @@ -28,6 +28,7 @@ import { GetSemesters, GetSubjects, GetYearLevels, + PatchStudentStatus, PatchUserInfo, UserInfo, } from "../../components/Api/Api"; @@ -47,6 +48,20 @@ export default function UserInfoPage() { const dispatch = useDispatch(); const queryClient = useQueryClient(); const [feedback, setFeedback] = useState(""); + + // Student Status + const studentstatus_mutation = useMutation({ + mutationFn: PatchStudentStatus, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["user"] }); + queryClient.invalidateQueries({ queryKey: ["user_status"] }); + setFeedback("Changes applied successfully"); + }, + onError: () => { + setFeedback("An error has occured\nChanges have not been saved"); + }, + }); + // User Info const [user, setUser] = useState({ first_name: "", @@ -86,11 +101,16 @@ export default function UserInfoPage() { setFeedback("Unable to query user info"); }, }); + const mutation = useMutation({ mutationFn: PatchUserInfo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["subjects"] }); + // Reset student status when changing user info to prevent bugs + studentstatus_mutation.mutate({ + active: false, + }); setFeedback("Changes applied successfully"); dispatch(setUserinState(user)); }, From ff114b496c42b5b5ced4c213672b4c0bf8f5c226 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Mon, 14 Aug 2023 21:13:46 +0800 Subject: [PATCH 16/51] Move to modals for user feedback --- App.tsx | 48 ++++++++------- package-lock.json | 10 +++ package.json | 1 + src/routes/Home/Home.tsx | 33 ++++++---- src/routes/Login/Login.tsx | 24 ++++++-- src/routes/Onboarding/Onboarding.tsx | 22 +++++-- src/routes/Register/Register.tsx | 24 +++++--- src/routes/Revalidation/Revalidation.tsx | 20 ++++++ src/routes/StartStudying/StartStudying.tsx | 32 ++++++---- src/routes/UserInfoPage/UserInfoPage.tsx | 71 +++++++++++++++------- 10 files changed, 203 insertions(+), 82 deletions(-) diff --git a/App.tsx b/App.tsx index f381c9b..92d2b63 100644 --- a/App.tsx +++ b/App.tsx @@ -24,6 +24,8 @@ import UserInfoPage from "./src/routes/UserInfoPage/UserInfoPage"; import SubjectsPage from "./src/routes/SubjectsPage/SubjectsPage"; import Loading from "./src/routes/Loading/Loading"; import StartStudying from "./src/routes/StartStudying/StartStudying"; +import { ToastProvider } from "react-native-toast-notifications"; +import AppIcon from "./src/icons/AppIcon/AppIcon"; const Drawer = createDrawerNavigator(); @@ -58,28 +60,30 @@ export default function App() { } }, [initialRoute]); return ( - - - + }> + + + - }> - - - - - - - - - - - - - - + }> + + + + + + + + + + + + + + + ); } diff --git a/package-lock.json b/package-lock.json index 14006b9..e918be3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "react-native-screens": "~3.20.0", "react-native-select-dropdown": "^3.3.4", "react-native-svg": "13.4.0", + "react-native-toast-notifications": "^3.3.1", "react-query": "^3.39.3", "react-redux": "^8.1.1", "redux": "^4.2.1" @@ -12453,6 +12454,15 @@ "react-native": "*" } }, + "node_modules/react-native-toast-notifications": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/react-native-toast-notifications/-/react-native-toast-notifications-3.3.1.tgz", + "integrity": "sha512-yc1Q2nOdIYvAf0GAIlmg8q42hiwpEHnLxkxJ6P+tN6jpcKZ1qzMXlgnmNdyF9cm9VOyHQexEP8952IKNAv1Olw==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native/node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", diff --git a/package.json b/package.json index 5af9ec0..df97e93 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "react-native-screens": "~3.20.0", "react-native-select-dropdown": "^3.3.4", "react-native-svg": "13.4.0", + "react-native-toast-notifications": "^3.3.1", "react-query": "^3.39.3", "react-redux": "^8.1.1", "redux": "^4.2.1" diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 0b79bae..aad9597 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -18,6 +18,7 @@ import { urlProvider, } from "../../components/Api/Api"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useToast } from "react-native-toast-notifications"; export default function Home() { // Switch this condition to see the main map when debugging @@ -29,6 +30,7 @@ export default function Home() { "To continue, please allow Stud-E permission to location services" ); const queryClient = useQueryClient(); + const toast = useToast(); const ustpCoords = { latitude: 8.4857, @@ -40,9 +42,14 @@ export default function Home() { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== "granted") { setFeedback("Allow location permissions to continue"); - ToastAndroid.show( + toast.show( "Location permission was denied. Please allow in order to use StudE", - ToastAndroid.SHORT + { + type: "warning", + placement: "bottom", + duration: 4000, + animationType: "slide-in", + } ); return; } @@ -122,16 +129,20 @@ export default function Home() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["user_status"] }); - ToastAndroid.show( - "You are no longer studying " + subject, - ToastAndroid.SHORT - ); + toast.show("You are no longer studying " + subject, { + type: "success", + placement: "bottom", + duration: 4000, + animationType: "slide-in", + }); }, onError: () => { - ToastAndroid.show( - "Server error. Unable to update student status", - ToastAndroid.SHORT - ); + toast.show("Server error. Unable to update student status", { + type: "warning", + placement: "bottom", + duration: 4000, + animationType: "slide-in", + }); }, }); function CustomCallout() { @@ -139,7 +150,7 @@ export default function Home() { if (studying) { return ( - + You are here {"\n"} X: {Math.round(location.coords.longitude) + "\n"} Z: {Math.round(location.coords.latitude) + "\n"} diff --git a/src/routes/Login/Login.tsx b/src/routes/Login/Login.tsx index 8789fad..5050439 100644 --- a/src/routes/Login/Login.tsx +++ b/src/routes/Login/Login.tsx @@ -23,6 +23,7 @@ import { setOnboarding, unsetOnboarding, } from "../../features/redux/slices/StatusSlice/StatusSlice"; +import { useToast } from "react-native-toast-notifications"; export default function Login() { const navigation = useNavigation(); @@ -31,7 +32,7 @@ export default function Login() { username: "", password: "", }); - const [error, setError] = useState(""); + const toast = useToast(); return ( @@ -66,8 +67,6 @@ export default function Login() { setCreds({ ...creds, password: e.nativeEvent.text }); }} /> - - {error} - {feedback} From 2ca1dd13ca7f7d1892192aa265e88305de0fb267 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Mon, 14 Aug 2023 21:57:52 +0800 Subject: [PATCH 17/51] Finalize changes to using modals for user feedback --- App.tsx | 7 +++- src/components/Api/Api.tsx | 2 +- src/routes/Activation/Activation.tsx | 28 ++++++++----- src/routes/Home/Home.tsx | 16 +++---- src/routes/Login/Login.tsx | 13 +++--- src/routes/Onboarding/Onboarding.tsx | 32 ++++++++++++-- src/routes/Register/Register.tsx | 4 +- src/routes/Revalidation/Revalidation.tsx | 12 +++--- src/routes/StartStudying/StartStudying.tsx | 18 ++++---- src/routes/SubjectsPage/SubjectsPage.tsx | 49 ++++++++++------------ src/routes/UserInfoPage/UserInfoPage.tsx | 28 ++++++------- 11 files changed, 119 insertions(+), 90 deletions(-) diff --git a/App.tsx b/App.tsx index 92d2b63..7130bbe 100644 --- a/App.tsx +++ b/App.tsx @@ -1,5 +1,5 @@ import "react-native-gesture-handler"; -import { Text } from "react-native"; +import styles from "./src/styles"; import { NavigationContainer } from "@react-navigation/native"; import { createDrawerNavigator } from "@react-navigation/drawer"; import { Provider } from "react-redux"; @@ -60,7 +60,10 @@ export default function App() { } }, [initialRoute]); return ( - }> + } + textStyle={{ ...styles.text_white_tiny_bold }} + > diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 54fb465..4564ee4 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -28,7 +28,7 @@ const instance = axios.create({ timeout: 1000, }); -console.log(backendURL); +console.log("Using backend API:", backendURL); // 3rd Party APIs export const urlProvider = diff --git a/src/routes/Activation/Activation.tsx b/src/routes/Activation/Activation.tsx index 10aab6a..c2846c2 100644 --- a/src/routes/Activation/Activation.tsx +++ b/src/routes/Activation/Activation.tsx @@ -6,6 +6,7 @@ import { useNavigation, useRoute } from "@react-navigation/native"; import { useEffect, useState } from "react"; import { UserActivate } from "../../components/Api/Api"; import { RootDrawerParamList } from "../../interfaces/Interfaces"; +import { useToast } from "react-native-toast-notifications"; interface ActivationRouteParams { uid?: string; @@ -16,9 +17,7 @@ export default function Activation() { const route = useRoute(); const { uid, token } = (route.params as ActivationRouteParams) || ""; const navigation = useNavigation(); - const [state, setState] = useState( - "Activating with UID " + uid + " and Token " + token - ); + const toast = useToast(); const [loading, setLoading] = useState(true); useEffect(() => { @@ -28,16 +27,22 @@ export default function Activation() { token: String(token), }); if (result) { - setTimeout(() => { - setState("Activation successful!"); - }, 1000); + toast.show("Activation successful", { + type: "success", + placement: "top", + duration: 4000, + animationType: "slide-in", + }); setTimeout(() => { navigation.navigate("Login"); }, 2000); } else { - setTimeout(() => { - setState("Activation unsuccessful\nPlease contact support"); - }, 1000); + toast.show("Activation unsuccessful. Please contact support", { + type: "warning", + placement: "top", + duration: 4000, + animationType: "slide-in", + }); } setLoading(false); } @@ -63,8 +68,9 @@ export default function Activation() { size={96} color={colors.secondary_1} /> - {state} - {uid + "\n" + token} + + {"Activating with UID: " + uid + "\nToken: " + token} + ); diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index aad9597..759eef4 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -1,5 +1,5 @@ import styles, { Viewport, colors } from "../../styles"; -import { View, Text, ToastAndroid } from "react-native"; +import { View, Text } from "react-native"; import AnimatedContainer from "../../components/AnimatedContainer/AnimatedContainer"; import { useState, useEffect } from "react"; import MapView, { Callout, Marker, UrlTile } from "react-native-maps"; @@ -46,8 +46,8 @@ export default function Home() { "Location permission was denied. Please allow in order to use StudE", { type: "warning", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", } ); @@ -129,18 +129,18 @@ export default function Home() { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["user_status"] }); - toast.show("You are no longer studying " + subject, { + toast.show("You are no longer studying \n" + subject, { type: "success", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); }, onError: () => { toast.show("Server error. Unable to update student status", { type: "warning", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); }, diff --git a/src/routes/Login/Login.tsx b/src/routes/Login/Login.tsx index 5050439..aad5f4c 100644 --- a/src/routes/Login/Login.tsx +++ b/src/routes/Login/Login.tsx @@ -8,7 +8,6 @@ import { TextInputChangeEventData, } from "react-native"; import { useDispatch } from "react-redux"; -import { colors } from "../../styles"; import { useState } from "react"; import LoginIcon from "../../icons/LoginIcon/LoginIcon"; import Button from "../../components/Button/Button"; @@ -89,16 +88,16 @@ export default function Login() { navigation.navigate("Onboarding"); toast.show("Successfully logged in", { type: "success", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); } else { dispatch(unsetOnboarding()); toast.show("Successfully logged in", { type: "success", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); navigation.navigate("Home"); @@ -108,8 +107,8 @@ export default function Login() { console.log(ParseLoginError(JSON.stringify(result[1]))); toast.show(JSON.stringify(result[1]), { type: "warning", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); } diff --git a/src/routes/Onboarding/Onboarding.tsx b/src/routes/Onboarding/Onboarding.tsx index 9aef47e..0d52063 100644 --- a/src/routes/Onboarding/Onboarding.tsx +++ b/src/routes/Onboarding/Onboarding.tsx @@ -48,6 +48,14 @@ export default function Onboarding() { })); setSemesters(semesters); }, + onError: () => { + toast.show("Server error: Unable to query available semesters", { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, }); // Year Level const [selected_yearlevel, setSelectedYearLevel] = useState(""); @@ -66,6 +74,14 @@ export default function Onboarding() { })); setYearLevels(year_levels); }, + onError: () => { + toast.show("Server error: Unable to query available year levels", { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, }); // Course const [selected_course, setSelectedCourse] = useState(""); @@ -87,6 +103,14 @@ export default function Onboarding() { })); setCourses(courses); }, + onError: () => { + toast.show("Server error: Unable to query available courses", { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, }); if (yearlevel_query.error || semester_query.error || course_query.error) { return ( @@ -230,8 +254,8 @@ export default function Onboarding() { dispatch(setUser(result[1])); toast.show("Changes applied successfully", { type: "success", - placement: "bottom", - duration: 6000, + placement: "top", + duration: 2000, animationType: "slide-in", }); navigation.navigate("Home"); @@ -241,8 +265,8 @@ export default function Onboarding() { "An error has occured\nChanges have not been saved", { type: "warning", - placement: "bottom", - duration: 6000, + placement: "top", + duration: 2000, animationType: "slide-in", } ); diff --git a/src/routes/Register/Register.tsx b/src/routes/Register/Register.tsx index f9d50cf..37f3a0f 100644 --- a/src/routes/Register/Register.tsx +++ b/src/routes/Register/Register.tsx @@ -146,7 +146,7 @@ export default function Register() { "Success! An email has been sent to activate your account", { type: "success", - placement: "bottom", + placement: "top", duration: 6000, animationType: "slide-in", } @@ -157,7 +157,7 @@ export default function Register() { } else { toast.show(JSON.stringify(result[1]), { type: "warning", - placement: "bottom", + placement: "top", duration: 6000, animationType: "slide-in", }); diff --git a/src/routes/Revalidation/Revalidation.tsx b/src/routes/Revalidation/Revalidation.tsx index 9d8bd93..c633f99 100644 --- a/src/routes/Revalidation/Revalidation.tsx +++ b/src/routes/Revalidation/Revalidation.tsx @@ -37,8 +37,8 @@ export default function Revalidation() { dispatch(setOnboarding()); toast.show("Previous session restored", { type: "success", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); await setTimeout(() => { @@ -48,8 +48,8 @@ export default function Revalidation() { dispatch(unsetOnboarding()); toast.show("Previous session restored", { type: "success", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); await setTimeout(() => { @@ -60,8 +60,8 @@ export default function Revalidation() { await setState("Session expired"); toast.show("Session expired. Please login again", { type: "warning", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); await setTimeout(() => { diff --git a/src/routes/StartStudying/StartStudying.tsx b/src/routes/StartStudying/StartStudying.tsx index 265b23e..fb46b29 100644 --- a/src/routes/StartStudying/StartStudying.tsx +++ b/src/routes/StartStudying/StartStudying.tsx @@ -40,9 +40,9 @@ export default function StartStudying({ route }: any) { }, onError: () => { toast.show("Server error: Unable to query available subjects", { - type: "error", - placement: "bottom", - duration: 4000, + type: "warning", + placement: "top", + duration: 2000, animationType: "slide-in", }); }, @@ -53,19 +53,19 @@ export default function StartStudying({ route }: any) { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["user_status"] }); - toast.show("You are now studying " + selected_subject, { + toast.show("You are now studying \n" + selected_subject, { type: "success", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); navigation.navigate("Home"); }, onError: () => { toast.show("A server error has occured. Please try again", { - type: "error", - placement: "bottom", - duration: 4000, + type: "warning", + placement: "top", + duration: 2000, animationType: "slide-in", }); }, diff --git a/src/routes/SubjectsPage/SubjectsPage.tsx b/src/routes/SubjectsPage/SubjectsPage.tsx index 2db5e3b..68b5bd5 100644 --- a/src/routes/SubjectsPage/SubjectsPage.tsx +++ b/src/routes/SubjectsPage/SubjectsPage.tsx @@ -1,45 +1,28 @@ import * as React from "react"; import styles from "../../styles"; +import { View, Text } from "react-native"; +import { useState } from "react"; import { - View, - Text, - TextInput, - NativeSyntheticEvent, - TextInputChangeEventData, -} from "react-native"; -import { useEffect, useState } from "react"; -import { - SemesterParams, UserInfoParams, - Semester, SubjectParams, Subject, - YearLevel, - Course, OptionType, - Subjects, } from "../../interfaces/Interfaces"; import Button from "../../components/Button/Button"; import { Image } from "react-native"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { - GetCourses, - GetSemesters, - GetSubjects, - GetYearLevels, - PatchUserInfo, - UserInfo, -} from "../../components/Api/Api"; +import { GetSubjects, PatchUserInfo, UserInfo } from "../../components/Api/Api"; import { colors } from "../../styles"; import DropDownPicker from "react-native-dropdown-picker"; import AnimatedContainerNoScroll from "../../components/AnimatedContainer/AnimatedContainerNoScroll"; import { useSelector } from "react-redux"; import { RootState } from "../../features/redux/Store/Store"; +import { useToast } from "react-native-toast-notifications"; export default function SubjectsPage() { const logged_in_user = useSelector((state: RootState) => state.user.user); const queryClient = useQueryClient(); - const [feedback, setFeedback] = useState(""); + const toast = useToast(); // User Info const [user, setUser] = useState({ first_name: "", @@ -72,7 +55,12 @@ export default function SubjectsPage() { setSelectedSubjects(data[1].subjects); }, onError: () => { - setFeedback("Unable to query user info"); + toast.show("Server Error: Unable to query user info", { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); }, }); const mutation = useMutation({ @@ -81,7 +69,12 @@ export default function SubjectsPage() { queryClient.invalidateQueries({ queryKey: ["user"] }); queryClient.invalidateQueries({ queryKey: ["subjects"] }); setSelectedSubjects([]); - setFeedback("Changes applied successfully"); + toast.show("Changes applied successfully", { + type: "success", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); }, }); @@ -106,7 +99,12 @@ export default function SubjectsPage() { } }, onError: () => { - setFeedback("Unable to query subject info"); + toast.show("Server Error: Unable to query subject info", { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); }, }); @@ -189,7 +187,6 @@ export default function SubjectsPage() { Save Changes - {feedback} diff --git a/src/routes/UserInfoPage/UserInfoPage.tsx b/src/routes/UserInfoPage/UserInfoPage.tsx index 3be501b..6292c11 100644 --- a/src/routes/UserInfoPage/UserInfoPage.tsx +++ b/src/routes/UserInfoPage/UserInfoPage.tsx @@ -58,16 +58,16 @@ export default function UserInfoPage() { queryClient.invalidateQueries({ queryKey: ["user_status"] }); toast.show("Changes applied successfully", { type: "success", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); }, onError: () => { toast.show("An error has occured\nChanges have not been saved", { type: "warning", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); }, @@ -112,7 +112,7 @@ export default function UserInfoPage() { toast.show("Server Error: Unable to query user info", { type: "warning", placement: "bottom", - duration: 4000, + duration: 2000, animationType: "slide-in", }); }, @@ -130,7 +130,7 @@ export default function UserInfoPage() { toast.show("Changes applied successfully", { type: "success", placement: "bottom", - duration: 4000, + duration: 2000, animationType: "slide-in", }); dispatch(setUserinState(user)); @@ -139,7 +139,7 @@ export default function UserInfoPage() { toast.show("An error has occured\nChanges have not been saved", { type: "warning", placement: "bottom", - duration: 4000, + duration: 2000, animationType: "slide-in", }); }, @@ -164,8 +164,8 @@ export default function UserInfoPage() { onError: () => { toast.show("Server Error: Unable to query semester info", { type: "warning", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); }, @@ -188,8 +188,8 @@ export default function UserInfoPage() { onError: () => { toast.show("Server Error: Unable to query year level info", { type: "warning", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); }, @@ -212,8 +212,8 @@ export default function UserInfoPage() { onError: () => { toast.show("Server Error: Unable to query course info", { type: "warning", - placement: "bottom", - duration: 4000, + placement: "top", + duration: 2000, animationType: "slide-in", }); }, @@ -426,7 +426,7 @@ export default function UserInfoPage() { Irregular From 040ffb622c7a6616f6e8653060b9c5ae6311a1ec Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Tue, 15 Aug 2023 14:53:58 +0800 Subject: [PATCH 23/51] Moved callouts to a separate component --- .../CustomMapCallout/CustomMapCallout.tsx | 41 +++++++++++++++++++ src/routes/Home/Home.tsx | 35 +++------------- 2 files changed, 47 insertions(+), 29 deletions(-) create mode 100644 src/components/CustomMapCallout/CustomMapCallout.tsx diff --git a/src/components/CustomMapCallout/CustomMapCallout.tsx b/src/components/CustomMapCallout/CustomMapCallout.tsx new file mode 100644 index 0000000..26cea15 --- /dev/null +++ b/src/components/CustomMapCallout/CustomMapCallout.tsx @@ -0,0 +1,41 @@ +import { Callout } from "react-native-maps"; +import { LocationType } from "../../interfaces/Interfaces"; +import styles from "../../styles"; +import { Text } from "react-native"; + +// Map popup for user's location + +type props = { + location: LocationType; + studying: boolean; + subject?: string; +}; + +export default function CustomMapCallout(props: props) { + let { location, studying, subject } = props; + if (location && location.coords) { + if (studying) { + return ( + + + You are here {"\n"} + X: {Math.round(location.coords.longitude) + "\n"} + Z: {Math.round(location.coords.latitude) + "\n"} + Studying: {subject} + + + ); + } else { + return ( + + + You are here {"\n"} + X: {Math.round(location.coords.longitude) + "\n"} + Z: {Math.round(location.coords.latitude)} + + + ); + } + } + return <>; +} diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 8328f3a..2b910e2 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -22,6 +22,7 @@ import { } from "../../components/Api/Api"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useToast } from "react-native-toast-notifications"; +import CustomMapCallout from "../../components/CustomMapCallout/CustomMapCallout"; export default function Home() { // Switch this condition to see the main map when debugging @@ -188,34 +189,6 @@ export default function Home() { }, }); - // Map popup for user's location - function CustomCallout() { - if (location && location.coords) { - if (studying) { - return ( - - - You are here {"\n"} - X: {Math.round(location.coords.longitude) + "\n"} - Z: {Math.round(location.coords.latitude) + "\n"} - Studying: {subject} - - - ); - } else { - return ( - - - You are here {"\n"} - X: {Math.round(location.coords.longitude) + "\n"} - Z: {Math.round(location.coords.latitude)} - - - ); - } - } - return <>; - } function CustomMap() { if (dist && location) { if (dist <= 2 || map_debug) { @@ -289,7 +262,11 @@ export default function Home() { }} pinColor={colors.primary_1} > - + + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + /> + ); + })} + Date: Fri, 8 Sep 2023 21:41:46 +0800 Subject: [PATCH 26/51] Changed to circle rendering for student status --- src/routes/Home/Home.tsx | 183 ++++++++++++++++++++++++--------------- 1 file changed, 115 insertions(+), 68 deletions(-) diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 951045e..e1fc2d8 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -176,6 +176,7 @@ export default function Home() { }, }); + // Can probably just get the max distance from the array and use it as radius const [student_statuses, setStudentStatuses] = useState([]); // Student Status List const StudentStatusList = useQuery({ @@ -190,6 +191,7 @@ export default function Home() { }, onSuccess: (data: StudentStatusListReturnType) => { if (data[1]) { + // Circle generation for students in a study group // We first flatten the data to remove nested entries let flattened_data = data[1].map((item) => ({ active: item.active, @@ -200,30 +202,77 @@ export default function Home() { study_group: "", subject: item.subject, user: item.user, + weight: 1, })); + // Dummy data flattened_data.push({ active: true, distance: 50, landmark: "", - latitude: 9.498298904115586, - longitude: 125.59552381187677, + latitude: 8.498837, + longitude: 124.595422, study_group: "", subject: "Introduction to Computing", - user: "Keannu", + user: "Dummy", + weight: 1, }); // We get each unique subject let unique_subjects = [ ...new Set(flattened_data.map((item) => item.subject)), ]; - console.log("Unique Subjects:", unique_subjects); let result: any[] = []; // Then append all entries belonging to that subject to its own array - unique_subjects.forEach((subject) => { + unique_subjects.forEach((subject, index: number) => { + index++; let filteredData = flattened_data.filter( (item) => item.subject === subject && item.study_group === "" ); - // We then concatenate this into the final array - result = result.concat([filteredData]); + /*console.log( + "Subject #", + index, + "-", + filteredData[0].subject, + filteredData + );*/ + // We get the circle radius based on the furthest point + let circle_radius = Math.max( + ...filteredData.map((item) => item.distance) + ); + console.log( + "Radius of circle:", + Math.max(...filteredData.map((item) => item.distance)) + ); + // We get the circle's center by averaging all the points + // Calculate the average latitude and longitude + const totalLat = filteredData.reduce( + (sum, point) => sum + point.latitude, + 0 + ); + const totalLng = filteredData.reduce( + (sum, point) => sum + point.longitude, + 0 + ); + + const avgLat = totalLat / filteredData.length; + const avgLng = totalLng / filteredData.length; + + console.log("Center Latitude:", avgLat); + console.log("Center Longitude:", avgLng); + // We now build the object + const subjectUserMap: any = {}; + filteredData.forEach((item) => { + if (!subjectUserMap["users"]) { + subjectUserMap["users"] = []; + } + subjectUserMap["subject"] = item.subject; + subjectUserMap["latitude"] = avgLat; + subjectUserMap["longitude"] = avgLng; + subjectUserMap["radius"] = circle_radius; + subjectUserMap["users"].push(item.user); + }); + console.log(subjectUserMap); + + result = result.concat([subjectUserMap]); }); console.log("Final Result:", result); @@ -258,7 +307,6 @@ export default function Home() { ], }, ]} - mapType="none" scrollEnabled={true} zoomEnabled={true} toolbarEnabled={false} @@ -275,73 +323,72 @@ export default function Home() { loadingBackgroundColor={colors.secondary_2} > {student_statuses.map((student_status: any, index: number) => { - console.log("TEST INDEX", index, student_status); + const randomColorWithOpacity = `rgba(${Math.floor( + Math.random() * 256 + )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( + Math.random() * 256 + )}, 0.7)`; + return ( - { - const users = student_statuses.map((item: any) => ({ - user: item.user, - })); - toast.hideAll(); - toast.show( - - - Subject: {student_status[0].subject} - - - Students Studying: {student_status.length} - - - , - { - type: "normal", - placement: "top", - duration: 2000, - animationType: "slide-in", - style: { - backgroundColor: colors.secondary_2, - borderWidth: 1, - borderColor: colors.primary_1, - }, - } - ); - }} - /> + + Students Studying: {student_status.users.length} + + + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + /> + + ); })} - - Date: Sat, 9 Sep 2023 20:45:29 +0800 Subject: [PATCH 27/51] Added Haversine Formula calculation to get the radius of circles for study groups required for rendering --- .../CustomMapCallout/CustomMapCallout.tsx | 4 +- .../ParseStudyGroupList.tsx | 125 ++++++++++ src/interfaces/Interfaces.tsx | 29 ++- src/routes/Home/Home.tsx | 226 ++++++------------ 4 files changed, 225 insertions(+), 159 deletions(-) create mode 100644 src/components/ParseStudyGroupList/ParseStudyGroupList.tsx diff --git a/src/components/CustomMapCallout/CustomMapCallout.tsx b/src/components/CustomMapCallout/CustomMapCallout.tsx index 26cea15..23661e3 100644 --- a/src/components/CustomMapCallout/CustomMapCallout.tsx +++ b/src/components/CustomMapCallout/CustomMapCallout.tsx @@ -1,12 +1,12 @@ import { Callout } from "react-native-maps"; -import { LocationType } from "../../interfaces/Interfaces"; +import { RawLocationType } from "../../interfaces/Interfaces"; import styles from "../../styles"; import { Text } from "react-native"; // Map popup for user's location type props = { - location: LocationType; + location: RawLocationType; studying: boolean; subject?: string; }; diff --git a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx new file mode 100644 index 0000000..492b649 --- /dev/null +++ b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx @@ -0,0 +1,125 @@ +import * as React from "react"; +import { View, Text } from "react-native"; +import { + StudentStatusFilterType, + LocationType, + subjectUserMapType, + StudentStatusListType, + StudentStatusFilterTypeFlattened, +} from "../../interfaces/Interfaces"; +import { Double, Float } from "react-native/Libraries/Types/CodegenTypes"; + +export default function ParseStudyGroupList(data: any) { + let result: any[] = []; + // Circle generation for students in a study group + // We first flatten the data to remove nested entries + console.log("Initial Data:", data); + let flattened_data = data + .filter((item: StudentStatusFilterType) => item.study_group !== "") + .map((item: StudentStatusFilterType) => ({ + active: item.active, + distance: item.distance, + landmark: item.landmark, + latitude: item.location.latitude, + longitude: item.location.longitude, + study_group: item.study_group, + subject: item.subject, + user: item.user, + weight: 1, + })); + console.log("Filtered Data:", flattened_data); + + // We get each unique subject + let unique_subjects = [ + ...new Set( + flattened_data.map((item: StudentStatusFilterType) => item.subject) + ), + ]; + + // Then append all entries belonging to that subject to its own array + unique_subjects.forEach((subject, index: number) => { + index++; + let filteredData = flattened_data.filter( + (item: StudentStatusFilterTypeFlattened) => item.subject === subject + ); + console.log("Subject #", index, "-", filteredData[0].subject, filteredData); + // We get the circle's center by averaging all the points + // Calculate the average latitude and longitude + const totalLat = filteredData.reduce( + (sum: Double, point: LocationType) => sum + point.latitude, + 0 + ); + const totalLng = filteredData.reduce( + (sum: Double, point: LocationType) => sum + point.longitude, + 0 + ); + + const avgLat = totalLat / filteredData.length; + const avgLng = totalLng / filteredData.length; + + console.log("Center Latitude:", avgLat); + console.log("Center Longitude:", avgLng); + + // We now calculate the radius of the circle using the Haversine Distance Formula + + function haversineDistance( + lat1: number, + lon1: number, + lat2: number, + lon2: number + ) { + function toRad(x: number) { + return (x * Math.PI) / 180; + } + + var R = 6371; // km + var x1 = lat2 - lat1; + var dLat = toRad(x1); + var x2 = lon2 - lon1; + var dLon = toRad(x2); + var a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * + Math.cos(toRad(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; + } + + let circle_radius = + Math.max( + ...filteredData.map((item: StudentStatusFilterTypeFlattened) => + haversineDistance(avgLat, avgLng, item.latitude, item.longitude) + ) + ) * 1000; + console.log("Radius:", circle_radius); + + // We now build the object + const subjectUserMap: subjectUserMapType = { + subject: "", + users: [], + latitude: 0, + longitude: 0, + radius: 0, + }; + filteredData.forEach((item: StudentStatusFilterType) => { + if (!subjectUserMap["users"]) { + subjectUserMap["users"] = []; + } + subjectUserMap["subject"] = item.subject; + subjectUserMap["latitude"] = avgLat; + subjectUserMap["longitude"] = avgLng; + subjectUserMap["radius"] = circle_radius; + subjectUserMap["users"].push(item.user); + }); + console.log(subjectUserMap); + + result = result.concat([subjectUserMap]); + }); + + // console.log("Final Result:", result); + + return result; +} diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index a21360e..ac42a41 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -124,7 +124,7 @@ export interface PatchUserInfoType { avatar?: string; } -interface Location { +export interface LocationType { latitude: Float; longitude: Float; } @@ -132,7 +132,7 @@ interface Location { export interface StudentStatusType { user?: string; subject?: string; - location?: Location; + location?: LocationType; landmark?: string | null; active?: boolean; } @@ -141,10 +141,23 @@ export interface StudentStatusFilterType { active: boolean; distance: number; landmark: string | null; - location: Location; + 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 type StudentStatusReturnType = [boolean, StudentStatusType]; @@ -152,7 +165,7 @@ export type StudentStatusReturnType = [boolean, StudentStatusType]; export type StudentStatusListType = Array; export type StudentStatusListReturnType = [boolean, StudentStatusListType]; -export type LocationType = Location.LocationObject; +export type RawLocationType = Location.LocationObject; export interface UserInfoType { first_name: string; @@ -172,3 +185,11 @@ export interface UserInfoType { } export type UserInfoReturnType = [boolean, UserInfoType]; + +export type subjectUserMapType = { + subject: string; + users: string[]; + latitude: Float; + longitude: Float; + radius: Float; +}; diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index e1fc2d8..cf8332a 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -17,10 +17,11 @@ import Button from "../../components/Button/Button"; import { RootDrawerParamList, StudentStatusReturnType, - LocationType, + RawLocationType, StudentStatusType, StudentStatusListReturnType, StudentStatusListType, + subjectUserMapType, } from "../../interfaces/Interfaces"; import { useNavigation } from "@react-navigation/native"; import { @@ -33,12 +34,14 @@ import { import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useToast } from "react-native-toast-notifications"; import CustomMapCallout from "../../components/CustomMapCallout/CustomMapCallout"; +import ParseStudentStatusList from "../../components/ParseStudyGroupList/ParseStudyGroupList"; +import React from "react"; export default function Home() { // Switch this condition to see the main map when debugging const map_debug = true; const navigation = useNavigation(); - const [location, setLocation] = useState(null); + const [location, setLocation] = useState(null); const [dist, setDist] = useState(null); const [feedback, setFeedback] = useState( "To continue, please allow Stud-E permission to location services" @@ -97,7 +100,7 @@ export default function Home() { requestLocation(); }, []); - async function GetDistanceRoundedOff(location: LocationType) { + async function GetDistanceRoundedOff(location: RawLocationType) { let dist = GetDistance( location.coords.latitude, location.coords.longitude, @@ -176,8 +179,8 @@ export default function Home() { }, }); - // Can probably just get the max distance from the array and use it as radius const [student_statuses, setStudentStatuses] = useState([]); + const [study_groups, setStudyGroups] = useState([]); // Student Status List const StudentStatusList = useQuery({ enabled: studying, @@ -191,93 +194,7 @@ export default function Home() { }, onSuccess: (data: StudentStatusListReturnType) => { if (data[1]) { - // Circle generation for students in a study group - // We first flatten the data to remove nested entries - let flattened_data = data[1].map((item) => ({ - active: item.active, - distance: item.distance, - landmark: item.landmark, - latitude: item.location.latitude, - longitude: item.location.longitude, - study_group: "", - subject: item.subject, - user: item.user, - weight: 1, - })); - // Dummy data - flattened_data.push({ - active: true, - distance: 50, - landmark: "", - latitude: 8.498837, - longitude: 124.595422, - study_group: "", - subject: "Introduction to Computing", - user: "Dummy", - weight: 1, - }); - // We get each unique subject - let unique_subjects = [ - ...new Set(flattened_data.map((item) => item.subject)), - ]; - let result: any[] = []; - // Then append all entries belonging to that subject to its own array - unique_subjects.forEach((subject, index: number) => { - index++; - let filteredData = flattened_data.filter( - (item) => item.subject === subject && item.study_group === "" - ); - /*console.log( - "Subject #", - index, - "-", - filteredData[0].subject, - filteredData - );*/ - // We get the circle radius based on the furthest point - let circle_radius = Math.max( - ...filteredData.map((item) => item.distance) - ); - console.log( - "Radius of circle:", - Math.max(...filteredData.map((item) => item.distance)) - ); - // We get the circle's center by averaging all the points - // Calculate the average latitude and longitude - const totalLat = filteredData.reduce( - (sum, point) => sum + point.latitude, - 0 - ); - const totalLng = filteredData.reduce( - (sum, point) => sum + point.longitude, - 0 - ); - - const avgLat = totalLat / filteredData.length; - const avgLng = totalLng / filteredData.length; - - console.log("Center Latitude:", avgLat); - console.log("Center Longitude:", avgLng); - // We now build the object - const subjectUserMap: any = {}; - filteredData.forEach((item) => { - if (!subjectUserMap["users"]) { - subjectUserMap["users"] = []; - } - subjectUserMap["subject"] = item.subject; - subjectUserMap["latitude"] = avgLat; - subjectUserMap["longitude"] = avgLng; - subjectUserMap["radius"] = circle_radius; - subjectUserMap["users"].push(item.user); - }); - console.log(subjectUserMap); - - result = result.concat([subjectUserMap]); - }); - - console.log("Final Result:", result); - - setStudentStatuses(result); + setStudyGroups(ParseStudentStatusList(data[1])); } }, onError: (error: Error) => { @@ -322,73 +239,76 @@ export default function Home() { }} loadingBackgroundColor={colors.secondary_2} > - {student_statuses.map((student_status: any, index: number) => { - const randomColorWithOpacity = `rgba(${Math.floor( - Math.random() * 256 - )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( - Math.random() * 256 - )}, 0.7)`; + {study_groups.map( + (student_status: subjectUserMapType, index: number) => { + const randomColorWithOpacity = `rgba(${Math.floor( + Math.random() * 256 + )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( + Math.random() * 256 + )}, 0.7)`; - return ( - <> - { - toast.hideAll(); - toast.show( - - - Subject: {student_status.subject} - - - Students Studying: {student_status.users.length} - - - , - { - type: "normal", - placement: "top", - duration: 2000, - animationType: "slide-in", - style: { - backgroundColor: colors.secondary_2, - borderWidth: 1, - borderColor: colors.primary_1, - }, - } - ); - }} - /> - - - ); - })} + + Students Studying: {student_status.users.length} + + + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + /> + + + ); + } + )} Date: Mon, 11 Sep 2023 19:02:11 +0800 Subject: [PATCH 28/51] Fixed and made changes to the Haversine distance formula calculation. We now point it to the user's location as intended --- .../ParseStudyGroupList.tsx | 150 ++++++++++++------ src/routes/Home/Home.tsx | 9 +- 2 files changed, 106 insertions(+), 53 deletions(-) diff --git a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx index 492b649..b6685e9 100644 --- a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx +++ b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx @@ -9,59 +9,82 @@ import { } from "../../interfaces/Interfaces"; import { Double, Float } from "react-native/Libraries/Types/CodegenTypes"; -export default function ParseStudyGroupList(data: any) { - let result: any[] = []; +export default function ParseStudyGroupList( + data: any, + user_location: LocationType +) { // Circle generation for students in a study group + let result: any[] = []; + // We first remove any instances that do not have a study group associated with it + let data_filtered = data.filter( + (item: StudentStatusFilterType) => item.study_group !== "" + ); + // console.log("Filtered Data:", data_filtered); + // Then we flatten the data so that all attributes are in the first layer // We first flatten the data to remove nested entries - console.log("Initial Data:", data); - let flattened_data = data - .filter((item: StudentStatusFilterType) => item.study_group !== "") - .map((item: StudentStatusFilterType) => ({ - active: item.active, - distance: item.distance, - landmark: item.landmark, - latitude: item.location.latitude, - longitude: item.location.longitude, - study_group: item.study_group, - subject: item.subject, - user: item.user, - weight: 1, - })); - console.log("Filtered Data:", flattened_data); + let data_flattened = data_filtered.map((item: StudentStatusFilterType) => ({ + active: item.active, + distance: item.distance, + landmark: item.landmark, + latitude: item.location.latitude, + longitude: item.location.longitude, + study_group: item.study_group, + subject: item.subject, + user: item.user, + weight: 1, + })); + // console.log("Flattened Data:", data_flattened); - // We get each unique subject + // We take from the array all unique subject names let unique_subjects = [ ...new Set( - flattened_data.map((item: StudentStatusFilterType) => item.subject) + data_flattened.map((item: StudentStatusFilterType) => item.subject) ), ]; - // Then append all entries belonging to that subject to its own array + // Then we create arrays unique to each subject unique_subjects.forEach((subject, index: number) => { - index++; - let filteredData = flattened_data.filter( + // We build another array for each subject, including only those instances that are the same subject name + let unique_subject_list = data_flattened + .filter( + (item: StudentStatusFilterTypeFlattened) => item.subject === subject + ) + .map((item: StudentStatusFilterTypeFlattened) => ({ + active: item.active, + distance: item.distance, + landmark: item.landmark, + latitude: item.latitude, + longitude: item.longitude, + study_group: item.study_group, + subject: item.subject, + user: item.user, + weight: 1, + })); + + /* + let unique_subject_object = data_flattened.filter( (item: StudentStatusFilterTypeFlattened) => item.subject === subject ); - console.log("Subject #", index, "-", filteredData[0].subject, filteredData); + */ + // We get the circle's center by averaging all the points // Calculate the average latitude and longitude - const totalLat = filteredData.reduce( + const totalLat = unique_subject_list.reduce( (sum: Double, point: LocationType) => sum + point.latitude, 0 ); - const totalLng = filteredData.reduce( + const totalLng = unique_subject_list.reduce( (sum: Double, point: LocationType) => sum + point.longitude, 0 ); - const avgLat = totalLat / filteredData.length; - const avgLng = totalLng / filteredData.length; + let avgLat = totalLat / unique_subject_list.length; + let avgLng = totalLng / unique_subject_list.length; - console.log("Center Latitude:", avgLat); - console.log("Center Longitude:", avgLng); - - // We now calculate the radius of the circle using the Haversine Distance Formula + // console.log("Center Latitude:", avgLat); + // console.log("Center Longitude:", avgLng); + // Haversine Distance Function function haversineDistance( lat1: number, lon1: number, @@ -72,31 +95,56 @@ export default function ParseStudyGroupList(data: any) { return (x * Math.PI) / 180; } - var R = 6371; // km - var x1 = lat2 - lat1; - var dLat = toRad(x1); - var x2 = lon2 - lon1; - var dLon = toRad(x2); - var a = + lat1 = toRad(lat1); + lon1 = toRad(lon1); + lat2 = toRad(lat2); + lon2 = toRad(lon2); + + let dLat = lat2 - lat1; + let dLon = lon2 - lon1; + + let a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(toRad(lat1)) * - Math.cos(toRad(lat2)) * + Math.cos(lat1) * + Math.cos(lat2) * Math.sin(dLon / 2) * Math.sin(dLon / 2); - var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - var d = R * c; - return d; + let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + // Multiply by Earth's radius (in kilometers) to obtain distance + let distance = 6371 * c; + + // Convert to meters + return distance * 1000; } - let circle_radius = - Math.max( - ...filteredData.map((item: StudentStatusFilterTypeFlattened) => - haversineDistance(avgLat, avgLng, item.latitude, item.longitude) - ) - ) * 1000; - console.log("Radius:", circle_radius); + // We now calculate the radius of the circle using the Haversine Distance Formula + // For each entry, we calculate the Haversine Distance from the user's location. + // The largest value is used as the circle radius - // We now build the object + let circle_radius = Math.max( + ...unique_subject_list.map( + (item: StudentStatusFilterTypeFlattened, index: number) => { + let distance = haversineDistance( + item.latitude, + item.longitude, + user_location.latitude, + user_location.longitude + ); + + console.log( + "Haversine Distance for entry #", + index + 1, + ":", + distance + ); + return distance; + } + ) + ); + // console.log("Radius:", circle_radius); + + // We now build the object that we will return const subjectUserMap: subjectUserMapType = { subject: "", users: [], @@ -104,7 +152,7 @@ export default function ParseStudyGroupList(data: any) { longitude: 0, radius: 0, }; - filteredData.forEach((item: StudentStatusFilterType) => { + unique_subject_list.forEach((item: StudentStatusFilterType) => { if (!subjectUserMap["users"]) { subjectUserMap["users"] = []; } diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index cf8332a..095cd95 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -193,8 +193,13 @@ export default function Home() { return data; }, onSuccess: (data: StudentStatusListReturnType) => { - if (data[1]) { - setStudyGroups(ParseStudentStatusList(data[1])); + if (data[1] && location) { + setStudyGroups( + ParseStudentStatusList(data[1], { + latitude: location.coords.latitude, + longitude: location.coords.longitude, + }) + ); } }, onError: (error: Error) => { From 980d1e636e2a8c471c7b4ea9666bc7abf5b78cdd Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Mon, 11 Sep 2023 19:16:48 +0800 Subject: [PATCH 29/51] Added student status point rendering --- .../ParseStudentStatusList.tsx | 35 ++++++++++ .../ParseStudyGroupList.tsx | 4 +- src/routes/Home/Home.tsx | 64 +++++++++++++++++-- 3 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 src/components/ParseStudentStatusList/ParseStudentStatusList.tsx diff --git a/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx b/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx new file mode 100644 index 0000000..4e673c9 --- /dev/null +++ b/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import { View, Text } from "react-native"; +import { + StudentStatusFilterType, + LocationType, + subjectUserMapType, + StudentStatusListType, + StudentStatusFilterTypeFlattened, +} from "../../interfaces/Interfaces"; +import { Double, Float } from "react-native/Libraries/Types/CodegenTypes"; + +export default function ParseStudentStatusList(data: any) { + // Individual map point generation for student statuses + // Include only those that do not have study groups + // Then we simply flatten the data. Much simpler compared to study groups + let data_filtered = data.filter( + (item: StudentStatusFilterType) => item.study_group == "" + ); + console.log("Filtered Data:", data_filtered); + // Then we flatten the data so that all attributes are in the first layer + // We first flatten the data to remove nested entries + let data_flattened = data_filtered.map((item: StudentStatusFilterType) => ({ + active: item.active, + distance: item.distance, + landmark: item.landmark, + latitude: item.location.latitude, + longitude: item.location.longitude, + study_group: item.study_group, + subject: item.subject, + user: item.user, + weight: 1, + })); + + return data_flattened; +} diff --git a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx index b6685e9..93c70cb 100644 --- a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx +++ b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx @@ -132,12 +132,12 @@ export default function ParseStudyGroupList( user_location.longitude ); - console.log( + /*console.log( "Haversine Distance for entry #", index + 1, ":", distance - ); + );*/ return distance; } ) diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 095cd95..f8bdf57 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -22,6 +22,7 @@ import { StudentStatusListReturnType, StudentStatusListType, subjectUserMapType, + StudentStatusFilterTypeFlattened, } from "../../interfaces/Interfaces"; import { useNavigation } from "@react-navigation/native"; import { @@ -34,8 +35,9 @@ import { import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useToast } from "react-native-toast-notifications"; import CustomMapCallout from "../../components/CustomMapCallout/CustomMapCallout"; -import ParseStudentStatusList from "../../components/ParseStudyGroupList/ParseStudyGroupList"; import React from "react"; +import ParseStudyGroupList from "../../components/ParseStudyGroupList/ParseStudyGroupList"; +import ParseStudentStatusList from "../../components/ParseStudentStatusList/ParseStudentStatusList"; export default function Home() { // Switch this condition to see the main map when debugging @@ -179,7 +181,9 @@ export default function Home() { }, }); - const [student_statuses, setStudentStatuses] = useState([]); + const [student_statuses, setStudentStatuses] = useState< + StudentStatusFilterTypeFlattened[] + >([]); const [study_groups, setStudyGroups] = useState([]); // Student Status List const StudentStatusList = useQuery({ @@ -195,11 +199,12 @@ export default function Home() { onSuccess: (data: StudentStatusListReturnType) => { if (data[1] && location) { setStudyGroups( - ParseStudentStatusList(data[1], { + ParseStudyGroupList(data[1], { latitude: location.coords.latitude, longitude: location.coords.longitude, }) ); + setStudentStatuses(ParseStudentStatusList(data[1])); } }, onError: (error: Error) => { @@ -244,6 +249,57 @@ export default function Home() { }} loadingBackgroundColor={colors.secondary_2} > + {student_statuses.map( + ( + student_status: StudentStatusFilterTypeFlattened, + index: number + ) => { + const randomColorWithOpacity = `rgba(${Math.floor( + Math.random() * 256 + )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( + Math.random() * 256 + )}, 0.7)`; + + return ( + { + toast.hideAll(); + toast.show( + + + Student: {student_status.user} + + + Studying Subject: {student_status.subject} + + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + /> + ); + } + )} {study_groups.map( (student_status: subjectUserMapType, index: number) => { const randomColorWithOpacity = `rgba(${Math.floor( @@ -285,7 +341,7 @@ export default function Home() { }} > - Create Group & Invite + Join Group , From 511f293ff1aa4766d67a17fc283cb9946734b53f Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Wed, 20 Sep 2023 17:42:37 +0800 Subject: [PATCH 30/51] Changed debug backend url --- src/components/Api/Api.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index f518496..c3faa04 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -12,8 +12,8 @@ import { export let backendURL = "https://stude.keannu1.duckdns.org"; export let backendURLWebsocket = "ws://stude.keannu1.duckdns.org"; if (__DEV__) { - backendURL = "http://10.0.10.8:8000"; - backendURLWebsocket = "ws://10.0.10.8:8000"; + 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 From 12e3d2982209007bd6c4100db96aa1adb47776c9 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Wed, 20 Sep 2023 17:44:58 +0800 Subject: [PATCH 31/51] Do not autocapitalize password fields --- src/routes/Login/Login.tsx | 1 + src/routes/Register/Register.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/src/routes/Login/Login.tsx b/src/routes/Login/Login.tsx index b141a3d..5a72b1a 100644 --- a/src/routes/Login/Login.tsx +++ b/src/routes/Login/Login.tsx @@ -59,6 +59,7 @@ export default function Login() { placeholderTextColor="white" secureTextEntry={true} value={creds.password} + autoCapitalize={"none"} onChange={( e: NativeSyntheticEvent ): void => { diff --git a/src/routes/Register/Register.tsx b/src/routes/Register/Register.tsx index 09633a5..c563f26 100644 --- a/src/routes/Register/Register.tsx +++ b/src/routes/Register/Register.tsx @@ -113,6 +113,7 @@ export default function Register() { placeholderTextColor={colors.text_default} secureTextEntry={true} value={user.password} + autoCapitalize={"none"} onChange={( e: NativeSyntheticEvent ): void => { From 790574daee95dd5b9a9b33ba7de7cf039584879a Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Wed, 20 Sep 2023 19:36:06 +0800 Subject: [PATCH 32/51] Separated distance calculation and far map renderer into own components --- src/components/Api/Api.tsx | 3 +- .../GetDistance/GetDistanceFromUSTP.tsx | 20 +++++ src/components/MapRenderer/MapRendererFar.tsx | 79 ++++++++++++++++++ src/interfaces/Interfaces.tsx | 8 +- src/routes/Home/Home.tsx | 81 ++----------------- 5 files changed, 116 insertions(+), 75 deletions(-) create mode 100644 src/components/GetDistance/GetDistanceFromUSTP.tsx create mode 100644 src/components/MapRenderer/MapRendererFar.tsx diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index c3faa04..74b86d5 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -6,6 +6,7 @@ import { OnboardingType, PatchUserInfoType, RegistrationType, + StudentStatusPatchType, StudentStatusType, } from "../../interfaces/Interfaces"; @@ -243,7 +244,7 @@ export async function GetStudentStatus() { }); } -export async function PatchStudentStatus(info: StudentStatusType) { +export async function PatchStudentStatus(info: StudentStatusPatchType) { const config = await GetConfig(); console.log(info); return instance diff --git a/src/components/GetDistance/GetDistanceFromUSTP.tsx b/src/components/GetDistance/GetDistanceFromUSTP.tsx new file mode 100644 index 0000000..391df27 --- /dev/null +++ b/src/components/GetDistance/GetDistanceFromUSTP.tsx @@ -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); + return dist; +} diff --git a/src/components/MapRenderer/MapRendererFar.tsx b/src/components/MapRenderer/MapRendererFar.tsx new file mode 100644 index 0000000..776df86 --- /dev/null +++ b/src/components/MapRenderer/MapRendererFar.tsx @@ -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 ( + + + You are too far from USTP {"\n"} + Get closer to use Stud-E + + + + + + + You are here {"\n"} + X: {Math.round(props.location.longitude) + "\n"} + Z: {Math.round(props.location.latitude)} + + + + + + {props.dist}km away from USTP {"\n"} + + + ); +} diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index ac42a41..7623b88 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -130,7 +130,13 @@ export interface LocationType { } export interface StudentStatusType { - user?: string; + subject: string; + location: LocationType; + landmark: string | null; + active: boolean; +} + +export interface StudentStatusPatchType { subject?: string; location?: LocationType; landmark?: string | null; diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index f8bdf57..2a9cd8d 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -23,6 +23,7 @@ import { StudentStatusListType, subjectUserMapType, StudentStatusFilterTypeFlattened, + StudentStatusPatchType, } from "../../interfaces/Interfaces"; import { useNavigation } from "@react-navigation/native"; import { @@ -34,14 +35,16 @@ import { } from "../../components/Api/Api"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useToast } from "react-native-toast-notifications"; -import CustomMapCallout from "../../components/CustomMapCallout/CustomMapCallout"; import React from "react"; import ParseStudyGroupList from "../../components/ParseStudyGroupList/ParseStudyGroupList"; import ParseStudentStatusList from "../../components/ParseStudentStatusList/ParseStudentStatusList"; +import CustomMapCallout from "../../components/CustomMapCallout/CustomMapCallout"; +import MapRendererFar from "../../components/MapRenderer/MapRendererFar"; +import GetDistanceFromUSTP from "../../components/GetDistance/GetDistanceFromUSTP"; export default function Home() { // Switch this condition to see the main map when debugging - const map_debug = true; + const map_debug = false; const navigation = useNavigation(); const [location, setLocation] = useState(null); const [dist, setDist] = useState(null); @@ -103,12 +106,7 @@ export default function Home() { }, []); async function GetDistanceRoundedOff(location: RawLocationType) { - let dist = GetDistance( - location.coords.latitude, - location.coords.longitude, - ustpCoords.latitude, - ustpCoords.longitude - ); + let dist = GetDistanceFromUSTP(location.coords); setDist(Math.round(dist)); // Deactivate student status if too far away if (dist >= 2 && !map_debug) @@ -154,7 +152,7 @@ export default function Home() { }); const mutation = useMutation({ - mutationFn: async (info: StudentStatusType) => { + mutationFn: async (info: StudentStatusPatchType) => { const data = await PatchStudentStatus(info); if (data[0] != true) { return Promise.reject(new Error()); @@ -426,70 +424,7 @@ export default function Home() { ); } else { - return ( - - - You are too far from USTP {"\n"} - Get closer to use Stud-E - - - - - - - You are here {"\n"} - X: {Math.round(location.coords.longitude) + "\n"} - Z: {Math.round(location.coords.latitude)} - - - - - - {dist}km away from USTP {"\n"} - - - ); + return ; } } else { requestLocation(); From 14e14b8bb6deba6076b6b43e5d852572b9ecee97 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Wed, 20 Sep 2023 19:53:25 +0800 Subject: [PATCH 33/51] Code cleanup --- src/routes/Home/Home.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 2a9cd8d..c8f9718 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -85,7 +85,7 @@ export default function Home() { newLocation.coords.longitude !== location.coords.longitude ) { setLocation(newLocation); - GetDistanceRoundedOff(newLocation); + DistanceHandler(newLocation); } } } @@ -105,9 +105,9 @@ export default function Home() { requestLocation(); }, []); - async function GetDistanceRoundedOff(location: RawLocationType) { + async function DistanceHandler(location: RawLocationType) { let dist = GetDistanceFromUSTP(location.coords); - setDist(Math.round(dist)); + setDist(dist); // Deactivate student status if too far away if (dist >= 2 && !map_debug) mutation.mutate({ From 68778cea7aa23745b95c6615e57d18e62e0aa380 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Wed, 20 Sep 2023 21:16:54 +0800 Subject: [PATCH 34/51] Code cleanup for multiple pages and components --- .../CustomMapCallout/CustomMapCallout.tsx | 14 +++++++------- src/routes/Home/Home.tsx | 12 +++++++++--- src/routes/StartStudying/StartStudying.tsx | 3 ++- src/routes/SubjectsPage/SubjectsPage.tsx | 3 ++- src/routes/UserInfoPage/UserInfoPage.tsx | 3 ++- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/components/CustomMapCallout/CustomMapCallout.tsx b/src/components/CustomMapCallout/CustomMapCallout.tsx index 23661e3..6184c9a 100644 --- a/src/components/CustomMapCallout/CustomMapCallout.tsx +++ b/src/components/CustomMapCallout/CustomMapCallout.tsx @@ -1,26 +1,26 @@ import { Callout } from "react-native-maps"; -import { RawLocationType } from "../../interfaces/Interfaces"; +import { LocationType, RawLocationType } from "../../interfaces/Interfaces"; import styles from "../../styles"; import { Text } from "react-native"; // Map popup for user's location type props = { - location: RawLocationType; + location: LocationType; studying: boolean; subject?: string; }; export default function CustomMapCallout(props: props) { let { location, studying, subject } = props; - if (location && location.coords) { + if (location && location.latitude && location.longitude) { if (studying) { return ( You are here {"\n"} - X: {Math.round(location.coords.longitude) + "\n"} - Z: {Math.round(location.coords.latitude) + "\n"} + X: {Math.round(location.longitude) + "\n"} + Z: {Math.round(location.latitude) + "\n"} Studying: {subject} @@ -30,8 +30,8 @@ export default function CustomMapCallout(props: props) { You are here {"\n"} - X: {Math.round(location.coords.longitude) + "\n"} - Z: {Math.round(location.coords.latitude)} + X: {Math.round(location.longitude) + "\n"} + Z: {Math.round(location.latitude)} ); diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index c8f9718..3a0317a 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -44,7 +44,7 @@ import GetDistanceFromUSTP from "../../components/GetDistance/GetDistanceFromUST export default function Home() { // Switch this condition to see the main map when debugging - const map_debug = false; + const map_debug = true; const navigation = useNavigation(); const [location, setLocation] = useState(null); const [dist, setDist] = useState(null); @@ -119,6 +119,7 @@ export default function Home() { const [studying, setStudying] = useState(false); const [subject, setSubject] = useState(""); const [buttonLabel, setButtonLabel] = useState("Start studying"); + const [student_status, setStudentStatus] = useState(); const StudentStatus = useQuery({ queryKey: ["user_status"], queryFn: async () => { @@ -140,6 +141,8 @@ export default function Home() { } else if (data[1].active == false) { setButtonLabel("Start Studying"); } + setStudentStatus(data[1]); + console.log(student_status); }, onError: (error: Error) => { toast.show(String(error), { @@ -373,7 +376,7 @@ export default function Home() { latitude: location.coords.latitude, longitude: location.coords.longitude, }} - draggable + draggable={student_status?.active} onDragEnd={(e) => { const newLocation = e.nativeEvent.coordinate; const distance = GetDistance( @@ -402,7 +405,10 @@ export default function Home() { pinColor={colors.primary_1} > diff --git a/src/routes/StartStudying/StartStudying.tsx b/src/routes/StartStudying/StartStudying.tsx index 9f420d2..2aa8677 100644 --- a/src/routes/StartStudying/StartStudying.tsx +++ b/src/routes/StartStudying/StartStudying.tsx @@ -8,6 +8,7 @@ import { RootDrawerParamList, StudentStatusType, StudentStatusReturnType, + StudentStatusPatchType, } from "../../interfaces/Interfaces"; import Button from "../../components/Button/Button"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -61,7 +62,7 @@ export default function StartStudying({ route }: any) { }); const mutation = useMutation({ - mutationFn: async (info: StudentStatusType) => { + mutationFn: async (info: StudentStatusPatchType) => { const data = await PatchStudentStatus(info); if (data[0] == false) { return Promise.reject(new Error(JSON.stringify(data[1]))); diff --git a/src/routes/SubjectsPage/SubjectsPage.tsx b/src/routes/SubjectsPage/SubjectsPage.tsx index 76c841d..cfee8f7 100644 --- a/src/routes/SubjectsPage/SubjectsPage.tsx +++ b/src/routes/SubjectsPage/SubjectsPage.tsx @@ -9,6 +9,7 @@ import { OptionType, StudentStatusType, PatchUserInfoType, + StudentStatusPatchType, } from "../../interfaces/Interfaces"; import Button from "../../components/Button/Button"; import { Image } from "react-native"; @@ -33,7 +34,7 @@ export default function SubjectsPage() { // Student Status const studentstatus_mutation = useMutation({ - mutationFn: async (info: StudentStatusType) => { + mutationFn: async (info: StudentStatusPatchType) => { const data = await PatchStudentStatus(info); if (data[0] != true) { return Promise.reject(new Error()); diff --git a/src/routes/UserInfoPage/UserInfoPage.tsx b/src/routes/UserInfoPage/UserInfoPage.tsx index 512ea89..5d8a49b 100644 --- a/src/routes/UserInfoPage/UserInfoPage.tsx +++ b/src/routes/UserInfoPage/UserInfoPage.tsx @@ -18,6 +18,7 @@ import { OptionType, StudentStatusType, PatchUserInfoType, + StudentStatusPatchType, } from "../../interfaces/Interfaces"; import Button from "../../components/Button/Button"; import { Image } from "react-native"; @@ -50,7 +51,7 @@ export default function UserInfoPage() { // Student Status const studentstatus_mutation = useMutation({ - mutationFn: async (info: StudentStatusType) => { + mutationFn: async (info: StudentStatusPatchType) => { const data = await PatchStudentStatus(info); if (data[0] != true) { return Promise.reject(new Error()); From 81bead43ff7b090b13cb100864830c44a3c86dda Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Fri, 22 Sep 2023 22:55:36 +0800 Subject: [PATCH 35/51] Fixed filtering for study groups --- .../ParseStudyGroupList.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx index 93c70cb..fdd1ddc 100644 --- a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx +++ b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx @@ -17,7 +17,8 @@ export default function ParseStudyGroupList( let result: any[] = []; // We first remove any instances that do not have a study group associated with it let data_filtered = data.filter( - (item: StudentStatusFilterType) => item.study_group !== "" + (item: StudentStatusFilterType) => + item.study_group !== undefined && item.study_group.length > 0 ); // console.log("Filtered Data:", data_filtered); // Then we flatten the data so that all attributes are in the first layer @@ -35,19 +36,20 @@ export default function ParseStudyGroupList( })); // console.log("Flattened Data:", data_flattened); - // We take from the array all unique subject names - let unique_subjects = [ + // We take from the array all unique study groups + let unique_studygroups = [ ...new Set( - data_flattened.map((item: StudentStatusFilterType) => item.subject) + data_flattened.map((item: StudentStatusFilterType) => item.study_group) ), ]; // Then we create arrays unique to each subject - unique_subjects.forEach((subject, index: number) => { + unique_studygroups.forEach((studygroup, index: number) => { // We build another array for each subject, including only those instances that are the same subject name let unique_subject_list = data_flattened .filter( - (item: StudentStatusFilterTypeFlattened) => item.subject === subject + (item: StudentStatusFilterTypeFlattened) => + item.study_group === studygroup ) .map((item: StudentStatusFilterTypeFlattened) => ({ active: item.active, @@ -147,6 +149,7 @@ export default function ParseStudyGroupList( // We now build the object that we will return const subjectUserMap: subjectUserMapType = { subject: "", + study_group: "", users: [], latitude: 0, longitude: 0, @@ -156,6 +159,9 @@ export default function ParseStudyGroupList( if (!subjectUserMap["users"]) { subjectUserMap["users"] = []; } + if (!subjectUserMap["study_group"]) { + subjectUserMap["study_group"] = unique_subject_list[0].study_group; + } subjectUserMap["subject"] = item.subject; subjectUserMap["latitude"] = avgLat; subjectUserMap["longitude"] = avgLng; @@ -167,7 +173,7 @@ export default function ParseStudyGroupList( result = result.concat([subjectUserMap]); }); - // console.log("Final Result:", result); + console.log("Final Result:", result); return result; } From c4413a185d8b01107a3c6bb3a0772a18f831d35c Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Fri, 22 Sep 2023 23:08:06 +0800 Subject: [PATCH 36/51] Hidden unused console.log --- .../ParseStudentStatusList/ParseStudentStatusList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx b/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx index 4e673c9..1f0b16e 100644 --- a/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx +++ b/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx @@ -16,7 +16,7 @@ export default function ParseStudentStatusList(data: any) { let data_filtered = data.filter( (item: StudentStatusFilterType) => item.study_group == "" ); - console.log("Filtered Data:", data_filtered); + // console.log("Filtered Data:", data_filtered); // Then we flatten the data so that all attributes are in the first layer // We first flatten the data to remove nested entries let data_flattened = data_filtered.map((item: StudentStatusFilterType) => ({ From 7da7d0f2175ff03ac1c741c0f4dcb9d000486278 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Fri, 22 Sep 2023 23:08:20 +0800 Subject: [PATCH 37/51] Hidden unused console.log in api --- src/components/Api/Api.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 74b86d5..9d9f826 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -246,7 +246,6 @@ export async function GetStudentStatus() { export async function PatchStudentStatus(info: StudentStatusPatchType) { const config = await GetConfig(); - console.log(info); return instance .patch("/api/v1/student_status/self/", info, config) .then((response) => { From 19d19c3dd5ccece39cbb7a93f83d1bb40e7cbd8c Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Fri, 22 Sep 2023 23:08:54 +0800 Subject: [PATCH 38/51] Reflect user pin location change if user manually overrides location --- src/routes/Home/Home.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 3a0317a..10f15ca 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -243,8 +243,12 @@ export default function Home() { minZoomLevel={19} zoomTapEnabled initialRegion={{ - latitude: location.coords.latitude, - longitude: location.coords.longitude, + latitude: + student_status?.location?.latitude || + location.coords.latitude, + longitude: + student_status?.location?.longitude || + location.coords.longitude, latitudeDelta: 0.4, longitudeDelta: 0.4, }} @@ -372,11 +376,16 @@ export default function Home() { } )} { const newLocation = e.nativeEvent.coordinate; const distance = GetDistance( From 1bd07f9edd83964b260cc6d3907e750bb383ca42 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Sun, 24 Sep 2023 21:02:34 +0800 Subject: [PATCH 39/51] Optimized homepage rendering and removed overly complicated components --- src/components/Api/Api.tsx | 14 ++ .../ParseStudentStatusList.tsx | 35 ---- .../ParseStudyGroupList.tsx | 179 ----------------- src/interfaces/Interfaces.tsx | 11 + src/routes/Home/Home.tsx | 188 ++++++++++-------- 5 files changed, 129 insertions(+), 298 deletions(-) delete mode 100644 src/components/ParseStudentStatusList/ParseStudentStatusList.tsx delete mode 100644 src/components/ParseStudyGroupList/ParseStudyGroupList.tsx diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 9d9f826..195b8cf 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -296,3 +296,17 @@ export async function GetStudentStatusListFilteredCurrentLocation() { return [false, error_message]; }); } + +export async function GetStudyGroupListFiltered() { + const config = await GetConfig(); + return instance + .get("/api/v1/study_groups/near/", config) + .then((response) => { + console.log("Data:", response.data); + return [true, response.data]; + }) + .catch((error) => { + let error_message = ParseError(error); + return [false, error_message]; + }); +} diff --git a/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx b/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx deleted file mode 100644 index 1f0b16e..0000000 --- a/src/components/ParseStudentStatusList/ParseStudentStatusList.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from "react"; -import { View, Text } from "react-native"; -import { - StudentStatusFilterType, - LocationType, - subjectUserMapType, - StudentStatusListType, - StudentStatusFilterTypeFlattened, -} from "../../interfaces/Interfaces"; -import { Double, Float } from "react-native/Libraries/Types/CodegenTypes"; - -export default function ParseStudentStatusList(data: any) { - // Individual map point generation for student statuses - // Include only those that do not have study groups - // Then we simply flatten the data. Much simpler compared to study groups - let data_filtered = data.filter( - (item: StudentStatusFilterType) => item.study_group == "" - ); - // console.log("Filtered Data:", data_filtered); - // Then we flatten the data so that all attributes are in the first layer - // We first flatten the data to remove nested entries - let data_flattened = data_filtered.map((item: StudentStatusFilterType) => ({ - active: item.active, - distance: item.distance, - landmark: item.landmark, - latitude: item.location.latitude, - longitude: item.location.longitude, - study_group: item.study_group, - subject: item.subject, - user: item.user, - weight: 1, - })); - - return data_flattened; -} diff --git a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx deleted file mode 100644 index fdd1ddc..0000000 --- a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import * as React from "react"; -import { View, Text } from "react-native"; -import { - StudentStatusFilterType, - LocationType, - subjectUserMapType, - StudentStatusListType, - StudentStatusFilterTypeFlattened, -} from "../../interfaces/Interfaces"; -import { Double, Float } from "react-native/Libraries/Types/CodegenTypes"; - -export default function ParseStudyGroupList( - data: any, - user_location: LocationType -) { - // Circle generation for students in a study group - let result: any[] = []; - // We first remove any instances that do not have a study group associated with it - let data_filtered = data.filter( - (item: StudentStatusFilterType) => - item.study_group !== undefined && item.study_group.length > 0 - ); - // console.log("Filtered Data:", data_filtered); - // Then we flatten the data so that all attributes are in the first layer - // We first flatten the data to remove nested entries - let data_flattened = data_filtered.map((item: StudentStatusFilterType) => ({ - active: item.active, - distance: item.distance, - landmark: item.landmark, - latitude: item.location.latitude, - longitude: item.location.longitude, - study_group: item.study_group, - subject: item.subject, - user: item.user, - weight: 1, - })); - // console.log("Flattened Data:", data_flattened); - - // We take from the array all unique study groups - let unique_studygroups = [ - ...new Set( - data_flattened.map((item: StudentStatusFilterType) => item.study_group) - ), - ]; - - // Then we create arrays unique to each subject - unique_studygroups.forEach((studygroup, index: number) => { - // We build another array for each subject, including only those instances that are the same subject name - let unique_subject_list = data_flattened - .filter( - (item: StudentStatusFilterTypeFlattened) => - item.study_group === studygroup - ) - .map((item: StudentStatusFilterTypeFlattened) => ({ - active: item.active, - distance: item.distance, - landmark: item.landmark, - latitude: item.latitude, - longitude: item.longitude, - study_group: item.study_group, - subject: item.subject, - user: item.user, - weight: 1, - })); - - /* - let unique_subject_object = data_flattened.filter( - (item: StudentStatusFilterTypeFlattened) => item.subject === subject - ); - */ - - // We get the circle's center by averaging all the points - // Calculate the average latitude and longitude - const totalLat = unique_subject_list.reduce( - (sum: Double, point: LocationType) => sum + point.latitude, - 0 - ); - const totalLng = unique_subject_list.reduce( - (sum: Double, point: LocationType) => sum + point.longitude, - 0 - ); - - let avgLat = totalLat / unique_subject_list.length; - let avgLng = totalLng / unique_subject_list.length; - - // console.log("Center Latitude:", avgLat); - // console.log("Center Longitude:", avgLng); - - // Haversine Distance Function - function haversineDistance( - lat1: number, - lon1: number, - lat2: number, - lon2: number - ) { - function toRad(x: number) { - return (x * Math.PI) / 180; - } - - lat1 = toRad(lat1); - lon1 = toRad(lon1); - lat2 = toRad(lat2); - lon2 = toRad(lon2); - - let dLat = lat2 - lat1; - let dLon = lon2 - lon1; - - let a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(lat1) * - Math.cos(lat2) * - Math.sin(dLon / 2) * - Math.sin(dLon / 2); - let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - // Multiply by Earth's radius (in kilometers) to obtain distance - let distance = 6371 * c; - - // Convert to meters - return distance * 1000; - } - - // We now calculate the radius of the circle using the Haversine Distance Formula - // For each entry, we calculate the Haversine Distance from the user's location. - // The largest value is used as the circle radius - - let circle_radius = Math.max( - ...unique_subject_list.map( - (item: StudentStatusFilterTypeFlattened, index: number) => { - let distance = haversineDistance( - item.latitude, - item.longitude, - user_location.latitude, - user_location.longitude - ); - - /*console.log( - "Haversine Distance for entry #", - index + 1, - ":", - distance - );*/ - return distance; - } - ) - ); - // console.log("Radius:", circle_radius); - - // We now build the object that we will return - const subjectUserMap: subjectUserMapType = { - subject: "", - study_group: "", - users: [], - latitude: 0, - longitude: 0, - radius: 0, - }; - unique_subject_list.forEach((item: StudentStatusFilterType) => { - if (!subjectUserMap["users"]) { - subjectUserMap["users"] = []; - } - if (!subjectUserMap["study_group"]) { - subjectUserMap["study_group"] = unique_subject_list[0].study_group; - } - subjectUserMap["subject"] = item.subject; - subjectUserMap["latitude"] = avgLat; - subjectUserMap["longitude"] = avgLng; - subjectUserMap["radius"] = circle_radius; - subjectUserMap["users"].push(item.user); - }); - console.log(subjectUserMap); - - result = result.concat([subjectUserMap]); - }); - - console.log("Final Result:", result); - - return result; -} diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index 7623b88..be6d983 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -166,6 +166,17 @@ export interface StudentStatusFilterTypeFlattened { weight?: number; } +export interface StudyGroupType { + name: string; + users: string[]; + distance: number; + landmark: string | null; + location: LocationType; + subject: string; + radius: number; +} +export type StudyGroupReturnType = [boolean, StudyGroupType[]]; + export type StudentStatusReturnType = [boolean, StudentStatusType]; export type StudentStatusListType = Array; diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 10f15ca..7e19e6e 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -24,20 +24,22 @@ import { subjectUserMapType, StudentStatusFilterTypeFlattened, StudentStatusPatchType, + StudyGroupType, + StudyGroupReturnType, + StudentStatusFilterType, } from "../../interfaces/Interfaces"; import { useNavigation } from "@react-navigation/native"; import { GetStudentStatus, GetStudentStatusList, GetStudentStatusListFiltered, + GetStudyGroupListFiltered, PatchStudentStatus, urlProvider, } from "../../components/Api/Api"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useToast } from "react-native-toast-notifications"; import React from "react"; -import ParseStudyGroupList from "../../components/ParseStudyGroupList/ParseStudyGroupList"; -import ParseStudentStatusList from "../../components/ParseStudentStatusList/ParseStudentStatusList"; import CustomMapCallout from "../../components/CustomMapCallout/CustomMapCallout"; import MapRendererFar from "../../components/MapRenderer/MapRendererFar"; import GetDistanceFromUSTP from "../../components/GetDistance/GetDistanceFromUSTP"; @@ -120,7 +122,7 @@ export default function Home() { const [subject, setSubject] = useState(""); const [buttonLabel, setButtonLabel] = useState("Start studying"); const [student_status, setStudentStatus] = useState(); - const StudentStatus = useQuery({ + const StudentStatusQuery = useQuery({ queryKey: ["user_status"], queryFn: async () => { const data = await GetStudentStatus(); @@ -182,12 +184,10 @@ export default function Home() { }, }); - const [student_statuses, setStudentStatuses] = useState< - StudentStatusFilterTypeFlattened[] - >([]); - const [study_groups, setStudyGroups] = useState([]); + const [student_statuses, setStudentStatuses] = + useState([]); // Student Status List - const StudentStatusList = useQuery({ + const StudentStatusListQuery = useQuery({ enabled: studying, queryKey: ["user_status_list"], queryFn: async () => { @@ -199,13 +199,38 @@ export default function Home() { }, onSuccess: (data: StudentStatusListReturnType) => { if (data[1] && location) { - setStudyGroups( - ParseStudyGroupList(data[1], { - latitude: location.coords.latitude, - longitude: location.coords.longitude, - }) + // Filter to only include students studying solo + let data_filtered = data[1].filter( + (item: StudentStatusFilterType) => item.study_group == "" ); - setStudentStatuses(ParseStudentStatusList(data[1])); + setStudentStatuses(data_filtered); + } + }, + onError: (error: Error) => { + toast.show(String(error), { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, + }); + + const [study_groups, setStudyGroups] = useState([]); + // Student Status List + const StudyGroupQuery = useQuery({ + enabled: studying, + queryKey: ["study_group_list"], + queryFn: async () => { + const data = await GetStudyGroupListFiltered(); + if (data[0] == false) { + return Promise.reject(new Error(JSON.stringify(data[1]))); + } + return data; + }, + onSuccess: (data: StudyGroupReturnType) => { + if (data[1] && location) { + setStudyGroups(data[1]); } }, onError: (error: Error) => { @@ -255,10 +280,7 @@ export default function Home() { loadingBackgroundColor={colors.secondary_2} > {student_statuses.map( - ( - student_status: StudentStatusFilterTypeFlattened, - index: number - ) => { + (student_status: StudentStatusFilterType, index: number) => { const randomColorWithOpacity = `rgba(${Math.floor( Math.random() * 256 )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( @@ -268,7 +290,7 @@ export default function Home() { return ( { @@ -305,76 +327,74 @@ export default function Home() { ); } )} - {study_groups.map( - (student_status: subjectUserMapType, index: number) => { - const randomColorWithOpacity = `rgba(${Math.floor( - Math.random() * 256 - )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( - Math.random() * 256 - )}, 0.7)`; + {study_groups.map((studygroup: StudyGroupType, index: number) => { + const randomColorWithOpacity = `rgba(${Math.floor( + Math.random() * 256 + )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( + Math.random() * 256 + )}, 0.7)`; - return ( - - { - toast.hideAll(); - toast.show( - + { + toast.hideAll(); + toast.show( + + + Subject: {studygroup.subject} + + + Students Studying: {studygroup.users.length} + + - , - { - type: "normal", - placement: "top", - duration: 2000, - animationType: "slide-in", - style: { - backgroundColor: colors.secondary_2, - borderWidth: 1, - borderColor: colors.primary_1, - }, - } - ); - }} - /> - - - ); - } - )} + + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + /> + + + ); + })} Date: Sun, 24 Sep 2023 21:26:15 +0800 Subject: [PATCH 40/51] Added global study groups rendering --- src/components/Api/Api.tsx | 15 +++++- src/routes/Home/Home.tsx | 99 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 195b8cf..430124f 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -302,7 +302,20 @@ export async function GetStudyGroupListFiltered() { return instance .get("/api/v1/study_groups/near/", config) .then((response) => { - console.log("Data:", response.data); + 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) => { + console.log("test", response.data); return [true, response.data]; }) .catch((error) => { diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index 7e19e6e..f926c26 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -33,6 +33,7 @@ import { GetStudentStatus, GetStudentStatusList, GetStudentStatusListFiltered, + GetStudyGroupList, GetStudyGroupListFiltered, PatchStudentStatus, urlProvider, @@ -242,6 +243,34 @@ export default function Home() { }); }, }); + const [study_groups_global, setStudyGroupsGlobal] = useState< + StudyGroupType[] + >([]); + // Student Status List + const StudyGroupGlobalQuery = useQuery({ + enabled: !studying, + queryKey: ["study_group_list_global"], + queryFn: async () => { + const data = await GetStudyGroupList(); + if (data[0] == false) { + return Promise.reject(new Error(JSON.stringify(data[1]))); + } + return data; + }, + onSuccess: (data: StudyGroupReturnType) => { + if (data[1] && location) { + setStudyGroupsGlobal(data[1]); + } + }, + onError: (error: Error) => { + toast.show(String(error), { + type: "warning", + placement: "top", + duration: 2000, + animationType: "slide-in", + }); + }, + }); function CustomMap() { if (dist && location) { @@ -395,6 +424,76 @@ export default function Home() { ); })} + {study_groups_global.map( + (studygroup: StudyGroupType, index: number) => { + const randomColorWithOpacity = `rgba(${Math.floor( + Math.random() * 256 + )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( + Math.random() * 256 + )}, 0.7)`; + + return ( + + { + toast.hideAll(); + toast.show( + + + Subject: {studygroup.subject} + + + Students Studying: {studygroup.users.length} + + + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + /> + + + ); + } + )} Date: Tue, 26 Sep 2023 20:29:21 +0800 Subject: [PATCH 41/51] Added study group creation --- src/components/Api/Api.tsx | 18 +++++- src/interfaces/Interfaces.tsx | 7 +++ src/routes/Home/Home.tsx | 110 ++++++++++++++++++++++++++++++---- 3 files changed, 124 insertions(+), 11 deletions(-) diff --git a/src/components/Api/Api.tsx b/src/components/Api/Api.tsx index 430124f..3130d12 100644 --- a/src/components/Api/Api.tsx +++ b/src/components/Api/Api.tsx @@ -8,6 +8,8 @@ import { RegistrationType, StudentStatusPatchType, StudentStatusType, + StudyGroupCreateType, + StudyGroupType, } from "../../interfaces/Interfaces"; export let backendURL = "https://stude.keannu1.duckdns.org"; @@ -275,6 +277,7 @@ export async function GetStudentStatusListFiltered() { return instance .get("/api/v1/student_status/filter/near_student_status", config) .then((response) => { + console.log("test", response.data); return [true, response.data]; }) .catch((error) => { @@ -315,7 +318,20 @@ export async function GetStudyGroupList() { return instance .get("/api/v1/study_groups/", config) .then((response) => { - console.log("test", response.data); + 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("Payload:", info); + return instance + .post("/api/v1/study_groups/create/", info, config) + .then((response) => { return [true, response.data]; }) .catch((error) => { diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index be6d983..33844eb 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -175,6 +175,13 @@ export interface StudyGroupType { subject: string; radius: number; } + +export interface StudyGroupCreateType { + name: string; + location: LocationType; + subject: string; +} + export type StudyGroupReturnType = [boolean, StudyGroupType[]]; export type StudentStatusReturnType = [boolean, StudentStatusType]; diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index f926c26..b3465b3 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -27,9 +27,11 @@ import { StudyGroupType, StudyGroupReturnType, StudentStatusFilterType, + StudyGroupCreateType, } from "../../interfaces/Interfaces"; import { useNavigation } from "@react-navigation/native"; import { + CreateStudyGroup, GetStudentStatus, GetStudentStatusList, GetStudentStatusListFiltered, @@ -44,10 +46,13 @@ import React from "react"; import CustomMapCallout from "../../components/CustomMapCallout/CustomMapCallout"; import MapRendererFar from "../../components/MapRenderer/MapRendererFar"; import GetDistanceFromUSTP from "../../components/GetDistance/GetDistanceFromUSTP"; +import { useSelector } from "react-redux"; +import { RootState } from "../../features/redux/Store/Store"; export default function Home() { // Switch this condition to see the main map when debugging const map_debug = true; + const user_state = useSelector((state: RootState) => state.user); const navigation = useNavigation(); const [location, setLocation] = useState(null); const [dist, setDist] = useState(null); @@ -185,6 +190,34 @@ export default function Home() { }, }); + 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"] }); + 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_statuses, setStudentStatuses] = useState([]); // Student Status List @@ -531,16 +564,73 @@ export default function Home() { } }} pinColor={colors.primary_1} - > - - + onPress={() => { + toast.hideAll(); + toast.show( + + + You are here + + + + {"x: " + + (student_status?.location?.longitude != 0 + ? student_status?.location?.longitude.toFixed(4) + : location.coords.longitude.toFixed(4))} + + + {"y: " + + (student_status?.location?.latitude != 0 + ? student_status?.location?.latitude.toFixed(4) + : location.coords.latitude.toFixed(4))} + + {studying ? ( + <> + + {studying + ? "Studying " + student_status?.subject + : ""} + + + + ) : ( + <> + )} + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + > + + + + ); + } + return ; +} diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index b3465b3..8adfa3f 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -416,7 +416,7 @@ export default function Home() { Subject: {studygroup.subject} - Students Studying: {studygroup.users.length} + Students Studying: {studygroup.students.length} - , - { - type: "normal", - placement: "top", - duration: 2000, - animationType: "slide-in", - style: { - backgroundColor: colors.secondary_2, - borderWidth: 1, - borderColor: colors.primary_1, - }, - } - ); - }} - /> - - - ); - })} - {study_groups_global.map( - (studygroup: StudyGroupType, index: number) => { - const randomColorWithOpacity = `rgba(${Math.floor( - Math.random() * 256 - )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( - Math.random() * 256 - )}, 0.7)`; - - return ( - - { - toast.hideAll(); - toast.show( - - - Subject: {studygroup.subject} - - - Students Studying: {studygroup.students.length} - - - , - { - type: "normal", - placement: "top", - duration: 2000, - animationType: "slide-in", - style: { - backgroundColor: colors.secondary_2, - borderWidth: 1, - borderColor: colors.primary_1, - }, - } - ); - }} - /> - - - ); - } - )} - { - const newLocation = e.nativeEvent.coordinate; - const distance = GetDistance( - newLocation.latitude, - newLocation.longitude, - location.coords.latitude, - location.coords.longitude - ); - if (distance <= 0.1) { - // If the new location is within 100 meters of the actual location, update the location state - setLocation({ - ...location, - coords: { - ...location.coords, - latitude: newLocation.latitude, - longitude: newLocation.longitude, - }, - }); - } else { - // If the new location is more than 100 meters away from the actual location, reset the marker to the actual location - setLocation({ - ...location, - }); - } - }} - pinColor={colors.primary_1} - onPress={() => { - toast.hideAll(); - toast.show( - - - You are here - - - - {"x: " + - (student_status?.location?.longitude != undefined - ? student_status?.location?.longitude.toFixed(4) - : location.coords.longitude.toFixed(4))} - - - {"y: " + - (student_status?.location?.latitude != undefined - ? student_status?.location?.latitude.toFixed(4) - : location.coords.latitude.toFixed(4))} - - {student_status?.active && - !student_status?.study_group ? ( - <> - - {student_status?.active - ? "Studying " + student_status?.subject - : ""} - - + ) : ( + <> + )} + {student_status?.study_group == + studygroup.name ? ( + + ) : ( + <> + )} + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, } - }} - > - - Create Group - - - - ) : ( - <> - )} - {student_status?.study_group ? ( - <> - - {`Studying ${student_status?.subject}`} - - - {`In group ${student_status?.study_group}`} - - - ) : ( - <> - )} - , - { - type: "normal", - placement: "top", - duration: 2000, - animationType: "slide-in", - style: { - backgroundColor: colors.secondary_2, - borderWidth: 1, - borderColor: colors.primary_1, - }, + ); + }} + /> + + + ); + } + ) + ) : ( + <> + )} + {!studying ? ( + study_groups_global.map( + (studygroup: StudyGroupType, index: number) => { + const randomColorWithOpacity = `rgba(${Math.floor( + Math.random() * 256 + )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( + Math.random() * 256 + )}, 0.7)`; + + return ( + + { + toast.hideAll(); + toast.show( + + + Subject: {studygroup.subject} + + + Name: {studygroup.name} + + + Students Studying:{" "} + {studygroup.students.length} + + {student_status?.study_group != + studygroup.name ? ( + + Get closer to join + + ) : ( + <> + )} + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + /> + + + ); + } + ) + ) : ( + <> + )} + {!studying || !student_status?.study_group ? ( + { + const newLocation = e.nativeEvent.coordinate; + const distance = GetDistance( + newLocation.latitude, + newLocation.longitude, + location.coords.latitude, + location.coords.longitude + ); + if (distance <= 0.1) { + // If the new location is within 100 meters of the actual location, update the location state + setLocation({ + ...location, + coords: { + ...location.coords, + latitude: newLocation.latitude, + longitude: newLocation.longitude, + }, + }); + } else { + // If the new location is more than 100 meters away from the actual location, reset the marker to the actual location + setLocation({ + ...location, + }); } - ); - }} - > + }} + pinColor={colors.primary_1} + onPress={() => { + toast.hideAll(); + toast.show( + + + You are here + + {student_status?.active && + !student_status?.study_group ? ( + <> + + {student_status?.active + ? "Studying " + student_status?.subject + : ""} + + + + ) : ( + <> + )} + {student_status?.study_group ? ( + <> + + {`Studying: ${student_status?.subject}`} + + + {`In group: ${student_status?.study_group}`} + + + ) : ( + <> + )} + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + > + ) : ( + <> + )}