Application Android et IOS pour l'amicale des élèves
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PlanningScreen.tsx 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /*
  2. * Copyright (c) 2019 - 2020 Arnaud Vergnet.
  3. *
  4. * This file is part of Campus INSAT.
  5. *
  6. * Campus INSAT is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Campus INSAT is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import * as React from 'react';
  20. import {BackHandler, View} from 'react-native';
  21. import i18n from 'i18n-js';
  22. import {Agenda, LocaleConfig} from 'react-native-calendars';
  23. import {Avatar, Divider, List} from 'react-native-paper';
  24. import {StackNavigationProp} from '@react-navigation/stack';
  25. import {readData} from '../../utils/WebData';
  26. import {
  27. generateEventAgenda,
  28. getCurrentDateString,
  29. getDateOnlyString,
  30. getTimeOnlyString,
  31. PlanningEventType,
  32. } from '../../utils/Planning';
  33. import CustomAgenda from '../../components/Overrides/CustomAgenda';
  34. import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
  35. import MascotPopup from '../../components/Mascot/MascotPopup';
  36. import AsyncStorageManager from '../../managers/AsyncStorageManager';
  37. LocaleConfig.locales.fr = {
  38. monthNames: [
  39. 'Janvier',
  40. 'Février',
  41. 'Mars',
  42. 'Avril',
  43. 'Mai',
  44. 'Juin',
  45. 'Juillet',
  46. 'Août',
  47. 'Septembre',
  48. 'Octobre',
  49. 'Novembre',
  50. 'Décembre',
  51. ],
  52. monthNamesShort: [
  53. 'Janv.',
  54. 'Févr.',
  55. 'Mars',
  56. 'Avril',
  57. 'Mai',
  58. 'Juin',
  59. 'Juil.',
  60. 'Août',
  61. 'Sept.',
  62. 'Oct.',
  63. 'Nov.',
  64. 'Déc.',
  65. ],
  66. dayNames: [
  67. 'Dimanche',
  68. 'Lundi',
  69. 'Mardi',
  70. 'Mercredi',
  71. 'Jeudi',
  72. 'Vendredi',
  73. 'Samedi',
  74. ],
  75. dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
  76. };
  77. type PropsType = {
  78. navigation: StackNavigationProp<any>;
  79. };
  80. type StateType = {
  81. refreshing: boolean;
  82. agendaItems: {[key: string]: Array<PlanningEventType>};
  83. calendarShowing: boolean;
  84. };
  85. const FETCH_URL = 'https://www.amicale-insat.fr/api/event/list';
  86. const AGENDA_MONTH_SPAN = 3;
  87. /**
  88. * Class defining the app's planning screen
  89. */
  90. class PlanningScreen extends React.Component<PropsType, StateType> {
  91. agendaRef: null | Agenda<any>;
  92. lastRefresh: Date | null;
  93. minTimeBetweenRefresh = 60;
  94. currentDate: string | null;
  95. constructor(props: PropsType) {
  96. super(props);
  97. if (i18n.currentLocale().startsWith('fr')) {
  98. LocaleConfig.defaultLocale = 'fr';
  99. }
  100. this.agendaRef = null;
  101. this.currentDate = getDateOnlyString(getCurrentDateString());
  102. this.lastRefresh = null;
  103. this.state = {
  104. refreshing: false,
  105. agendaItems: {},
  106. calendarShowing: false,
  107. };
  108. }
  109. /**
  110. * Captures focus and blur events to hook on android back button
  111. */
  112. componentDidMount() {
  113. const {navigation} = this.props;
  114. this.onRefresh();
  115. navigation.addListener('focus', () => {
  116. BackHandler.addEventListener(
  117. 'hardwareBackPress',
  118. this.onBackButtonPressAndroid,
  119. );
  120. });
  121. navigation.addListener('blur', () => {
  122. BackHandler.removeEventListener(
  123. 'hardwareBackPress',
  124. this.onBackButtonPressAndroid,
  125. );
  126. });
  127. }
  128. /**
  129. * Overrides default android back button behaviour to close the calendar if it was open.
  130. *
  131. * @return {boolean}
  132. */
  133. onBackButtonPressAndroid = (): boolean => {
  134. const {calendarShowing} = this.state;
  135. if (calendarShowing && this.agendaRef != null) {
  136. // @ts-ignore
  137. this.agendaRef.chooseDay(this.agendaRef.state.selectedDay);
  138. return true;
  139. }
  140. return false;
  141. };
  142. /**
  143. * Refreshes data and shows an animation while doing it
  144. */
  145. onRefresh = () => {
  146. let canRefresh;
  147. if (this.lastRefresh) {
  148. canRefresh =
  149. (new Date().getTime() - this.lastRefresh.getTime()) / 1000 >
  150. this.minTimeBetweenRefresh;
  151. } else {
  152. canRefresh = true;
  153. }
  154. if (canRefresh) {
  155. this.setState({refreshing: true});
  156. readData(FETCH_URL)
  157. .then((fetchedData: Array<PlanningEventType>) => {
  158. this.setState({
  159. refreshing: false,
  160. agendaItems: generateEventAgenda(fetchedData, AGENDA_MONTH_SPAN),
  161. });
  162. this.lastRefresh = new Date();
  163. })
  164. .catch(() => {
  165. this.setState({
  166. refreshing: false,
  167. });
  168. });
  169. }
  170. };
  171. /**
  172. * Callback used when receiving the agenda ref
  173. *
  174. * @param ref
  175. */
  176. onAgendaRef = (ref: Agenda<any>) => {
  177. this.agendaRef = ref;
  178. };
  179. /**
  180. * Callback used when a button is pressed to toggle the calendar
  181. *
  182. * @param isCalendarOpened True is the calendar is already open, false otherwise
  183. */
  184. onCalendarToggled = (isCalendarOpened: boolean) => {
  185. this.setState({calendarShowing: isCalendarOpened});
  186. };
  187. /**
  188. * Gets an event render item
  189. *
  190. * @param item The current event to render
  191. * @return {*}
  192. */
  193. getRenderItem = (item: PlanningEventType) => {
  194. const {navigation} = this.props;
  195. const onPress = () => {
  196. navigation.navigate('planning-information', {
  197. data: item,
  198. });
  199. };
  200. const logo = item.logo;
  201. if (logo) {
  202. return (
  203. <View>
  204. <Divider />
  205. <List.Item
  206. title={item.title}
  207. description={getTimeOnlyString(item.date_begin)}
  208. left={() => (
  209. <Avatar.Image
  210. source={{uri: logo}}
  211. style={{backgroundColor: 'transparent'}}
  212. />
  213. )}
  214. onPress={onPress}
  215. />
  216. </View>
  217. );
  218. }
  219. return (
  220. <View>
  221. <Divider />
  222. <List.Item
  223. title={item.title}
  224. description={getTimeOnlyString(item.date_begin)}
  225. onPress={onPress}
  226. />
  227. </View>
  228. );
  229. };
  230. /**
  231. * Gets an empty render item for an empty date
  232. *
  233. * @return {*}
  234. */
  235. getRenderEmptyDate = () => <Divider />;
  236. render() {
  237. const {state, props} = this;
  238. return (
  239. <View style={{flex: 1}}>
  240. <CustomAgenda
  241. {...props}
  242. // the list of items that have to be displayed in agenda. If you want to render item as empty date
  243. // the value of date key kas to be an empty array []. If there exists no value for date key it is
  244. // considered that the date in question is not yet loaded
  245. items={state.agendaItems}
  246. // initially selected day
  247. selected={this.currentDate ? this.currentDate : undefined}
  248. // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
  249. minDate={this.currentDate ? this.currentDate : undefined}
  250. // Max amount of months allowed to scroll to the past. Default = 50
  251. pastScrollRange={1}
  252. // Max amount of months allowed to scroll to the future. Default = 50
  253. futureScrollRange={AGENDA_MONTH_SPAN}
  254. // If provided, a standard RefreshControl will be added for "Pull to Refresh" functionality. Make sure to also set the refreshing prop correctly.
  255. onRefresh={this.onRefresh}
  256. // callback that fires when the calendar is opened or closed
  257. onCalendarToggled={this.onCalendarToggled}
  258. // Set this true while waiting for new data from a refresh
  259. refreshing={state.refreshing}
  260. renderItem={this.getRenderItem}
  261. renderEmptyDate={this.getRenderEmptyDate}
  262. // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
  263. firstDay={1}
  264. // ref to this agenda in order to handle back button event
  265. onRef={this.onAgendaRef}
  266. rowHasChanged={(r1: PlanningEventType, r2: PlanningEventType) =>
  267. r1.id !== r2.id
  268. }
  269. />
  270. <MascotPopup
  271. prefKey={AsyncStorageManager.PREFERENCES.eventsShowMascot.key}
  272. title={i18n.t('screens.planning.mascotDialog.title')}
  273. message={i18n.t('screens.planning.mascotDialog.message')}
  274. icon="party-popper"
  275. buttons={{
  276. cancel: {
  277. message: i18n.t('screens.planning.mascotDialog.button'),
  278. icon: 'check',
  279. },
  280. }}
  281. emotion={MASCOT_STYLE.HAPPY}
  282. />
  283. </View>
  284. );
  285. }
  286. }
  287. export default PlanningScreen;