diff --git a/src/components/Animations/AnimatedBottomBar.tsx b/src/components/Animations/AnimatedBottomBar.tsx deleted file mode 100644 index a334bd3..0000000 --- a/src/components/Animations/AnimatedBottomBar.tsx +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright (c) 2019 - 2020 Arnaud Vergnet. - * - * This file is part of Campus INSAT. - * - * Campus INSAT is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Campus INSAT is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Campus INSAT. If not, see . - */ - -import * as React from 'react'; -import { - NativeScrollEvent, - NativeSyntheticEvent, - StyleSheet, - View, -} from 'react-native'; -import { FAB, IconButton, Surface, withTheme } from 'react-native-paper'; -import * as Animatable from 'react-native-animatable'; -import { StackNavigationProp } from '@react-navigation/stack'; -import AutoHideHandler from '../../utils/AutoHideHandler'; -import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; - -type PropsType = { - navigation: StackNavigationProp; - theme: ReactNativePaper.Theme; - onPress: (action: string, data?: string) => void; - seekAttention: boolean; -}; - -type StateType = { - currentMode: string; -}; - -const DISPLAY_MODES = { - DAY: 'agendaDay', - WEEK: 'agendaWeek', - MONTH: 'month', -}; - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - left: '5%', - width: '90%', - }, - surface: { - position: 'relative', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - borderRadius: 50, - elevation: 2, - }, - fabContainer: { - position: 'absolute', - left: 0, - right: 0, - alignItems: 'center', - width: '100%', - height: '100%', - }, - fab: { - position: 'absolute', - alignSelf: 'center', - top: '-25%', - }, - side: { - flexDirection: 'row', - }, - icon: { - marginLeft: 5, - }, -}); - -class AnimatedBottomBar extends React.Component { - ref: { current: null | (Animatable.View & View) }; - - hideHandler: AutoHideHandler; - - displayModeIcons: { [key: string]: string }; - - constructor(props: PropsType) { - super(props); - this.state = { - currentMode: DISPLAY_MODES.WEEK, - }; - this.ref = React.createRef(); - this.hideHandler = new AutoHideHandler(false); - this.hideHandler.addListener(this.onHideChange); - - this.displayModeIcons = {}; - this.displayModeIcons[DISPLAY_MODES.DAY] = 'calendar-text'; - this.displayModeIcons[DISPLAY_MODES.WEEK] = 'calendar-week'; - this.displayModeIcons[DISPLAY_MODES.MONTH] = 'calendar-range'; - } - - shouldComponentUpdate(nextProps: PropsType, nextState: StateType): boolean { - const { props, state } = this; - return ( - nextProps.seekAttention !== props.seekAttention || - nextState.currentMode !== state.currentMode - ); - } - - onHideChange = (shouldHide: boolean) => { - const ref = this.ref; - if (ref && ref.current && ref.current.fadeOutDown && ref.current.fadeInUp) { - if (shouldHide) { - ref.current.fadeOutDown(500); - } else { - ref.current.fadeInUp(500); - } - } - }; - - onScroll = (event: NativeSyntheticEvent) => { - this.hideHandler.onScroll(event); - }; - - changeDisplayMode = () => { - const { props, state } = this; - let newMode; - switch (state.currentMode) { - case DISPLAY_MODES.DAY: - newMode = DISPLAY_MODES.WEEK; - break; - case DISPLAY_MODES.WEEK: - newMode = DISPLAY_MODES.MONTH; - break; - case DISPLAY_MODES.MONTH: - newMode = DISPLAY_MODES.DAY; - break; - default: - newMode = DISPLAY_MODES.WEEK; - break; - } - this.setState({ currentMode: newMode }); - props.onPress('changeView', newMode); - }; - - render() { - const { props, state } = this; - const buttonColor = props.theme.colors.primary; - return ( - - - - - props.navigation.navigate('group-select')} - /> - - - - - props.onPress('today')} - /> - - - props.onPress('prev')} - /> - props.onPress('next')} - /> - - - - ); - } -} - -export default withTheme(AnimatedBottomBar); diff --git a/src/components/Animations/PlanexBottomBar.tsx b/src/components/Animations/PlanexBottomBar.tsx new file mode 100644 index 0000000..2013d41 --- /dev/null +++ b/src/components/Animations/PlanexBottomBar.tsx @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2019 - 2020 Arnaud Vergnet. + * + * This file is part of Campus INSAT. + * + * Campus INSAT is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Campus INSAT is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Campus INSAT. If not, see . + */ + +import React, { useState } from 'react'; +import { StyleSheet, View, Animated } from 'react-native'; +import { FAB, IconButton, Surface, useTheme } from 'react-native-paper'; +import * as Animatable from 'react-native-animatable'; +import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; +import { useNavigation } from '@react-navigation/core'; +import { useCollapsible } from '../../utils/CollapsibleContext'; + +type Props = { + onPress: (action: string, data?: string) => void; + seekAttention: boolean; +}; + +const DISPLAY_MODES = { + DAY: 'agendaDay', + WEEK: 'agendaWeek', + MONTH: 'month', +}; + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + left: '5%', + width: '90%', + }, + surface: { + position: 'relative', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + borderRadius: 50, + elevation: 2, + }, + fabContainer: { + position: 'absolute', + left: 0, + right: 0, + alignItems: 'center', + width: '100%', + height: '100%', + }, + fab: { + position: 'absolute', + alignSelf: 'center', + top: '-25%', + }, + side: { + flexDirection: 'row', + }, + icon: { + marginLeft: 5, + }, +}); + +const DISPLAY_MODE_ICONS = { + [DISPLAY_MODES.DAY]: 'calendar-text', + [DISPLAY_MODES.WEEK]: 'calendar-week', + [DISPLAY_MODES.MONTH]: 'calendar-range', +}; + +function PlanexBottomBar(props: Props) { + const navigation = useNavigation(); + const theme = useTheme(); + const [currentMode, setCurrentMode] = useState(DISPLAY_MODES.WEEK); + + const { collapsible } = useCollapsible(); + + const changeDisplayMode = () => { + let newMode; + switch (currentMode) { + case DISPLAY_MODES.DAY: + newMode = DISPLAY_MODES.WEEK; + break; + case DISPLAY_MODES.WEEK: + newMode = DISPLAY_MODES.MONTH; + break; + case DISPLAY_MODES.MONTH: + newMode = DISPLAY_MODES.DAY; + break; + default: + newMode = DISPLAY_MODES.WEEK; + break; + } + setCurrentMode(newMode); + props.onPress('changeView', newMode); + }; + + let translateY: number | Animated.AnimatedInterpolation = 0; + let opacity: number | Animated.AnimatedInterpolation = 1; + let scale: number | Animated.AnimatedInterpolation = 1; + if (collapsible) { + translateY = Animated.multiply(-3, collapsible.translateY); + opacity = Animated.subtract(1, collapsible.progress); + scale = Animated.add( + 0.5, + Animated.multiply(0.5, Animated.subtract(1, collapsible.progress)) + ); + } + + const buttonColor = theme.colors.primary; + return ( + + + + + navigation.navigate('group-select')} + /> + + + + + props.onPress('today')} + /> + + + props.onPress('prev')} + /> + props.onPress('next')} + /> + + + + ); +} + +export default PlanexBottomBar; diff --git a/src/components/Screens/PlanexWebview.tsx b/src/components/Screens/PlanexWebview.tsx new file mode 100644 index 0000000..0429b0e --- /dev/null +++ b/src/components/Screens/PlanexWebview.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { View } from 'react-native'; +import GENERAL_STYLES from '../../constants/Styles'; +import Urls from '../../constants/Urls'; +import DateManager from '../../managers/DateManager'; +import ThemeManager from '../../managers/ThemeManager'; +import { PlanexGroupType } from '../../screens/Planex/GroupSelectionScreen'; +import ErrorView from './ErrorView'; +import WebViewScreen from './WebViewScreen'; +import i18n from 'i18n-js'; + +type Props = { + currentGroup?: PlanexGroupType; + injectJS: string; + onMessage: (event: { nativeEvent: { data: string } }) => void; +}; + +// Watch for changes in the calendar and call the remove alpha function to prevent invisible events +const OBSERVE_MUTATIONS_INJECTED = + 'function removeAlpha(node) {\n' + + ' let bg = node.css("background-color");\n' + + ' if (bg.match("^rgba")) {\n' + + " let a = bg.slice(5).split(',');\n" + + ' // Fix for tooltips with broken background\n' + + ' if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {\n' + + " a[0] = a[1] = a[2] = '255';\n" + + ' }\n' + + " let newBg ='rgb(' + a[0] + ',' + a[1] + ',' + a[2] + ')';\n" + + ' node.css("background-color", newBg);\n' + + ' }\n' + + '}\n' + + '// Observe for planning DOM changes\n' + + 'let observer = new MutationObserver(function(mutations) {\n' + + ' for (let i = 0; i < mutations.length; i++) {\n' + + " if (mutations[i]['addedNodes'].length > 0 &&\n" + + ' ($(mutations[i][\'addedNodes\'][0]).hasClass("fc-event") || $(mutations[i][\'addedNodes\'][0]).hasClass("tooltiptopicevent")))\n' + + " removeAlpha($(mutations[i]['addedNodes'][0]))\n" + + ' }\n' + + '});\n' + + '// observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' + + 'observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' + + '// Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.\n' + + '$(".fc-event-container .fc-event").each(function(index) {\n' + + ' removeAlpha($(this));\n' + + '});'; + +// Overrides default settings to send a message to the webview when clicking on an event +const FULL_CALENDAR_SETTINGS = ` +let calendar = $('#calendar').fullCalendar('getCalendar'); +calendar.option({ + eventClick: function (data, event, view) { + let message = { + title: data.title, + color: data.color, + start: data.start._d, + end: data.end._d, + }; + window.ReactNativeWebView.postMessage(JSON.stringify(message)); + } +});`; + +// Mobile friendly CSS +const CUSTOM_CSS = + 'body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}'; + +// Dark mode CSS, to be used with the mobile friendly css +const CUSTOM_CSS_DARK = + 'body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}'; + +// Inject the custom css into the webpage +const INJECT_STYLE = `$('head').append('');`; + +// Inject the dark mode into the webpage, to call after the custom css inject above +const INJECT_STYLE_DARK = `$('head').append('');`; + +/** + * Generates custom JavaScript to be injected into the webpage + * + * @param groupID The current group selected + */ +const generateInjectedJS = (group: PlanexGroupType | undefined) => { + let customInjectedJS = `$(document).ready(function() { + ${OBSERVE_MUTATIONS_INJECTED} + ${INJECT_STYLE} + ${FULL_CALENDAR_SETTINGS}`; + if (group) { + customInjectedJS += `displayAde(${group.id});`; + } + if (DateManager.isWeekend(new Date())) { + customInjectedJS += `calendar.next();`; + } + if (ThemeManager.getNightMode()) { + customInjectedJS += INJECT_STYLE_DARK; + } + customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios + return customInjectedJS; +}; + +function PlanexWebview(props: Props) { + return ( + + {!props.currentGroup ? ( + + ) : null} + + + ); +} + +export default PlanexWebview; diff --git a/src/screens/Planex/PlanexScreen.tsx b/src/screens/Planex/PlanexScreen.tsx index 3596639..2c3e62e 100644 --- a/src/screens/Planex/PlanexScreen.tsx +++ b/src/screens/Planex/PlanexScreen.tsx @@ -17,122 +17,27 @@ * along with Campus INSAT. If not, see . */ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { Title, useTheme } from 'react-native-paper'; import i18n from 'i18n-js'; -import { - NativeScrollEvent, - NativeSyntheticEvent, - StyleSheet, - View, -} from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { CommonActions, useFocusEffect, useNavigation, } from '@react-navigation/native'; import Autolink from 'react-native-autolink'; -import ThemeManager from '../../managers/ThemeManager'; -import WebViewScreen from '../../components/Screens/WebViewScreen'; import AsyncStorageManager from '../../managers/AsyncStorageManager'; import AlertDialog from '../../components/Dialogs/AlertDialog'; import { dateToString, getTimeOnlyString } from '../../utils/Planning'; import DateManager from '../../managers/DateManager'; -import AnimatedBottomBar from '../../components/Animations/AnimatedBottomBar'; -import ErrorView from '../../components/Screens/ErrorView'; import type { PlanexGroupType } from './GroupSelectionScreen'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import MascotPopup from '../../components/Mascot/MascotPopup'; import { getPrettierPlanexGroupName } from '../../utils/Utils'; import GENERAL_STYLES from '../../constants/Styles'; -import Urls from '../../constants/Urls'; - -// // JS + JQuery functions used to remove alpha from events. Copy paste in browser console for quick testing -// // Remove alpha from given Jquery node -// function removeAlpha(node) { -// let bg = node.css("background-color"); -// if (bg.match("^rgba")) { -// let a = bg.slice(5).split(','); -// // Fix for tooltips with broken background -// if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) { -// a[0] = a[1] = a[2] = '255'; -// } -// let newBg ='rgb(' + a[0] + ',' + a[1] + ',' + a[2] + ')'; -// node.css("background-color", newBg); -// } -// } -// // Observe for planning DOM changes -// let observer = new MutationObserver(function(mutations) { -// for (let i = 0; i < mutations.length; i++) { -// if (mutations[i]['addedNodes'].length > 0 && -// ($(mutations[i]['addedNodes'][0]).hasClass("fc-event") || $(mutations[i]['addedNodes'][0]).hasClass("tooltiptopicevent"))) -// removeAlpha($(mutations[i]['addedNodes'][0])) -// } -// }); -// // observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true}); -// observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true}); -// // Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded. -// $(".fc-event-container .fc-event").each(function(index) { -// removeAlpha($(this)); -// }); - -// Watch for changes in the calendar and call the remove alpha function to prevent invisible events -const OBSERVE_MUTATIONS_INJECTED = - 'function removeAlpha(node) {\n' + - ' let bg = node.css("background-color");\n' + - ' if (bg.match("^rgba")) {\n' + - " let a = bg.slice(5).split(',');\n" + - ' // Fix for tooltips with broken background\n' + - ' if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {\n' + - " a[0] = a[1] = a[2] = '255';\n" + - ' }\n' + - " let newBg ='rgb(' + a[0] + ',' + a[1] + ',' + a[2] + ')';\n" + - ' node.css("background-color", newBg);\n' + - ' }\n' + - '}\n' + - '// Observe for planning DOM changes\n' + - 'let observer = new MutationObserver(function(mutations) {\n' + - ' for (let i = 0; i < mutations.length; i++) {\n' + - " if (mutations[i]['addedNodes'].length > 0 &&\n" + - ' ($(mutations[i][\'addedNodes\'][0]).hasClass("fc-event") || $(mutations[i][\'addedNodes\'][0]).hasClass("tooltiptopicevent")))\n' + - " removeAlpha($(mutations[i]['addedNodes'][0]))\n" + - ' }\n' + - '});\n' + - '// observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' + - 'observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' + - '// Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.\n' + - '$(".fc-event-container .fc-event").each(function(index) {\n' + - ' removeAlpha($(this));\n' + - '});'; - -// Overrides default settings to send a message to the webview when clicking on an event -const FULL_CALENDAR_SETTINGS = ` -let calendar = $('#calendar').fullCalendar('getCalendar'); -calendar.option({ - eventClick: function (data, event, view) { - let message = { - title: data.title, - color: data.color, - start: data.start._d, - end: data.end._d, - }; - window.ReactNativeWebView.postMessage(JSON.stringify(message)); - } -});`; - -// Mobile friendly CSS -const CUSTOM_CSS = - 'body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}'; - -// Dark mode CSS, to be used with the mobile friendly css -const CUSTOM_CSS_DARK = - 'body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}'; - -// Inject the custom css into the webpage -const INJECT_STYLE = `$('head').append('');`; - -// Inject the dark mode into the webpage, to call after the custom css inject above -const INJECT_STYLE_DARK = `$('head').append('');`; +import PlanexWebview from '../../components/Screens/PlanexWebview'; +import PlanexBottomBar from '../../components/Animations/PlanexBottomBar'; const styles = StyleSheet.create({ container: { @@ -140,6 +45,9 @@ const styles = StyleSheet.create({ height: '100%', width: '100%', }, + popup: { + borderWidth: 2, + }, }); type Props = { @@ -153,7 +61,6 @@ type Props = { function PlanexScreen(props: Props) { const navigation = useNavigation(); const theme = useTheme(); - const barRef = useRef(); const [dialogContent, setDialogContent] = useState< | undefined @@ -199,26 +106,13 @@ function PlanexScreen(props: Props) { * * @returns {*} */ - const getWebView = () => { - return ( - - {!currentGroup ? ( - - ) : null} - - - ); - }; + const getWebView = () => ( + + ); /** * Callback used when the user clicks on the navigate to settings button. @@ -300,17 +194,6 @@ function PlanexScreen(props: Props) { const hideDialog = () => setDialogContent(undefined); - /** - * Binds the onScroll event to the control bar for automatic hiding based on scroll direction and speed - * - * @param event - */ - const onScroll = (event: NativeSyntheticEvent) => { - if (barRef.current) { - barRef.current.onScroll(event); - } - }; - /** * Sends the webpage a message with the new group to select and save it to preferences * @@ -323,30 +206,8 @@ function PlanexScreen(props: Props) { AsyncStorageManager.PREFERENCES.planexCurrentGroup.key, group ); - navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) }); - }; - /** - * Generates custom JavaScript to be injected into the webpage - * - * @param groupID The current group selected - */ - const generateInjectedJS = (group: PlanexGroupType | undefined) => { - let customInjectedJS = `$(document).ready(function() { - ${OBSERVE_MUTATIONS_INJECTED} - ${INJECT_STYLE} - ${FULL_CALENDAR_SETTINGS}`; - if (group) { - customInjectedJS += `displayAde(${group.id});`; - } - if (DateManager.isWeekend(new Date())) { - customInjectedJS += `calendar.next();`; - } - if (ThemeManager.getNightMode()) { - customInjectedJS += INJECT_STYLE_DARK; - } - customInjectedJS += 'removeAlpha();});true;'; // Prevents crash on ios - return customInjectedJS; + navigation.setOptions({ title: getPrettierPlanexGroupName(group.name) }); }; return ( @@ -389,13 +250,11 @@ function PlanexScreen(props: Props) { message={dialogContent ? dialogContent.message : ''} style={ dialogContent - ? { borderColor: dialogContent.color, borderWidth: 2 } + ? { borderColor: dialogContent.color, ...styles.popup } : undefined } /> - diff --git a/src/utils/customHooks.tsx b/src/utils/customHooks.tsx index 86ec9a3..6862e1c 100644 --- a/src/utils/customHooks.tsx +++ b/src/utils/customHooks.tsx @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2019 - 2020 Arnaud Vergnet. + * + * This file is part of Campus INSAT. + * + * Campus INSAT is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Campus INSAT is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Campus INSAT. If not, see . + */ + import { DependencyList, useEffect, useRef, useState } from 'react'; import { REQUEST_STATUS } from './Requests';