Application Android et IOS pour l'amicale des élèves https://play.google.com/store/apps/details?id=fr.amicaleinsat.application
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.

VoteScreen.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. // @flow
  2. import * as React from 'react';
  3. import {RefreshControl, View} from 'react-native';
  4. import {StackNavigationProp} from '@react-navigation/stack';
  5. import i18n from 'i18n-js';
  6. import {Button} from 'react-native-paper';
  7. import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
  8. import {getTimeOnlyString, stringToDate} from '../../utils/Planning';
  9. import VoteTease from '../../components/Amicale/Vote/VoteTease';
  10. import VoteSelect from '../../components/Amicale/Vote/VoteSelect';
  11. import VoteResults from '../../components/Amicale/Vote/VoteResults';
  12. import VoteWait from '../../components/Amicale/Vote/VoteWait';
  13. import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
  14. import MascotPopup from '../../components/Mascot/MascotPopup';
  15. import AsyncStorageManager from '../../managers/AsyncStorageManager';
  16. import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
  17. import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
  18. import type {ApiGenericDataType} from '../../utils/WebData';
  19. export type VoteTeamType = {
  20. id: number,
  21. name: string,
  22. votes: number,
  23. };
  24. type TeamResponseType = {
  25. has_voted: boolean,
  26. teams: Array<VoteTeamType>,
  27. };
  28. type VoteDatesStringType = {
  29. date_begin: string,
  30. date_end: string,
  31. date_result_begin: string,
  32. date_result_end: string,
  33. };
  34. type VoteDatesObjectType = {
  35. date_begin: Date,
  36. date_end: Date,
  37. date_result_begin: Date,
  38. date_result_end: Date,
  39. };
  40. // const FAKE_DATE = {
  41. // "date_begin": "2020-08-19 15:50",
  42. // "date_end": "2020-08-19 15:50",
  43. // "date_result_begin": "2020-08-19 19:50",
  44. // "date_result_end": "2020-08-19 22:50",
  45. // };
  46. //
  47. // const FAKE_DATE2 = {
  48. // "date_begin": null,
  49. // "date_end": null,
  50. // "date_result_begin": null,
  51. // "date_result_end": null,
  52. // };
  53. //
  54. // const FAKE_TEAMS = {
  55. // has_voted: false,
  56. // teams: [
  57. // {
  58. // id: 1,
  59. // name: "TEST TEAM1",
  60. // },
  61. // {
  62. // id: 2,
  63. // name: "TEST TEAM2",
  64. // },
  65. // ],
  66. // };
  67. // const FAKE_TEAMS2 = {
  68. // has_voted: false,
  69. // teams: [
  70. // {
  71. // id: 1,
  72. // name: "TEST TEAM1",
  73. // votes: 9,
  74. // },
  75. // {
  76. // id: 2,
  77. // name: "TEST TEAM2",
  78. // votes: 9,
  79. // },
  80. // {
  81. // id: 3,
  82. // name: "TEST TEAM3",
  83. // votes: 5,
  84. // },
  85. // ],
  86. // };
  87. const MIN_REFRESH_TIME = 5 * 1000;
  88. type PropsType = {
  89. navigation: StackNavigationProp,
  90. };
  91. type StateType = {
  92. hasVoted: boolean,
  93. mascotDialogVisible: boolean,
  94. };
  95. /**
  96. * Screen displaying vote information and controls
  97. */
  98. export default class VoteScreen extends React.Component<PropsType, StateType> {
  99. teams: Array<VoteTeamType>;
  100. hasVoted: boolean;
  101. datesString: null | VoteDatesStringType;
  102. dates: null | VoteDatesObjectType;
  103. today: Date;
  104. mainFlatListData: Array<{key: string}>;
  105. lastRefresh: Date | null;
  106. authRef: {current: null | AuthenticatedScreen};
  107. constructor() {
  108. super();
  109. this.state = {
  110. hasVoted: false,
  111. mascotDialogVisible: AsyncStorageManager.getBool(
  112. AsyncStorageManager.PREFERENCES.voteShowMascot.key,
  113. ),
  114. };
  115. this.hasVoted = false;
  116. this.today = new Date();
  117. this.authRef = React.createRef();
  118. this.lastRefresh = null;
  119. this.mainFlatListData = [{key: 'main'}, {key: 'info'}];
  120. }
  121. /**
  122. * Gets the string representation of the given date.
  123. *
  124. * If the given date is the same day as today, only return the tile.
  125. * Otherwise, return the full date.
  126. *
  127. * @param date The Date object representation of the wanted date
  128. * @param dateString The string representation of the wanted date
  129. * @returns {string}
  130. */
  131. getDateString(date: Date, dateString: string): string {
  132. if (this.today.getDate() === date.getDate()) {
  133. const str = getTimeOnlyString(dateString);
  134. return str != null ? str : '';
  135. }
  136. return dateString;
  137. }
  138. getMainRenderItem = ({item}: {item: {key: string}}): React.Node => {
  139. if (item.key === 'info')
  140. return (
  141. <View>
  142. <Button
  143. mode="contained"
  144. icon="help-circle"
  145. onPress={this.showMascotDialog}
  146. style={{
  147. marginLeft: 'auto',
  148. marginRight: 'auto',
  149. marginTop: 20,
  150. }}>
  151. {i18n.t('screens.vote.mascotDialog.title')}
  152. </Button>
  153. </View>
  154. );
  155. return this.getContent();
  156. };
  157. getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
  158. const {state} = this;
  159. // data[0] = FAKE_TEAMS2;
  160. // data[1] = FAKE_DATE;
  161. this.lastRefresh = new Date();
  162. const teams: TeamResponseType | null = data[0];
  163. const dateStrings: VoteDatesStringType | null = data[1];
  164. if (dateStrings != null && dateStrings.date_begin == null)
  165. this.datesString = null;
  166. else this.datesString = dateStrings;
  167. if (teams != null) {
  168. this.teams = teams.teams;
  169. this.hasVoted = teams.has_voted;
  170. }
  171. this.generateDateObject();
  172. return (
  173. <CollapsibleFlatList
  174. data={this.mainFlatListData}
  175. refreshControl={
  176. <RefreshControl refreshing={false} onRefresh={this.reloadData} />
  177. }
  178. extraData={state.hasVoted.toString()}
  179. renderItem={this.getMainRenderItem}
  180. />
  181. );
  182. };
  183. getContent(): React.Node {
  184. const {state} = this;
  185. if (!this.isVoteStarted()) return this.getTeaseVoteCard();
  186. if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted)
  187. return this.getVoteCard();
  188. if (!this.isResultStarted()) return this.getWaitVoteCard();
  189. if (this.isResultRunning()) return this.getVoteResultCard();
  190. return <VoteNotAvailable />;
  191. }
  192. onVoteSuccess = (): void => this.setState({hasVoted: true});
  193. /**
  194. * The user has not voted yet, and the votes are open
  195. */
  196. getVoteCard(): React.Node {
  197. return (
  198. <VoteSelect
  199. teams={this.teams}
  200. onVoteSuccess={this.onVoteSuccess}
  201. onVoteError={this.reloadData}
  202. />
  203. );
  204. }
  205. /**
  206. * Votes have ended, results can be displayed
  207. */
  208. getVoteResultCard(): React.Node {
  209. if (this.dates != null && this.datesString != null)
  210. return (
  211. <VoteResults
  212. teams={this.teams}
  213. dateEnd={this.getDateString(
  214. this.dates.date_result_end,
  215. this.datesString.date_result_end,
  216. )}
  217. />
  218. );
  219. return <VoteNotAvailable />;
  220. }
  221. /**
  222. * Vote will open shortly
  223. */
  224. getTeaseVoteCard(): React.Node {
  225. if (this.dates != null && this.datesString != null)
  226. return (
  227. <VoteTease
  228. startDate={this.getDateString(
  229. this.dates.date_begin,
  230. this.datesString.date_begin,
  231. )}
  232. />
  233. );
  234. return <VoteNotAvailable />;
  235. }
  236. /**
  237. * Votes have ended, or user has voted waiting for results
  238. */
  239. getWaitVoteCard(): React.Node {
  240. const {state} = this;
  241. let startDate = null;
  242. if (
  243. this.dates != null &&
  244. this.datesString != null &&
  245. this.dates.date_result_begin != null
  246. )
  247. startDate = this.getDateString(
  248. this.dates.date_result_begin,
  249. this.datesString.date_result_begin,
  250. );
  251. return (
  252. <VoteWait
  253. startDate={startDate}
  254. hasVoted={this.hasVoted || state.hasVoted}
  255. justVoted={state.hasVoted}
  256. isVoteRunning={this.isVoteRunning()}
  257. />
  258. );
  259. }
  260. /**
  261. * Reloads vote data if last refresh delta is smaller than the minimum refresh time
  262. */
  263. reloadData = () => {
  264. let canRefresh;
  265. const {lastRefresh} = this;
  266. if (lastRefresh != null)
  267. canRefresh =
  268. new Date().getTime() - lastRefresh.getTime() > MIN_REFRESH_TIME;
  269. else canRefresh = true;
  270. if (canRefresh && this.authRef.current != null)
  271. this.authRef.current.reload();
  272. };
  273. showMascotDialog = () => {
  274. this.setState({mascotDialogVisible: true});
  275. };
  276. hideMascotDialog = () => {
  277. AsyncStorageManager.set(
  278. AsyncStorageManager.PREFERENCES.voteShowMascot.key,
  279. false,
  280. );
  281. this.setState({mascotDialogVisible: false});
  282. };
  283. isVoteStarted(): boolean {
  284. return this.dates != null && this.today > this.dates.date_begin;
  285. }
  286. isResultRunning(): boolean {
  287. return (
  288. this.dates != null &&
  289. this.today > this.dates.date_result_begin &&
  290. this.today < this.dates.date_result_end
  291. );
  292. }
  293. isResultStarted(): boolean {
  294. return this.dates != null && this.today > this.dates.date_result_begin;
  295. }
  296. isVoteRunning(): boolean {
  297. return (
  298. this.dates != null &&
  299. this.today > this.dates.date_begin &&
  300. this.today < this.dates.date_end
  301. );
  302. }
  303. /**
  304. * Generates the objects containing string and Date representations of key vote dates
  305. */
  306. generateDateObject() {
  307. const strings = this.datesString;
  308. if (strings != null) {
  309. const dateBegin = stringToDate(strings.date_begin);
  310. const dateEnd = stringToDate(strings.date_end);
  311. const dateResultBegin = stringToDate(strings.date_result_begin);
  312. const dateResultEnd = stringToDate(strings.date_result_end);
  313. if (
  314. dateBegin != null &&
  315. dateEnd != null &&
  316. dateResultBegin != null &&
  317. dateResultEnd != null
  318. ) {
  319. this.dates = {
  320. date_begin: dateBegin,
  321. date_end: dateEnd,
  322. date_result_begin: dateResultBegin,
  323. date_result_end: dateResultEnd,
  324. };
  325. } else this.dates = null;
  326. } else this.dates = null;
  327. }
  328. /**
  329. * Renders the authenticated screen.
  330. *
  331. * Teams and dates are not mandatory to allow showing the information box even if api requests fail
  332. *
  333. * @returns {*}
  334. */
  335. render(): React.Node {
  336. const {props, state} = this;
  337. return (
  338. <View style={{flex: 1}}>
  339. <AuthenticatedScreen
  340. navigation={props.navigation}
  341. ref={this.authRef}
  342. requests={[
  343. {
  344. link: 'elections/teams',
  345. params: {},
  346. mandatory: false,
  347. },
  348. {
  349. link: 'elections/dates',
  350. params: {},
  351. mandatory: false,
  352. },
  353. ]}
  354. renderFunction={this.getScreen}
  355. />
  356. <MascotPopup
  357. visible={state.mascotDialogVisible}
  358. title={i18n.t('screens.vote.mascotDialog.title')}
  359. message={i18n.t('screens.vote.mascotDialog.message')}
  360. icon="vote"
  361. buttons={{
  362. action: null,
  363. cancel: {
  364. message: i18n.t('screens.vote.mascotDialog.button'),
  365. icon: 'check',
  366. onPress: this.hideMascotDialog,
  367. },
  368. }}
  369. emotion={MASCOT_STYLE.CUTE}
  370. />
  371. </View>
  372. );
  373. }
  374. }