Compare commits

...

41 commits

Author SHA1 Message Date
Arnaud Vergnet
5d692c6840 Update docs to replace flow references 2020-09-24 13:55:53 +02:00
Arnaud Vergnet
09ed0058ae Remove useless console.log 2020-09-23 19:08:29 +02:00
Arnaud Vergnet
1639544810 Open current webview url instead of start url 2020-09-23 19:08:09 +02:00
Arnaud Vergnet
4492debad9 Fix french translation for go back/next 2020-09-23 19:03:25 +02:00
Arnaud Vergnet
faac5688f8 Improve equipment confirm screen dates display 2020-09-23 18:58:13 +02:00
Arnaud Vergnet
346b00defd Fix group accordions expanding incorrectly 2020-09-23 18:47:00 +02:00
Arnaud Vergnet
11609f8277 Fix props equality checking functions 2020-09-23 18:40:22 +02:00
Arnaud Vergnet
74f757ce66 Fix animated fab position 2020-09-23 09:22:36 +02:00
Arnaud Vergnet
df8f1cab24 Fix tab bar not animating in certain cases 2020-09-23 09:20:06 +02:00
Arnaud Vergnet
eaf1c52af5 Really fix translations not loading 2020-09-23 09:14:54 +02:00
Arnaud Vergnet
da55abed8f Fix proxiwash translations not loading 2020-09-23 09:08:47 +02:00
Arnaud Vergnet
f5233c53f8 Fix crash on app start 2020-09-23 09:03:48 +02:00
Arnaud Vergnet
c5287611c4 Fix errors induced by refactoring 2020-09-22 23:20:16 +02:00
Arnaud Vergnet
fde9a12ef9 Update game to use TypeScript 2020-09-22 23:13:09 +02:00
Arnaud Vergnet
c198a40148 Update home screens to use TypeScript 2020-09-22 22:52:35 +02:00
Arnaud Vergnet
38afbf02a3 Update services screens to use TypeScript 2020-09-22 22:40:38 +02:00
Arnaud Vergnet
2eb4a3e0c1 Update Proximo screens to use TypeScript 2020-09-22 22:35:24 +02:00
Arnaud Vergnet
9f4dcda7d9 Update proxiwash screens to use TypeScript 2020-09-22 22:29:39 +02:00
Arnaud Vergnet
b78357968a Update planning screens to use TypeScript 2020-09-22 22:18:05 +02:00
Arnaud Vergnet
742cb1802d Update Planex screens to use TypeScript 2020-09-22 22:04:39 +02:00
Arnaud Vergnet
4d0df7a5b7 Update misc screens to use TypeScript 2020-09-22 21:58:09 +02:00
Arnaud Vergnet
b8e7272d2c Update navigation components to use TypeScript 2020-09-22 21:49:39 +02:00
Arnaud Vergnet
300558ac56 Update image gallery screen to use TypeScript 2020-09-22 19:55:41 +02:00
Arnaud Vergnet
d70f22bdae Update remaining Amicale screens to use TypeScript 2020-09-22 19:50:31 +02:00
Arnaud Vergnet
67cb96dd03 Update equipment screens to use TypeScript 2020-09-22 19:36:48 +02:00
Arnaud Vergnet
5977ce257b Update clubs screens to use TypeScript 2020-09-22 19:08:44 +02:00
Arnaud Vergnet
f7e767748a Update about screens to use TypeScript 2020-09-22 18:58:56 +02:00
Arnaud Vergnet
172b7e8187 Update mascot components to use TypeScript 2020-09-22 18:25:19 +02:00
Arnaud Vergnet
e4530ded18 Update Home base components to use TypeScript 2020-09-22 18:06:08 +02:00
Arnaud Vergnet
140bcf3675 Update animated components to use TypeScript 2020-09-22 17:43:40 +02:00
Arnaud Vergnet
f43dc55735 Update basic screen components to use TypeScript 2020-09-22 17:23:17 +02:00
Arnaud Vergnet
8ac19f36de Update constants to use TypeScript 2020-09-22 15:19:54 +02:00
Arnaud Vergnet
5261e85254 Update custom tab bar to use TypeScript 2020-09-22 15:16:25 +02:00
Arnaud Vergnet
e4adcd0057 Update list components to use TypeScript 2020-09-22 15:06:38 +02:00
Arnaud Vergnet
acc4f8cdcc Update component overrides and intro slides to use TypeScript 2020-09-22 14:26:44 +02:00
Arnaud Vergnet
18f8c64302 Update image gallery button to use TypeScript 2020-09-22 11:52:17 +02:00
Arnaud Vergnet
98518c46b6 Update dialogs to use TypeScript 2020-09-22 11:49:31 +02:00
Arnaud Vergnet
f95635136e Update Amicale and related components to use TypeScript 2020-09-22 11:34:18 +02:00
Arnaud Vergnet
18ec6e0a59 Update utility files to use TypeScript 2020-09-21 22:40:58 +02:00
Arnaud Vergnet
375fc8b971 Update App.tsx and related files to use TypeScript 2020-09-21 21:44:07 +02:00
Arnaud Vergnet
54486d1deb Update project config files to use TypeScript 2020-09-21 21:14:42 +02:00
169 changed files with 5520 additions and 5422 deletions

View file

@ -1,46 +1,6 @@
module.exports = { module.exports = {
root: true, root: true,
extends: [ extends: '@react-native-community',
'airbnb', parser: '@typescript-eslint/parser',
'plugin:flowtype/recommended', plugins: ['@typescript-eslint'],
'prettier',
'prettier/flowtype',
'prettier/react',
],
parser: 'babel-eslint',
plugins: ['flowtype'],
env: {
jest: true,
},
rules: {
'react/jsx-filename-extension': [1, {extensions: ['.js', '.jsx']}],
'react/static-property-placement': [2, 'static public field'],
'flowtype/define-flow-type': 1,
'flowtype/no-mixed': 2,
'flowtype/no-primitive-constructor-types': 2,
'flowtype/no-types-missing-file-annotation': 2,
'flowtype/no-weak-types': 2,
'flowtype/require-parameter-type': 2,
'flowtype/require-readonly-react-props': 0,
'flowtype/require-return-type': [
2,
'always',
{
annotateUndefined: 'never',
},
],
'flowtype/require-valid-file-annotation': 2,
'flowtype/type-id-match': [2, '^([A-Z][a-z0-9]+)+Type$'],
'flowtype/use-flow-type': 1,
'flowtype/valid-syntax': 1,
},
settings: {
flowtype: {
onlyFilesWithFlowAnnotation: false,
},
},
globals: {
fetch: false,
Headers: false,
},
}; };

View file

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {LogBox, Platform, SafeAreaView, View} from 'react-native'; import {LogBox, Platform, SafeAreaView, View} from 'react-native';
import {NavigationContainer} from '@react-navigation/native'; import {NavigationContainer} from '@react-navigation/native';
@ -28,7 +26,6 @@ import SplashScreen from 'react-native-splash-screen';
import {OverflowMenuProvider} from 'react-navigation-header-buttons'; import {OverflowMenuProvider} from 'react-navigation-header-buttons';
import AsyncStorageManager from './src/managers/AsyncStorageManager'; import AsyncStorageManager from './src/managers/AsyncStorageManager';
import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider'; import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider';
import type {CustomThemeType} from './src/managers/ThemeManager';
import ThemeManager from './src/managers/ThemeManager'; import ThemeManager from './src/managers/ThemeManager';
import MainNavigator from './src/navigation/MainNavigator'; import MainNavigator from './src/navigation/MainNavigator';
import AprilFoolsManager from './src/managers/AprilFoolsManager'; import AprilFoolsManager from './src/managers/AprilFoolsManager';
@ -38,6 +35,7 @@ import type {ParsedUrlDataType} from './src/utils/URLHandler';
import URLHandler from './src/utils/URLHandler'; import URLHandler from './src/utils/URLHandler';
import {setupStatusBar} from './src/utils/Utils'; import {setupStatusBar} from './src/utils/Utils';
import initLocales from './src/utils/Locales'; import initLocales from './src/utils/Locales';
import {NavigationContainerRef} from '@react-navigation/core';
// Native optimizations https://reactnavigation.org/docs/react-native-screens // Native optimizations https://reactnavigation.org/docs/react-native-screens
// Crashes app when navigating away from webview on android 9+ // Crashes app when navigating away from webview on android 9+
@ -50,15 +48,15 @@ LogBox.ignoreLogs([
]); ]);
type StateType = { type StateType = {
isLoading: boolean, isLoading: boolean;
showIntro: boolean, showIntro: boolean;
showUpdate: boolean, showUpdate: boolean;
showAprilFools: boolean, showAprilFools: boolean;
currentTheme: CustomThemeType | null, currentTheme: ReactNativePaper.Theme | undefined;
}; };
export default class App extends React.Component<null, StateType> { export default class App extends React.Component<{}, StateType> {
navigatorRef: {current: null | NavigationContainer}; navigatorRef: {current: null | NavigationContainerRef};
defaultHomeRoute: string | null; defaultHomeRoute: string | null;
@ -66,14 +64,14 @@ export default class App extends React.Component<null, StateType> {
urlHandler: URLHandler; urlHandler: URLHandler;
constructor() { constructor(props: {}) {
super(); super(props);
this.state = { this.state = {
isLoading: true, isLoading: true,
showIntro: true, showIntro: true,
showUpdate: true, showUpdate: true,
showAprilFools: false, showAprilFools: false,
currentTheme: null, currentTheme: undefined,
}; };
initLocales(); initLocales();
this.navigatorRef = React.createRef(); this.navigatorRef = React.createRef();
@ -155,8 +153,11 @@ export default class App extends React.Component<null, StateType> {
// Only show intro if this is the first time starting the app // Only show intro if this is the first time starting the app
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme); ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
// Status bar goes dark if set too fast on ios // Status bar goes dark if set too fast on ios
if (Platform.OS === 'ios') setTimeout(setupStatusBar, 1000); if (Platform.OS === 'ios') {
else setupStatusBar(); setTimeout(setupStatusBar, 1000);
} else {
setupStatusBar();
}
this.setState({ this.setState({
isLoading: false, isLoading: false,
@ -192,7 +193,7 @@ export default class App extends React.Component<null, StateType> {
/** /**
* Renders the app based on loading state * Renders the app based on loading state
*/ */
render(): React.Node { render() {
const {state} = this; const {state} = this;
if (state.isLoading) { if (state.isLoading) {
return null; return null;

View file

@ -1,3 +1,8 @@
module.exports = { module.exports = {
presets: ['module:metro-react-native-babel-preset', '@babel/preset-flow'], presets: ['module:metro-react-native-babel-preset'],
env: {
production: {
plugins: ['react-native-paper/babel'],
},
},
}; };

View file

@ -9,13 +9,14 @@ Le strict minimum pour pouvoir comprendre le code de l'application. Il n'est pas
* [**Des cours d'anglais**](https://www.wikihow.com/Be-Good-at-English) : Toutes les ressources sont en anglais, le code est en anglais, tu trouveras presque rien en français, donc profite-en pour t'améliorer ! * [**Des cours d'anglais**](https://www.wikihow.com/Be-Good-at-English) : Toutes les ressources sont en anglais, le code est en anglais, tu trouveras presque rien en français, donc profite-en pour t'améliorer !
* [**Tutoriel Git**](https://learngitbranching.js.org/) : Le système utilisé pour synchroniser le code entre plusieurs ordinateurs. Tout le projet repose sur cette technologie, une compréhension minimale de son fonctionnement est nécessaire. Si tu ne sais pas ce que veut dire commit, pull, push, merge, ou branch, alors lis ce tuto ! * [**Tutoriel Git**](https://learngitbranching.js.org/) : Le système utilisé pour synchroniser le code entre plusieurs ordinateurs. Tout le projet repose sur cette technologie, une compréhension minimale de son fonctionnement est nécessaire. Si tu ne sais pas ce que veut dire commit, pull, push, merge, ou branch, alors lis ce tuto !
* [**Tutoriel JavaScript**](https://www.w3schools.com/js) : Un minimum de connaissances en JavaScript est nécessaire pour pouvoir comprendre le code. Pas besoin de lire tout le tutoriel. Pour les bases, tu peux t'arrêter à la partie `JS Dates` ou un peu avant. Il est utile de revenir souvent vers ce guide quand tu rencontres des difficultés. * [**Tutoriel JavaScript**](https://www.w3schools.com/js) : Un minimum de connaissances en JavaScript est nécessaire pour pouvoir comprendre le code. Pas besoin de lire tout le tutoriel. Pour les bases, tu peux t'arrêter à la partie `JS Dates` ou un peu avant. Il est utile de revenir souvent vers ce guide quand tu rencontres des difficultés.
* [**Tutoriel TypeScript**](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) : Un tuto rapide de cette surcouche à JavaScript, permettant de le rendre typé statique.
* [**Documentation React Native**](https://reactnative.dev/docs/getting-started) : La techno de base, qui utilise JavaScript. Lire au moins les articles de la catégorie `The Basics`, tout est interactif c'est plutôt simple et rapide à comprendre. * [**Documentation React Native**](https://reactnative.dev/docs/getting-started) : La techno de base, qui utilise JavaScript. Lire au moins les articles de la catégorie `The Basics`, tout est interactif c'est plutôt simple et rapide à comprendre.
## 🤔 Comprendre les librairies ## 🤔 Comprendre les librairies
Si tu as compris les bases et que tu veux te plonger un peu plus en profondeur dans le code, tu peux utiliser les liens ci-dessous pour accéder aux frameworks les plus importants. Si tu as compris les bases et que tu veux te plonger un peu plus en profondeur dans le code, tu peux utiliser les liens ci-dessous pour accéder aux frameworks les plus importants.
* [**Documentation Flow**](https://flow.org/en/docs/react/) : Un utilitaire pour rendre JavaScript typé statique (c'est-à-dire plus robuste pour de gros projets). Flow permet de rajouter des annotations pour donner un type aux variables. * [**TypeScript Handbook**](https://www.typescriptlang.org/docs/handbook/intro.html) : Un tuto TypeScript complet permettant de bien maitriser cette technologie.
* [**Documentation React Native Paper**](https://callstack.github.io/react-native-paper/) : Le framework utilisé pour créer l'interface utilisateur (UI). Paper met à disposition de nombreux composants respectant les normes Material Design. Comparé à d'autres frameworks, paper est léger et facile à utiliser. * [**Documentation React Native Paper**](https://callstack.github.io/react-native-paper/) : Le framework utilisé pour créer l'interface utilisateur (UI). Paper met à disposition de nombreux composants respectant les normes Material Design. Comparé à d'autres frameworks, paper est léger et facile à utiliser.
* [**Documentation React Navigation**](https://reactnavigation.org/docs/getting-started) : Le framework utilisé pour faciliter la navigation classique entre différents écrans. Permet de créer facilement une navigation par onglets/menu déroulant. * [**Documentation React Navigation**](https://reactnavigation.org/docs/getting-started) : Le framework utilisé pour faciliter la navigation classique entre différents écrans. Permet de créer facilement une navigation par onglets/menu déroulant.
* [**Liste des librairies**](../package.json) : Tu trouveras dans ce fichier la liste de toutes les librairies utilisées dans ce projet (catégorie `dependencies`). Pour accéder à leur documentation, fais une simple recherche de leur nom dans un moteur de recherche. * [**Liste des librairies**](../package.json) : Tu trouveras dans ce fichier la liste de toutes les librairies utilisées dans ce projet (catégorie `dependencies`). Pour accéder à leur documentation, fais une simple recherche de leur nom dans un moteur de recherche.

View file

@ -4,6 +4,16 @@ Ce fichier permet de regrouper les différentes informations sur des décisions
Ces notes pouvant évoluer dans le temps, leur date d'écriture est aussi indiquée. Ces notes pouvant évoluer dans le temps, leur date d'écriture est aussi indiquée.
## _2020-09-24_ | Flow
Flow est un système d'annotation permettant de rendre JavaScript typé statique. Développée par Facebook, cette technologie à initialement été adoptée. En revanche, de nombreux problèmes sont apparus :
* Système très complexe donnant de nombreuses erreurs inconnues, rendant la contribution complexe pour les non-initiés
* Manque de compatibilité avec les librairies existantes (la majorité utilisant TypeScript)
* Utilisation excessive du système lors du développement
* Plantage régulier du service Flow, nécessitant un redémarrage manuel
Ainsi, il a été décidé de migrer le projet vers Typescript.
## _2020-06-23_ | Expo ## _2020-06-23_ | Expo
Expo est une surcouche à react native permettant de simplifier le processus de build. Le projet à commencé en l'utilisant, mais de nombreux problèmes ont été rencontrés : Expo est une surcouche à react native permettant de simplifier le processus de build. Le projet à commencé en l'utilisant, mais de nombreux problèmes ont été rencontrés :

View file

@ -475,8 +475,8 @@
"loading": "Chargement...", "loading": "Chargement...",
"retry": "Réessayer", "retry": "Réessayer",
"networkError": "Impossible de contacter les serveurs. Assure-toi d'être connecté à Internet.", "networkError": "Impossible de contacter les serveurs. Assure-toi d'être connecté à Internet.",
"goBack": "Suivant", "goBack": "Précédent",
"goForward": "Précédent", "goForward": "Suivant",
"openInBrowser": "Ouvrir dans le navigateur", "openInBrowser": "Ouvrir dans le navigateur",
"notAvailable": "Non disponible", "notAvailable": "Non disponible",
"listUpdateFail": "Erreur lors de la mise à jour de la liste" "listUpdateFail": "Erreur lors de la mise à jour de la liste"

365
package-lock.json generated
View file

@ -810,16 +810,6 @@
"@babel/helper-plugin-utils": "^7.10.4" "@babel/helper-plugin-utils": "^7.10.4"
} }
}, },
"@babel/preset-flow": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.10.4.tgz",
"integrity": "sha512-XI6l1CptQCOBv+ZKYwynyswhtOKwpZZp5n0LG1QKCo8erRhqjoQV6nvx61Eg30JHpysWQSBwA2AWRU3pBbSY5g==",
"dev": true,
"requires": {
"@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-transform-flow-strip-types": "^7.10.4"
}
},
"@babel/register": { "@babel/register": {
"version": "7.10.5", "version": "7.10.5",
"resolved": "https://registry.npmjs.org/@babel/register/-/register-7.10.5.tgz", "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.10.5.tgz",
@ -2297,6 +2287,79 @@
"resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-4.10.1.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-4.10.1.tgz",
"integrity": "sha512-ael2f1onoPF3vF7YqHGWy7NnafzGu+yp88BbFbP0ydoCP2xGSUzmZVw0zakPTC040Id+JQ9WeFczujMkDy6jYQ==" "integrity": "sha512-ael2f1onoPF3vF7YqHGWy7NnafzGu+yp88BbFbP0ydoCP2xGSUzmZVw0zakPTC040Id+JQ9WeFczujMkDy6jYQ=="
}, },
"@react-native-community/eslint-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@react-native-community/eslint-config/-/eslint-config-1.1.0.tgz",
"integrity": "sha512-hwb1hC28BhkwLwnO6vDISV6XZbipw2RIEhBVBN+pE7AUG9HjFXxoksiiOSoYgox9C8g86VJwHnKpak/3NnVBkQ==",
"dev": true,
"requires": {
"@react-native-community/eslint-plugin": "^1.1.0",
"@typescript-eslint/eslint-plugin": "^2.25.0",
"@typescript-eslint/parser": "^2.25.0",
"babel-eslint": "10.1.0",
"eslint-config-prettier": "^6.10.1",
"eslint-plugin-eslint-comments": "^3.1.2",
"eslint-plugin-flowtype": "2.50.3",
"eslint-plugin-jest": "22.4.1",
"eslint-plugin-prettier": "3.1.2",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "^3.0.0",
"eslint-plugin-react-native": "3.8.1",
"prettier": "^2.0.2"
},
"dependencies": {
"doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"requires": {
"esutils": "^2.0.2"
}
},
"eslint-plugin-flowtype": {
"version": "2.50.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-2.50.3.tgz",
"integrity": "sha512-X+AoKVOr7Re0ko/yEXyM5SSZ0tazc6ffdIOocp2fFUlWoDt7DV0Bz99mngOkAFLOAWjqRA5jPwqUCbrx13XoxQ==",
"dev": true,
"requires": {
"lodash": "^4.17.10"
}
},
"eslint-plugin-react": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.19.0.tgz",
"integrity": "sha512-SPT8j72CGuAP+JFbT0sJHOB80TX/pu44gQ4vXH/cq+hQTiY2PuZ6IHkqXJV6x1b28GDdo1lbInjKUrrdUf0LOQ==",
"dev": true,
"requires": {
"array-includes": "^3.1.1",
"doctrine": "^2.1.0",
"has": "^1.0.3",
"jsx-ast-utils": "^2.2.3",
"object.entries": "^1.1.1",
"object.fromentries": "^2.0.2",
"object.values": "^1.1.1",
"prop-types": "^15.7.2",
"resolve": "^1.15.1",
"semver": "^6.3.0",
"string.prototype.matchall": "^4.0.2",
"xregexp": "^4.3.0"
}
},
"eslint-plugin-react-hooks": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-3.0.0.tgz",
"integrity": "sha512-EjxTHxjLKIBWFgDJdhKKzLh5q+vjTFrqNZX36uIxWS4OfyXe5DawqPj3U5qeJ1ngLwatjzQnmR0Lz0J0YH3kxw==",
"dev": true
}
}
},
"@react-native-community/eslint-plugin": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@react-native-community/eslint-plugin/-/eslint-plugin-1.1.0.tgz",
"integrity": "sha512-W/J0fNYVO01tioHjvYWQ9m6RgndVtbElzYozBq1ZPrHO/iCzlqoySHl4gO/fpCl9QEFjvJfjPgtPMTMlsoq5DQ==",
"dev": true
},
"@react-native-community/masked-view": { "@react-native-community/masked-view": {
"version": "0.1.10", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.10.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.10.tgz",
@ -2418,6 +2481,12 @@
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
}, },
"@types/eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
"dev": true
},
"@types/graceful-fs": { "@types/graceful-fs": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz",
@ -2432,6 +2501,12 @@
"resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz",
"integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==" "integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ=="
}, },
"@types/i18n-js": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/i18n-js/-/i18n-js-3.0.3.tgz",
"integrity": "sha512-GiZzazvxQ5j+EA4Zf4MtDsSaokAR/gW7FxxTlHi2p2xKFUhwAUT0B/MB8WL77P1TcqAO3MefWorFFyZS8F7s0Q==",
"dev": true
},
"@types/istanbul-lib-coverage": { "@types/istanbul-lib-coverage": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@ -2454,6 +2529,36 @@
"@types/istanbul-lib-report": "*" "@types/istanbul-lib-report": "*"
} }
}, },
"@types/jest": {
"version": "25.2.3",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.3.tgz",
"integrity": "sha512-JXc1nK/tXHiDhV55dvfzqtmP4S3sy3T3ouV2tkViZgxY/zeUkcpQcQPGRlgF4KmWzWW5oiWYSZwtCB+2RsE4Fw==",
"dev": true,
"requires": {
"jest-diff": "^25.2.1",
"pretty-format": "^25.2.1"
},
"dependencies": {
"pretty-format": {
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz",
"integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==",
"dev": true,
"requires": {
"@jest/types": "^25.5.0",
"ansi-regex": "^5.0.0",
"ansi-styles": "^4.0.0",
"react-is": "^16.12.0"
}
}
}
},
"@types/json-schema": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz",
"integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==",
"dev": true
},
"@types/json5": { "@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@ -2478,11 +2583,72 @@
"integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==",
"dev": true "dev": true
}, },
"@types/prop-types": {
"version": "15.7.3",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
"dev": true
},
"@types/react": {
"version": "16.9.49",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz",
"integrity": "sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==",
"dev": true,
"requires": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"@types/react-native": {
"version": "0.63.20",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.63.20.tgz",
"integrity": "sha512-APnxRTDxbWw/IYjvwvXkhYJiz1gahyVA579pJqAVsEfZ+ZUwUHZpWKnexobyH5NmRJHuA/8LrThyps/BW3SYXA==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-native-calendars": {
"version": "1.20.10",
"resolved": "https://registry.npmjs.org/@types/react-native-calendars/-/react-native-calendars-1.20.10.tgz",
"integrity": "sha512-bmWlkFa/6SNF98aM9rjKMGUOSDb15VBsfxBW5oo/iJ5tm5THf+eAGlxH72hGZFqJpr93plBs+ctkRVHQA7fx1w==",
"dev": true,
"requires": {
"@types/react": "*",
"@types/react-native": "*",
"@types/xdate": "*"
}
},
"@types/react-native-vector-icons": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.6.tgz",
"integrity": "sha512-lAyxNfMd5L1xZvXWsGcJmNegDf61TAp40uL6ashNNWj9W3IrDJO59L9+9inh0Y2MsEZpLTdxzVU8Unb4/0FQng==",
"dev": true,
"requires": {
"@types/react": "*",
"@types/react-native": "*"
}
},
"@types/react-test-renderer": {
"version": "16.9.3",
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-16.9.3.tgz",
"integrity": "sha512-wJ7IlN5NI82XMLOyHSa+cNN4Z0I+8/YaLl04uDgcZ+W+ExWCmCiVTLT/7fRNqzy4OhStZcUwIqLNF7q+AdW43Q==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/stack-utils": { "@types/stack-utils": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw=="
}, },
"@types/xdate": {
"version": "0.8.31",
"resolved": "https://registry.npmjs.org/@types/xdate/-/xdate-0.8.31.tgz",
"integrity": "sha512-iZYRKKK8UZXoepNh2kwK6TPITMj/dwdv0NzNi9DFMt2foGkU7h+ncaCpGsdD2fp/CXMs9dxPAzV9uddFy7c4QA==",
"dev": true
},
"@types/yargs": { "@types/yargs": {
"version": "15.0.5", "version": "15.0.5",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz",
@ -2496,6 +2662,80 @@
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz",
"integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw=="
}, },
"@typescript-eslint/eslint-plugin": {
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz",
"integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "2.34.0",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
"tsutils": "^3.17.1"
}
},
"@typescript-eslint/experimental-utils": {
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz",
"integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.34.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
}
},
"@typescript-eslint/parser": {
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz",
"integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==",
"dev": true,
"requires": {
"@types/eslint-visitor-keys": "^1.0.0",
"@typescript-eslint/experimental-utils": "2.34.0",
"@typescript-eslint/typescript-estree": "2.34.0",
"eslint-visitor-keys": "^1.1.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz",
"integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"eslint-visitor-keys": "^1.1.0",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
"dependencies": {
"debug": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"semver": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
},
"abab": { "abab": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz",
@ -3554,6 +3794,12 @@
} }
} }
}, },
"csstype": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.3.tgz",
"integrity": "sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag==",
"dev": true
},
"damerau-levenshtein": { "damerau-levenshtein": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
@ -4242,6 +4488,30 @@
} }
} }
}, },
"eslint-plugin-eslint-comments": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz",
"integrity": "sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==",
"dev": true,
"requires": {
"escape-string-regexp": "^1.0.5",
"ignore": "^5.0.5"
},
"dependencies": {
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"ignore": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz",
"integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==",
"dev": true
}
}
},
"eslint-plugin-flowtype": { "eslint-plugin-flowtype": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.2.0.tgz",
@ -4285,6 +4555,12 @@
} }
} }
}, },
"eslint-plugin-jest": {
"version": "22.4.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-22.4.1.tgz",
"integrity": "sha512-gcLfn6P2PrFAVx3AobaOzlIEevpAEf9chTpFZz7bYfc7pz8XRv7vuKTIE4hxPKZSha6XWKKplDQ0x9Pq8xX2mg==",
"dev": true
},
"eslint-plugin-jsx-a11y": { "eslint-plugin-jsx-a11y": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz",
@ -4312,6 +4588,15 @@
} }
} }
}, },
"eslint-plugin-prettier": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz",
"integrity": "sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==",
"dev": true,
"requires": {
"prettier-linter-helpers": "^1.0.0"
}
},
"eslint-plugin-react": { "eslint-plugin-react": {
"version": "7.20.6", "version": "7.20.6",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz",
@ -4348,6 +4633,21 @@
"integrity": "sha512-6SSb5AiMCPd8FDJrzah+Z4F44P2CdOaK026cXFV+o/xSRzfOiV1FNFeLl2z6xm3yqWOQEZ5OfVgiec90qV2xrQ==", "integrity": "sha512-6SSb5AiMCPd8FDJrzah+Z4F44P2CdOaK026cXFV+o/xSRzfOiV1FNFeLl2z6xm3yqWOQEZ5OfVgiec90qV2xrQ==",
"dev": true "dev": true
}, },
"eslint-plugin-react-native": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-native/-/eslint-plugin-react-native-3.8.1.tgz",
"integrity": "sha512-6Z4s4nvgFRdda/1s1+uu4a6EMZwEjjJ9Bk/1yBImv0fd9U2CsGu2cUakAtV83cZKhizbWhSouXoaK4JtlScdFg==",
"dev": true,
"requires": {
"eslint-plugin-react-native-globals": "^0.1.1"
}
},
"eslint-plugin-react-native-globals": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz",
"integrity": "sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==",
"dev": true
},
"eslint-scope": { "eslint-scope": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
@ -4708,6 +5008,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true "dev": true
}, },
"fast-diff": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
"dev": true
},
"fast-json-stable-stringify": { "fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -4941,12 +5247,6 @@
"integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==",
"dev": true "dev": true
}, },
"flow-bin": {
"version": "0.123.0",
"resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.123.0.tgz",
"integrity": "sha512-Ylcf8YDIM/KrqtxkPuq+f8O+6sdYA2Nuz5f+sWHlp539DatZz3YMcsO1EiXaf1C11HJgpT/3YGYe7xZ9/UZmvQ==",
"dev": true
},
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -10032,6 +10332,15 @@
"integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==",
"dev": true "dev": true
}, },
"prettier-linter-helpers": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
"dev": true,
"requires": {
"fast-diff": "^1.1.2"
}
},
"pretty-format": { "pretty-format": {
"version": "24.9.0", "version": "24.9.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz",
@ -11907,6 +12216,15 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==" "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
}, },
"tsutils": {
"version": "3.17.1",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
"integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
},
"tunnel-agent": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -11956,6 +12274,12 @@
"is-typedarray": "^1.0.0" "is-typedarray": "^1.0.0"
} }
}, },
"typescript": {
"version": "3.9.7",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
"integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
"dev": true
},
"ua-parser-js": { "ua-parser-js": {
"version": "0.7.21", "version": "0.7.21",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz",
@ -12375,6 +12699,15 @@
"resolved": "https://registry.npmjs.org/xpipe/-/xpipe-1.0.5.tgz", "resolved": "https://registry.npmjs.org/xpipe/-/xpipe-1.0.5.tgz",
"integrity": "sha1-jdi/Rfw/f1Xw4FS4ePQ6YmFNr98=" "integrity": "sha1-jdi/Rfw/f1Xw4FS4ePQ6YmFNr98="
}, },
"xregexp": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz",
"integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==",
"dev": true,
"requires": {
"@babel/runtime-corejs3": "^7.8.3"
}
},
"xtend": { "xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View file

@ -8,12 +8,17 @@
"android-release": "react-native run-android --variant=release", "android-release": "react-native run-android --variant=release",
"ios": "react-native run-ios", "ios": "react-native run-ios",
"test": "jest", "test": "jest",
"lint": "eslint ." "lint": "eslint . --ext .js,.jsx,.ts,.tsx"
}, },
"jest": { "jest": {
"preset": "react-native", "preset": "react-native",
"transformIgnorePatterns": [ "moduleFileExtensions": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base)" "ts",
"tsx",
"js",
"jsx",
"json",
"node"
], ],
"setupFilesAfterEnv": [ "setupFilesAfterEnv": [
"jest-extended" "jest-extended"
@ -59,9 +64,16 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.11.0", "@babel/core": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@babel/runtime": "^7.11.0", "@babel/runtime": "^7.11.0",
"babel-eslint": "^10.1.0", "@react-native-community/eslint-config": "^1.1.0",
"@types/i18n-js": "^3.0.3",
"@types/jest": "^25.2.3",
"@types/react-native": "^0.63.2",
"@types/react-native-calendars": "^1.20.10",
"@types/react-native-vector-icons": "^6.4.6",
"@types/react-test-renderer": "^16.9.2",
"@typescript-eslint/eslint-plugin": "^2.27.0",
"@typescript-eslint/parser": "^2.27.0",
"babel-jest": "^25.1.0", "babel-jest": "^25.1.0",
"eslint": "^7.2.0", "eslint": "^7.2.0",
"eslint-config-airbnb": "^18.2.0", "eslint-config-airbnb": "^18.2.0",
@ -71,11 +83,11 @@
"eslint-plugin-jsx-a11y": "^6.3.1", "eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.20.5", "eslint-plugin-react": "^7.20.5",
"eslint-plugin-react-hooks": "^4.0.0", "eslint-plugin-react-hooks": "^4.0.0",
"flow-bin": "^0.123.0",
"jest": "^25.1.0", "jest": "^25.1.0",
"jest-extended": "^0.11.5", "jest-extended": "^0.11.5",
"metro-react-native-babel-preset": "^0.59.0", "metro-react-native-babel-preset": "^0.59.0",
"prettier": "2.0.5", "prettier": "2.0.5",
"react-test-renderer": "16.13.1" "react-test-renderer": "16.13.1",
"typescript": "^3.8.3"
} }
} }

View file

@ -17,37 +17,34 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import ConnectionManager from '../../managers/ConnectionManager'; import ConnectionManager from '../../managers/ConnectionManager';
import type {ApiGenericDataType} from '../../utils/WebData';
import {ERROR_TYPE} from '../../utils/WebData'; import {ERROR_TYPE} from '../../utils/WebData';
import ErrorView from '../Screens/ErrorView'; import ErrorView from '../Screens/ErrorView';
import BasicLoadingScreen from '../Screens/BasicLoadingScreen'; import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
type PropsType = { type PropsType<T> = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
requests: Array<{ requests: Array<{
link: string, link: string;
params: {...}, params: object;
mandatory: boolean, mandatory: boolean;
}>, }>;
renderFunction: (Array<ApiGenericDataType | null>) => React.Node, renderFunction: (data: Array<T | null>) => React.ReactNode;
errorViewOverride?: Array<{ errorViewOverride?: Array<{
errorCode: number, errorCode: number;
message: string, message: string;
icon: string, icon: string;
showRetryButton: boolean, showRetryButton: boolean;
}> | null, }> | null;
}; };
type StateType = { type StateType = {
loading: boolean, loading: boolean;
}; };
class AuthenticatedScreen extends React.Component<PropsType, StateType> { class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
static defaultProps = { static defaultProps = {
errorViewOverride: null, errorViewOverride: null,
}; };
@ -58,13 +55,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
errors: Array<number>; errors: Array<number>;
fetchedData: Array<ApiGenericDataType | null>; fetchedData: Array<T | null>;
constructor(props: PropsType) { constructor(props: PropsType<T>) {
super(props); super(props);
this.state = { this.state = {
loading: true, loading: true,
}; };
this.currentUserToken = null;
this.connectionManager = ConnectionManager.getInstance(); this.connectionManager = ConnectionManager.getInstance();
props.navigation.addListener('focus', this.onScreenFocus); props.navigation.addListener('focus', this.onScreenFocus);
this.fetchedData = new Array(props.requests.length); this.fetchedData = new Array(props.requests.length);
@ -91,20 +89,20 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
* @param index The index for the data * @param index The index for the data
* @param error The error code received * @param error The error code received
*/ */
onRequestFinished( onRequestFinished(data: T | null, index: number, error?: number) {
data: ApiGenericDataType | null,
index: number,
error?: number,
) {
const {props} = this; const {props} = this;
if (index >= 0 && index < props.requests.length) { if (index >= 0 && index < props.requests.length) {
this.fetchedData[index] = data; this.fetchedData[index] = data;
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS; this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
} }
// Token expired, logout user // Token expired, logout user
if (error === ERROR_TYPE.BAD_TOKEN) this.connectionManager.disconnect(); if (error === ERROR_TYPE.BAD_TOKEN) {
this.connectionManager.disconnect();
}
if (this.allRequestsFinished()) this.setState({loading: false}); if (this.allRequestsFinished()) {
this.setState({loading: false});
}
} }
/** /**
@ -132,7 +130,7 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getErrorRender(): React.Node { getErrorRender() {
const {props} = this; const {props} = this;
const errorCode = this.getError(); const errorCode = this.getError();
let shouldOverride = false; let shouldOverride = false;
@ -169,18 +167,18 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
*/ */
fetchData = () => { fetchData = () => {
const {state, props} = this; const {state, props} = this;
if (!state.loading) this.setState({loading: true}); if (!state.loading) {
this.setState({loading: true});
}
if (this.connectionManager.isLoggedIn()) { if (this.connectionManager.isLoggedIn()) {
for (let i = 0; i < props.requests.length; i += 1) { for (let i = 0; i < props.requests.length; i += 1) {
this.connectionManager this.connectionManager
.authenticatedRequest( .authenticatedRequest<T>(
props.requests[i].link, props.requests[i].link,
props.requests[i].params, props.requests[i].params,
) )
.then((response: ApiGenericDataType): void => .then((response: T): void => this.onRequestFinished(response, i))
this.onRequestFinished(response, i),
)
.catch((error: number): void => .catch((error: number): void =>
this.onRequestFinished(null, i, error), this.onRequestFinished(null, i, error),
); );
@ -200,7 +198,9 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
allRequestsFinished(): boolean { allRequestsFinished(): boolean {
let finished = true; let finished = true;
this.errors.forEach((error: number | null) => { this.errors.forEach((error: number | null) => {
if (error == null) finished = false; if (error == null) {
finished = false;
}
}); });
return finished; return finished;
} }
@ -212,11 +212,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
this.fetchData(); this.fetchData();
} }
render(): React.Node { render() {
const {state, props} = this; const {state, props} = this;
if (state.loading) return <BasicLoadingScreen />; if (state.loading) {
if (this.getError() === ERROR_TYPE.SUCCESS) return <BasicLoadingScreen />;
}
if (this.getError() === ERROR_TYPE.SUCCESS) {
return props.renderFunction(this.fetchedData); return props.renderFunction(this.fetchedData);
}
return this.getErrorRender(); return this.getErrorRender();
} }
} }

View file

@ -17,28 +17,25 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
import ConnectionManager from '../../managers/ConnectionManager'; import ConnectionManager from '../../managers/ConnectionManager';
import {useNavigation} from '@react-navigation/native';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, visible: boolean;
visible: boolean, onDismiss: () => void;
onDismiss: () => void,
}; };
class LogoutDialog extends React.PureComponent<PropsType> { function LogoutDialog(props: PropsType) {
onClickAccept = async (): Promise<void> => { const navigation = useNavigation();
const {props} = this; const onClickAccept = async (): Promise<void> => {
return new Promise((resolve: () => void) => { return new Promise((resolve: () => void) => {
ConnectionManager.getInstance() ConnectionManager.getInstance()
.disconnect() .disconnect()
.then(() => { .then(() => {
props.navigation.reset({ navigation.reset({
index: 0, index: 0,
routes: [{name: 'main'}], routes: [{name: 'main'}],
}); });
@ -48,19 +45,16 @@ class LogoutDialog extends React.PureComponent<PropsType> {
}); });
}; };
render(): React.Node {
const {props} = this;
return ( return (
<LoadingConfirmDialog <LoadingConfirmDialog
visible={props.visible} visible={props.visible}
onDismiss={props.onDismiss} onDismiss={props.onDismiss}
onAccept={this.onClickAccept} onAccept={onClickAccept}
title={i18n.t('dialog.disconnect.title')} title={i18n.t('dialog.disconnect.title')}
titleLoading={i18n.t('dialog.disconnect.titleLoading')} titleLoading={i18n.t('dialog.disconnect.titleLoading')}
message={i18n.t('dialog.disconnect.message')} message={i18n.t('dialog.disconnect.message')}
/> />
); );
}
} }
export default LogoutDialog; export default LogoutDialog;

View file

@ -1,58 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {Headline, withTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
theme: CustomThemeType,
};
class VoteNotAvailable extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return (
<View
style={{
width: '100%',
marginTop: 10,
marginBottom: 10,
}}>
<Headline
style={{
color: props.theme.colors.textDisabled,
textAlign: 'center',
}}>
{i18n.t('screens.vote.noVote')}
</Headline>
</View>
);
}
}
export default withTheme(VoteNotAvailable);

View file

@ -0,0 +1,45 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import React from 'react';
import {View} from 'react-native';
import {Headline, useTheme} from 'react-native-paper';
import i18n from 'i18n-js';
function VoteNotAvailable() {
const theme = useTheme();
return (
<View
style={{
width: '100%',
marginTop: 10,
marginBottom: 10,
}}>
<Headline
style={{
color: theme.colors.textDisabled,
textAlign: 'center',
}}>
{i18n.t('screens.vote.noVote')}
</Headline>
</View>
);
}
export default VoteNotAvailable;

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import { import {
Avatar, Avatar,
@ -31,16 +29,11 @@ import {
import {FlatList, StyleSheet} from 'react-native'; import {FlatList, StyleSheet} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen'; import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {
CardTitleIconPropsType,
ListIconPropsType,
} from '../../../constants/PaperStyles';
type PropsType = { type PropsType = {
teams: Array<VoteTeamType>, teams: Array<VoteTeamType>;
dateEnd: string, dateEnd: string;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -58,10 +51,10 @@ class VoteResults extends React.Component<PropsType> {
winnerIds: Array<number>; winnerIds: Array<number>;
constructor(props: PropsType) { constructor(props: PropsType) {
super(); super(props);
props.teams.sort(this.sortByVotes); props.teams.sort(this.sortByVotes);
this.getTotalVotes(props.teams); this.totalVotes = this.getTotalVotes(props.teams);
this.getWinnerIds(props.teams); this.winnerIds = this.getWinnerIds(props.teams);
} }
shouldComponentUpdate(): boolean { shouldComponentUpdate(): boolean {
@ -69,26 +62,31 @@ class VoteResults extends React.Component<PropsType> {
} }
getTotalVotes(teams: Array<VoteTeamType>) { getTotalVotes(teams: Array<VoteTeamType>) {
this.totalVotes = 0; let totalVotes = 0;
for (let i = 0; i < teams.length; i += 1) { for (let i = 0; i < teams.length; i += 1) {
this.totalVotes += teams[i].votes; totalVotes += teams[i].votes;
} }
return totalVotes;
} }
getWinnerIds(teams: Array<VoteTeamType>) { getWinnerIds(teams: Array<VoteTeamType>) {
const max = teams[0].votes; const max = teams[0].votes;
this.winnerIds = []; let winnerIds = [];
for (let i = 0; i < teams.length; i += 1) { for (let i = 0; i < teams.length; i += 1) {
if (teams[i].votes === max) this.winnerIds.push(teams[i].id); if (teams[i].votes === max) {
else break; winnerIds.push(teams[i].id);
} else {
break;
} }
} }
return winnerIds;
}
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes; sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString(); voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
resultRenderItem = ({item}: {item: VoteTeamType}): React.Node => { resultRenderItem = ({item}: {item: VoteTeamType}) => {
const isWinner = this.winnerIds.indexOf(item.id) !== -1; const isWinner = this.winnerIds.indexOf(item.id) !== -1;
const isDraw = this.winnerIds.length > 1; const isDraw = this.winnerIds.length > 1;
const {props} = this; const {props} = this;
@ -101,7 +99,7 @@ class VoteResults extends React.Component<PropsType> {
<List.Item <List.Item
title={item.name} title={item.name}
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`} description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
left={(iconProps: ListIconPropsType): React.Node => left={(iconProps) =>
isWinner ? ( isWinner ? (
<List.Icon <List.Icon
style={iconProps.style} style={iconProps.style}
@ -125,7 +123,7 @@ class VoteResults extends React.Component<PropsType> {
); );
}; };
render(): React.Node { render() {
const {props} = this; const {props} = this;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
@ -134,15 +132,14 @@ class VoteResults extends React.Component<PropsType> {
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${ subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
props.dateEnd props.dateEnd
}`} }`}
left={(iconProps: CardTitleIconPropsType): React.Node => ( left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="podium-gold" /> <Avatar.Icon size={iconProps.size} icon="podium-gold" />
)} )}
/> />
<Card.Content> <Card.Content>
<Subheading>{`${i18n.t('screens.vote.results.totalVotes')} ${ <Subheading>
this.totalVotes {`${i18n.t('screens.vote.results.totalVotes')} ${this.totalVotes}`}
}`}</Subheading> </Subheading>
{/* $FlowFixMe */}
<FlatList <FlatList
data={props.teams} data={props.teams}
keyExtractor={this.voteKeyExtractor} keyExtractor={this.voteKeyExtractor}

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Button, Card, RadioButton} from 'react-native-paper'; import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
import {FlatList, StyleSheet, View} from 'react-native'; import {FlatList, StyleSheet, View} from 'react-native';
@ -27,19 +25,18 @@ import ConnectionManager from '../../../managers/ConnectionManager';
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../Dialogs/ErrorDialog'; import ErrorDialog from '../../Dialogs/ErrorDialog';
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen'; import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = { type PropsType = {
teams: Array<VoteTeamType>, teams: Array<VoteTeamType>;
onVoteSuccess: () => void, onVoteSuccess: () => void;
onVoteError: () => void, onVoteError: () => void;
}; };
type StateType = { type StateType = {
selectedTeam: string, selectedTeam: string;
voteDialogVisible: boolean, voteDialogVisible: boolean;
errorDialogVisible: boolean, errorDialogVisible: boolean;
currentError: number, currentError: number;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -53,10 +50,10 @@ const styles = StyleSheet.create({
export default class VoteSelect extends React.PureComponent< export default class VoteSelect extends React.PureComponent<
PropsType, PropsType,
StateType, StateType
> { > {
constructor() { constructor(props: PropsType) {
super(); super(props);
this.state = { this.state = {
selectedTeam: 'none', selectedTeam: 'none',
voteDialogVisible: false, voteDialogVisible: false,
@ -70,7 +67,7 @@ export default class VoteSelect extends React.PureComponent<
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString(); voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
voteRenderItem = ({item}: {item: VoteTeamType}): React.Node => ( voteRenderItem = ({item}: {item: VoteTeamType}) => (
<RadioButton.Item label={item.name} value={item.id.toString()} /> <RadioButton.Item label={item.name} value={item.id.toString()} />
); );
@ -111,7 +108,7 @@ export default class VoteSelect extends React.PureComponent<
props.onVoteError(); props.onVoteError();
}; };
render(): React.Node { render() {
const {state, props} = this; const {state, props} = this;
return ( return (
<View> <View>
@ -119,7 +116,7 @@ export default class VoteSelect extends React.PureComponent<
<Card.Title <Card.Title
title={i18n.t('screens.vote.select.title')} title={i18n.t('screens.vote.select.title')}
subtitle={i18n.t('screens.vote.select.subtitle')} subtitle={i18n.t('screens.vote.select.subtitle')}
left={(iconProps: CardTitleIconPropsType): React.Node => ( left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="alert-decagram" /> <Avatar.Icon size={iconProps.size} icon="alert-decagram" />
)} )}
/> />
@ -127,7 +124,6 @@ export default class VoteSelect extends React.PureComponent<
<RadioButton.Group <RadioButton.Group
onValueChange={this.onVoteSelectionChange} onValueChange={this.onVoteSelectionChange}
value={state.selectedTeam}> value={state.selectedTeam}>
{/* $FlowFixMe */}
<FlatList <FlatList
data={props.teams} data={props.teams}
keyExtractor={this.voteKeyExtractor} keyExtractor={this.voteKeyExtractor}

View file

@ -17,16 +17,13 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Card, Paragraph} from 'react-native-paper'; import {Avatar, Card, Paragraph} from 'react-native-paper';
import {StyleSheet} from 'react-native'; import {StyleSheet} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = { type PropsType = {
startDate: string, startDate: string;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -38,21 +35,13 @@ const styles = StyleSheet.create({
}, },
}); });
export default class VoteTease extends React.Component<PropsType> { export default function VoteTease(props: PropsType) {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
title={i18n.t('screens.vote.tease.title')} title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')} subtitle={i18n.t('screens.vote.tease.subtitle')}
left={(iconProps: CardTitleIconPropsType): React.Node => ( left={(iconProps) => <Avatar.Icon size={iconProps.size} icon="vote" />}
<Avatar.Icon size={iconProps.size} icon="vote" />
)}
/> />
<Card.Content> <Card.Content>
<Paragraph> <Paragraph>
@ -61,5 +50,4 @@ export default class VoteTease extends React.Component<PropsType> {
</Card.Content> </Card.Content>
</Card> </Card>
); );
}
} }

View file

@ -1,93 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, Card, Paragraph, withTheme} from 'react-native-paper';
import {StyleSheet} from 'react-native';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
startDate: string | null,
justVoted: boolean,
hasVoted: boolean,
isVoteRunning: boolean,
theme: CustomThemeType,
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
});
class VoteWait extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
const {startDate} = props;
return (
<Card style={styles.card}>
<Card.Title
title={
props.isVoteRunning
? i18n.t('screens.vote.wait.titleSubmitted')
: i18n.t('screens.vote.wait.titleEnded')
}
subtitle={i18n.t('screens.vote.wait.subtitle')}
left={(iconProps: CardTitleIconPropsType): React.Node => (
<Avatar.Icon size={iconProps.size} icon="progress-check" />
)}
/>
<Card.Content>
{props.justVoted ? (
<Paragraph style={{color: props.theme.colors.success}}>
{i18n.t('screens.vote.wait.messageSubmitted')}
</Paragraph>
) : null}
{props.hasVoted ? (
<Paragraph style={{color: props.theme.colors.success}}>
{i18n.t('screens.vote.wait.messageVoted')}
</Paragraph>
) : null}
{startDate != null ? (
<Paragraph>
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
</Paragraph>
) : (
<Paragraph>
{i18n.t('screens.vote.wait.messageDateUndefined')}
</Paragraph>
)}
</Card.Content>
</Card>
);
}
}
export default withTheme(VoteWait);

View file

@ -0,0 +1,80 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Avatar, Card, Paragraph, useTheme} from 'react-native-paper';
import {StyleSheet} from 'react-native';
import i18n from 'i18n-js';
type PropsType = {
startDate: string | null;
justVoted: boolean;
hasVoted: boolean;
isVoteRunning: boolean;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
});
export default function VoteWait(props: PropsType) {
const theme = useTheme();
const {startDate} = props;
return (
<Card style={styles.card}>
<Card.Title
title={
props.isVoteRunning
? i18n.t('screens.vote.wait.titleSubmitted')
: i18n.t('screens.vote.wait.titleEnded')
}
subtitle={i18n.t('screens.vote.wait.subtitle')}
left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="progress-check" />
)}
/>
<Card.Content>
{props.justVoted ? (
<Paragraph style={{color: theme.colors.success}}>
{i18n.t('screens.vote.wait.messageSubmitted')}
</Paragraph>
) : null}
{props.hasVoted ? (
<Paragraph style={{color: theme.colors.success}}>
{i18n.t('screens.vote.wait.messageVoted')}
</Paragraph>
) : null}
{startDate != null ? (
<Paragraph>
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
</Paragraph>
) : (
<Paragraph>
{i18n.t('screens.vote.wait.messageDateUndefined')}
</Paragraph>
)}
</Card.Content>
</Card>
);
}

View file

@ -17,42 +17,37 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View, ViewStyle} from 'react-native';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import Collapsible from 'react-native-collapsible'; import Collapsible from 'react-native-collapsible';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {ListIconPropsType} from '../../constants/PaperStyles';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: ReactNativePaper.Theme;
title: string, title: string;
subtitle?: string, subtitle?: string;
left?: () => React.Node, style?: ViewStyle;
opened?: boolean, left?: (props: {
unmountWhenCollapsed?: boolean, color: string;
children?: React.Node, style?: {
marginRight: number;
marginVertical?: number;
};
}) => React.ReactNode;
opened?: boolean;
unmountWhenCollapsed?: boolean;
children?: React.ReactNode;
}; };
type StateType = { type StateType = {
expanded: boolean, expanded: boolean;
}; };
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon); const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.Component<PropsType, StateType> { class AnimatedAccordion extends React.Component<PropsType, StateType> {
static defaultProps = { chevronRef: {current: null | (typeof AnimatedListIcon & List.Icon)};
subtitle: '',
left: null,
opened: null,
unmountWhenCollapsed: false,
children: null,
};
chevronRef: {current: null | AnimatedListIcon};
chevronIcon: string; chevronIcon: string;
@ -62,6 +57,9 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.chevronIcon = '';
this.animStart = '';
this.animEnd = '';
this.state = { this.state = {
expanded: props.opened != null ? props.opened : false, expanded: props.opened != null ? props.opened : false,
}; };
@ -71,8 +69,9 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
shouldComponentUpdate(nextProps: PropsType): boolean { shouldComponentUpdate(nextProps: PropsType): boolean {
const {state, props} = this; const {state, props} = this;
if (nextProps.opened != null && nextProps.opened !== props.opened) if (nextProps.opened != null && nextProps.opened !== props.opened) {
state.expanded = nextProps.opened; state.expanded = nextProps.opened;
}
return true; return true;
} }
@ -101,17 +100,17 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
} }
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
const {colors} = props.theme; const {colors} = props.theme;
return ( return (
<View> <View style={props.style}>
<List.Item <List.Item
title={props.title} title={props.title}
subtitle={props.subtitle} description={props.subtitle}
titleStyle={state.expanded ? {color: colors.primary} : null} titleStyle={state.expanded ? {color: colors.primary} : null}
onPress={this.toggleAccordion} onPress={this.toggleAccordion}
right={(iconProps: ListIconPropsType): React.Node => ( right={(iconProps) => (
<AnimatedListIcon <AnimatedListIcon
ref={this.chevronRef} ref={this.chevronRef}
style={iconProps.style} style={iconProps.style}

View file

@ -17,29 +17,30 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from 'react-native'; import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
View,
} from 'react-native';
import {FAB, IconButton, Surface, withTheme} from 'react-native-paper'; import {FAB, IconButton, Surface, withTheme} from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import AutoHideHandler from '../../utils/AutoHideHandler'; import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {OnScrollType} from '../../utils/AutoHideHandler';
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
onPress: (action: string, data?: string) => void, onPress: (action: string, data?: string) => void;
seekAttention: boolean, seekAttention: boolean;
}; };
type StateType = { type StateType = {
currentMode: string, currentMode: string;
}; };
const DISPLAY_MODES = { const DISPLAY_MODES = {
@ -78,14 +79,14 @@ const styles = StyleSheet.create({
}); });
class AnimatedBottomBar extends React.Component<PropsType, StateType> { class AnimatedBottomBar extends React.Component<PropsType, StateType> {
ref: {current: null | Animatable.View}; ref: {current: null | (Animatable.View & View)};
hideHandler: AutoHideHandler; hideHandler: AutoHideHandler;
displayModeIcons: {[key: string]: string}; displayModeIcons: {[key: string]: string};
constructor() { constructor(props: PropsType) {
super(); super(props);
this.state = { this.state = {
currentMode: DISPLAY_MODES.WEEK, currentMode: DISPLAY_MODES.WEEK,
}; };
@ -108,13 +109,17 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
} }
onHideChange = (shouldHide: boolean) => { onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) { const ref = this.ref;
if (shouldHide) this.ref.current.fadeOutDown(500); if (ref && ref.current && ref.current.fadeOutDown && ref.current.fadeInUp) {
else this.ref.current.fadeInUp(500); if (shouldHide) {
ref.current.fadeOutDown(500);
} else {
ref.current.fadeInUp(500);
}
} }
}; };
onScroll = (event: OnScrollType) => { onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
this.hideHandler.onScroll(event); this.hideHandler.onScroll(event);
}; };
@ -139,7 +144,7 @@ class AnimatedBottomBar extends React.Component<PropsType, StateType> {
props.onPress('changeView', newMode); props.onPress('changeView', newMode);
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
const buttonColor = props.theme.colors.primary; const buttonColor = props.theme.colors.primary;
return ( return (

View file

@ -17,22 +17,23 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet} from 'react-native'; import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
View,
} from 'react-native';
import {FAB} from 'react-native-paper'; import {FAB} from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import AutoHideHandler from '../../utils/AutoHideHandler'; import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
type PropsType = { type PropsType = {
icon: string, icon: string;
onPress: () => void, onPress: () => void;
}; };
const AnimatedFab = Animatable.createAnimatableComponent(FAB);
const styles = StyleSheet.create({ const styles = StyleSheet.create({
fab: { fab: {
position: 'absolute', position: 'absolute',
@ -42,41 +43,49 @@ const styles = StyleSheet.create({
}); });
export default class AnimatedFAB extends React.Component<PropsType> { export default class AnimatedFAB extends React.Component<PropsType> {
ref: {current: null | Animatable.View}; ref: {current: null | (Animatable.View & View)};
hideHandler: AutoHideHandler; hideHandler: AutoHideHandler;
constructor() { constructor(props: PropsType) {
super(); super(props);
this.ref = React.createRef(); this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false); this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange); this.hideHandler.addListener(this.onHideChange);
} }
onScroll = (event: SyntheticEvent<EventTarget>) => { onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
this.hideHandler.onScroll(event); this.hideHandler.onScroll(event);
}; };
onHideChange = (shouldHide: boolean) => { onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) { const ref = this.ref;
if (shouldHide) this.ref.current.bounceOutDown(1000); if (
else this.ref.current.bounceInUp(1000); ref &&
ref.current &&
ref.current.bounceOutDown &&
ref.current.bounceInUp
) {
if (shouldHide) {
ref.current.bounceOutDown(1000);
} else {
ref.current.bounceInUp(1000);
}
} }
}; };
render(): React.Node { render() {
const {props} = this; const {props} = this;
return ( return (
<AnimatedFab <Animatable.View
ref={this.ref} ref={this.ref}
useNativeDriver useNativeDriver={true}
icon={props.icon}
onPress={props.onPress}
style={{ style={{
...styles.fab, ...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT, bottom: CustomTabBar.TAB_BAR_HEIGHT,
}} }}>
/> <FAB icon={props.icon} onPress={props.onPress} />
</Animatable.View>
); );
} }
} }

View file

@ -1,78 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Collapsible} from 'react-navigation-collapsible';
import withCollapsible from '../../utils/withCollapsible';
import CustomTabBar from '../Tabbar/CustomTabBar';
export type CollapsibleComponentPropsType = {
children?: React.Node,
hasTab?: boolean,
onScroll?: (event: SyntheticEvent<EventTarget>) => void,
};
type PropsType = {
...CollapsibleComponentPropsType,
collapsibleStack: Collapsible,
// eslint-disable-next-line flowtype/no-weak-types
component: any,
};
class CollapsibleComponent extends React.Component<PropsType> {
static defaultProps = {
children: null,
hasTab: false,
onScroll: null,
};
onScroll = (event: SyntheticEvent<EventTarget>) => {
const {props} = this;
if (props.onScroll) props.onScroll(event);
};
render(): React.Node {
const {props} = this;
const Comp = props.component;
const {
containerPaddingTop,
scrollIndicatorInsetTop,
onScrollWithListener,
} = props.collapsibleStack;
return (
<Comp
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onScroll={onScrollWithListener(this.onScroll)}
contentContainerStyle={{
paddingTop: containerPaddingTop,
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
minHeight: '100%',
}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
{props.children}
</Comp>
);
}
}
export default withCollapsible(CollapsibleComponent);

View file

@ -0,0 +1,63 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {useCollapsibleStack} from 'react-navigation-collapsible';
import CustomTabBar from '../Tabbar/CustomTabBar';
import {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
export interface CollapsibleComponentPropsType {
children?: React.ReactNode;
hasTab?: boolean;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}
interface PropsType extends CollapsibleComponentPropsType {
component: React.ComponentType<any>;
}
function CollapsibleComponent(props: PropsType) {
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (props.onScroll) {
props.onScroll(event);
}
};
const Comp = props.component;
const {
containerPaddingTop,
scrollIndicatorInsetTop,
onScrollWithListener,
} = useCollapsibleStack();
return (
<Comp
{...props}
onScroll={onScrollWithListener(onScroll)}
contentContainerStyle={{
paddingTop: containerPaddingTop,
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
minHeight: '100%',
}}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
{props.children}
</Comp>
);
}
export default CollapsibleComponent;

View file

@ -17,29 +17,19 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native'; import {Animated, FlatListProps} from 'react-native';
import type {CollapsibleComponentPropsType} from './CollapsibleComponent'; import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent'; import CollapsibleComponent from './CollapsibleComponent';
type PropsType = { type Props<T> = FlatListProps<T> & CollapsibleComponentPropsType;
...CollapsibleComponentPropsType,
};
// eslint-disable-next-line react/prefer-stateless-function function CollapsibleFlatList<T>(props: Props<T>) {
class CollapsibleFlatList extends React.Component<PropsType> {
render(): React.Node {
const {props} = this;
return ( return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading <CollapsibleComponent {...props} component={Animated.FlatList}>
{...props}
component={Animated.FlatList}>
{props.children} {props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
}
} }
export default CollapsibleFlatList; export default CollapsibleFlatList;

View file

@ -17,29 +17,19 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native'; import {Animated, ScrollViewProps} from 'react-native';
import type {CollapsibleComponentPropsType} from './CollapsibleComponent'; import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent'; import CollapsibleComponent from './CollapsibleComponent';
type PropsType = { type Props = ScrollViewProps & CollapsibleComponentPropsType;
...CollapsibleComponentPropsType,
};
// eslint-disable-next-line react/prefer-stateless-function function CollapsibleScrollView(props: Props) {
class CollapsibleScrollView extends React.Component<PropsType> {
render(): React.Node {
const {props} = this;
return ( return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading <CollapsibleComponent {...props} component={Animated.ScrollView}>
{...props}
component={Animated.ScrollView}>
{props.children} {props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
}
} }
export default CollapsibleScrollView; export default CollapsibleScrollView;

View file

@ -17,29 +17,19 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native'; import {Animated, SectionListProps} from 'react-native';
import type {CollapsibleComponentPropsType} from './CollapsibleComponent'; import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent'; import CollapsibleComponent from './CollapsibleComponent';
type PropsType = { type Props<T> = SectionListProps<T> & CollapsibleComponentPropsType;
...CollapsibleComponentPropsType,
};
// eslint-disable-next-line react/prefer-stateless-function function CollapsibleSectionList<T>(props: Props<T>) {
class CollapsibleSectionList extends React.Component<PropsType> {
render(): React.Node {
const {props} = this;
return ( return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading <CollapsibleComponent {...props} component={Animated.SectionList}>
{...props}
component={Animated.SectionList}>
{props.children} {props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
}
} }
export default CollapsibleSectionList; export default CollapsibleSectionList;

View file

@ -17,22 +17,18 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
type PropsType = { type PropsType = {
visible: boolean, visible: boolean;
onDismiss: () => void, onDismiss: () => void;
title: string | React.Node, title: string | React.ReactNode;
message: string | React.Node, message: string | React.ReactNode;
}; };
class AlertDialog extends React.PureComponent<PropsType> { function AlertDialog(props: PropsType) {
render(): React.Node {
const {props} = this;
return ( return (
<Portal> <Portal>
<Dialog visible={props.visible} onDismiss={props.onDismiss}> <Dialog visible={props.visible} onDismiss={props.onDismiss}>
@ -46,7 +42,6 @@ class AlertDialog extends React.PureComponent<PropsType> {
</Dialog> </Dialog>
</Portal> </Portal>
); );
}
} }
export default AlertDialog; export default AlertDialog;

View file

@ -1,90 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import i18n from 'i18n-js';
import {ERROR_TYPE} from '../../utils/WebData';
import AlertDialog from './AlertDialog';
type PropsType = {
visible: boolean,
onDismiss: () => void,
errorCode: number,
};
class ErrorDialog extends React.PureComponent<PropsType> {
title: string;
message: string;
generateMessage() {
const {props} = this;
this.title = i18n.t('errors.title');
switch (props.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
this.message = i18n.t('errors.badCredentials');
break;
case ERROR_TYPE.BAD_TOKEN:
this.message = i18n.t('errors.badToken');
break;
case ERROR_TYPE.NO_CONSENT:
this.message = i18n.t('errors.noConsent');
break;
case ERROR_TYPE.TOKEN_SAVE:
this.message = i18n.t('errors.tokenSave');
break;
case ERROR_TYPE.TOKEN_RETRIEVE:
this.message = i18n.t('errors.unknown');
break;
case ERROR_TYPE.BAD_INPUT:
this.message = i18n.t('errors.badInput');
break;
case ERROR_TYPE.FORBIDDEN:
this.message = i18n.t('errors.forbidden');
break;
case ERROR_TYPE.CONNECTION_ERROR:
this.message = i18n.t('errors.connectionError');
break;
case ERROR_TYPE.SERVER_ERROR:
this.message = i18n.t('errors.serverError');
break;
default:
this.message = i18n.t('errors.unknown');
break;
}
this.message += `\n\nCode ${props.errorCode}`;
}
render(): React.Node {
this.generateMessage();
const {props} = this;
return (
<AlertDialog
visible={props.visible}
onDismiss={props.onDismiss}
title={this.title}
message={this.message}
/>
);
}
}
export default ErrorDialog;

View file

@ -0,0 +1,80 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import i18n from 'i18n-js';
import {ERROR_TYPE} from '../../utils/WebData';
import AlertDialog from './AlertDialog';
type PropsType = {
visible: boolean;
onDismiss: () => void;
errorCode: number;
};
function ErrorDialog(props: PropsType) {
let title: string;
let message: string;
title = i18n.t('errors.title');
switch (props.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS:
message = i18n.t('errors.badCredentials');
break;
case ERROR_TYPE.BAD_TOKEN:
message = i18n.t('errors.badToken');
break;
case ERROR_TYPE.NO_CONSENT:
message = i18n.t('errors.noConsent');
break;
case ERROR_TYPE.TOKEN_SAVE:
message = i18n.t('errors.tokenSave');
break;
case ERROR_TYPE.TOKEN_RETRIEVE:
message = i18n.t('errors.unknown');
break;
case ERROR_TYPE.BAD_INPUT:
message = i18n.t('errors.badInput');
break;
case ERROR_TYPE.FORBIDDEN:
message = i18n.t('errors.forbidden');
break;
case ERROR_TYPE.CONNECTION_ERROR:
message = i18n.t('errors.connectionError');
break;
case ERROR_TYPE.SERVER_ERROR:
message = i18n.t('errors.serverError');
break;
default:
message = i18n.t('errors.unknown');
break;
}
message += `\n\nCode ${props.errorCode}`;
return (
<AlertDialog
visible={props.visible}
onDismiss={props.onDismiss}
title={title}
message={message}
/>
);
}
export default ErrorDialog;

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
@ -30,20 +28,23 @@ import {
import i18n from 'i18n-js'; import i18n from 'i18n-js';
type PropsType = { type PropsType = {
visible: boolean, visible: boolean;
onDismiss?: () => void, onDismiss?: () => void;
onAccept?: () => Promise<void>, // async function to be executed onAccept?: () => Promise<void>; // async function to be executed
title?: string, title?: string;
titleLoading?: string, titleLoading?: string;
message?: string, message?: string;
startLoading?: boolean, startLoading?: boolean;
}; };
type StateType = { type StateType = {
loading: boolean, loading: boolean;
}; };
class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> { export default class LoadingConfirmDialog extends React.PureComponent<
PropsType,
StateType
> {
static defaultProps = { static defaultProps = {
onDismiss: () => {}, onDismiss: () => {},
onAccept: (): Promise<void> => { onAccept: (): Promise<void> => {
@ -71,14 +72,16 @@ class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
onClickAccept = () => { onClickAccept = () => {
const {props} = this; const {props} = this;
this.setState({loading: true}); this.setState({loading: true});
if (props.onAccept != null) props.onAccept().then(this.hideLoading); if (props.onAccept != null) {
props.onAccept().then(this.hideLoading);
}
}; };
/** /**
* Waits for fade out animations to finish before hiding loading * Waits for fade out animations to finish before hiding loading
* @returns {TimeoutID} * @returns {NodeJS.Timeout}
*/ */
hideLoading = (): TimeoutID => hideLoading = (): NodeJS.Timeout =>
setTimeout(() => { setTimeout(() => {
this.setState({loading: false}); this.setState({loading: false});
}, 200); }, 200);
@ -88,10 +91,12 @@ class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
*/ */
onDismiss = () => { onDismiss = () => {
const {state, props} = this; const {state, props} = this;
if (!state.loading && props.onDismiss != null) props.onDismiss(); if (!state.loading && props.onDismiss != null) {
props.onDismiss();
}
}; };
render(): React.Node { render() {
const {state, props} = this; const {state, props} = this;
return ( return (
<Portal> <Portal>
@ -121,5 +126,3 @@ class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
); );
} }
} }
export default LoadingConfirmDialog;

View file

@ -17,28 +17,26 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import {FlatList} from 'react-native'; import {FlatList} from 'react-native';
export type OptionsDialogButtonType = { export type OptionsDialogButtonType = {
title: string, title: string;
icon?: string, icon?: string;
onPress: () => void, onPress: () => void;
}; };
type PropsType = { type PropsType = {
visible: boolean, visible: boolean;
title: string, title: string;
message: string, message: string;
buttons: Array<OptionsDialogButtonType>, buttons: Array<OptionsDialogButtonType>;
onDismiss: () => void, onDismiss: () => void;
}; };
class OptionsDialog extends React.PureComponent<PropsType> { function OptionsDialog(props: PropsType) {
getButtonRender = ({item}: {item: OptionsDialogButtonType}): React.Node => { const getButtonRender = ({item}: {item: OptionsDialogButtonType}) => {
return ( return (
<Button onPress={item.onPress} icon={item.icon}> <Button onPress={item.onPress} icon={item.icon}>
{item.title} {item.title}
@ -46,15 +44,13 @@ class OptionsDialog extends React.PureComponent<PropsType> {
); );
}; };
keyExtractor = (item: OptionsDialogButtonType): string => { const keyExtractor = (item: OptionsDialogButtonType): string => {
if (item.icon != null) { if (item.icon != null) {
return item.title + item.icon; return item.title + item.icon;
} }
return item.title; return item.title;
}; };
render(): React.Node {
const {props} = this;
return ( return (
<Portal> <Portal>
<Dialog visible={props.visible} onDismiss={props.onDismiss}> <Dialog visible={props.visible} onDismiss={props.onDismiss}>
@ -65,8 +61,8 @@ class OptionsDialog extends React.PureComponent<PropsType> {
<Dialog.Actions> <Dialog.Actions>
<FlatList <FlatList
data={props.buttons} data={props.buttons}
renderItem={this.getButtonRender} renderItem={getButtonRender}
keyExtractor={this.keyExtractor} keyExtractor={keyExtractor}
horizontal horizontal
inverted inverted
/> />
@ -74,7 +70,6 @@ class OptionsDialog extends React.PureComponent<PropsType> {
</Dialog> </Dialog>
</Portal> </Portal>
); );
}
} }
export default OptionsDialog; export default OptionsDialog;

View file

@ -1,75 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {List, withTheme} from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {ListIconPropsType} from '../../constants/PaperStyles';
type PropsType = {
navigation: StackNavigationProp,
theme: CustomThemeType,
};
class ActionsDashBoardItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return nextProps.theme.dark !== props.theme.dark;
}
render(): React.Node {
const {navigation} = this.props;
return (
<View>
<List.Item
title={i18n.t('screens.feedback.homeButtonTitle')}
description={i18n.t('screens.feedback.homeButtonSubtitle')}
left={(props: ListIconPropsType): React.Node => (
<List.Icon
color={props.color}
style={props.style}
icon="comment-quote"
/>
)}
right={(props: ListIconPropsType): React.Node => (
<List.Icon
color={props.color}
style={props.style}
icon="chevron-right"
/>
)}
onPress={(): void => navigation.navigate('feedback')}
style={{
paddingTop: 0,
paddingBottom: 0,
marginLeft: 10,
marginRight: 10,
}}
/>
</View>
);
}
}
export default withTheme(ActionsDashBoardItem);

View file

@ -0,0 +1,59 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {List} from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import {useNavigation} from '@react-navigation/native';
function ActionsDashBoardItem() {
const navigation = useNavigation();
return (
<View>
<List.Item
title={i18n.t('screens.feedback.homeButtonTitle')}
description={i18n.t('screens.feedback.homeButtonSubtitle')}
left={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="comment-quote"
/>
)}
right={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="chevron-right"
/>
)}
onPress={(): void => navigation.navigate('feedback')}
style={{
paddingTop: 0,
paddingBottom: 0,
marginLeft: 10,
marginRight: 10,
}}
/>
</View>
);
}
export default ActionsDashBoardItem;

View file

@ -1,116 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {
Avatar,
Card,
Text,
TouchableRipple,
withTheme,
} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {CardTitleIconPropsType} from '../../constants/PaperStyles';
type PropsType = {
eventNumber: number,
clickAction: () => void,
theme: CustomThemeType,
children?: React.Node,
};
const styles = StyleSheet.create({
card: {
width: 'auto',
marginLeft: 10,
marginRight: 10,
marginTop: 10,
overflow: 'hidden',
},
avatar: {
backgroundColor: 'transparent',
},
});
/**
* Component used to display a dashboard item containing a preview event
*/
class EventDashBoardItem extends React.Component<PropsType> {
static defaultProps = {
children: null,
};
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.eventNumber !== props.eventNumber
);
}
render(): React.Node {
const {props} = this;
const {colors} = props.theme;
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable ? colors.planningColor : colors.textDisabled;
const textColor = isAvailable ? colors.text : colors.textDisabled;
let subtitle;
if (isAvailable) {
subtitle = (
<Text>
<Text style={{fontWeight: 'bold'}}>{props.eventNumber}</Text>
<Text>
{props.eventNumber > 1
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
</Text>
</Text>
);
} else subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
return (
<Card style={styles.card}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
<Card.Title
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
titleStyle={{color: textColor}}
subtitle={subtitle}
subtitleStyle={{color: textColor}}
left={(iconProps: CardTitleIconPropsType): React.Node => (
<Avatar.Icon
icon="calendar-range"
color={iconColor}
size={iconProps.size}
style={styles.avatar}
/>
)}
/>
<Card.Content>{props.children}</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
}
export default withTheme(EventDashBoardItem);

View file

@ -0,0 +1,104 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {
Avatar,
Card,
Text,
TouchableRipple,
useTheme,
} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
type PropsType = {
eventNumber: number;
clickAction: () => void;
children?: React.ReactNode;
};
const styles = StyleSheet.create({
card: {
width: 'auto',
marginLeft: 10,
marginRight: 10,
marginTop: 10,
overflow: 'hidden',
},
avatar: {
backgroundColor: 'transparent',
},
});
/**
* Component used to display a dashboard item containing a preview event
*/
function EventDashBoardItem(props: PropsType) {
const theme = useTheme();
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable
? theme.colors.planningColor
: theme.colors.textDisabled;
const textColor = isAvailable ? theme.colors.text : theme.colors.textDisabled;
let subtitle;
if (isAvailable) {
subtitle = (
<Text>
<Text style={{fontWeight: 'bold'}}>{props.eventNumber}</Text>
<Text>
{props.eventNumber > 1
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
</Text>
</Text>
);
} else {
subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
}
return (
<Card style={styles.card}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
<Card.Title
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
titleStyle={{color: textColor}}
subtitle={subtitle}
subtitleStyle={{color: textColor}}
left={(iconProps) => (
<Avatar.Icon
icon="calendar-range"
color={iconColor}
size={iconProps.size}
style={styles.avatar}
/>
)}
/>
<Card.Content>{props.children}</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.eventNumber === prevProps.eventNumber;
};
export default React.memo(EventDashBoardItem, areEqual);

View file

@ -1,139 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import Autolink from 'react-native-autolink';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {FeedItemType} from '../../screens/Home/HomeScreen';
import NewsSourcesConstants from '../../constants/NewsSourcesConstants';
import type {NewsSourceType} from '../../constants/NewsSourcesConstants';
import ImageGalleryButton from '../Media/ImageGalleryButton';
type PropsType = {
navigation: StackNavigationProp,
item: FeedItemType,
height: number,
};
/**
* Component used to display a feed item
*/
class FeedItem extends React.Component<PropsType> {
/**
* Converts a dateString using Unix Timestamp to a formatted date
*
* @param dateString {string} The Unix Timestamp representation of a date
* @return {string} The formatted output date
*/
static getFormattedDate(dateString: number): string {
const date = new Date(dateString * 1000);
return date.toLocaleString();
}
shouldComponentUpdate(): boolean {
return false;
}
onPress = () => {
const {item, navigation} = this.props;
navigation.navigate('feed-information', {
data: item,
date: FeedItem.getFormattedDate(item.time),
});
};
render(): React.Node {
const {item, height, navigation} = this.props;
const image = item.image !== '' && item.image != null ? item.image : null;
const pageSource: NewsSourceType = NewsSourcesConstants[item.page_id];
const cardMargin = 10;
const cardHeight = height - 2 * cardMargin;
const imageSize = 250;
const titleHeight = 80;
const actionsHeight = 60;
const textHeight =
image != null
? cardHeight - titleHeight - actionsHeight - imageSize
: cardHeight - titleHeight - actionsHeight;
return (
<Card
style={{
margin: cardMargin,
height: cardHeight,
}}>
<TouchableRipple style={{flex: 1}} onPress={this.onPress}>
<View>
<Card.Title
title={pageSource.name}
subtitle={FeedItem.getFormattedDate(item.time)}
left={(): React.Node => (
<Image
size={48}
source={pageSource.icon}
style={{
width: 48,
height: 48,
}}
/>
)}
style={{height: titleHeight}}
/>
{image != null ? (
<ImageGalleryButton
navigation={navigation}
images={[{url: image}]}
style={{
width: imageSize,
height: imageSize,
marginLeft: 'auto',
marginRight: 'auto',
}}
/>
) : null}
<Card.Content>
{item.message !== undefined ? (
<Autolink
text={item.message}
hashtag="facebook"
component={Text}
style={{height: textHeight}}
/>
) : null}
</Card.Content>
<Card.Actions style={{height: actionsHeight}}>
<Button
onPress={this.onPress}
icon="plus"
style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
}
export default FeedItem;

View file

@ -0,0 +1,128 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import Autolink from 'react-native-autolink';
import i18n from 'i18n-js';
import type {FeedItemType} from '../../screens/Home/HomeScreen';
import NewsSourcesConstants, {
AvailablePages,
} from '../../constants/NewsSourcesConstants';
import type {NewsSourceType} from '../../constants/NewsSourcesConstants';
import ImageGalleryButton from '../Media/ImageGalleryButton';
import {useNavigation} from '@react-navigation/native';
type PropsType = {
item: FeedItemType;
height: number;
};
/**
* Converts a dateString using Unix Timestamp to a formatted date
*
* @param dateString {string} The Unix Timestamp representation of a date
* @return {string} The formatted output date
*/
function getFormattedDate(dateString: number): string {
const date = new Date(dateString * 1000);
return date.toLocaleString();
}
/**
* Component used to display a feed item
*/
function FeedItem(props: PropsType) {
const navigation = useNavigation();
const onPress = () => {
navigation.navigate('feed-information', {
data: item,
date: getFormattedDate(props.item.time),
});
};
const {item, height} = props;
const image = item.image !== '' && item.image != null ? item.image : null;
const pageSource: NewsSourceType =
NewsSourcesConstants[item.page_id as AvailablePages];
const cardMargin = 10;
const cardHeight = height - 2 * cardMargin;
const imageSize = 250;
const titleHeight = 80;
const actionsHeight = 60;
const textHeight =
image != null
? cardHeight - titleHeight - actionsHeight - imageSize
: cardHeight - titleHeight - actionsHeight;
return (
<Card
style={{
margin: cardMargin,
height: cardHeight,
}}>
<TouchableRipple style={{flex: 1}} onPress={onPress}>
<View>
<Card.Title
title={pageSource.name}
subtitle={getFormattedDate(item.time)}
left={() => (
<Image
source={pageSource.icon}
style={{
width: 48,
height: 48,
}}
/>
)}
style={{height: titleHeight}}
/>
{image != null ? (
<ImageGalleryButton
images={[{url: image}]}
style={{
width: imageSize,
height: imageSize,
marginLeft: 'auto',
marginRight: 'auto',
}}
/>
) : null}
<Card.Content>
{item.message !== undefined ? (
<Autolink<typeof Text>
text={item.message}
hashtag="facebook"
component={Text}
style={{height: textHeight}}
/>
) : null}
</Card.Content>
<Card.Actions style={{height: actionsHeight}}>
<Button onPress={onPress} icon="plus" style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
export default React.memo(FeedItem, () => true);

View file

@ -1,113 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
import {getTimeOnlyString, isDescriptionEmpty} from '../../utils/Planning';
import CustomHTML from '../Overrides/CustomHTML';
import type {PlanningEventType} from '../../utils/Planning';
type PropsType = {
event?: PlanningEventType | null,
clickAction: () => void,
};
const styles = StyleSheet.create({
card: {
marginBottom: 10,
},
content: {
maxHeight: 150,
overflow: 'hidden',
},
actions: {
marginLeft: 'auto',
marginTop: 'auto',
flexDirection: 'row',
},
avatar: {
backgroundColor: 'transparent',
},
});
/**
* Component used to display an event preview if an event is available
*/
// eslint-disable-next-line react/prefer-stateless-function
class PreviewEventDashboardItem extends React.Component<PropsType> {
static defaultProps = {
event: null,
};
render(): React.Node {
const {props} = this;
const {event} = props;
const isEmpty =
event == null ? true : isDescriptionEmpty(event.description);
if (event != null) {
const hasImage = event.logo !== '' && event.logo != null;
const getImage = (): React.Node => (
<Avatar.Image
source={{uri: event.logo}}
size={50}
style={styles.avatar}
/>
);
return (
<Card style={styles.card} elevation={3}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
{hasImage ? (
<Card.Title
title={event.title}
subtitle={getTimeOnlyString(event.date_begin)}
left={getImage}
/>
) : (
<Card.Title
title={event.title}
subtitle={getTimeOnlyString(event.date_begin)}
/>
)}
{!isEmpty ? (
<Card.Content style={styles.content}>
<CustomHTML html={event.description} />
</Card.Content>
) : null}
<Card.Actions style={styles.actions}>
<Button icon="chevron-right">
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
return null;
}
}
export default PreviewEventDashboardItem;

View file

@ -0,0 +1,93 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
import {getTimeOnlyString, isDescriptionEmpty} from '../../utils/Planning';
import CustomHTML from '../Overrides/CustomHTML';
import type {PlanningEventType} from '../../utils/Planning';
type PropsType = {
event?: PlanningEventType | null;
clickAction: () => void;
};
const styles = StyleSheet.create({
card: {
marginBottom: 10,
},
content: {
maxHeight: 150,
overflow: 'hidden',
},
actions: {
marginLeft: 'auto',
marginTop: 'auto',
flexDirection: 'row',
},
avatar: {
backgroundColor: 'transparent',
},
});
/**
* Component used to display an event preview if an event is available
*/
function PreviewEventDashboardItem(props: PropsType) {
const {event} = props;
const isEmpty = event == null ? true : isDescriptionEmpty(event.description);
if (event != null) {
const logo = event.logo;
const getImage = logo
? () => (
<Avatar.Image source={{uri: logo}} size={50} style={styles.avatar} />
)
: () => null;
return (
<Card style={styles.card} elevation={3}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
<Card.Title
title={event.title}
subtitle={getTimeOnlyString(event.date_begin)}
left={getImage}
/>
{!isEmpty ? (
<Card.Content style={styles.content}>
<CustomHTML html={event.description} />
</Card.Content>
) : null}
<Card.Actions style={styles.actions}>
<Button icon="chevron-right">
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
return null;
}
export default PreviewEventDashboardItem;

View file

@ -1,106 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = {
image: string | null,
onPress: () => void | null,
badgeCount: number | null,
theme: CustomThemeType,
};
/**
* Component used to render a small dashboard item
*/
class SmallDashboardItem extends React.Component<PropsType> {
itemSize: number;
constructor(props: PropsType) {
super(props);
this.itemSize = Dimensions.get('window').width / 8;
}
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.badgeCount !== props.badgeCount
);
}
render(): React.Node {
const {props} = this;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: this.itemSize / 6,
marginRight: this.itemSize / 6,
}}>
<View
style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: '80%',
height: '80%',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
marginBottom: 'auto',
}}
/>
{props.badgeCount != null && props.badgeCount > 0 ? (
<Animatable.View
animation="zoomIn"
duration={300}
useNativeDriver
style={{
position: 'absolute',
top: 0,
right: 0,
}}>
<Badge
style={{
backgroundColor: props.theme.colors.primary,
borderColor: props.theme.colors.background,
borderWidth: 2,
}}>
{props.badgeCount}
</Badge>
</Animatable.View>
) : null}
</View>
</TouchableRipple>
);
}
}
export default withTheme(SmallDashboardItem);

View file

@ -0,0 +1,94 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Badge, TouchableRipple, useTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
type PropsType = {
image?: string | number;
onPress?: () => void;
badgeCount?: number;
};
/**
* Component used to render a small dashboard item
*/
function SmallDashboardItem(props: PropsType) {
const itemSize = Dimensions.get('window').width / 8;
const theme = useTheme();
const {image} = props;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: itemSize / 6,
marginRight: itemSize / 6,
}}>
<View
style={{
width: itemSize,
height: itemSize,
}}>
{image ? (
<Image
source={typeof image === 'string' ? {uri: image} : image}
style={{
width: '80%',
height: '80%',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
marginBottom: 'auto',
}}
/>
) : null}
{props.badgeCount != null && props.badgeCount > 0 ? (
<Animatable.View
animation="zoomIn"
duration={300}
useNativeDriver
style={{
position: 'absolute',
top: 0,
right: 0,
}}>
<Badge
visible={true}
style={{
backgroundColor: theme.colors.primary,
borderColor: theme.colors.background,
borderWidth: 2,
}}>
{props.badgeCount}
</Badge>
</Animatable.View>
) : null}
</View>
</TouchableRipple>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.badgeCount === prevProps.badgeCount;
};
export default React.memo(SmallDashboardItem, areEqual);

View file

@ -17,15 +17,13 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
type PropsType = { type PropsType = {
icon: string, icon: string;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -37,24 +35,14 @@ const styles = StyleSheet.create({
}, },
}); });
class IntroIcon extends React.Component<PropsType> { function IntroIcon(props: PropsType) {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {icon} = this.props;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Animatable.View <Animatable.View useNativeDriver style={styles.center} animation="fadeIn">
useNativeDriver <MaterialCommunityIcons name={props.icon} color="#fff" size={200} />
style={styles.center}
animation="fadeIn">
<MaterialCommunityIcons name={icon} color="#fff" size={200} />
</Animatable.View> </Animatable.View>
</View> </View>
); );
}
} }
export default IntroIcon; export default IntroIcon;

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native';
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot'; import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
@ -32,12 +30,7 @@ const styles = StyleSheet.create({
}, },
}); });
class MascotIntroEnd extends React.Component<null> { function MascotIntroEnd() {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Mascot <Mascot
@ -59,7 +52,6 @@ class MascotIntroEnd extends React.Component<null> {
/> />
</View> </View>
); );
}
} }
export default MascotIntroEnd; export default MascotIntroEnd;

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
@ -34,12 +32,7 @@ const styles = StyleSheet.create({
}, },
}); });
class MascotIntroWelcome extends React.Component<null> { function MascotIntroWelcome() {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Mascot <Mascot
@ -89,7 +82,6 @@ class MascotIntroWelcome extends React.Component<null> {
</Animatable.View> </Animatable.View>
</View> </View>
); );
}
} }
export default MascotIntroWelcome; export default MascotIntroWelcome;

View file

@ -17,19 +17,16 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Animated, Dimensions} from 'react-native'; import {Animated, Dimensions, ViewStyle} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
import ImageListItem from './ImageListItem'; import ImageListItem from './ImageListItem';
import CardListItem from './CardListItem'; import CardListItem from './CardListItem';
import type {ServiceItemType} from '../../../managers/ServicesManager'; import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = { type PropsType = {
dataset: Array<ServiceItemType>, dataset: Array<ServiceItemType>;
isHorizontal?: boolean, isHorizontal?: boolean;
contentContainerStyle?: ViewStyle | null, contentContainerStyle?: ViewStyle;
}; };
export default class CardList extends React.Component<PropsType> { export default class CardList extends React.Component<PropsType> {
@ -45,12 +42,12 @@ export default class CardList extends React.Component<PropsType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.windowWidth = Dimensions.get('window').width; this.windowWidth = Dimensions.get('window').width;
this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items, and a part of the 4th => user knows he can scroll
} }
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => { getRenderItem = ({item}: {item: ServiceItemType}) => {
const {props} = this; const {props} = this;
if (props.isHorizontal) if (props.isHorizontal) {
return ( return (
<ImageListItem <ImageListItem
item={item} item={item}
@ -58,12 +55,13 @@ export default class CardList extends React.Component<PropsType> {
width={this.horizontalItemSize} width={this.horizontalItemSize}
/> />
); );
}
return <CardListItem item={item} key={item.title} />; return <CardListItem item={item} key={item.title} />;
}; };
keyExtractor = (item: ServiceItemType): string => item.key; keyExtractor = (item: ServiceItemType): string => item.key;
render(): React.Node { render() {
const {props} = this; const {props} = this;
let containerStyle = {}; let containerStyle = {};
if (props.isHorizontal) { if (props.isHorizontal) {
@ -84,7 +82,7 @@ export default class CardList extends React.Component<PropsType> {
} }
pagingEnabled={props.isHorizontal} pagingEnabled={props.isHorizontal}
snapToInterval={ snapToInterval={
props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : null props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : undefined
} }
/> />
); );

View file

@ -17,24 +17,16 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper'; import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
import {View} from 'react-native'; import {View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager'; import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = { type PropsType = {
item: ServiceItemType, item: ServiceItemType;
}; };
export default class CardListItem extends React.Component<PropsType> { function CardListItem(props: PropsType) {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
const {item} = props; const {item} = props;
const source = const source =
typeof item.image === 'number' ? item.image : {uri: item.image}; typeof item.image === 'number' ? item.image : {uri: item.image};
@ -57,5 +49,6 @@ export default class CardListItem extends React.Component<PropsType> {
</TouchableRipple> </TouchableRipple>
</Card> </Card>
); );
}
} }
export default React.memo(CardListItem, () => true);

View file

@ -1,73 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceItemType,
width: number,
};
export default class ImageListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
const {item} = props;
const source =
typeof item.image === 'number' ? item.image : {uri: item.image};
return (
<TouchableRipple
style={{
width: props.width,
height: props.width + 40,
margin: 5,
}}
onPress={item.onPress}>
<View>
<Image
style={{
width: props.width - 20,
height: props.width - 20,
marginLeft: 'auto',
marginRight: 'auto',
}}
source={source}
/>
<Text
style={{
marginTop: 5,
marginLeft: 'auto',
marginRight: 'auto',
textAlign: 'center',
}}>
{item.title}
</Text>
</View>
</TouchableRipple>
);
}
}

View file

@ -0,0 +1,66 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceItemType;
width: number;
};
function ImageListItem(props: PropsType) {
const {item} = props;
const source =
typeof item.image === 'number' ? item.image : {uri: item.image};
return (
<TouchableRipple
style={{
width: props.width,
height: props.width + 40,
margin: 5,
}}
onPress={item.onPress}>
<View>
<Image
style={{
width: props.width - 20,
height: props.width - 20,
marginLeft: 'auto',
marginRight: 'auto',
}}
source={source}
/>
<Text
style={{
marginTop: 5,
marginLeft: 'auto',
marginRight: 'auto',
textAlign: 'center',
}}>
{item.title}
</Text>
</View>
</TouchableRipple>
);
}
export default React.memo(ImageListItem, () => true);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Card, Chip, List, Text} from 'react-native-paper'; import {Card, Chip, List, Text} from 'react-native-paper';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native';
@ -26,12 +24,11 @@ import i18n from 'i18n-js';
import AnimatedAccordion from '../../Animations/AnimatedAccordion'; import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import {isItemInCategoryFilter} from '../../../utils/Search'; import {isItemInCategoryFilter} from '../../../utils/Search';
import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen'; import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = { type PropsType = {
categories: Array<ClubCategoryType>, categories: Array<ClubCategoryType>;
onChipSelect: (id: number) => void, onChipSelect: (id: number) => void;
selectedCategories: Array<number>, selectedCategories: Array<number>;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -54,16 +51,8 @@ const styles = StyleSheet.create({
}, },
}); });
class ClubListHeader extends React.Component<PropsType> { function ClubListHeader(props: PropsType) {
shouldComponentUpdate(nextProps: PropsType): boolean { const getChipRender = (category: ClubCategoryType, key: string) => {
const {props} = this;
return (
nextProps.selectedCategories.length !== props.selectedCategories.length
);
}
getChipRender = (category: ClubCategoryType, key: string): React.Node => {
const {props} = this;
const onPress = (): void => props.onChipSelect(category.id); const onPress = (): void => props.onChipSelect(category.id);
return ( return (
<Chip <Chip
@ -80,32 +69,39 @@ class ClubListHeader extends React.Component<PropsType> {
); );
}; };
getCategoriesRender(): React.Node { const getCategoriesRender = () => {
const {props} = this; const final: Array<React.ReactNode> = [];
const final = [];
props.categories.forEach((cat: ClubCategoryType) => { props.categories.forEach((cat: ClubCategoryType) => {
final.push(this.getChipRender(cat, cat.id.toString())); final.push(getChipRender(cat, cat.id.toString()));
}); });
return final; return final;
} };
render(): React.Node {
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<AnimatedAccordion <AnimatedAccordion
title={i18n.t('screens.clubs.categories')} title={i18n.t('screens.clubs.categories')}
left={(props: ListIconPropsType): React.Node => ( left={(iconProps) => (
<List.Icon color={props.color} style={props.style} icon="star" /> <List.Icon
color={iconProps.color}
style={iconProps.style}
icon="star"
/>
)} )}
opened> opened>
<Text style={styles.text}> <Text style={styles.text}>
{i18n.t('screens.clubs.categoriesFilterMessage')} {i18n.t('screens.clubs.categoriesFilterMessage')}
</Text> </Text>
<View style={styles.chipContainer}>{this.getCategoriesRender()}</View> <View style={styles.chipContainer}>{getCategoriesRender()}</View>
</AnimatedAccordion> </AnimatedAccordion>
</Card> </Card>
); );
}
} }
export default ClubListHeader; const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return (
prevProps.selectedCategories.length === nextProps.selectedCategories.length
);
};
export default React.memo(ClubListHeader, areEqual);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Chip, List, withTheme} from 'react-native-paper'; import {Avatar, Chip, List, withTheme} from 'react-native-paper';
import {View} from 'react-native'; import {View} from 'react-native';
@ -26,14 +24,13 @@ import type {
ClubCategoryType, ClubCategoryType,
ClubType, ClubType,
} from '../../../screens/Amicale/Clubs/ClubListScreen'; } from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
onPress: () => void, onPress: () => void;
categoryTranslator: (id: number) => ClubCategoryType, categoryTranslator: (id: number) => ClubCategoryType | null;
item: ClubType, item: ClubType;
height: number, height: number;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
}; };
class ClubListItem extends React.Component<PropsType> { class ClubListItem extends React.Component<PropsType> {
@ -48,12 +45,13 @@ class ClubListItem extends React.Component<PropsType> {
return false; return false;
} }
getCategoriesRender(categories: Array<number | null>): React.Node { getCategoriesRender(categories: Array<number | null>) {
const {props} = this; const {props} = this;
const final = []; const final: Array<React.ReactNode> = [];
categories.forEach((cat: number | null) => { categories.forEach((cat: number | null) => {
if (cat != null) { if (cat != null) {
const category: ClubCategoryType = props.categoryTranslator(cat); const category = props.categoryTranslator(cat);
if (category) {
final.push( final.push(
<Chip <Chip
style={{marginRight: 5, marginBottom: 5}} style={{marginRight: 5, marginBottom: 5}}
@ -62,13 +60,14 @@ class ClubListItem extends React.Component<PropsType> {
</Chip>, </Chip>,
); );
} }
}
}); });
return <View style={{flexDirection: 'row'}}>{final}</View>; return <View style={{flexDirection: 'row'}}>{final}</View>;
} }
render(): React.Node { render() {
const {props} = this; const {props} = this;
const categoriesRender = (): React.Node => const categoriesRender = () =>
this.getCategoriesRender(props.item.category); this.getCategoriesRender(props.item.category);
const {colors} = props.theme; const {colors} = props.theme;
return ( return (
@ -76,7 +75,7 @@ class ClubListItem extends React.Component<PropsType> {
title={props.item.name} title={props.item.name}
description={categoriesRender} description={categoriesRender}
onPress={props.onPress} onPress={props.onPress}
left={(): React.Node => ( left={() => (
<Avatar.Image <Avatar.Image
style={{ style={{
backgroundColor: 'transparent', backgroundColor: 'transparent',
@ -87,7 +86,7 @@ class ClubListItem extends React.Component<PropsType> {
source={{uri: props.item.logo}} source={{uri: props.item.logo}}
/> />
)} )}
right={(): React.Node => ( right={() => (
<Avatar.Icon <Avatar.Icon
style={{ style={{
marginTop: 'auto', marginTop: 'auto',

View file

@ -1,108 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {withTheme} from 'react-native-paper';
import {FlatList, Image, View} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import DashboardEditItem from './DashboardEditItem';
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import type {
ServiceCategoryType,
ServiceItemType,
} from '../../../managers/ServicesManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
item: ServiceCategoryType,
activeDashboard: Array<string>,
onPress: (service: ServiceItemType) => void,
theme: CustomThemeType,
};
const LIST_ITEM_HEIGHT = 64;
class DashboardEditAccordion extends React.Component<PropsType> {
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => {
const {props} = this;
return (
<DashboardEditItem
height={LIST_ITEM_HEIGHT}
item={item}
isActive={props.activeDashboard.includes(item.key)}
onPress={() => {
props.onPress(item);
}}
/>
);
};
getItemLayout = (
data: ?Array<ServiceItemType>,
index: number,
): {length: number, offset: number, index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
});
render(): React.Node {
const {props} = this;
const {item} = props;
return (
<View>
<AnimatedAccordion
title={item.title}
left={(): React.Node =>
typeof item.image === 'number' ? (
<Image
source={item.image}
style={{
width: 40,
height: 40,
}}
/>
) : (
<MaterialCommunityIcons
// $FlowFixMe
name={item.image}
color={props.theme.colors.primary}
size={40}
/>
)
}>
{/* $FlowFixMe */}
<FlatList
data={item.content}
extraData={props.activeDashboard.toString()}
renderItem={this.getRenderItem}
listKey={item.key}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
getItemLayout={this.getItemLayout}
removeClippedSubviews
/>
</AnimatedAccordion>
</View>
);
}
}
export default withTheme(DashboardEditAccordion);

View file

@ -0,0 +1,100 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {useTheme} from 'react-native-paper';
import {FlatList, Image, View} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import DashboardEditItem from './DashboardEditItem';
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import type {
ServiceCategoryType,
ServiceItemType,
} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceCategoryType;
activeDashboard: Array<string>;
onPress: (service: ServiceItemType) => void;
};
const LIST_ITEM_HEIGHT = 64;
function DashboardEditAccordion(props: PropsType) {
const theme = useTheme();
const getRenderItem = ({item}: {item: ServiceItemType}) => {
return (
<DashboardEditItem
height={LIST_ITEM_HEIGHT}
item={item}
isActive={props.activeDashboard.includes(item.key)}
onPress={() => {
props.onPress(item);
}}
/>
);
};
const getItemLayout = (
data: Array<ServiceItemType> | null | undefined,
index: number,
): {length: number; offset: number; index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
});
const {item} = props;
return (
<View>
<AnimatedAccordion
title={item.title}
left={() =>
typeof item.image === 'number' ? (
<Image
source={item.image}
style={{
width: 40,
height: 40,
}}
/>
) : (
<MaterialCommunityIcons
name={item.image}
color={theme.colors.primary}
size={40}
/>
)
}>
<FlatList
data={item.content}
extraData={props.activeDashboard.toString()}
renderItem={getRenderItem}
listKey={item.key}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
getItemLayout={getItemLayout}
removeClippedSubviews
/>
</AnimatedAccordion>
</View>
);
}
export default DashboardEditAccordion;

View file

@ -1,81 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Image} from 'react-native';
import {List, withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ServiceItemType} from '../../../managers/ServicesManager';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
item: ServiceItemType,
isActive: boolean,
height: number,
onPress: () => void,
theme: CustomThemeType,
};
class DashboardEditItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {isActive} = this.props;
return nextProps.isActive !== isActive;
}
render(): React.Node {
const {item, onPress, height, isActive, theme} = this.props;
return (
<List.Item
title={item.title}
description={item.subtitle}
onPress={isActive ? null : onPress}
left={(): React.Node => (
<Image
source={{uri: item.image}}
style={{
width: 40,
height: 40,
}}
/>
)}
right={(props: ListIconPropsType): React.Node =>
isActive ? (
<List.Icon
style={props.style}
icon="check"
color={theme.colors.success}
/>
) : null
}
style={{
height,
justifyContent: 'center',
paddingLeft: 30,
backgroundColor: isActive
? theme.colors.proxiwashFinishedColor
: 'transparent',
}}
/>
);
}
}
export default withTheme(DashboardEditItem);

View file

@ -0,0 +1,76 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Image} from 'react-native';
import {List, useTheme} from 'react-native-paper';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceItemType;
isActive: boolean;
height: number;
onPress: () => void;
};
function DashboardEditItem(props: PropsType) {
const theme = useTheme();
const {item, onPress, height, isActive} = props;
return (
<List.Item
title={item.title}
description={item.subtitle}
onPress={isActive ? undefined : onPress}
left={() => (
<Image
source={
typeof item.image === 'string' ? {uri: item.image} : item.image
}
style={{
width: 40,
height: 40,
}}
/>
)}
right={(iconProps) =>
isActive ? (
<List.Icon
style={iconProps.style}
icon="check"
color={theme.colors.success}
/>
) : null
}
style={{
height,
justifyContent: 'center',
paddingLeft: 30,
backgroundColor: isActive
? theme.colors.proxiwashFinishedColor
: 'transparent',
}}
/>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.isActive === prevProps.isActive;
};
export default React.memo(DashboardEditItem, areEqual);

View file

@ -17,33 +17,23 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {TouchableRipple, withTheme} from 'react-native-paper'; import {TouchableRipple, useTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native'; import {Dimensions, Image, View} from 'react-native';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
image: string, image?: string | number;
isActive: boolean, isActive: boolean;
onPress: () => void, onPress: () => void;
theme: CustomThemeType,
}; };
/** /**
* Component used to render a small dashboard item * Component used to render a small dashboard item
*/ */
class DashboardEditPreviewItem extends React.Component<PropsType> { function DashboardEditPreviewItem(props: PropsType) {
itemSize: number; const theme = useTheme();
const itemSize = Dimensions.get('window').width / 8;
constructor(props: PropsType) {
super(props);
this.itemSize = Dimensions.get('window').width / 8;
}
render(): React.Node {
const {props} = this;
return ( return (
<TouchableRipple <TouchableRipple
onPress={props.onPress} onPress={props.onPress}
@ -52,26 +42,29 @@ class DashboardEditPreviewItem extends React.Component<PropsType> {
marginLeft: 5, marginLeft: 5,
marginRight: 5, marginRight: 5,
backgroundColor: props.isActive backgroundColor: props.isActive
? props.theme.colors.textDisabled ? theme.colors.textDisabled
: 'transparent', : 'transparent',
borderRadius: 5, borderRadius: 5,
}}> }}>
<View <View
style={{ style={{
width: this.itemSize, width: itemSize,
height: this.itemSize, height: itemSize,
}}> }}>
{props.image ? (
<Image <Image
source={{uri: props.image}} source={
typeof props.image === 'string' ? {uri: props.image} : props.image
}
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
}} }}
/> />
) : null}
</View> </View>
</TouchableRipple> </TouchableRipple>
); );
}
} }
export default withTheme(DashboardEditPreviewItem); export default DashboardEditPreviewItem;

View file

@ -1,132 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, List, withTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import {
getFirstEquipmentAvailability,
getRelativeDateString,
isEquipmentAvailable,
} from '../../../utils/EquipmentBooking';
type PropsType = {
navigation: StackNavigationProp,
userDeviceRentDates: [string, string],
item: DeviceType,
height: number,
theme: CustomThemeType,
};
class EquipmentListItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {userDeviceRentDates} = this.props;
return nextProps.userDeviceRentDates !== userDeviceRentDates;
}
render(): React.Node {
const {item, userDeviceRentDates, navigation, height, theme} = this.props;
const isRented = userDeviceRentDates != null;
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
let onPress;
if (isRented)
onPress = () => {
navigation.navigate('equipment-confirm', {
item,
dates: userDeviceRentDates,
});
};
else
onPress = () => {
navigation.navigate('equipment-rent', {item});
};
let description;
if (isRented) {
const start = new Date(userDeviceRentDates[0]);
const end = new Date(userDeviceRentDates[1]);
if (start.getTime() !== end.getTime())
description = i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start),
end: getRelativeDateString(end),
});
else
description = i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start),
});
} else if (isAvailable)
description = i18n.t('screens.equipment.bail', {cost: item.caution});
else
description = i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
});
let icon;
if (isRented) icon = 'bookmark-check';
else if (isAvailable) icon = 'check-circle-outline';
else icon = 'update';
let color;
if (isRented) color = theme.colors.warning;
else if (isAvailable) color = theme.colors.success;
else color = theme.colors.primary;
return (
<List.Item
title={item.name}
description={description}
onPress={onPress}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
style={{
backgroundColor: 'transparent',
}}
icon={icon}
color={color}
/>
)}
right={(): React.Node => (
<Avatar.Icon
style={{
marginTop: 'auto',
marginBottom: 'auto',
backgroundColor: 'transparent',
}}
size={48}
icon="chevron-right"
/>
)}
style={{
height,
justifyContent: 'center',
}}
/>
);
}
}
export default withTheme(EquipmentListItem);

View file

@ -0,0 +1,136 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Avatar, List, useTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import {
getFirstEquipmentAvailability,
getRelativeDateString,
isEquipmentAvailable,
} from '../../../utils/EquipmentBooking';
type PropsType = {
navigation: StackNavigationProp<any>;
userDeviceRentDates: [string, string] | null;
item: DeviceType;
height: number;
};
function EquipmentListItem(props: PropsType) {
const theme = useTheme();
const {item, userDeviceRentDates, navigation, height} = props;
const isRented = userDeviceRentDates != null;
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
let onPress;
if (isRented) {
onPress = () => {
navigation.navigate('equipment-confirm', {
item,
dates: userDeviceRentDates,
});
};
} else {
onPress = () => {
navigation.navigate('equipment-rent', {item});
};
}
let description;
if (isRented && userDeviceRentDates) {
const start = new Date(userDeviceRentDates[0]);
const end = new Date(userDeviceRentDates[1]);
if (start.getTime() !== end.getTime()) {
description = i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start),
end: getRelativeDateString(end),
});
} else {
description = i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start),
});
}
} else if (isAvailable) {
description = i18n.t('screens.equipment.bail', {cost: item.caution});
} else {
description = i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
});
}
let icon: string;
if (isRented) {
icon = 'bookmark-check';
} else if (isAvailable) {
icon = 'check-circle-outline';
} else {
icon = 'update';
}
let color: string;
if (isRented) {
color = theme.colors.warning;
} else if (isAvailable) {
color = theme.colors.success;
} else {
color = theme.colors.primary;
}
return (
<List.Item
title={item.name}
description={description}
onPress={onPress}
left={() => (
<Avatar.Icon
style={{
backgroundColor: 'transparent',
}}
icon={icon}
color={color}
/>
)}
right={() => (
<Avatar.Icon
style={{
marginTop: 'auto',
marginBottom: 'auto',
backgroundColor: 'transparent',
}}
size={48}
icon="chevron-right"
/>
)}
style={{
height,
justifyContent: 'center',
}}
/>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.userDeviceRentDates === prevProps.userDeviceRentDates;
};
export default React.memo(EquipmentListItem, areEqual);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import {FlatList, View} from 'react-native'; import {FlatList, View} from 'react-native';
@ -29,17 +27,14 @@ import type {
PlanexGroupType, PlanexGroupType,
PlanexGroupCategoryType, PlanexGroupCategoryType,
} from '../../../screens/Planex/GroupSelectionScreen'; } from '../../../screens/Planex/GroupSelectionScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = { type PropsType = {
item: PlanexGroupCategoryType, item: PlanexGroupCategoryType;
favorites: Array<PlanexGroupType>, favorites: Array<PlanexGroupType>;
onGroupPress: (PlanexGroupType) => void, onGroupPress: (data: PlanexGroupType) => void;
onFavoritePress: (PlanexGroupType) => void, onFavoritePress: (data: PlanexGroupType) => void;
currentSearchString: string, currentSearchString: string;
height: number, theme: ReactNativePaper.Theme;
theme: CustomThemeType,
}; };
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -55,7 +50,7 @@ class GroupListAccordion extends React.Component<PropsType> {
); );
} }
getRenderItem = ({item}: {item: PlanexGroupType}): React.Node => { getRenderItem = ({item}: {item: PlanexGroupType}) => {
const {props} = this; const {props} = this;
const onPress = () => { const onPress = () => {
props.onGroupPress(item); props.onGroupPress(item);
@ -77,18 +72,19 @@ class GroupListAccordion extends React.Component<PropsType> {
getData(): Array<PlanexGroupType> { getData(): Array<PlanexGroupType> {
const {props} = this; const {props} = this;
const originalData = props.item.content; const originalData = props.item.content;
const displayData = []; const displayData: Array<PlanexGroupType> = [];
originalData.forEach((data: PlanexGroupType) => { originalData.forEach((data: PlanexGroupType) => {
if (stringMatchQuery(data.name, props.currentSearchString)) if (stringMatchQuery(data.name, props.currentSearchString)) {
displayData.push(data); displayData.push(data);
}
}); });
return displayData; return displayData;
} }
itemLayout = ( itemLayout = (
data: ?Array<PlanexGroupType>, data: Array<PlanexGroupType> | null | undefined,
index: number, index: number,
): {length: number, offset: number, index: number} => ({ ): {length: number; offset: number; index: number} => ({
length: LIST_ITEM_HEIGHT, length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index, offset: LIST_ITEM_HEIGHT * index,
index, index,
@ -96,7 +92,7 @@ class GroupListAccordion extends React.Component<PropsType> {
keyExtractor = (item: PlanexGroupType): string => item.id.toString(); keyExtractor = (item: PlanexGroupType): string => item.id.toString();
render(): React.Node { render() {
const {props} = this; const {props} = this;
const {item} = this.props; const {item} = this.props;
return ( return (
@ -104,10 +100,9 @@ class GroupListAccordion extends React.Component<PropsType> {
<AnimatedAccordion <AnimatedAccordion
title={item.name.replace(REPLACE_REGEX, ' ')} title={item.name.replace(REPLACE_REGEX, ' ')}
style={{ style={{
height: props.height,
justifyContent: 'center', justifyContent: 'center',
}} }}
left={(iconProps: ListIconPropsType): React.Node => left={(iconProps) =>
item.id === 0 ? ( item.id === 0 ? (
<List.Icon <List.Icon
style={iconProps.style} style={iconProps.style}

View file

@ -17,23 +17,20 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {List, TouchableRipple, withTheme} from 'react-native-paper'; import {List, TouchableRipple, withTheme} from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen'; import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
import type {ListIconPropsType} from '../../../constants/PaperStyles'; import {View} from 'react-native';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: ReactNativePaper.Theme;
onPress: () => void, onPress: () => void;
onStarPress: () => void, onStarPress: () => void;
item: PlanexGroupType, item: PlanexGroupType;
favorites: Array<PlanexGroupType>, favorites: Array<PlanexGroupType>;
height: number, height: number;
}; };
const REPLACE_REGEX = /_/g; const REPLACE_REGEX = /_/g;
@ -41,10 +38,11 @@ const REPLACE_REGEX = /_/g;
class GroupListItem extends React.Component<PropsType> { class GroupListItem extends React.Component<PropsType> {
isFav: boolean; isFav: boolean;
starRef: null | Animatable.View; starRef: {current: null | (Animatable.View & View)};
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.starRef = React.createRef();
this.isFav = this.isGroupInFavorites(props.favorites); this.isFav = this.isGroupInFavorites(props.favorites);
} }
@ -52,7 +50,9 @@ class GroupListItem extends React.Component<PropsType> {
const {favorites} = this.props; const {favorites} = this.props;
const favChanged = favorites.length !== nextProps.favorites.length; const favChanged = favorites.length !== nextProps.favorites.length;
let newFavState = this.isFav; let newFavState = this.isFav;
if (favChanged) newFavState = this.isGroupInFavorites(nextProps.favorites); if (favChanged) {
newFavState = this.isGroupInFavorites(nextProps.favorites);
}
const shouldUpdate = this.isFav !== newFavState; const shouldUpdate = this.isFav !== newFavState;
this.isFav = newFavState; this.isFav = newFavState;
return shouldUpdate; return shouldUpdate;
@ -61,9 +61,12 @@ class GroupListItem extends React.Component<PropsType> {
onStarPress = () => { onStarPress = () => {
const {props} = this; const {props} = this;
const ref = this.starRef; const ref = this.starRef;
if (ref != null) { if (ref.current && ref.current.rubberBand && ref.current.swing) {
if (this.isFav) ref.rubberBand(); if (this.isFav) {
else ref.swing(); ref.current.rubberBand();
} else {
ref.current.swing();
}
} }
props.onStarPress(); props.onStarPress();
}; };
@ -71,31 +74,29 @@ class GroupListItem extends React.Component<PropsType> {
isGroupInFavorites(favorites: Array<PlanexGroupType>): boolean { isGroupInFavorites(favorites: Array<PlanexGroupType>): boolean {
const {item} = this.props; const {item} = this.props;
for (let i = 0; i < favorites.length; i += 1) { for (let i = 0; i < favorites.length; i += 1) {
if (favorites[i].id === item.id) return true; if (favorites[i].id === item.id) {
return true;
}
} }
return false; return false;
} }
render(): React.Node { render() {
const {props} = this; const {props} = this;
const {colors} = props.theme; const {colors} = props.theme;
return ( return (
<List.Item <List.Item
title={props.item.name.replace(REPLACE_REGEX, ' ')} title={props.item.name.replace(REPLACE_REGEX, ' ')}
onPress={props.onPress} onPress={props.onPress}
left={(iconProps: ListIconPropsType): React.Node => ( left={(iconProps) => (
<List.Icon <List.Icon
color={iconProps.color} color={iconProps.color}
style={iconProps.style} style={iconProps.style}
icon="chevron-right" icon="chevron-right"
/> />
)} )}
right={(iconProps: ListIconPropsType): React.Node => ( right={(iconProps) => (
<Animatable.View <Animatable.View ref={this.starRef} useNativeDriver>
ref={(ref: Animatable.View) => {
this.starRef = ref;
}}
useNativeDriver>
<TouchableRipple <TouchableRipple
onPress={this.onStarPress} onPress={this.onStarPress}
style={{ style={{

View file

@ -1,68 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, List, Text, withTheme} from 'react-native-paper';
import i18n from 'i18n-js';
import type {ProximoArticleType} from '../../../screens/Services/Proximo/ProximoMainScreen';
type PropsType = {
onPress: () => void,
color: string,
item: ProximoArticleType,
height: number,
};
class ProximoListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return (
<List.Item
title={props.item.name}
description={`${props.item.quantity} ${i18n.t(
'screens.proximo.inStock',
)}`}
descriptionStyle={{color: props.color}}
onPress={props.onPress}
left={(): React.Node => (
<Avatar.Image
style={{backgroundColor: 'transparent'}}
size={64}
source={{uri: props.item.image}}
/>
)}
right={(): React.Node => (
<Text style={{fontWeight: 'bold'}}>{props.item.price}</Text>
)}
style={{
height: props.height,
justifyContent: 'center',
}}
/>
);
}
}
export default withTheme(ProximoListItem);

View file

@ -0,0 +1,59 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Avatar, List, Text} from 'react-native-paper';
import i18n from 'i18n-js';
import type {ProximoArticleType} from '../../../screens/Services/Proximo/ProximoMainScreen';
type PropsType = {
onPress: () => void;
color: string;
item: ProximoArticleType;
height: number;
};
function ProximoListItem(props: PropsType) {
return (
<List.Item
title={props.item.name}
description={`${props.item.quantity} ${i18n.t(
'screens.proximo.inStock',
)}`}
descriptionStyle={{color: props.color}}
onPress={props.onPress}
left={() => (
<Avatar.Image
style={{backgroundColor: 'transparent'}}
size={64}
source={{uri: props.item.image}}
/>
)}
right={() => (
<Text style={{fontWeight: 'bold'}}>{props.item.price}</Text>
)}
style={{
height: props.height,
justifyContent: 'center',
}}
/>
);
}
export default React.memo(ProximoListItem, () => true);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import { import {
Avatar, Avatar,
@ -32,22 +30,23 @@ import {
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import ProxiwashConstants from '../../../constants/ProxiwashConstants'; import ProxiwashConstants, {
MachineStates,
} from '../../../constants/ProxiwashConstants';
import AprilFoolsManager from '../../../managers/AprilFoolsManager'; import AprilFoolsManager from '../../../managers/AprilFoolsManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ProxiwashMachineType} from '../../../screens/Proxiwash/ProxiwashScreen'; import type {ProxiwashMachineType} from '../../../screens/Proxiwash/ProxiwashScreen';
type PropsType = { type PropsType = {
item: ProxiwashMachineType, item: ProxiwashMachineType;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
onPress: ( onPress: (
title: string, title: string,
item: ProxiwashMachineType, item: ProxiwashMachineType,
isDryer: boolean, isDryer: boolean,
) => void, ) => void;
isWatched: boolean, isWatched: boolean;
isDryer: boolean, isDryer: boolean;
height: number, height: number;
}; };
const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon); const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon);
@ -72,9 +71,19 @@ const styles = StyleSheet.create({
* Component used to display a proxiwash item, showing machine progression and state * Component used to display a proxiwash item, showing machine progression and state
*/ */
class ProxiwashListItem extends React.Component<PropsType> { class ProxiwashListItem extends React.Component<PropsType> {
stateColors: {[key: string]: string}; stateStrings: {[key in MachineStates]: string} = {
[MachineStates.AVAILABLE]: i18n.t('screens.proxiwash.states.ready'),
[MachineStates.RUNNING]: i18n.t('screens.proxiwash.states.running'),
[MachineStates.RUNNING_NOT_STARTED]: i18n.t(
'screens.proxiwash.states.runningNotStarted',
),
[MachineStates.FINISHED]: i18n.t('screens.proxiwash.states.finished'),
[MachineStates.UNAVAILABLE]: i18n.t('screens.proxiwash.states.broken'),
[MachineStates.ERROR]: i18n.t('screens.proxiwash.states.error'),
[MachineStates.UNKNOWN]: i18n.t('screens.proxiwash.states.unknown'),
};
stateStrings: {[key: string]: string}; stateColors: {[key: string]: string};
title: string; title: string;
@ -83,16 +92,14 @@ class ProxiwashListItem extends React.Component<PropsType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.stateColors = {}; this.stateColors = {};
this.stateStrings = {};
this.updateStateStrings();
let displayNumber = props.item.number; let displayNumber = props.item.number;
const displayMaxWeight = props.item.maxWeight; const displayMaxWeight = props.item.maxWeight;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber( displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(
parseInt(props.item.number, 10), parseInt(props.item.number, 10),
); );
}
this.title = props.isDryer this.title = props.isDryer
? i18n.t('screens.proxiwash.dryer') ? i18n.t('screens.proxiwash.dryer')
@ -116,65 +123,38 @@ class ProxiwashListItem extends React.Component<PropsType> {
props.onPress(this.titlePopUp, props.item, props.isDryer); props.onPress(this.titlePopUp, props.item, props.isDryer);
}; };
updateStateStrings() {
this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t(
'screens.proxiwash.states.ready',
);
this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t(
'screens.proxiwash.states.running',
);
this.stateStrings[
ProxiwashConstants.machineStates.RUNNING_NOT_STARTED
] = i18n.t('screens.proxiwash.states.runningNotStarted');
this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t(
'screens.proxiwash.states.finished',
);
this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t(
'screens.proxiwash.states.broken',
);
this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t(
'screens.proxiwash.states.error',
);
this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t(
'screens.proxiwash.states.unknown',
);
}
updateStateColors() { updateStateColors() {
const {props} = this; const {props} = this;
const {colors} = props.theme; const {colors} = props.theme;
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] = this.stateColors[MachineStates.AVAILABLE] = colors.proxiwashReadyColor;
colors.proxiwashReadyColor; this.stateColors[MachineStates.RUNNING] = colors.proxiwashRunningColor;
this.stateColors[ProxiwashConstants.machineStates.RUNNING] = this.stateColors[MachineStates.RUNNING_NOT_STARTED] =
colors.proxiwashRunningColor;
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] =
colors.proxiwashRunningNotStartedColor; colors.proxiwashRunningNotStartedColor;
this.stateColors[ProxiwashConstants.machineStates.FINISHED] = this.stateColors[MachineStates.FINISHED] = colors.proxiwashFinishedColor;
colors.proxiwashFinishedColor; this.stateColors[MachineStates.UNAVAILABLE] = colors.proxiwashBrokenColor;
this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] = this.stateColors[MachineStates.ERROR] = colors.proxiwashErrorColor;
colors.proxiwashBrokenColor; this.stateColors[MachineStates.UNKNOWN] = colors.proxiwashUnknownColor;
this.stateColors[ProxiwashConstants.machineStates.ERROR] =
colors.proxiwashErrorColor;
this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] =
colors.proxiwashUnknownColor;
} }
render(): React.Node { render() {
const {props} = this; const {props} = this;
const {colors} = props.theme; const {colors} = props.theme;
const machineState = props.item.state; const machineState = props.item.state;
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING; const isRunning = machineState === MachineStates.RUNNING;
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE; const isReady = machineState === MachineStates.AVAILABLE;
const description = isRunning const description = isRunning
? `${props.item.startTime}/${props.item.endTime}` ? `${props.item.startTime}/${props.item.endTime}`
: ''; : '';
const stateIcon = ProxiwashConstants.stateIcons[machineState]; const stateIcon = ProxiwashConstants.stateIcons[machineState];
const stateString = this.stateStrings[machineState]; const stateString = this.stateStrings[machineState];
let progress; let progress;
if (isRunning && props.item.donePercent !== '') if (isRunning && props.item.donePercent !== '') {
progress = parseFloat(props.item.donePercent) / 100; progress = parseFloat(props.item.donePercent) / 100;
else if (isRunning) progress = 0; } else if (isRunning) {
else progress = 1; progress = 0;
} else {
progress = 1;
}
const icon = props.isWatched ? ( const icon = props.isWatched ? (
<AnimatedIcon <AnimatedIcon
@ -224,19 +204,19 @@ class ProxiwashListItem extends React.Component<PropsType> {
justifyContent: 'center', justifyContent: 'center',
}} }}
onPress={this.onListItemPress} onPress={this.onListItemPress}
left={(): React.Node => icon} left={() => icon}
right={(): React.Node => ( right={() => (
<View style={{flexDirection: 'row'}}> <View style={{flexDirection: 'row'}}>
<View style={{justifyContent: 'center'}}> <View style={{justifyContent: 'center'}}>
<Text <Text
style={ style={
machineState === ProxiwashConstants.machineStates.FINISHED machineState === MachineStates.FINISHED
? {fontWeight: 'bold'} ? {fontWeight: 'bold'}
: {} : {}
}> }>
{stateString} {stateString}
</Text> </Text>
{machineState === ProxiwashConstants.machineStates.RUNNING ? ( {machineState === MachineStates.RUNNING ? (
<Caption>{props.item.remainingTime} min</Caption> <Caption>{props.item.remainingTime} min</Caption>
) : null} ) : null}
</View> </View>

View file

@ -17,19 +17,16 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Text, withTheme} from 'react-native-paper'; import {Avatar, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: ReactNativePaper.Theme;
title: string, title: string;
isDryer: boolean, isDryer: boolean;
nbAvailable: number, nbAvailable: number;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -61,7 +58,7 @@ class ProxiwashListItem extends React.Component<PropsType> {
); );
} }
render(): React.Node { render() {
const {props} = this; const {props} = this;
const subtitle = `${props.nbAvailable} ${ const subtitle = `${props.nbAvailable} ${
props.nbAvailable <= 1 props.nbAvailable <= 1

View file

@ -17,27 +17,27 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {Image, TouchableWithoutFeedback, View} from 'react-native'; import {Image, TouchableWithoutFeedback, View, ViewStyle} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; import {AnimatableProperties} from 'react-native-animatable';
export type AnimatableViewRefType = {current: null | Animatable.View}; export type AnimatableViewRefType = {
current: null | (typeof Animatable.View & View);
};
type PropsType = { type PropsType = {
emotion?: number, emotion?: MASCOT_STYLE;
animated?: boolean, animated?: boolean;
style?: ViewStyle | null, style?: ViewStyle;
entryAnimation?: Animatable.AnimatableProperties | null, entryAnimation?: AnimatableProperties<ViewStyle>;
loopAnimation?: Animatable.AnimatableProperties | null, loopAnimation?: AnimatableProperties<ViewStyle>;
onPress?: null | ((viewRef: AnimatableViewRefType) => void), onPress?: null | ((viewRef: AnimatableViewRefType) => void);
onLongPress?: null | ((viewRef: AnimatableViewRefType) => void), onLongPress?: null | ((viewRef: AnimatableViewRefType) => void);
}; };
type StateType = { type StateType = {
currentEmotion: number, currentEmotion: MASCOT_STYLE;
}; };
const MASCOT_IMAGE = require('../../../assets/mascot/mascot.png'); const MASCOT_IMAGE = require('../../../assets/mascot/mascot.png');
@ -50,32 +50,32 @@ const MASCOT_EYES_ANGRY = require('../../../assets/mascot/mascot_eyes_angry.png'
const MASCOT_GLASSES = require('../../../assets/mascot/mascot_glasses.png'); const MASCOT_GLASSES = require('../../../assets/mascot/mascot_glasses.png');
const MASCOT_SUNGLASSES = require('../../../assets/mascot/mascot_sunglasses.png'); const MASCOT_SUNGLASSES = require('../../../assets/mascot/mascot_sunglasses.png');
export const EYE_STYLE = { enum EYE_STYLE {
NORMAL: 0, NORMAL,
GIRLY: 2, GIRLY,
CUTE: 3, CUTE,
WINK: 4, WINK,
HEART: 5, HEART,
ANGRY: 6, ANGRY,
}; }
const GLASSES_STYLE = { enum GLASSES_STYLE {
NORMAL: 0, NORMAL,
COOl: 1, COOl,
}; }
export const MASCOT_STYLE = { export enum MASCOT_STYLE {
NORMAL: 0, NORMAL,
HAPPY: 1, HAPPY,
GIRLY: 2, GIRLY,
WINK: 3, WINK,
CUTE: 4, CUTE,
INTELLO: 5, INTELLO,
LOVE: 6, LOVE,
COOL: 7, COOL,
ANGRY: 8, ANGRY,
RANDOM: 999, RANDOM = 999,
}; }
class Mascot extends React.Component<PropsType, StateType> { class Mascot extends React.Component<PropsType, StateType> {
static defaultProps = { static defaultProps = {
@ -100,9 +100,9 @@ class Mascot extends React.Component<PropsType, StateType> {
viewRef: AnimatableViewRefType; viewRef: AnimatableViewRefType;
eyeList: {[key: number]: number | string}; eyeList: {[key in EYE_STYLE]: number};
glassesList: {[key: number]: number | string}; glassesList: {[key in GLASSES_STYLE]: number};
onPress: (viewRef: AnimatableViewRefType) => void; onPress: (viewRef: AnimatableViewRefType) => void;
@ -113,23 +113,25 @@ class Mascot extends React.Component<PropsType, StateType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.viewRef = React.createRef(); this.viewRef = React.createRef();
this.eyeList = {}; this.eyeList = {
this.glassesList = {}; [EYE_STYLE.NORMAL]: MASCOT_EYES_NORMAL,
this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL; [EYE_STYLE.GIRLY]: MASCOT_EYES_GIRLY,
this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY; [EYE_STYLE.CUTE]: MASCOT_EYES_CUTE,
this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE; [EYE_STYLE.WINK]: MASCOT_EYES_WINK,
this.eyeList[EYE_STYLE.WINK] = MASCOT_EYES_WINK; [EYE_STYLE.HEART]: MASCOT_EYES_HEART,
this.eyeList[EYE_STYLE.HEART] = MASCOT_EYES_HEART; [EYE_STYLE.ANGRY]: MASCOT_EYES_ANGRY,
this.eyeList[EYE_STYLE.ANGRY] = MASCOT_EYES_ANGRY; };
this.glassesList = {
[GLASSES_STYLE.NORMAL]: MASCOT_GLASSES,
[GLASSES_STYLE.COOl]: MASCOT_SUNGLASSES,
};
this.initialEmotion = props.emotion
? props.emotion
: Mascot.defaultProps.emotion;
this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES; if (this.initialEmotion === MASCOT_STYLE.RANDOM) {
this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES;
this.initialEmotion =
props.emotion != null ? props.emotion : Mascot.defaultProps.emotion;
if (this.initialEmotion === MASCOT_STYLE.RANDOM)
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1; this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
}
this.state = { this.state = {
currentEmotion: this.initialEmotion, currentEmotion: this.initialEmotion,
@ -138,29 +140,33 @@ class Mascot extends React.Component<PropsType, StateType> {
if (props.onPress == null) { if (props.onPress == null) {
this.onPress = (viewRef: AnimatableViewRefType) => { this.onPress = (viewRef: AnimatableViewRefType) => {
const ref = viewRef.current; const ref = viewRef.current;
if (ref != null) { if (ref && ref.rubberBand) {
this.setState({currentEmotion: MASCOT_STYLE.LOVE}); this.setState({currentEmotion: MASCOT_STYLE.LOVE});
ref.rubberBand(1500).then(() => { ref.rubberBand(1500).then(() => {
this.setState({currentEmotion: this.initialEmotion}); this.setState({currentEmotion: this.initialEmotion});
}); });
} }
}; };
} else this.onPress = props.onPress; } else {
this.onPress = props.onPress;
}
if (props.onLongPress == null) { if (props.onLongPress == null) {
this.onLongPress = (viewRef: AnimatableViewRefType) => { this.onLongPress = (viewRef: AnimatableViewRefType) => {
const ref = viewRef.current; const ref = viewRef.current;
if (ref != null) { if (ref && ref.tada) {
this.setState({currentEmotion: MASCOT_STYLE.ANGRY}); this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
ref.tada(1000).then(() => { ref.tada(1000).then(() => {
this.setState({currentEmotion: this.initialEmotion}); this.setState({currentEmotion: this.initialEmotion});
}); });
} }
}; };
} else this.onLongPress = props.onLongPress; } else {
this.onLongPress = props.onLongPress;
}
} }
getGlasses(style: number): React.Node { getGlasses(style: GLASSES_STYLE) {
const glasses = this.glassesList[style]; const glasses = this.glassesList[style];
return ( return (
<Image <Image
@ -179,11 +185,7 @@ class Mascot extends React.Component<PropsType, StateType> {
); );
} }
getEye( getEye(style: EYE_STYLE, isRight: boolean, rotation: string = '0deg') {
style: number,
isRight: boolean,
rotation: string = '0deg',
): React.Node {
const eye = this.eyeList[style]; const eye = this.eyeList[style];
return ( return (
<Image <Image
@ -201,7 +203,7 @@ class Mascot extends React.Component<PropsType, StateType> {
); );
} }
getEyes(emotion: number): React.Node { getEyes(emotion: MASCOT_STYLE) {
const final = []; const final = [];
final.push( final.push(
<View <View
@ -246,7 +248,7 @@ class Mascot extends React.Component<PropsType, StateType> {
return final; return final;
} }
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
const entryAnimation = props.animated ? props.entryAnimation : null; const entryAnimation = props.animated ? props.entryAnimation : null;
const loopAnimation = props.animated ? props.loopAnimation : null; const loopAnimation = props.animated ? props.loopAnimation : null;
@ -256,7 +258,6 @@ class Mascot extends React.Component<PropsType, StateType> {
aspectRatio: 1, aspectRatio: 1,
...props.style, ...props.style,
}} }}
// eslint-disable-next-line react/jsx-props-no-spreading
{...entryAnimation}> {...entryAnimation}>
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress={() => { onPress={() => {
@ -266,9 +267,7 @@ class Mascot extends React.Component<PropsType, StateType> {
this.onLongPress(this.viewRef); this.onLongPress(this.viewRef);
}}> }}>
<Animatable.View ref={this.viewRef}> <Animatable.View ref={this.viewRef}>
<Animatable.View <Animatable.View {...loopAnimation}>
// eslint-disable-next-line react/jsx-props-no-spreading
{...loopAnimation}>
<Image <Image
source={MASCOT_IMAGE} source={MASCOT_IMAGE}
style={{ style={{

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import { import {
Avatar, Avatar,
@ -37,48 +35,42 @@ import {
View, View,
} from 'react-native'; } from 'react-native';
import Mascot from './Mascot'; import Mascot from './Mascot';
import type {CustomThemeType} from '../../managers/ThemeManager';
import SpeechArrow from './SpeechArrow'; import SpeechArrow from './SpeechArrow';
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import AsyncStorageManager from '../../managers/AsyncStorageManager';
type PropsType = { type PropsType = {
theme: CustomThemeType, theme: ReactNativePaper.Theme;
icon: string, icon: string;
title: string, title: string;
message: string, message: string;
buttons: { buttons: {
action: { action?: {
message: string, message: string;
icon: string | null, icon?: string;
color: string | null, color?: string;
onPress?: () => void, onPress?: () => void;
}, };
cancel: { cancel?: {
message: string, message: string;
icon: string | null, icon?: string;
color: string | null, color?: string;
onPress?: () => void, onPress?: () => void;
}, };
}, };
emotion: number, emotion: number;
visible?: boolean, visible?: boolean;
prefKey?: string, prefKey?: string;
}; };
type StateType = { type StateType = {
shouldRenderDialog: boolean, // Used to stop rendering after hide animation shouldRenderDialog: boolean; // Used to stop rendering after hide animation
dialogVisible: boolean, dialogVisible: boolean;
}; };
/** /**
* Component used to display a popup with the mascot. * Component used to display a popup with the mascot.
*/ */
class MascotPopup extends React.Component<PropsType, StateType> { class MascotPopup extends React.Component<PropsType, StateType> {
static defaultProps = {
visible: null,
prefKey: null,
};
mascotSize: number; mascotSize: number;
windowWidth: number; windowWidth: number;
@ -112,7 +104,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
} }
} }
componentDidMount(): * { componentDidMount() {
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid, this.onBackButtonPressAndroid,
@ -146,14 +138,20 @@ class MascotPopup extends React.Component<PropsType, StateType> {
if (state.dialogVisible) { if (state.dialogVisible) {
const {cancel} = props.buttons; const {cancel} = props.buttons;
const {action} = props.buttons; const {action} = props.buttons;
if (cancel != null) this.onDismiss(cancel.onPress); if (cancel) {
else this.onDismiss(action.onPress); this.onDismiss(cancel.onPress);
} else if (action) {
this.onDismiss(action.onPress);
} else {
this.onDismiss();
}
return true; return true;
} }
return false; return false;
}; };
getSpeechBubble(): React.Node { getSpeechBubble() {
const {state, props} = this; const {state, props} = this;
return ( return (
<Animatable.View <Animatable.View
@ -179,7 +177,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
title={props.title} title={props.title}
left={ left={
props.icon != null props.icon != null
? (): React.Node => ( ? () => (
<Avatar.Icon <Avatar.Icon
size={48} size={48}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
@ -187,7 +185,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
icon={props.icon} icon={props.icon}
/> />
) )
: null : undefined
} }
/> />
<Card.Content <Card.Content
@ -207,7 +205,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
); );
} }
getMascot(): React.Node { getMascot() {
const {props, state} = this; const {props, state} = this;
return ( return (
<Animatable.View <Animatable.View
@ -223,7 +221,7 @@ class MascotPopup extends React.Component<PropsType, StateType> {
); );
} }
getButtons(): React.Node { getButtons() {
const {props} = this; const {props} = this;
const {action} = props.buttons; const {action} = props.buttons;
const {cancel} = props.buttons; const {cancel} = props.buttons;
@ -270,12 +268,12 @@ class MascotPopup extends React.Component<PropsType, StateType> {
); );
} }
getBackground(): React.Node { getBackground() {
const {props, state} = this; const {props, state} = this;
return ( return (
<TouchableWithoutFeedback <TouchableWithoutFeedback
onPress={() => { onPress={() => {
this.onDismiss(props.buttons.cancel.onPress); this.onDismiss(props.buttons.cancel?.onPress);
}}> }}>
<Animatable.View <Animatable.View
style={{ style={{
@ -298,10 +296,12 @@ class MascotPopup extends React.Component<PropsType, StateType> {
AsyncStorageManager.set(prefKey, false); AsyncStorageManager.set(prefKey, false);
this.setState({dialogVisible: false}); this.setState({dialogVisible: false});
} }
if (callback != null) callback(); if (callback != null) {
callback();
}
}; };
render(): React.Node { render() {
const {shouldRenderDialog} = this.state; const {shouldRenderDialog} = this.state;
if (shouldRenderDialog) { if (shouldRenderDialog) {
return ( return (

View file

@ -1,62 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {View} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
type PropsType = {
style?: ViewStyle | null,
size: number,
color: string,
};
export default class SpeechArrow extends React.Component<PropsType> {
static defaultProps = {
style: null,
};
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return (
<View style={props.style}>
<View
style={{
width: 0,
height: 0,
borderLeftWidth: 0,
borderRightWidth: props.size,
borderBottomWidth: props.size,
borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: props.color,
}}
/>
</View>
);
}
}

View file

@ -17,35 +17,32 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View, ViewStyle} from 'react-native';
import {withTheme} from 'react-native-paper';
export type CellType = {color: string, isEmpty: boolean, key: string};
type PropsType = { type PropsType = {
cell: CellType, style?: ViewStyle;
size: number;
color: string;
}; };
class CellComponent extends React.PureComponent<PropsType> { export default function SpeechArrow(props: PropsType) {
render(): React.Node {
const {props} = this;
const item = props.cell;
return ( return (
<View style={props.style}>
<View <View
style={{ style={{
flex: 1, width: 0,
backgroundColor: item.isEmpty ? 'transparent' : item.color, height: 0,
borderColor: 'transparent', borderLeftWidth: 0,
borderRadius: 4, borderRightWidth: props.size,
borderWidth: 1, borderBottomWidth: props.size,
aspectRatio: 1, borderStyle: 'solid',
backgroundColor: 'transparent',
borderLeftColor: 'transparent',
borderRightColor: 'transparent',
borderBottomColor: props.color,
}} }}
/> />
</View>
); );
}
} }
export default withTheme(CellComponent);

View file

@ -17,33 +17,29 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {TouchableRipple} from 'react-native-paper'; import {TouchableRipple} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack';
import {Image} from 'react-native-animatable'; import {Image} from 'react-native-animatable';
import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet'; import {useNavigation} from '@react-navigation/native';
import {ViewStyle} from 'react-native';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, images: Array<{url: string}>;
images: Array<{url: string}>, style: ViewStyle;
style: ViewStyleProp,
}; };
class ImageGalleryButton extends React.Component<PropsType> { function ImageGalleryButton(props: PropsType) {
onPress = () => { const navigation = useNavigation();
const {navigation, images} = this.props;
navigation.navigate('gallery', {images}); const onPress = () => {
navigation.navigate('gallery', {images: props.images});
}; };
render(): React.Node {
const {style, images} = this.props;
return ( return (
<TouchableRipple onPress={this.onPress} style={style}> <TouchableRipple onPress={onPress} style={props.style}>
<Image <Image
resizeMode="contain" resizeMode="contain"
source={{uri: images[0].url}} source={{uri: props.images[0].url}}
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
@ -51,7 +47,6 @@ class ImageGalleryButton extends React.Component<PropsType> {
/> />
</TouchableRipple> </TouchableRipple>
); );
}
} }
export default ImageGalleryButton; export default ImageGalleryButton;

View file

@ -1,82 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
import {Agenda} from 'react-native-calendars';
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = {
theme: CustomThemeType,
onRef: (ref: Agenda) => void,
};
/**
* Abstraction layer for Agenda component, using custom configuration
*/
class CustomAgenda extends React.Component<PropsType> {
getAgenda(): React.Node {
const {props} = this;
return (
<Agenda
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={props.onRef}
theme={{
backgroundColor: props.theme.colors.agendaBackgroundColor,
calendarBackground: props.theme.colors.background,
textSectionTitleColor: props.theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: props.theme.colors.primary,
selectedDayTextColor: '#ffffff',
todayTextColor: props.theme.colors.primary,
dayTextColor: props.theme.colors.text,
textDisabledColor: props.theme.colors.agendaDayTextColor,
dotColor: props.theme.colors.primary,
selectedDotColor: '#ffffff',
arrowColor: 'orange',
monthTextColor: props.theme.colors.primary,
indicatorColor: props.theme.colors.primary,
textDayFontWeight: '300',
textMonthFontWeight: 'bold',
textDayHeaderFontWeight: '300',
textDayFontSize: 16,
textMonthFontSize: 16,
textDayHeaderFontSize: 16,
agendaDayTextColor: props.theme.colors.agendaDayTextColor,
agendaDayNumColor: props.theme.colors.agendaDayTextColor,
agendaTodayColor: props.theme.colors.primary,
agendaKnobColor: props.theme.colors.primary,
}}
/>
);
}
render(): React.Node {
const {props} = this;
// Completely recreate the component on theme change to force theme reload
if (props.theme.dark)
return <View style={{flex: 1}}>{this.getAgenda()}</View>;
return this.getAgenda();
}
}
export default withTheme(CustomAgenda);

View file

@ -0,0 +1,75 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {View} from 'react-native';
import {useTheme} from 'react-native-paper';
import {Agenda, AgendaProps} from 'react-native-calendars';
type PropsType = {
onRef: (ref: Agenda<any>) => void;
} & AgendaProps<any>;
/**
* Abstraction layer for Agenda component, using custom configuration
*/
function CustomAgenda(props: PropsType) {
const theme = useTheme();
function getAgenda() {
return (
<Agenda
{...props}
ref={props.onRef}
theme={{
backgroundColor: theme.colors.agendaBackgroundColor,
calendarBackground: theme.colors.background,
textSectionTitleColor: theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: theme.colors.primary,
selectedDayTextColor: '#ffffff',
todayTextColor: theme.colors.primary,
dayTextColor: theme.colors.text,
textDisabledColor: theme.colors.agendaDayTextColor,
dotColor: theme.colors.primary,
selectedDotColor: '#ffffff',
arrowColor: 'orange',
monthTextColor: theme.colors.primary,
indicatorColor: theme.colors.primary,
textDayFontWeight: '300',
textMonthFontWeight: 'bold',
textDayHeaderFontWeight: '300',
textDayFontSize: 16,
textMonthFontSize: 16,
textDayHeaderFontSize: 16,
agendaDayTextColor: theme.colors.agendaDayTextColor,
agendaDayNumColor: theme.colors.agendaDayTextColor,
agendaTodayColor: theme.colors.primary,
agendaKnobColor: theme.colors.primary,
}}
/>
);
}
// Completely recreate the component on theme change to force theme reload
if (theme.dark) {
return <View style={{flex: 1}}>{getAgenda()}</View>;
}
return getAgenda();
}
export default CustomAgenda;

View file

@ -1,77 +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 <https://www.gnu.org/licenses/>.
*/
/* eslint-disable flowtype/require-parameter-type */
// @flow
import * as React from 'react';
import {Text, withTheme} from 'react-native-paper';
import HTML from 'react-native-render-html';
import {Linking} from 'react-native';
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = {
theme: CustomThemeType,
html: string,
};
/**
* Abstraction layer for Agenda component, using custom configuration
*/
class CustomHTML extends React.Component<PropsType> {
openWebLink = (event: {...}, link: string) => {
Linking.openURL(link);
};
getBasicText = (
htmlAttribs,
children,
convertedCSSStyles,
passProps,
): React.Node => {
// eslint-disable-next-line react/jsx-props-no-spreading
return <Text {...passProps}>{children}</Text>;
};
getListBullet = (): React.Node => {
return <Text>- </Text>;
};
render(): React.Node {
const {props} = this;
// Surround description with p to allow text styling if the description is not html
return (
<HTML
html={`<p>${props.html}</p>`}
renderers={{
p: this.getBasicText,
li: this.getBasicText,
}}
listsPrefixesRenderers={{
ul: this.getListBullet,
}}
ignoredTags={['img']}
ignoredStyles={['color', 'background-color']}
onLinkPress={this.openWebLink}
/>
);
}
}
export default withTheme(CustomHTML);

View file

@ -0,0 +1,68 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Text} from 'react-native-paper';
import HTML from 'react-native-render-html';
import {GestureResponderEvent, Linking} from 'react-native';
type PropsType = {
html: string;
};
/**
* Abstraction layer for Agenda component, using custom configuration
*/
function CustomHTML(props: PropsType) {
const openWebLink = (event: GestureResponderEvent, link: string) => {
Linking.openURL(link);
};
const getBasicText = (
htmlAttribs: any,
children: any,
convertedCSSStyles: any,
passProps: any,
) => {
return <Text {...passProps}>{children}</Text>;
};
const getListBullet = () => {
return <Text>- </Text>;
};
// Surround description with p to allow text styling if the description is not html
return (
<HTML
html={`<p>${props.html}</p>`}
renderers={{
p: getBasicText,
li: getBasicText,
}}
listsPrefixesRenderers={{
ul: getListBullet,
}}
ignoredTags={['img']}
ignoredStyles={['color', 'background-color']}
onLinkPress={openWebLink}
/>
);
}
export default CustomHTML;

View file

@ -17,39 +17,33 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {HeaderButton, HeaderButtons} from 'react-navigation-header-buttons'; import {
import {withTheme} from 'react-native-paper'; HeaderButton,
import type {CustomThemeType} from '../../managers/ThemeManager'; HeaderButtonProps,
HeaderButtons,
HeaderButtonsProps,
} from 'react-navigation-header-buttons';
import {useTheme} from 'react-native-paper';
const MaterialHeaderButton = (props: { const MaterialHeaderButton = (props: HeaderButtonProps) => {
theme: CustomThemeType, const theme = useTheme();
color: string,
}): React.Node => {
const {color, theme} = props;
return ( return (
// $FlowFixMe
<HeaderButton <HeaderButton
// eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}
IconComponent={MaterialCommunityIcons} IconComponent={MaterialCommunityIcons}
iconSize={26} iconSize={26}
color={color != null ? color : theme.colors.text} color={props.color ? props.color : theme.colors.text}
/> />
); );
}; };
const MaterialHeaderButtons = (props: {...}): React.Node => { const MaterialHeaderButtons = (
props: HeaderButtonsProps & {children?: React.ReactNode},
) => {
return ( return (
// $FlowFixMe <HeaderButtons {...props} HeaderButtonComponent={MaterialHeaderButton} />
<HeaderButtons
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
/>
); );
}; };

View file

@ -17,10 +17,14 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Platform, StatusBar, StyleSheet, View} from 'react-native'; import {
ListRenderItemInfo,
Platform,
StatusBar,
StyleSheet,
View,
} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AppIntroSlider from 'react-native-app-intro-slider'; import AppIntroSlider from 'react-native-app-intro-slider';
@ -35,26 +39,27 @@ import IntroIcon from '../Intro/IconIntro';
import MascotIntroEnd from '../Intro/MascotIntroEnd'; import MascotIntroEnd from '../Intro/MascotIntroEnd';
type PropsType = { type PropsType = {
onDone: () => void, onDone: () => void;
isUpdate: boolean, isUpdate: boolean;
isAprilFools: boolean, isAprilFools: boolean;
}; };
type StateType = { type StateType = {
currentSlide: number, currentSlide: number;
}; };
export type IntroSlideType = { export type IntroSlideType = {
key: string, key: string;
title: string, title: string;
text: string, text: string;
view: () => React.Node, view: () => React.ReactNode;
mascotStyle?: number, mascotStyle?: number;
colors: [string, string], colors: [string, string];
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
mainContent: { mainContent: {
flex: 1,
paddingBottom: 100, paddingBottom: 100,
}, },
text: { text: {
@ -83,7 +88,7 @@ const styles = StyleSheet.create({
*/ */
export default class CustomIntroSlider extends React.Component< export default class CustomIntroSlider extends React.Component<
PropsType, PropsType,
StateType, StateType
> { > {
sliderRef: {current: null | AppIntroSlider}; sliderRef: {current: null | AppIntroSlider};
@ -98,8 +103,9 @@ export default class CustomIntroSlider extends React.Component<
/** /**
* Generates intro slides * Generates intro slides
*/ */
constructor() { constructor(props: PropsType) {
super(); super(props);
this.currentSlides = [];
this.state = { this.state = {
currentSlide: 0, currentSlide: 0,
}; };
@ -109,14 +115,14 @@ export default class CustomIntroSlider extends React.Component<
key: '0', // Mascot key: '0', // Mascot
title: i18n.t('intro.slideMain.title'), title: i18n.t('intro.slideMain.title'),
text: i18n.t('intro.slideMain.text'), text: i18n.t('intro.slideMain.text'),
view: (): React.Node => <MascotIntroWelcome />, view: () => <MascotIntroWelcome />,
colors: ['#be1522', '#57080e'], colors: ['#be1522', '#57080e'],
}, },
{ {
key: '1', key: '1',
title: i18n.t('intro.slidePlanex.title'), title: i18n.t('intro.slidePlanex.title'),
text: i18n.t('intro.slidePlanex.text'), text: i18n.t('intro.slidePlanex.text'),
view: (): React.Node => <IntroIcon icon="calendar-clock" />, view: () => <IntroIcon icon="calendar-clock" />,
mascotStyle: MASCOT_STYLE.INTELLO, mascotStyle: MASCOT_STYLE.INTELLO,
colors: ['#be1522', '#57080e'], colors: ['#be1522', '#57080e'],
}, },
@ -124,7 +130,7 @@ export default class CustomIntroSlider extends React.Component<
key: '2', key: '2',
title: i18n.t('intro.slideEvents.title'), title: i18n.t('intro.slideEvents.title'),
text: i18n.t('intro.slideEvents.text'), text: i18n.t('intro.slideEvents.text'),
view: (): React.Node => <IntroIcon icon="calendar-star" />, view: () => <IntroIcon icon="calendar-star" />,
mascotStyle: MASCOT_STYLE.HAPPY, mascotStyle: MASCOT_STYLE.HAPPY,
colors: ['#be1522', '#57080e'], colors: ['#be1522', '#57080e'],
}, },
@ -132,7 +138,7 @@ export default class CustomIntroSlider extends React.Component<
key: '3', key: '3',
title: i18n.t('intro.slideServices.title'), title: i18n.t('intro.slideServices.title'),
text: i18n.t('intro.slideServices.text'), text: i18n.t('intro.slideServices.text'),
view: (): React.Node => <IntroIcon icon="view-dashboard-variant" />, view: () => <IntroIcon icon="view-dashboard-variant" />,
mascotStyle: MASCOT_STYLE.CUTE, mascotStyle: MASCOT_STYLE.CUTE,
colors: ['#be1522', '#57080e'], colors: ['#be1522', '#57080e'],
}, },
@ -140,7 +146,7 @@ export default class CustomIntroSlider extends React.Component<
key: '4', key: '4',
title: i18n.t('intro.slideDone.title'), title: i18n.t('intro.slideDone.title'),
text: i18n.t('intro.slideDone.text'), text: i18n.t('intro.slideDone.text'),
view: (): React.Node => <MascotIntroEnd />, view: () => <MascotIntroEnd />,
colors: ['#9c165b', '#3e042b'], colors: ['#9c165b', '#3e042b'],
}, },
]; ];
@ -152,7 +158,7 @@ export default class CustomIntroSlider extends React.Component<
key: '1', key: '1',
title: i18n.t('intro.aprilFoolsSlide.title'), title: i18n.t('intro.aprilFoolsSlide.title'),
text: i18n.t('intro.aprilFoolsSlide.text'), text: i18n.t('intro.aprilFoolsSlide.text'),
view: (): React.Node => <View />, view: () => <View />,
mascotStyle: MASCOT_STYLE.NORMAL, mascotStyle: MASCOT_STYLE.NORMAL,
colors: ['#e01928', '#be1522'], colors: ['#e01928', '#be1522'],
}, },
@ -162,21 +168,21 @@ export default class CustomIntroSlider extends React.Component<
/** /**
* Render item to be used for the intro introSlides * Render item to be used for the intro introSlides
* *
* @param item The item to be displayed * @param data
* @param dimensions Dimensions of the item
*/ */
getIntroRenderItem = ({ getIntroRenderItem = (
item, data:
dimensions, | (ListRenderItemInfo<IntroSlideType> & {
}: { dimensions: {width: number; height: number};
item: IntroSlideType, })
dimensions: {width: number, height: number}, | ListRenderItemInfo<IntroSlideType>,
}): React.Node => { ) => {
const item = data.item;
const {state} = this; const {state} = this;
const index = parseInt(item.key, 10); const index = parseInt(item.key, 10);
return ( return (
<LinearGradient <LinearGradient
style={[styles.mainContent, dimensions]} style={[styles.mainContent]}
colors={item.colors} colors={item.colors}
start={{x: 0, y: 0.1}} start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}> end={{x: 0.1, y: 1}}>
@ -254,7 +260,9 @@ export default class CustomIntroSlider extends React.Component<
}; };
static setStatusBarColor(color: string) { static setStatusBarColor(color: string) {
if (Platform.OS === 'android') StatusBar.setBackgroundColor(color, true); if (Platform.OS === 'android') {
StatusBar.setBackgroundColor(color, true);
}
} }
onSlideChange = (index: number) => { onSlideChange = (index: number) => {
@ -266,8 +274,9 @@ export default class CustomIntroSlider extends React.Component<
CustomIntroSlider.setStatusBarColor( CustomIntroSlider.setStatusBarColor(
this.currentSlides[this.currentSlides.length - 1].colors[0], this.currentSlides[this.currentSlides.length - 1].colors[0],
); );
if (this.sliderRef.current != null) if (this.sliderRef.current != null) {
this.sliderRef.current.goToSlide(this.currentSlides.length - 1); this.sliderRef.current.goToSlide(this.currentSlides.length - 1);
}
}; };
onDone = () => { onDone = () => {
@ -278,7 +287,7 @@ export default class CustomIntroSlider extends React.Component<
props.onDone(); props.onDone();
}; };
getRenderNextButton = (): React.Node => { getRenderNextButton = () => {
return ( return (
<Animatable.View <Animatable.View
useNativeDriver useNativeDriver
@ -293,7 +302,7 @@ export default class CustomIntroSlider extends React.Component<
); );
}; };
getRenderDoneButton = (): React.Node => { getRenderDoneButton = () => {
return ( return (
<Animatable.View <Animatable.View
useNativeDriver useNativeDriver
@ -308,11 +317,14 @@ export default class CustomIntroSlider extends React.Component<
); );
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
this.currentSlides = this.introSlides; this.currentSlides = this.introSlides;
if (props.isUpdate) this.currentSlides = this.updateSlides; if (props.isUpdate) {
else if (props.isAprilFools) this.currentSlides = this.aprilFoolsSlides; this.currentSlides = this.updateSlides;
} else if (props.isAprilFools) {
this.currentSlides = this.aprilFoolsSlides;
}
CustomIntroSlider.setStatusBarColor(this.currentSlides[0].colors[0]); CustomIntroSlider.setStatusBarColor(this.currentSlides[0].colors[0]);
return ( return (
<AppIntroSlider <AppIntroSlider

View file

@ -17,14 +17,11 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {withTheme} from 'react-native-paper'; import {useTheme} from 'react-native-paper';
import {Modalize} from 'react-native-modalize'; import {Modalize} from 'react-native-modalize';
import {View} from 'react-native-animatable'; import {View} from 'react-native-animatable';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
import type {CustomThemeType} from '../../managers/ThemeManager';
/** /**
* Abstraction layer for Modalize component, using custom configuration * Abstraction layer for Modalize component, using custom configuration
@ -33,11 +30,11 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
* @return {*} * @return {*}
*/ */
function CustomModal(props: { function CustomModal(props: {
theme: CustomThemeType, onRef: (re: Modalize) => void;
onRef: (re: Modalize) => void, children?: React.ReactNode;
children?: React.Node, }) {
}): React.Node { const theme = useTheme();
const {theme, onRef, children} = props; const {onRef, children} = props;
return ( return (
<Modalize <Modalize
ref={onRef} ref={onRef}
@ -55,6 +52,4 @@ function CustomModal(props: {
); );
} }
CustomModal.defaultProps = {children: null}; export default CustomModal;
export default withTheme(CustomModal);

View file

@ -1,84 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Text, withTheme} from 'react-native-paper';
import {View} from 'react-native-animatable';
import Slider, {SliderProps} from '@react-native-community/slider';
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = {
theme: CustomThemeType,
valueSuffix?: string,
...SliderProps,
};
type StateType = {
currentValue: number,
};
/**
* Abstraction layer for Modalize component, using custom configuration
*
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
* @return {*}
*/
class CustomSlider extends React.Component<PropsType, StateType> {
static defaultProps = {
valueSuffix: '',
};
constructor(props: PropsType) {
super(props);
this.state = {
currentValue: props.value,
};
}
onValueChange = (value: number) => {
const {props} = this;
this.setState({currentValue: value});
if (props.onValueChange != null) props.onValueChange(value);
};
render(): React.Node {
const {props, state} = this;
return (
<View style={{flex: 1, flexDirection: 'row'}}>
<Text
style={{
marginHorizontal: 10,
marginTop: 'auto',
marginBottom: 'auto',
}}>
{state.currentValue}min
</Text>
<Slider
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
onValueChange={this.onValueChange}
/>
</View>
);
}
}
export default withTheme(CustomSlider);

View file

@ -0,0 +1,61 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Text} from 'react-native-paper';
import {View} from 'react-native-animatable';
import Slider, {SliderProps} from '@react-native-community/slider';
import {useState} from 'react';
type PropsType = {
valueSuffix?: string;
} & SliderProps;
/**
* Abstraction layer for Modalize component, using custom configuration
*
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
* @return {*}
*/
function CustomSlider(props: PropsType) {
const [currentValue, setCurrentValue] = useState(props.value);
const onValueChange = (value: number) => {
setCurrentValue(value);
if (props.onValueChange) {
props.onValueChange(value);
}
};
return (
<View style={{flex: 1, flexDirection: 'row'}}>
<Text
style={{
marginHorizontal: 10,
marginTop: 'auto',
marginBottom: 'auto',
}}>
{currentValue}min
</Text>
<Slider {...props} ref={undefined} onValueChange={onValueChange} />
</View>
);
}
export default CustomSlider;

View file

@ -21,8 +21,11 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {ActivityIndicator, withTheme} from 'react-native-paper'; import {ActivityIndicator, useTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../managers/ThemeManager';
type Props = {
isAbsolute?: boolean;
};
/** /**
* Component used to display a header button * Component used to display a header button
@ -30,29 +33,21 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
* @param props Props to pass to the component * @param props Props to pass to the component
* @return {*} * @return {*}
*/ */
function BasicLoadingScreen(props: { export default function BasicLoadingScreen(props: Props) {
theme: CustomThemeType, const theme = useTheme();
isAbsolute: boolean, const {isAbsolute} = props;
}): React.Node {
const {theme, isAbsolute} = props;
const {colors} = theme;
let position;
if (isAbsolute != null && isAbsolute) position = 'absolute';
return ( return (
<View <View
style={{ style={{
backgroundColor: colors.background, backgroundColor: theme.colors.background,
position, position: isAbsolute ? 'absolute' : 'relative',
top: 0, top: 0,
right: 0, right: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
justifyContent: 'center', justifyContent: 'center',
}}> }}>
<ActivityIndicator animating size="large" color={colors.primary} /> <ActivityIndicator animating size="large" color={theme.colors.primary} />
</View> </View>
); );
} }
export default withTheme(BasicLoadingScreen);

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Subheading, withTheme} from 'react-native-paper'; import {Button, Subheading, withTheme} from 'react-native-paper';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from 'react-native';
@ -27,17 +25,16 @@ import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import {ERROR_TYPE} from '../../utils/WebData'; import {ERROR_TYPE} from '../../utils/WebData';
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation?: StackNavigationProp<any>;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
route: {name: string}, route?: {name: string};
onRefresh?: () => void, onRefresh?: () => void;
errorCode?: number, errorCode?: number;
icon?: string, icon?: string;
message?: string, message?: string;
showRetryButton?: boolean, showRetryButton?: boolean;
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -82,9 +79,11 @@ class ErrorView extends React.PureComponent<PropsType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.icon = ''; this.icon = '';
this.showLoginButton = false;
this.message = '';
} }
getRetryButton(): React.Node { getRetryButton() {
const {props} = this; const {props} = this;
return ( return (
<Button <Button
@ -97,7 +96,7 @@ class ErrorView extends React.PureComponent<PropsType> {
); );
} }
getLoginButton(): React.Node { getLoginButton() {
return ( return (
<Button <Button
mode="contained" mode="contained"
@ -111,10 +110,12 @@ class ErrorView extends React.PureComponent<PropsType> {
goToLogin = () => { goToLogin = () => {
const {props} = this; const {props} = this;
if (props.navigation) {
props.navigation.navigate('login', { props.navigation.navigate('login', {
screen: 'login', screen: 'login',
params: {nextScreen: props.route.name}, params: {nextScreen: props.route ? props.route.name : undefined},
}); });
}
}; };
generateMessage() { generateMessage() {
@ -169,13 +170,17 @@ class ErrorView extends React.PureComponent<PropsType> {
} }
} }
render(): React.Node { render() {
const {props} = this; const {props} = this;
this.generateMessage(); this.generateMessage();
let button; let button;
if (this.showLoginButton) button = this.getLoginButton(); if (this.showLoginButton) {
else if (props.showRetryButton) button = this.getRetryButton(); button = this.getLoginButton();
else button = null; } else if (props.showRetryButton) {
button = this.getRetryButton();
} else {
button = null;
}
return ( return (
<Animatable.View <Animatable.View

View file

@ -17,12 +17,15 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {Snackbar} from 'react-native-paper'; import {Snackbar} from 'react-native-paper';
import {RefreshControl, View} from 'react-native'; import {
NativeSyntheticEvent,
RefreshControl,
SectionListData,
View,
} from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {Collapsible} from 'react-navigation-collapsible'; import {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
@ -32,42 +35,44 @@ import withCollapsible from '../../utils/withCollapsible';
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from '../Tabbar/CustomTabBar';
import {ERROR_TYPE, readData} from '../../utils/WebData'; import {ERROR_TYPE, readData} from '../../utils/WebData';
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList'; import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
import type {ApiGenericDataType} from '../../utils/WebData';
export type SectionListDataType<T> = Array<{ export type SectionListDataType<ItemT> = Array<{
title: string, title: string;
data: Array<T>, icon?: string;
keyExtractor?: (T) => string, data: Array<ItemT>;
keyExtractor?: (data: ItemT) => string;
}>; }>;
type PropsType<T> = { type PropsType<ItemT, RawData> = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
fetchUrl: string, fetchUrl: string;
autoRefreshTime: number, autoRefreshTime: number;
refreshOnFocus: boolean, refreshOnFocus: boolean;
renderItem: (data: {item: T}) => React.Node, renderItem: (data: {item: ItemT}) => React.ReactNode;
createDataset: ( createDataset: (
data: ApiGenericDataType | null, data: RawData | null,
isLoading?: boolean, isLoading?: boolean,
) => SectionListDataType<T>, ) => SectionListDataType<ItemT>;
onScroll: (event: SyntheticEvent<EventTarget>) => void, onScroll: (event: NativeSyntheticEvent<EventTarget>) => void;
collapsibleStack: Collapsible, collapsibleStack: Collapsible;
showError?: boolean, showError?: boolean;
itemHeight?: number | null, itemHeight?: number | null;
updateData?: number, updateData?: number;
renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node, renderListHeaderComponent?: (
data: RawData | null,
) => React.ComponentType<any> | React.ReactElement | null;
renderSectionHeader?: ( renderSectionHeader?: (
data: {section: {title: string}}, data: {section: SectionListData<ItemT>},
isLoading?: boolean, isLoading?: boolean,
) => React.Node, ) => React.ReactElement | null;
stickyHeader?: boolean, stickyHeader?: boolean;
}; };
type StateType = { type StateType<RawData> = {
refreshing: boolean, refreshing: boolean;
fetchedData: ApiGenericDataType | null, fetchedData: RawData | null;
snackbarVisible: boolean, snackbarVisible: boolean;
}; };
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
@ -78,22 +83,25 @@ const MIN_REFRESH_TIME = 5 * 1000;
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different. * This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
* To force the component to update, change the value of updateData. * To force the component to update, change the value of updateData.
*/ */
class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> { class WebSectionList<ItemT, RawData> extends React.PureComponent<
PropsType<ItemT, RawData>,
StateType<RawData>
> {
static defaultProps = { static defaultProps = {
showError: true, showError: true,
itemHeight: null, itemHeight: null,
updateData: 0, updateData: 0,
renderListHeaderComponent: (): React.Node => null, renderListHeaderComponent: () => null,
renderSectionHeader: (): React.Node => null, renderSectionHeader: () => null,
stickyHeader: false, stickyHeader: false,
}; };
refreshInterval: IntervalID; refreshInterval: NodeJS.Timeout | undefined;
lastRefresh: Date | null; lastRefresh: Date | undefined;
constructor() { constructor(props: PropsType<ItemT, RawData>) {
super(); super(props);
this.state = { this.state = {
refreshing: false, refreshing: false,
fetchedData: null, fetchedData: null,
@ -109,7 +117,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
const {navigation} = this.props; const {navigation} = this.props;
navigation.addListener('focus', this.onScreenFocus); navigation.addListener('focus', this.onScreenFocus);
navigation.addListener('blur', this.onScreenBlur); navigation.addListener('blur', this.onScreenBlur);
this.lastRefresh = null; this.lastRefresh = undefined;
this.onRefresh(); this.onRefresh();
} }
@ -121,15 +129,18 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
if (props.refreshOnFocus && this.lastRefresh) { if (props.refreshOnFocus && this.lastRefresh) {
setTimeout(this.onRefresh, 200); setTimeout(this.onRefresh, 200);
} }
if (props.autoRefreshTime > 0) if (props.autoRefreshTime > 0) {
this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime); this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
}
}; };
/** /**
* Removes any interval on un-focus * Removes any interval on un-focus
*/ */
onScreenBlur = () => { onScreenBlur = () => {
if (this.refreshInterval) {
clearInterval(this.refreshInterval); clearInterval(this.refreshInterval);
}
}; };
/** /**
@ -138,7 +149,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
* *
* @param fetchedData The newly fetched data * @param fetchedData The newly fetched data
*/ */
onFetchSuccess = (fetchedData: ApiGenericDataType) => { onFetchSuccess = (fetchedData: RawData) => {
this.setState({ this.setState({
fetchedData, fetchedData,
refreshing: false, refreshing: false,
@ -167,7 +178,9 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
if (this.lastRefresh != null) { if (this.lastRefresh != null) {
const last = this.lastRefresh; const last = this.lastRefresh;
canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME; canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
} else canRefresh = true; } else {
canRefresh = true;
}
if (canRefresh) { if (canRefresh) {
this.setState({refreshing: true}); this.setState({refreshing: true});
readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError); readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
@ -189,19 +202,18 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
}; };
getItemLayout = ( getItemLayout = (
data: T, height: number,
data: Array<SectionListData<ItemT>> | null,
index: number, index: number,
): {length: number, offset: number, index: number} | null => { ): {length: number; offset: number; index: number} => {
const {itemHeight} = this.props;
if (itemHeight == null) return null;
return { return {
length: itemHeight, length: height,
offset: itemHeight * index, offset: height * index,
index, index,
}; };
}; };
getRenderSectionHeader = (data: {section: {title: string}}): React.Node => { getRenderSectionHeader = (data: {section: SectionListData<ItemT>}) => {
const {renderSectionHeader} = this.props; const {renderSectionHeader} = this.props;
const {refreshing} = this.state; const {refreshing} = this.state;
if (renderSectionHeader != null) { if (renderSectionHeader != null) {
@ -214,7 +226,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
return null; return null;
}; };
getRenderItem = (data: {item: T}): React.Node => { getRenderItem = (data: {item: ItemT}) => {
const {renderItem} = this.props; const {renderItem} = this.props;
return ( return (
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver> <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
@ -223,19 +235,23 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
); );
}; };
onScroll = (event: SyntheticEvent<EventTarget>) => { onScroll = (event: NativeSyntheticEvent<EventTarget>) => {
const {onScroll} = this.props; const {onScroll} = this.props;
if (onScroll != null) onScroll(event); if (onScroll != null) {
onScroll(event);
}
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
let dataset = []; const {itemHeight} = props;
let dataset: SectionListDataType<ItemT> = [];
if ( if (
state.fetchedData != null || state.fetchedData != null ||
(state.fetchedData == null && !props.showError) (state.fetchedData == null && !props.showError)
) ) {
dataset = props.createDataset(state.fetchedData, state.refreshing); dataset = props.createDataset(state.fetchedData, state.refreshing);
}
const {containerPaddingTop} = props.collapsibleStack; const {containerPaddingTop} = props.collapsibleStack;
return ( return (
@ -270,7 +286,11 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
/> />
) )
} }
getItemLayout={props.itemHeight != null ? this.getItemLayout : null} getItemLayout={
itemHeight
? (data, index) => this.getItemLayout(itemHeight, data, index)
: undefined
}
onScroll={this.onScroll} onScroll={this.onScroll}
hasTab hasTab
/> />

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import WebView from 'react-native-webview'; import WebView from 'react-native-webview';
import { import {
@ -27,12 +25,17 @@ import {
OverflowMenu, OverflowMenu,
} from 'react-navigation-header-buttons'; } from 'react-navigation-header-buttons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {Animated, BackHandler, Linking} from 'react-native'; import {
Animated,
BackHandler,
Linking,
NativeScrollEvent,
NativeSyntheticEvent,
} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import {Collapsible} from 'react-navigation-collapsible'; import {Collapsible} from 'react-navigation-collapsible';
import type {CustomThemeType} from '../../managers/ThemeManager';
import withCollapsible from '../../utils/withCollapsible'; import withCollapsible from '../../utils/withCollapsible';
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton'; import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
import {ERROR_TYPE} from '../../utils/WebData'; import {ERROR_TYPE} from '../../utils/WebData';
@ -40,15 +43,15 @@ import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen'; import BasicLoadingScreen from './BasicLoadingScreen';
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
url: string, url: string;
collapsibleStack: Collapsible, collapsibleStack: Collapsible;
onMessage: (event: {nativeEvent: {data: string}}) => void, onMessage: (event: {nativeEvent: {data: string}}) => void;
onScroll: (event: SyntheticEvent<EventTarget>) => void, onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
customJS?: string, customJS?: string;
customPaddingFunction?: null | ((padding: number) => string), customPaddingFunction?: null | ((padding: number) => string);
showAdvancedControls?: boolean, showAdvancedControls?: boolean;
}; };
const AnimatedWebView = Animated.createAnimatedComponent(WebView); const AnimatedWebView = Animated.createAnimatedComponent(WebView);
@ -63,14 +66,17 @@ class WebViewScreen extends React.PureComponent<PropsType> {
customPaddingFunction: null, customPaddingFunction: null,
}; };
currentUrl: string;
webviewRef: {current: null | WebView}; webviewRef: {current: null | WebView};
canGoBack: boolean; canGoBack: boolean;
constructor() { constructor(props: PropsType) {
super(); super(props);
this.webviewRef = React.createRef(); this.webviewRef = React.createRef();
this.canGoBack = false; this.canGoBack = false;
this.currentUrl = props.url;
} }
/** /**
@ -115,7 +121,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @return {*} * @return {*}
*/ */
getBasicButton = (): React.Node => { getBasicButton = () => {
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
@ -138,7 +144,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @returns {*} * @returns {*}
*/ */
getAdvancedButtons = (): React.Node => { getAdvancedButtons = () => {
const {props} = this; const {props} = this;
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
@ -179,7 +185,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @return {*} * @return {*}
*/ */
getRenderLoading = (): React.Node => <BasicLoadingScreen isAbsolute />; getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
/** /**
* Gets the javascript needed to generate a padding on top of the page * Gets the javascript needed to generate a padding on top of the page
@ -201,25 +207,32 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* Callback to use when refresh button is clicked. Reloads the webview. * Callback to use when refresh button is clicked. Reloads the webview.
*/ */
onRefreshClicked = () => { onRefreshClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.reload(); if (this.webviewRef.current != null) {
this.webviewRef.current.reload();
}
}; };
onGoBackClicked = () => { onGoBackClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goBack(); if (this.webviewRef.current != null) {
this.webviewRef.current.goBack();
}
}; };
onGoForwardClicked = () => { onGoForwardClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goForward(); if (this.webviewRef.current != null) {
this.webviewRef.current.goForward();
}
}; };
onOpenClicked = () => { onOpenClicked = () => {
const {url} = this.props; Linking.openURL(this.currentUrl);
Linking.openURL(url);
}; };
onScroll = (event: SyntheticEvent<EventTarget>) => { onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const {onScroll} = this.props; const {onScroll} = this.props;
if (onScroll) onScroll(event); if (onScroll) {
onScroll(event);
}
}; };
/** /**
@ -228,11 +241,12 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* @param script The script to inject * @param script The script to inject
*/ */
injectJavaScript = (script: string) => { injectJavaScript = (script: string) => {
if (this.webviewRef.current != null) if (this.webviewRef.current != null) {
this.webviewRef.current.injectJavaScript(script); this.webviewRef.current.injectJavaScript(script);
}
}; };
render(): React.Node { render() {
const {props} = this; const {props} = this;
const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack; const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack;
return ( return (
@ -243,13 +257,14 @@ class WebViewScreen extends React.PureComponent<PropsType> {
injectedJavaScript={props.customJS} injectedJavaScript={props.customJS}
javaScriptEnabled javaScriptEnabled
renderLoading={this.getRenderLoading} renderLoading={this.getRenderLoading}
renderError={(): React.Node => ( renderError={() => (
<ErrorView <ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR} errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked} onRefresh={this.onRefreshClicked}
/> />
)} )}
onNavigationStateChange={(navState: {canGoBack: boolean}) => { onNavigationStateChange={(navState) => {
this.currentUrl = navState.url;
this.canGoBack = navState.canGoBack; this.canGoBack = navState.canGoBack;
}} }}
onMessage={props.onMessage} onMessage={props.onMessage}
@ -257,7 +272,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop)); this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
}} }}
// Animations // Animations
onScroll={onScrollWithListener(this.onScroll)} onScroll={(event) => onScrollWithListener(this.onScroll)(event)}
/> />
); );
} }

View file

@ -17,49 +17,33 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native'; import {Animated} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import {Collapsible} from 'react-navigation-collapsible'; import {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from '@react-navigation/stack';
import TabIcon from './TabIcon'; import TabIcon from './TabIcon';
import TabHomeIcon from './TabHomeIcon'; import TabHomeIcon from './TabHomeIcon';
import type {CustomThemeType} from '../../managers/ThemeManager'; import {BottomTabBarProps} from '@react-navigation/bottom-tabs';
import {NavigationState} from '@react-navigation/native';
import {
PartialState,
Route,
} from '@react-navigation/routers/lib/typescript/src/types';
type RouteType = { type RouteType = Route<string> & {
name: string, state?: NavigationState | PartialState<NavigationState>;
key: string,
params: {collapsible: Collapsible},
state: {
index: number,
routes: Array<RouteType>,
},
}; };
type PropsType = { interface PropsType extends BottomTabBarProps {
state: { theme: ReactNativePaper.Theme;
index: number, }
routes: Array<RouteType>,
},
descriptors: {
[key: string]: {
options: {
tabBarLabel: string,
title: string,
},
},
},
navigation: StackNavigationProp,
theme: CustomThemeType,
};
type StateType = { type StateType = {
// eslint-disable-next-line flowtype/no-weak-types translateY: any;
translateY: any,
}; };
type validRoutes = 'proxiwash' | 'services' | 'planning' | 'planex';
const TAB_ICONS = { const TAB_ICONS = {
proxiwash: 'tshirt-crew', proxiwash: 'tshirt-crew',
services: 'account-circle', services: 'account-circle',
@ -70,11 +54,13 @@ const TAB_ICONS = {
class CustomTabBar extends React.Component<PropsType, StateType> { class CustomTabBar extends React.Component<PropsType, StateType> {
static TAB_BAR_HEIGHT = 48; static TAB_BAR_HEIGHT = 48;
constructor() { constructor(props: PropsType) {
super(); super(props);
this.state = { this.state = {
translateY: new Animated.Value(0), translateY: new Animated.Value(0),
}; };
// @ts-ignore
props.navigation.addListener('state', this.onRouteChange);
} }
/** /**
@ -86,14 +72,10 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
*/ */
onItemPress(route: RouteType, currentIndex: number, destIndex: number) { onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
const {navigation} = this.props; const {navigation} = this.props;
const event = navigation.emit({ if (currentIndex !== destIndex) {
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (currentIndex !== destIndex && !event.defaultPrevented)
navigation.navigate(route.name); navigation.navigate(route.name);
} }
}
/** /**
* Navigates to tetris screen on home button long press * Navigates to tetris screen on home button long press
@ -102,14 +84,10 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
*/ */
onItemLongPress(route: RouteType) { onItemLongPress(route: RouteType) {
const {navigation} = this.props; const {navigation} = this.props;
const event = navigation.emit({ if (route.name === 'home') {
type: 'tabLongPress',
target: route.key,
canPreventDefault: true,
});
if (route.name === 'home' && !event.defaultPrevented)
navigation.navigate('game-start'); navigation.navigate('game-start');
} }
}
/** /**
* Finds the active route and syncs the tab bar animation with the header bar * Finds the active route and syncs the tab bar animation with the header bar
@ -126,11 +104,13 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
* @param focused * @param focused
* @returns {null} * @returns {null}
*/ */
getTabBarIcon = (route: RouteType, focused: boolean): React.Node => { getTabBarIcon = (route: RouteType, focused: boolean) => {
let icon = TAB_ICONS[route.name]; let icon = TAB_ICONS[route.name as validRoutes];
icon = focused ? icon : `${icon}-outline`; icon = focused ? icon : `${icon}-outline`;
if (route.name !== 'home') return icon; if (route.name !== 'home') {
return null; return icon;
}
return '';
}; };
/** /**
@ -141,14 +121,18 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
* @param index The index of the current route * @param index The index of the current route
* @returns {*} * @returns {*}
*/ */
getRenderIcon = (route: RouteType, index: number): React.Node => { getRenderIcon = (route: RouteType, index: number) => {
const {props} = this; const {props} = this;
const {state} = props; const {state} = props;
const {options} = props.descriptors[route.key]; const {options} = props.descriptors[route.key];
let label; let label;
if (options.tabBarLabel != null) label = options.tabBarLabel; if (options.tabBarLabel != null) {
else if (options.title != null) label = options.title; label = options.tabBarLabel;
else label = route.name; } else if (options.title != null) {
label = options.title;
} else {
label = route.name;
}
const onPress = () => { const onPress = () => {
this.onItemPress(route, state.index, index); this.onItemPress(route, state.index, index);
@ -168,7 +152,7 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
onLongPress={onLongPress} onLongPress={onLongPress}
icon={this.getTabBarIcon(route, isFocused)} icon={this.getTabBarIcon(route, isFocused)}
color={color} color={color}
label={label} label={label as string}
focused={isFocused} focused={isFocused}
extraData={state.index > index} extraData={state.index > index}
key={route.key} key={route.key}
@ -186,7 +170,7 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
); );
}; };
getIcons(): React.Node { getIcons() {
const {props} = this; const {props} = this;
return props.state.routes.map(this.getRenderIcon); return props.state.routes.map(this.getRenderIcon);
} }
@ -197,9 +181,12 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
if (isFocused) { if (isFocused) {
const stackState = route.state; const stackState = route.state;
const stackRoute = const stackRoute =
stackState != null ? stackState.routes[stackState.index] : null; stackState && stackState.index != null
const params: {collapsible: Collapsible} | null = ? stackState.routes[stackState.index]
stackRoute != null ? stackRoute.params : null; : null;
const params: {collapsible: Collapsible} | null | undefined = stackRoute
? (stackRoute.params as {collapsible: Collapsible})
: null;
const collapsible = params != null ? params.collapsible : null; const collapsible = params != null ? params.collapsible : null;
if (collapsible != null) { if (collapsible != null) {
this.setState({ this.setState({
@ -209,14 +196,11 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
} }
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
props.navigation.addListener('state', this.onRouteChange);
const icons = this.getIcons(); const icons = this.getIcons();
return ( return (
// $FlowFixMe
<Animated.View <Animated.View
useNativeDriver
style={{ style={{
flexDirection: 'row', flexDirection: 'row',
height: CustomTabBar.TAB_BAR_HEIGHT, height: CustomTabBar.TAB_BAR_HEIGHT,

View file

@ -17,20 +17,18 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {Image, View} from 'react-native'; import {Image, View} from 'react-native';
import {FAB} from 'react-native-paper'; import {FAB} from 'react-native-paper';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import FOCUSED_ICON from '../../../assets/tab-icon.png'; const FOCUSED_ICON = require('../../../assets/tab-icon.png');
import UNFOCUSED_ICON from '../../../assets/tab-icon-outline.png'; const UNFOCUSED_ICON = require('../../../assets/tab-icon-outline.png');
type PropsType = { type PropsType = {
focused: boolean, focused: boolean;
onPress: () => void, onPress: () => void;
onLongPress: () => void, onLongPress: () => void;
tabBarHeight: number, tabBarHeight: number;
}; };
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
@ -44,6 +42,7 @@ class TabHomeIcon extends React.Component<PropsType> {
Animatable.initializeRegistryWithDefinitions({ Animatable.initializeRegistryWithDefinitions({
fabFocusIn: { fabFocusIn: {
'0': { '0': {
// @ts-ignore
scale: 1, scale: 1,
translateY: 0, translateY: 0,
}, },
@ -58,6 +57,7 @@ class TabHomeIcon extends React.Component<PropsType> {
}, },
fabFocusOut: { fabFocusOut: {
'0': { '0': {
// @ts-ignore
scale: 1.1, scale: 1.1,
translateY: -6, translateY: -6,
}, },
@ -74,13 +74,7 @@ class TabHomeIcon extends React.Component<PropsType> {
return nextProps.focused !== focused; return nextProps.focused !== focused;
} }
getIconRender = ({ getIconRender = ({size, color}: {size: number; color: string}) => {
size,
color,
}: {
size: number,
color: string,
}): React.Node => {
const {focused} = this.props; const {focused} = this.props;
return ( return (
<Image <Image
@ -94,7 +88,7 @@ class TabHomeIcon extends React.Component<PropsType> {
); );
}; };
render(): React.Node { render() {
const {props} = this; const {props} = this;
return ( return (
<View <View

View file

@ -17,25 +17,21 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {TouchableRipple, withTheme} from 'react-native-paper'; import {TouchableRipple, withTheme} from 'react-native-paper';
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager';
type PropsType = { type PropsType = {
focused: boolean, focused: boolean;
color: string, color: string;
label: string, label: string;
icon: MaterialCommunityIconsGlyphs, icon: string;
onPress: () => void, onPress: () => void;
onLongPress: () => void, onLongPress: () => void;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
extraData: null | boolean | number | string, extraData: null | boolean | number | string;
}; };
/** /**
@ -49,6 +45,7 @@ class TabIcon extends React.Component<PropsType> {
Animatable.initializeRegistryWithDefinitions({ Animatable.initializeRegistryWithDefinitions({
focusIn: { focusIn: {
'0': { '0': {
// @ts-ignore
scale: 1, scale: 1,
translateY: 0, translateY: 0,
}, },
@ -63,6 +60,7 @@ class TabIcon extends React.Component<PropsType> {
}, },
focusOut: { focusOut: {
'0': { '0': {
// @ts-ignore
scale: 1.2, scale: 1.2,
translateY: 6, translateY: 6,
}, },
@ -88,7 +86,7 @@ class TabIcon extends React.Component<PropsType> {
); );
} }
render(): React.Node { render() {
const {props} = this; const {props} = this;
return ( return (
<TouchableRipple <TouchableRipple
@ -99,7 +97,7 @@ class TabIcon extends React.Component<PropsType> {
style={{ style={{
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
borderRadius: 10 borderRadius: 10,
}}> }}>
<View> <View>
<Animatable.View <Animatable.View

View file

@ -19,14 +19,16 @@
// @flow // @flow
import ICON_AMICALE from '../../assets/amicale.png'; const ICON_AMICALE = require('../../assets/amicale.png');
import ICON_CAMPUS from '../../assets/android.icon.png'; const ICON_CAMPUS = require('../../assets/android.icon.png');
export type NewsSourceType = { export type NewsSourceType = {
icon: number, icon: number;
name: string, name: string;
}; };
export type AvailablePages = 'amicale.deseleves' | 'campus.insat';
export default { export default {
'amicale.deseleves': { 'amicale.deseleves': {
icon: ICON_AMICALE, icon: ICON_AMICALE,

View file

@ -17,25 +17,26 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
export enum MachineStates {
AVAILABLE,
RUNNING,
RUNNING_NOT_STARTED,
FINISHED,
UNAVAILABLE,
ERROR,
UNKNOWN,
}
export default { export default {
machineStates: { stateIcons: [
AVAILABLE: 0, 'radiobox-blank',
RUNNING: 1, 'progress-check',
RUNNING_NOT_STARTED: 2, 'alert-circle-outline',
FINISHED: 3, 'check-circle',
UNAVAILABLE: 4, 'alert-octagram-outline',
ERROR: 5, 'alert',
UNKNOWN: 6, 'help-circle-outline',
}, ],
stateIcons: {
0: 'radiobox-blank',
1: 'progress-check',
2: 'alert-circle-outline',
3: 'check-circle',
4: 'alert-octagram-outline',
5: 'alert',
6: 'help-circle-outline',
},
washinsa: { washinsa: {
id: 'washinsa', id: 'washinsa',
title: 'screens.proxiwash.washinsa.title', title: 'screens.proxiwash.washinsa.title',

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {IntroSlideType} from '../components/Overrides/CustomIntroSlider'; import type {IntroSlideType} from '../components/Overrides/CustomIntroSlider';
@ -49,16 +47,16 @@ export default class Update {
this.updateSlides = [ this.updateSlides = [
{ {
key: '0', key: '0',
title: i18n.t(`intro.updateSlide0.title`), title: i18n.t('intro.updateSlide0.title'),
text: i18n.t(`intro.updateSlide0.text`), text: i18n.t('intro.updateSlide0.text'),
view: (): React.Node => <MascotIntroWelcome />, view: () => <MascotIntroWelcome />,
colors: ['#be1522', '#57080e'], colors: ['#be1522', '#57080e'],
}, },
{ {
key: '1', key: '1',
title: i18n.t(`intro.updateSlide1.title`), title: i18n.t('intro.updateSlide1.title'),
text: i18n.t(`intro.updateSlide1.text`), text: i18n.t('intro.updateSlide1.text'),
view: (): React.Node => <IntroIcon icon="account-heart-outline" />, view: () => <IntroIcon icon="account-heart-outline" />,
colors: ['#9c165b', '#3e042b'], colors: ['#9c165b', '#3e042b'],
}, },
]; ];

View file

@ -17,10 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import type {ProxiwashMachineType} from '../screens/Proxiwash/ProxiwashScreen'; import type {ProxiwashMachineType} from '../screens/Proxiwash/ProxiwashScreen';
import type {CustomThemeType} from './ThemeManager';
import type {RuFoodCategoryType} from '../screens/Services/SelfMenuScreen'; import type {RuFoodCategoryType} from '../screens/Services/SelfMenuScreen';
/** /**
@ -57,8 +54,9 @@ export default class AprilFoolsManager {
* @returns {ThemeManager} * @returns {ThemeManager}
*/ */
static getInstance(): AprilFoolsManager { static getInstance(): AprilFoolsManager {
if (AprilFoolsManager.instance == null) if (AprilFoolsManager.instance == null) {
AprilFoolsManager.instance = new AprilFoolsManager(); AprilFoolsManager.instance = new AprilFoolsManager();
}
return AprilFoolsManager.instance; return AprilFoolsManager.instance;
} }
@ -130,7 +128,9 @@ export default class AprilFoolsManager {
* @param currentTheme * @param currentTheme
* @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}} * @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}}
*/ */
static getAprilFoolsTheme(currentTheme: CustomThemeType): CustomThemeType { static getAprilFoolsTheme(
currentTheme: ReactNativePaper.Theme,
): ReactNativePaper.Theme {
return { return {
...currentTheme, ...currentTheme,
colors: { colors: {

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import {SERVICES_KEY} from './ServicesManager'; import {SERVICES_KEY} from './ServicesManager';
@ -31,7 +29,7 @@ import {SERVICES_KEY} from './ServicesManager';
export default class AsyncStorageManager { export default class AsyncStorageManager {
static instance: AsyncStorageManager | null = null; static instance: AsyncStorageManager | null = null;
static PREFERENCES = { static PREFERENCES: {[key: string]: {key: string; default: string}} = {
debugUnlocked: { debugUnlocked: {
key: 'debugUnlocked', key: 'debugUnlocked',
default: '0', default: '0',
@ -132,10 +130,10 @@ export default class AsyncStorageManager {
}, },
}; };
#currentPreferences: {[key: string]: string}; private currentPreferences: {[key: string]: string};
constructor() { constructor() {
this.#currentPreferences = {}; this.currentPreferences = {};
} }
/** /**
@ -143,8 +141,9 @@ export default class AsyncStorageManager {
* @returns {AsyncStorageManager} * @returns {AsyncStorageManager}
*/ */
static getInstance(): AsyncStorageManager { static getInstance(): AsyncStorageManager {
if (AsyncStorageManager.instance == null) if (AsyncStorageManager.instance == null) {
AsyncStorageManager.instance = new AsyncStorageManager(); AsyncStorageManager.instance = new AsyncStorageManager();
}
return AsyncStorageManager.instance; return AsyncStorageManager.instance;
} }
@ -156,8 +155,7 @@ export default class AsyncStorageManager {
*/ */
static set( static set(
key: string, key: string,
// eslint-disable-next-line flowtype/no-weak-types value: number | string | boolean | object | Array<any>,
value: number | string | boolean | {...} | Array<any>,
) { ) {
AsyncStorageManager.getInstance().setPreference(key, value); AsyncStorageManager.getInstance().setPreference(key, value);
} }
@ -200,8 +198,7 @@ export default class AsyncStorageManager {
* @param key * @param key
* @returns {{...}} * @returns {{...}}
*/ */
// eslint-disable-next-line flowtype/no-weak-types static getObject<T>(key: string): T {
static getObject(key: string): any {
return JSON.parse(AsyncStorageManager.getString(key)); return JSON.parse(AsyncStorageManager.getString(key));
} }
@ -212,7 +209,7 @@ export default class AsyncStorageManager {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async loadPreferences() { async loadPreferences() {
const prefKeys = []; const prefKeys: Array<string> = [];
// Get all available keys // Get all available keys
Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => { Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => {
prefKeys.push(key); prefKeys.push(key);
@ -223,8 +220,10 @@ export default class AsyncStorageManager {
resultArray.forEach((item: [string, string | null]) => { resultArray.forEach((item: [string, string | null]) => {
const key = item[0]; const key = item[0];
let val = item[1]; let val = item[1];
if (val === null) val = AsyncStorageManager.PREFERENCES[key].default; if (val === null) {
this.#currentPreferences[key] = val; val = AsyncStorageManager.PREFERENCES[key].default;
}
this.currentPreferences[key] = val;
}); });
} }
@ -237,16 +236,18 @@ export default class AsyncStorageManager {
*/ */
setPreference( setPreference(
key: string, key: string,
// eslint-disable-next-line flowtype/no-weak-types value: number | string | boolean | object | Array<any>,
value: number | string | boolean | {...} | Array<any>,
) { ) {
if (AsyncStorageManager.PREFERENCES[key] != null) { if (AsyncStorageManager.PREFERENCES[key] != null) {
let convertedValue; let convertedValue;
if (typeof value === 'string') convertedValue = value; if (typeof value === 'string') {
else if (typeof value === 'boolean' || typeof value === 'number') convertedValue = value;
} else if (typeof value === 'boolean' || typeof value === 'number') {
convertedValue = value.toString(); convertedValue = value.toString();
else convertedValue = JSON.stringify(value); } else {
this.#currentPreferences[key] = convertedValue; convertedValue = JSON.stringify(value);
}
this.currentPreferences[key] = convertedValue;
AsyncStorage.setItem(key, convertedValue); AsyncStorage.setItem(key, convertedValue);
} }
} }
@ -259,6 +260,6 @@ export default class AsyncStorageManager {
* @returns {string|null} * @returns {string|null}
*/ */
getPreference(key: string): string | null { getPreference(key: string): string | null {
return this.#currentPreferences[key]; return this.currentPreferences[key];
} }
} }

View file

@ -20,7 +20,7 @@
// @flow // @flow
import * as Keychain from 'react-native-keychain'; import * as Keychain from 'react-native-keychain';
import type {ApiDataLoginType, ApiGenericDataType} from '../utils/WebData'; import type {ApiDataLoginType} from '../utils/WebData';
import {apiRequest, ERROR_TYPE} from '../utils/WebData'; import {apiRequest, ERROR_TYPE} from '../utils/WebData';
/** /**
@ -40,12 +40,10 @@ const AUTH_PATH = 'password';
export default class ConnectionManager { export default class ConnectionManager {
static instance: ConnectionManager | null = null; static instance: ConnectionManager | null = null;
#email: string; private token: string | null;
#token: string | null;
constructor() { constructor() {
this.#token = null; this.token = null;
} }
/** /**
@ -54,8 +52,9 @@ export default class ConnectionManager {
* @returns {ConnectionManager} * @returns {ConnectionManager}
*/ */
static getInstance(): ConnectionManager { static getInstance(): ConnectionManager {
if (ConnectionManager.instance == null) if (ConnectionManager.instance == null) {
ConnectionManager.instance = new ConnectionManager(); ConnectionManager.instance = new ConnectionManager();
}
return ConnectionManager.instance; return ConnectionManager.instance;
} }
@ -65,7 +64,7 @@ export default class ConnectionManager {
* @returns {string | null} * @returns {string | null}
*/ */
getToken(): string | null { getToken(): string | null {
return this.#token; return this.token;
} }
/** /**
@ -77,18 +76,17 @@ export default class ConnectionManager {
return new Promise( return new Promise(
(resolve: (token: string) => void, reject: () => void) => { (resolve: (token: string) => void, reject: () => void) => {
const token = this.getToken(); const token = this.getToken();
if (token != null) resolve(token); if (token != null) {
else { resolve(token);
} else {
Keychain.getInternetCredentials(SERVER_NAME) Keychain.getInternetCredentials(SERVER_NAME)
.then((data: Keychain.UserCredentials | false) => { .then((data: Keychain.UserCredentials | false) => {
if ( if (data && data.password != null) {
data != null && this.token = data.password;
data.password != null && resolve(this.token);
typeof data.password === 'string' } else {
) { reject();
this.#token = data.password; }
resolve(this.#token);
} else reject();
}) })
.catch((): void => reject()); .catch((): void => reject());
} }
@ -116,8 +114,7 @@ export default class ConnectionManager {
return new Promise((resolve: () => void, reject: () => void) => { return new Promise((resolve: () => void, reject: () => void) => {
Keychain.setInternetCredentials(SERVER_NAME, 'token', token) Keychain.setInternetCredentials(SERVER_NAME, 'token', token)
.then(() => { .then(() => {
this.#token = token; this.token = token;
this.#email = email;
resolve(); resolve();
}) })
.catch((): void => reject()); .catch((): void => reject());
@ -133,7 +130,7 @@ export default class ConnectionManager {
return new Promise((resolve: () => void, reject: () => void) => { return new Promise((resolve: () => void, reject: () => void) => {
Keychain.resetInternetCredentials(SERVER_NAME) Keychain.resetInternetCredentials(SERVER_NAME)
.then(() => { .then(() => {
this.#token = null; this.token = null;
resolve(); resolve();
}) })
.catch((): void => reject()); .catch((): void => reject());
@ -156,13 +153,15 @@ export default class ConnectionManager {
email, email,
password, password,
}; };
apiRequest(AUTH_PATH, 'POST', data) apiRequest<ApiDataLoginType>(AUTH_PATH, 'POST', data)
.then((response: ApiDataLoginType) => { .then((response: ApiDataLoginType) => {
if (response.token != null) { if (response.token != null) {
this.saveLogin(email, response.token) this.saveLogin(email, response.token)
.then((): void => resolve()) .then((): void => resolve())
.catch((): void => reject(ERROR_TYPE.TOKEN_SAVE)); .catch((): void => reject(ERROR_TYPE.TOKEN_SAVE));
} else reject(ERROR_TYPE.SERVER_ERROR); } else {
reject(ERROR_TYPE.SERVER_ERROR);
}
}) })
.catch((error: number): void => reject(error)); .catch((error: number): void => reject(error));
}, },
@ -176,24 +175,23 @@ export default class ConnectionManager {
* @param params * @param params
* @returns Promise<ApiGenericDataType> * @returns Promise<ApiGenericDataType>
*/ */
async authenticatedRequest( async authenticatedRequest<T>(
path: string, path: string,
params: {...}, params: {[key: string]: any},
): Promise<ApiGenericDataType> { ): Promise<T> {
return new Promise( return new Promise(
( (resolve: (response: T) => void, reject: (error: number) => void) => {
resolve: (response: ApiGenericDataType) => void,
reject: (error: number) => void,
) => {
if (this.getToken() !== null) { if (this.getToken() !== null) {
const data = { const data = {
...params, ...params,
token: this.getToken(), token: this.getToken(),
}; };
apiRequest(path, 'POST', data) apiRequest<T>(path, 'POST', data)
.then((response: ApiGenericDataType): void => resolve(response)) .then((response: T): void => resolve(response))
.catch((error: number): void => reject(error)); .catch((error: number): void => reject(error));
} else reject(ERROR_TYPE.TOKEN_RETRIEVE); } else {
reject(ERROR_TYPE.TOKEN_RETRIEVE);
}
}, },
); );
} }

View file

@ -21,12 +21,12 @@
import type {ServiceItemType} from './ServicesManager'; import type {ServiceItemType} from './ServicesManager';
import ServicesManager from './ServicesManager'; import ServicesManager from './ServicesManager';
import {getSublistWithIds} from '../utils/Utils'; import {getSublistWithIds} from '../utils/Services';
import AsyncStorageManager from './AsyncStorageManager'; import AsyncStorageManager from './AsyncStorageManager';
export default class DashboardManager extends ServicesManager { export default class DashboardManager extends ServicesManager {
getCurrentDashboard(): Array<ServiceItemType | null> { getCurrentDashboard(): Array<ServiceItemType | null> {
const dashboardIdList = AsyncStorageManager.getObject( const dashboardIdList = AsyncStorageManager.getObject<Array<string>>(
AsyncStorageManager.PREFERENCES.dashboardItems.key, AsyncStorageManager.PREFERENCES.dashboardItems.key,
); );
const allDatasets = [ const allDatasets = [

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import i18n from 'i18n-js'; import i18n from 'i18n-js';
/** /**
@ -28,9 +26,9 @@ import i18n from 'i18n-js';
export default class DateManager { export default class DateManager {
static instance: DateManager | null = null; static instance: DateManager | null = null;
daysOfWeek = []; daysOfWeek: Array<string> = [];
monthsOfYear = []; monthsOfYear: Array<string> = [];
constructor() { constructor() {
this.daysOfWeek.push(i18n.t('date.daysOfWeek.sunday')); // 0 represents sunday this.daysOfWeek.push(i18n.t('date.daysOfWeek.sunday')); // 0 represents sunday
@ -60,7 +58,9 @@ export default class DateManager {
* @returns {DateManager} * @returns {DateManager}
*/ */
static getInstance(): DateManager { static getInstance(): DateManager {
if (DateManager.instance == null) DateManager.instance = new DateManager(); if (DateManager.instance == null) {
DateManager.instance = new DateManager();
}
return DateManager.instance; return DateManager.instance;
} }

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import AvailableWebsites from '../constants/AvailableWebsites'; import AvailableWebsites from '../constants/AvailableWebsites';
@ -93,24 +91,24 @@ export const SERVICES_CATEGORIES_KEY = {
}; };
export type ServiceItemType = { export type ServiceItemType = {
key: string, key: string;
title: string, title: string;
subtitle: string, subtitle: string;
image: string, image: string | number;
onPress: () => void, onPress: () => void;
badgeFunction?: (dashboard: FullDashboardType) => number, badgeFunction?: (dashboard: FullDashboardType) => number;
}; };
export type ServiceCategoryType = { export type ServiceCategoryType = {
key: string, key: string;
title: string, title: string;
subtitle: string, subtitle: string;
image: string | number, image: string | number;
content: Array<ServiceItemType>, content: Array<ServiceItemType>;
}; };
export default class ServicesManager { export default class ServicesManager {
navigation: StackNavigationProp; navigation: StackNavigationProp<any>;
amicaleDataset: Array<ServiceItemType>; amicaleDataset: Array<ServiceItemType>;
@ -122,7 +120,7 @@ export default class ServicesManager {
categoriesDataset: Array<ServiceCategoryType>; categoriesDataset: Array<ServiceCategoryType>;
constructor(nav: StackNavigationProp) { constructor(nav: StackNavigationProp<any>) {
this.navigation = nav; this.navigation = nav;
this.amicaleDataset = [ this.amicaleDataset = [
{ {
@ -336,9 +334,11 @@ export default class ServicesManager {
* @returns {null} * @returns {null}
*/ */
onAmicaleServicePress(route: string) { onAmicaleServicePress(route: string) {
if (ConnectionManager.getInstance().isLoggedIn()) if (ConnectionManager.getInstance().isLoggedIn()) {
this.navigation.navigate(route); this.navigation.navigate(route);
else this.navigation.navigate('login', {nextScreen: route}); } else {
this.navigation.navigate('login', {nextScreen: route});
}
} }
/** /**
@ -348,8 +348,9 @@ export default class ServicesManager {
* @returns {Array<ServiceItemType>} * @returns {Array<ServiceItemType>}
*/ */
getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> { getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.amicaleDataset); return getStrippedServicesList(excludedItems, this.amicaleDataset);
}
return this.amicaleDataset; return this.amicaleDataset;
} }
@ -360,8 +361,9 @@ export default class ServicesManager {
* @returns {Array<ServiceItemType>} * @returns {Array<ServiceItemType>}
*/ */
getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> { getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.studentsDataset); return getStrippedServicesList(excludedItems, this.studentsDataset);
}
return this.studentsDataset; return this.studentsDataset;
} }
@ -372,8 +374,9 @@ export default class ServicesManager {
* @returns {Array<ServiceItemType>} * @returns {Array<ServiceItemType>}
*/ */
getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> { getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.insaDataset); return getStrippedServicesList(excludedItems, this.insaDataset);
}
return this.insaDataset; return this.insaDataset;
} }
@ -384,8 +387,9 @@ export default class ServicesManager {
* @returns {Array<ServiceItemType>} * @returns {Array<ServiceItemType>}
*/ */
getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> { getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.specialDataset); return getStrippedServicesList(excludedItems, this.specialDataset);
}
return this.specialDataset; return this.specialDataset;
} }
@ -396,8 +400,9 @@ export default class ServicesManager {
* @returns {Array<ServiceCategoryType>} * @returns {Array<ServiceCategoryType>}
*/ */
getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> { getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> {
if (excludedItems != null) if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.categoriesDataset); return getStrippedServicesList(excludedItems, this.categoriesDataset);
}
return this.categoriesDataset; return this.categoriesDataset;
} }
} }

View file

@ -1,307 +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 <https://www.gnu.org/licenses/>.
*/
// @flow
import {DarkTheme, DefaultTheme} from 'react-native-paper';
import {Appearance} from 'react-native-appearance';
import AsyncStorageManager from './AsyncStorageManager';
import AprilFoolsManager from './AprilFoolsManager';
const colorScheme = Appearance.getColorScheme();
export type CustomThemeType = {
...DefaultTheme,
colors: {
primary: string,
accent: string,
tabIcon: string,
card: string,
dividerBackground: string,
ripple: string,
textDisabled: string,
icon: string,
subtitle: string,
success: string,
warning: string,
danger: string,
// Calendar/Agenda
agendaBackgroundColor: string,
agendaDayTextColor: string,
// PROXIWASH
proxiwashFinishedColor: string,
proxiwashReadyColor: string,
proxiwashRunningColor: string,
proxiwashRunningNotStartedColor: string,
proxiwashRunningBgColor: string,
proxiwashBrokenColor: string,
proxiwashErrorColor: string,
proxiwashUnknownColor: string,
// Screens
planningColor: string,
proximoColor: string,
proxiwashColor: string,
menuColor: string,
tutorinsaColor: string,
// Tetris
tetrisBackground: string,
tetrisBorder: string,
tetrisScore: string,
tetrisI: string,
tetrisO: string,
tetrisT: string,
tetrisS: string,
tetrisZ: string,
tetrisJ: string,
tetrisL: string,
gameGold: string,
gameSilver: string,
gameBronze: string,
// Mascot Popup
mascotMessageArrow: string,
},
};
/**
* Singleton class used to manage themes
*/
export default class ThemeManager {
static instance: ThemeManager | null = null;
updateThemeCallback: null | (() => void);
constructor() {
this.updateThemeCallback = null;
}
/**
* Gets the light theme
*
* @return {CustomThemeType} Object containing theme variables
* */
static getWhiteTheme(): CustomThemeType {
return {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: '#be1522',
accent: '#be1522',
tabIcon: '#929292',
card: '#fff',
dividerBackground: '#e2e2e2',
ripple: 'rgba(0,0,0,0.2)',
textDisabled: '#c1c1c1',
icon: '#5d5d5d',
subtitle: '#707070',
success: '#5cb85c',
warning: '#f0ad4e',
danger: '#d9534f',
cc: 'dst',
// Calendar/Agenda
agendaBackgroundColor: '#f3f3f4',
agendaDayTextColor: '#636363',
// PROXIWASH
proxiwashFinishedColor: '#a5dc9d',
proxiwashReadyColor: 'transparent',
proxiwashRunningColor: '#a0ceff',
proxiwashRunningNotStartedColor: '#c9e0ff',
proxiwashRunningBgColor: '#c7e3ff',
proxiwashBrokenColor: '#ffa8a2',
proxiwashErrorColor: '#ffa8a2',
proxiwashUnknownColor: '#b6b6b6',
// Screens
planningColor: '#d9b10a',
proximoColor: '#ec5904',
proxiwashColor: '#1fa5ee',
menuColor: '#e91314',
tutorinsaColor: '#f93943',
// Tetris
tetrisBackground: '#f0f0f0',
tetrisScore: '#e2bd33',
tetrisI: '#3cd9e6',
tetrisO: '#ffdd00',
tetrisT: '#a716e5',
tetrisS: '#09c528',
tetrisZ: '#ff0009',
tetrisJ: '#2a67e3',
tetrisL: '#da742d',
gameGold: '#ffd610',
gameSilver: '#7b7b7b',
gameBronze: '#a15218',
// Mascot Popup
mascotMessageArrow: '#dedede',
},
};
}
/**
* Gets the dark theme
*
* @return {CustomThemeType} Object containing theme variables
* */
static getDarkTheme(): CustomThemeType {
return {
...DarkTheme,
colors: {
...DarkTheme.colors,
primary: '#be1522',
accent: '#be1522',
tabBackground: '#181818',
tabIcon: '#6d6d6d',
card: 'rgb(18,18,18)',
dividerBackground: '#222222',
ripple: 'rgba(255,255,255,0.2)',
textDisabled: '#5b5b5b',
icon: '#b3b3b3',
subtitle: '#aaaaaa',
success: '#5cb85c',
warning: '#f0ad4e',
danger: '#d9534f',
// Calendar/Agenda
agendaBackgroundColor: '#171717',
agendaDayTextColor: '#6d6d6d',
// PROXIWASH
proxiwashFinishedColor: '#31682c',
proxiwashReadyColor: 'transparent',
proxiwashRunningColor: '#213c79',
proxiwashRunningNotStartedColor: '#1e263e',
proxiwashRunningBgColor: '#1a2033',
proxiwashBrokenColor: '#7e2e2f',
proxiwashErrorColor: '#7e2e2f',
proxiwashUnknownColor: '#535353',
// Screens
planningColor: '#d99e09',
proximoColor: '#ec5904',
proxiwashColor: '#1fa5ee',
menuColor: '#b81213',
tutorinsaColor: '#f93943',
// Tetris
tetrisBackground: '#181818',
tetrisScore: '#e2d707',
tetrisI: '#30b3be',
tetrisO: '#c1a700',
tetrisT: '#9114c7',
tetrisS: '#08a121',
tetrisZ: '#b50008',
tetrisJ: '#0f37b9',
tetrisL: '#b96226',
gameGold: '#ffd610',
gameSilver: '#7b7b7b',
gameBronze: '#a15218',
// Mascot Popup
mascotMessageArrow: '#323232',
},
};
}
/**
* Get this class instance or create one if none is found
*
* @returns {ThemeManager}
*/
static getInstance(): ThemeManager {
if (ThemeManager.instance == null)
ThemeManager.instance = new ThemeManager();
return ThemeManager.instance;
}
/**
* Gets night mode status.
* If Follow System Preferences is enabled, will first use system theme.
* If disabled or not available, will use value stored din preferences
*
* @returns {boolean} Night mode state
*/
static getNightMode(): boolean {
return (
(AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.nightMode.key,
) &&
(!AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
) ||
colorScheme === 'no-preference')) ||
(AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
) &&
colorScheme === 'dark')
);
}
/**
* Get the current theme based on night mode and events
*
* @returns {CustomThemeType} The current theme
*/
static getCurrentTheme(): CustomThemeType {
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme());
return ThemeManager.getBaseTheme();
}
/**
* Get the theme based on night mode
*
* @return {CustomThemeType} The theme
*/
static getBaseTheme(): CustomThemeType {
if (ThemeManager.getNightMode()) return ThemeManager.getDarkTheme();
return ThemeManager.getWhiteTheme();
}
/**
* Sets the function to be called when the theme is changed (allows for general reload of the app)
*
* @param callback Function to call after theme change
*/
setUpdateThemeCallback(callback: () => void) {
this.updateThemeCallback = callback;
}
/**
* Set night mode and save it to preferences
*
* @param isNightMode True to enable night mode, false to disable
*/
setNightMode(isNightMode: boolean) {
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.nightMode.key,
isNightMode,
);
if (this.updateThemeCallback != null) this.updateThemeCallback();
}
}

View file

@ -0,0 +1,298 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
import {DarkTheme, DefaultTheme} from 'react-native-paper';
import {Appearance} from 'react-native-appearance';
import AsyncStorageManager from './AsyncStorageManager';
import AprilFoolsManager from './AprilFoolsManager';
const colorScheme = Appearance.getColorScheme();
declare global {
namespace ReactNativePaper {
interface ThemeColors {
primary: string;
accent: string;
border: string;
tabIcon: string;
card: string;
dividerBackground: string;
ripple: string;
textDisabled: string;
icon: string;
subtitle: string;
success: string;
warning: string;
danger: string;
// Calendar/Agenda
agendaBackgroundColor: string;
agendaDayTextColor: string;
// PROXIWASH
proxiwashFinishedColor: string;
proxiwashReadyColor: string;
proxiwashRunningColor: string;
proxiwashRunningNotStartedColor: string;
proxiwashRunningBgColor: string;
proxiwashBrokenColor: string;
proxiwashErrorColor: string;
proxiwashUnknownColor: string;
// Screens
planningColor: string;
proximoColor: string;
proxiwashColor: string;
menuColor: string;
tutorinsaColor: string;
// Tetris
tetrisBackground: string;
tetrisScore: string;
tetrisI: string;
tetrisO: string;
tetrisT: string;
tetrisS: string;
tetrisZ: string;
tetrisJ: string;
tetrisL: string;
gameGold: string;
gameSilver: string;
gameBronze: string;
// Mascot Popup
mascotMessageArrow: string;
}
}
}
const CustomWhiteTheme: ReactNativePaper.Theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: '#be1522',
accent: '#be1522',
border: '#e2e2e2',
tabIcon: '#929292',
card: '#fff',
dividerBackground: '#e2e2e2',
ripple: 'rgba(0,0,0,0.2)',
textDisabled: '#c1c1c1',
icon: '#5d5d5d',
subtitle: '#707070',
success: '#5cb85c',
warning: '#f0ad4e',
danger: '#d9534f',
// Calendar/Agenda
agendaBackgroundColor: '#f3f3f4',
agendaDayTextColor: '#636363',
// PROXIWASH
proxiwashFinishedColor: '#a5dc9d',
proxiwashReadyColor: 'transparent',
proxiwashRunningColor: '#a0ceff',
proxiwashRunningNotStartedColor: '#c9e0ff',
proxiwashRunningBgColor: '#c7e3ff',
proxiwashBrokenColor: '#ffa8a2',
proxiwashErrorColor: '#ffa8a2',
proxiwashUnknownColor: '#b6b6b6',
// Screens
planningColor: '#d9b10a',
proximoColor: '#ec5904',
proxiwashColor: '#1fa5ee',
menuColor: '#e91314',
tutorinsaColor: '#f93943',
// Tetris
tetrisBackground: '#f0f0f0',
tetrisScore: '#e2bd33',
tetrisI: '#3cd9e6',
tetrisO: '#ffdd00',
tetrisT: '#a716e5',
tetrisS: '#09c528',
tetrisZ: '#ff0009',
tetrisJ: '#2a67e3',
tetrisL: '#da742d',
gameGold: '#ffd610',
gameSilver: '#7b7b7b',
gameBronze: '#a15218',
// Mascot Popup
mascotMessageArrow: '#dedede',
},
};
const CustomDarkTheme: ReactNativePaper.Theme = {
...DarkTheme,
colors: {
...DarkTheme.colors,
primary: '#be1522',
accent: '#be1522',
border: '#222222',
tabIcon: '#6d6d6d',
card: 'rgb(18,18,18)',
dividerBackground: '#222222',
ripple: 'rgba(255,255,255,0.2)',
textDisabled: '#5b5b5b',
icon: '#b3b3b3',
subtitle: '#aaaaaa',
success: '#5cb85c',
warning: '#f0ad4e',
danger: '#d9534f',
// Calendar/Agenda
agendaBackgroundColor: '#171717',
agendaDayTextColor: '#6d6d6d',
// PROXIWASH
proxiwashFinishedColor: '#31682c',
proxiwashReadyColor: 'transparent',
proxiwashRunningColor: '#213c79',
proxiwashRunningNotStartedColor: '#1e263e',
proxiwashRunningBgColor: '#1a2033',
proxiwashBrokenColor: '#7e2e2f',
proxiwashErrorColor: '#7e2e2f',
proxiwashUnknownColor: '#535353',
// Screens
planningColor: '#d99e09',
proximoColor: '#ec5904',
proxiwashColor: '#1fa5ee',
menuColor: '#b81213',
tutorinsaColor: '#f93943',
// Tetris
tetrisBackground: '#181818',
tetrisScore: '#e2d707',
tetrisI: '#30b3be',
tetrisO: '#c1a700',
tetrisT: '#9114c7',
tetrisS: '#08a121',
tetrisZ: '#b50008',
tetrisJ: '#0f37b9',
tetrisL: '#b96226',
gameGold: '#ffd610',
gameSilver: '#7b7b7b',
gameBronze: '#a15218',
// Mascot Popup
mascotMessageArrow: '#323232',
},
};
/**
* Singleton class used to manage themes
*/
export default class ThemeManager {
static instance: ThemeManager | null = null;
updateThemeCallback: null | (() => void);
constructor() {
this.updateThemeCallback = null;
}
/**
* Get this class instance or create one if none is found
*
* @returns {ThemeManager}
*/
static getInstance(): ThemeManager {
if (ThemeManager.instance == null) {
ThemeManager.instance = new ThemeManager();
}
return ThemeManager.instance;
}
/**
* Gets night mode status.
* If Follow System Preferences is enabled, will first use system theme.
* If disabled or not available, will use value stored din preferences
*
* @returns {boolean} Night mode state
*/
static getNightMode(): boolean {
return (
(AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.nightMode.key,
) &&
(!AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
) ||
colorScheme === 'no-preference')) ||
(AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
) &&
colorScheme === 'dark')
);
}
/**
* Get the current theme based on night mode and events
*
* @returns {ReactNativePaper.Theme} The current theme
*/
static getCurrentTheme(): ReactNativePaper.Theme {
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
return AprilFoolsManager.getAprilFoolsTheme(CustomWhiteTheme);
}
return ThemeManager.getBaseTheme();
}
/**
* Get the theme based on night mode
*
* @return {ReactNativePaper.Theme} The theme
*/
static getBaseTheme(): ReactNativePaper.Theme {
if (ThemeManager.getNightMode()) {
return CustomDarkTheme;
}
return CustomWhiteTheme;
}
/**
* Sets the function to be called when the theme is changed (allows for general reload of the app)
*
* @param callback Function to call after theme change
*/
setUpdateThemeCallback(callback: () => void) {
this.updateThemeCallback = callback;
}
/**
* Set night mode and save it to preferences
*
* @param isNightMode True to enable night mode, false to disable
*/
setNightMode(isNightMode: boolean) {
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.nightMode.key,
isNightMode,
);
if (this.updateThemeCallback != null) {
this.updateThemeCallback();
}
}
}

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack'; import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
@ -40,18 +38,63 @@ import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen'; import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
import { import {
createScreenCollapsibleStack, CreateScreenCollapsibleStack,
getWebsiteStack, getWebsiteStack,
} from '../utils/CollapsibleUtils'; } from '../utils/CollapsibleUtils';
import BugReportScreen from '../screens/Other/FeedbackScreen'; import BugReportScreen from '../screens/Other/FeedbackScreen';
import WebsiteScreen from '../screens/Services/WebsiteScreen'; import WebsiteScreen from '../screens/Services/WebsiteScreen';
import EquipmentScreen from '../screens/Amicale/Equipment/EquipmentListScreen'; import EquipmentScreen, {
DeviceType,
} from '../screens/Amicale/Equipment/EquipmentListScreen';
import EquipmentLendScreen from '../screens/Amicale/Equipment/EquipmentRentScreen'; import EquipmentLendScreen from '../screens/Amicale/Equipment/EquipmentRentScreen';
import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfirmScreen'; import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfirmScreen';
import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen'; import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
import GameStartScreen from '../screens/Game/screens/GameStartScreen'; import GameStartScreen from '../screens/Game/screens/GameStartScreen';
import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen'; import ImageGalleryScreen from '../screens/Media/ImageGalleryScreen';
export enum MainRoutes {
Main = 'main',
Gallery = 'gallery',
Settings = 'settings',
DashboardEdit = 'dashboard-edit',
About = 'about',
Dependencies = 'dependencies',
Debug = 'debug',
GameStart = 'game-start',
GameMain = 'game-main',
Login = 'login',
SelfMenu = 'self-menu',
Proximo = 'proximo',
ProximoList = 'proximo-list',
ProximoAbout = 'proximo-about',
Profile = 'profile',
ClubList = 'club-list',
ClubInformation = 'club-information',
ClubAbout = 'club-about',
EquipmentList = 'equipment-list',
EquipmentRent = 'equipment-rent',
EquipmentConfirm = 'equipment-confirm',
Vote = 'vote',
Feedback = 'feedback',
}
type DefaultParams = {[key in MainRoutes]: object | undefined};
export interface FullParamsList extends DefaultParams {
login: {nextScreen: string};
'equipment-confirm': {
item?: DeviceType;
dates: [string, string];
};
'equipment-rent': {item?: DeviceType};
gallery: {images: Array<{url: string}>};
}
// Don't know why but TS is complaining without this
// See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined
export type MainStackParamsList = FullParamsList &
Record<string, object | undefined>;
const modalTransition = const modalTransition =
Platform.OS === 'ios' Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS ? TransitionPresets.ModalPresentationIOS
@ -63,19 +106,17 @@ const defaultScreenOptions = {
...TransitionPresets.SlideFromRightIOS, ...TransitionPresets.SlideFromRightIOS,
}; };
const MainStack = createStackNavigator(); const MainStack = createStackNavigator<MainStackParamsList>();
function MainStackComponent(props: { function MainStackComponent(props: {createTabNavigator: () => JSX.Element}) {
createTabNavigator: () => React.Node,
}): React.Node {
const {createTabNavigator} = props; const {createTabNavigator} = props;
return ( return (
<MainStack.Navigator <MainStack.Navigator
initialRouteName="main" initialRouteName={MainRoutes.Main}
headerMode="screen" headerMode="screen"
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}>
<MainStack.Screen <MainStack.Screen
name="main" name={MainRoutes.Main}
component={createTabNavigator} component={createTabNavigator}
options={{ options={{
headerShown: false, headerShown: false,
@ -83,62 +124,62 @@ function MainStackComponent(props: {
}} }}
/> />
<MainStack.Screen <MainStack.Screen
name="gallery" name={MainRoutes.Gallery}
component={ImageGalleryScreen} component={ImageGalleryScreen}
options={{ options={{
headerShown: false, headerShown: false,
...modalTransition, ...modalTransition,
}} }}
/> />
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'settings', MainRoutes.Settings,
MainStack, MainStack,
SettingsScreen, SettingsScreen,
i18n.t('screens.settings.title'), i18n.t('screens.settings.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'dashboard-edit', MainRoutes.DashboardEdit,
MainStack, MainStack,
DashboardEditScreen, DashboardEditScreen,
i18n.t('screens.settings.dashboardEdit.title'), i18n.t('screens.settings.dashboardEdit.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'about', MainRoutes.About,
MainStack, MainStack,
AboutScreen, AboutScreen,
i18n.t('screens.about.title'), i18n.t('screens.about.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'dependencies', MainRoutes.Dependencies,
MainStack, MainStack,
AboutDependenciesScreen, AboutDependenciesScreen,
i18n.t('screens.about.libs'), i18n.t('screens.about.libs'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'debug', MainRoutes.Debug,
MainStack, MainStack,
DebugScreen, DebugScreen,
i18n.t('screens.about.debug'), i18n.t('screens.about.debug'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'game-start', MainRoutes.GameStart,
MainStack, MainStack,
GameStartScreen, GameStartScreen,
i18n.t('screens.game.title'), i18n.t('screens.game.title'),
true, true,
null, undefined,
'transparent', 'transparent',
)} )}
<MainStack.Screen <MainStack.Screen
name="game-main" name={MainRoutes.GameMain}
component={GameMainScreen} component={GameMainScreen}
options={{ options={{
title: i18n.t('screens.game.title'), title: i18n.t('screens.game.title'),
}} }}
/> />
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'login', MainRoutes.Login,
MainStack, MainStack,
LoginScreen, LoginScreen,
i18n.t('screens.login.title'), i18n.t('screens.login.title'),
@ -148,26 +189,26 @@ function MainStackComponent(props: {
)} )}
{getWebsiteStack('website', MainStack, WebsiteScreen, '')} {getWebsiteStack('website', MainStack, WebsiteScreen, '')}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'self-menu', MainRoutes.SelfMenu,
MainStack, MainStack,
SelfMenuScreen, SelfMenuScreen,
i18n.t('screens.menu.title'), i18n.t('screens.menu.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'proximo', MainRoutes.Proximo,
MainStack, MainStack,
ProximoMainScreen, ProximoMainScreen,
i18n.t('screens.proximo.title'), i18n.t('screens.proximo.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'proximo-list', MainRoutes.ProximoList,
MainStack, MainStack,
ProximoListScreen, ProximoListScreen,
i18n.t('screens.proximo.articleList'), i18n.t('screens.proximo.articleList'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'proximo-about', MainRoutes.ProximoAbout,
MainStack, MainStack,
ProximoAboutScreen, ProximoAboutScreen,
i18n.t('screens.proximo.title'), i18n.t('screens.proximo.title'),
@ -175,60 +216,60 @@ function MainStackComponent(props: {
{...modalTransition}, {...modalTransition},
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'profile', MainRoutes.Profile,
MainStack, MainStack,
ProfileScreen, ProfileScreen,
i18n.t('screens.profile.title'), i18n.t('screens.profile.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'club-list', MainRoutes.ClubList,
MainStack, MainStack,
ClubListScreen, ClubListScreen,
i18n.t('screens.clubs.title'), i18n.t('screens.clubs.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'club-information', MainRoutes.ClubInformation,
MainStack, MainStack,
ClubDisplayScreen, ClubDisplayScreen,
i18n.t('screens.clubs.details'), i18n.t('screens.clubs.details'),
true, true,
{...modalTransition}, {...modalTransition},
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'club-about', MainRoutes.ClubAbout,
MainStack, MainStack,
ClubAboutScreen, ClubAboutScreen,
i18n.t('screens.clubs.title'), i18n.t('screens.clubs.title'),
true, true,
{...modalTransition}, {...modalTransition},
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'equipment-list', MainRoutes.EquipmentList,
MainStack, MainStack,
EquipmentScreen, EquipmentScreen,
i18n.t('screens.equipment.title'), i18n.t('screens.equipment.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'equipment-rent', MainRoutes.EquipmentRent,
MainStack, MainStack,
EquipmentLendScreen, EquipmentLendScreen,
i18n.t('screens.equipment.book'), i18n.t('screens.equipment.book'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'equipment-confirm', MainRoutes.EquipmentConfirm,
MainStack, MainStack,
EquipmentConfirmScreen, EquipmentConfirmScreen,
i18n.t('screens.equipment.confirm'), i18n.t('screens.equipment.confirm'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'vote', MainRoutes.Vote,
MainStack, MainStack,
VoteScreen, VoteScreen,
i18n.t('screens.vote.title'), i18n.t('screens.vote.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'feedback', MainRoutes.Feedback,
MainStack, MainStack,
BugReportScreen, BugReportScreen,
i18n.t('screens.feedback.title'), i18n.t('screens.feedback.title'),
@ -238,25 +279,14 @@ function MainStackComponent(props: {
} }
type PropsType = { type PropsType = {
defaultHomeRoute: string | null, defaultHomeRoute: string | null;
// eslint-disable-next-line flowtype/no-weak-types defaultHomeData: {[key: string]: string};
defaultHomeData: {[key: string]: string},
}; };
export default class MainNavigator extends React.Component<PropsType> { export default function MainNavigator(props: PropsType) {
createTabNavigator: () => React.Node; return (
<MainStackComponent
constructor(props: PropsType) { createTabNavigator={() => <TabNavigator {...props} />}
super(props);
this.createTabNavigator = (): React.Node => (
<TabNavigator
defaultHomeRoute={props.defaultHomeRoute}
defaultHomeData={props.defaultHomeData}
/> />
); );
}
render(): React.Node {
return <MainStackComponent createTabNavigator={this.createTabNavigator} />;
}
} }

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack'; import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs'; import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
@ -44,7 +42,7 @@ import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen'; import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen'; import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
import { import {
createScreenCollapsibleStack, CreateScreenCollapsibleStack,
getWebsiteStack, getWebsiteStack,
} from '../utils/CollapsibleUtils'; } from '../utils/CollapsibleUtils';
import Mascot, {MASCOT_STYLE} from '../components/Mascot/Mascot'; import Mascot, {MASCOT_STYLE} from '../components/Mascot/Mascot';
@ -62,25 +60,25 @@ const defaultScreenOptions = {
const ServicesStack = createStackNavigator(); const ServicesStack = createStackNavigator();
function ServicesStackComponent(): React.Node { function ServicesStackComponent() {
return ( return (
<ServicesStack.Navigator <ServicesStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode="screen" headerMode="screen"
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}>
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'index', 'index',
ServicesStack, ServicesStack,
WebsitesHomeScreen, WebsitesHomeScreen,
i18n.t('screens.services.title'), i18n.t('screens.services.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'services-section', 'services-section',
ServicesStack, ServicesStack,
ServicesSectionScreen, ServicesSectionScreen,
'SECTION', 'SECTION',
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'amicale-contact', 'amicale-contact',
ServicesStack, ServicesStack,
AmicaleContactScreen, AmicaleContactScreen,
@ -92,19 +90,19 @@ function ServicesStackComponent(): React.Node {
const ProxiwashStack = createStackNavigator(); const ProxiwashStack = createStackNavigator();
function ProxiwashStackComponent(): React.Node { function ProxiwashStackComponent() {
return ( return (
<ProxiwashStack.Navigator <ProxiwashStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode="screen" headerMode="screen"
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}>
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'index', 'index',
ProxiwashStack, ProxiwashStack,
ProxiwashScreen, ProxiwashScreen,
i18n.t('screens.proxiwash.title'), i18n.t('screens.proxiwash.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'proxiwash-about', 'proxiwash-about',
ProxiwashStack, ProxiwashStack,
ProxiwashAboutScreen, ProxiwashAboutScreen,
@ -116,7 +114,7 @@ function ProxiwashStackComponent(): React.Node {
const PlanningStack = createStackNavigator(); const PlanningStack = createStackNavigator();
function PlanningStackComponent(): React.Node { function PlanningStackComponent() {
return ( return (
<PlanningStack.Navigator <PlanningStack.Navigator
initialRouteName="index" initialRouteName="index"
@ -127,7 +125,7 @@ function PlanningStackComponent(): React.Node {
component={PlanningScreen} component={PlanningScreen}
options={{title: i18n.t('screens.planning.title')}} options={{title: i18n.t('screens.planning.title')}}
/> />
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'planning-information', 'planning-information',
PlanningStack, PlanningStack,
PlanningDisplayScreen, PlanningDisplayScreen,
@ -142,10 +140,11 @@ const HomeStack = createStackNavigator();
function HomeStackComponent( function HomeStackComponent(
initialRoute: string | null, initialRoute: string | null,
defaultData: {[key: string]: string}, defaultData: {[key: string]: string},
): React.Node { ) {
let params; let params;
if (initialRoute != null) if (initialRoute) {
params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true}; params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true};
}
const {colors} = useTheme(); const {colors} = useTheme();
return ( return (
<HomeStack.Navigator <HomeStack.Navigator
@ -161,7 +160,7 @@ function HomeStackComponent(
headerStyle: { headerStyle: {
backgroundColor: colors.surface, backgroundColor: colors.surface,
}, },
headerTitle: (): React.Node => ( headerTitle: () => (
<View style={{flexDirection: 'row'}}> <View style={{flexDirection: 'row'}}>
<Mascot <Mascot
style={{ style={{
@ -203,19 +202,19 @@ function HomeStackComponent(
options={{title: i18n.t('screens.scanner.title')}} options={{title: i18n.t('screens.scanner.title')}}
/> />
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'club-information', 'club-information',
HomeStack, HomeStack,
ClubDisplayScreen, ClubDisplayScreen,
i18n.t('screens.clubs.details'), i18n.t('screens.clubs.details'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'feed-information', 'feed-information',
HomeStack, HomeStack,
FeedItemScreen, FeedItemScreen,
i18n.t('screens.home.feed'), i18n.t('screens.home.feed'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'planning-information', 'planning-information',
HomeStack, HomeStack,
PlanningDisplayScreen, PlanningDisplayScreen,
@ -227,7 +226,7 @@ function HomeStackComponent(
const PlanexStack = createStackNavigator(); const PlanexStack = createStackNavigator();
function PlanexStackComponent(): React.Node { function PlanexStackComponent() {
return ( return (
<PlanexStack.Navigator <PlanexStack.Navigator
initialRouteName="index" initialRouteName="index"
@ -239,7 +238,7 @@ function PlanexStackComponent(): React.Node {
PlanexScreen, PlanexScreen,
i18n.t('screens.planex.title'), i18n.t('screens.planex.title'),
)} )}
{createScreenCollapsibleStack( {CreateScreenCollapsibleStack(
'group-select', 'group-select',
PlanexStack, PlanexStack,
GroupSelectionScreen, GroupSelectionScreen,
@ -252,35 +251,33 @@ function PlanexStackComponent(): React.Node {
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
type PropsType = { type PropsType = {
defaultHomeRoute: string | null, defaultHomeRoute: string | null;
defaultHomeData: {[key: string]: string}, defaultHomeData: {[key: string]: string};
}; };
export default class TabNavigator extends React.Component<PropsType> { export default class TabNavigator extends React.Component<PropsType> {
createHomeStackComponent: () => React.Node;
defaultRoute: string; defaultRoute: string;
createHomeStackComponent: () => any;
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
if (props.defaultHomeRoute != null) this.defaultRoute = 'home'; this.defaultRoute = 'home';
else if (!props.defaultHomeRoute) {
this.defaultRoute = AsyncStorageManager.getString( this.defaultRoute = AsyncStorageManager.getString(
AsyncStorageManager.PREFERENCES.defaultStartScreen.key, AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
).toLowerCase(); ).toLowerCase();
this.createHomeStackComponent = (): React.Node => }
this.createHomeStackComponent = () =>
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData); HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
} }
render(): React.Node { render() {
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={this.defaultRoute} initialRouteName={this.defaultRoute}
// eslint-disable-next-line react/jsx-props-no-spreading tabBar={(tabProps) => <CustomTabBar {...tabProps} />}>
tabBar={(props: {...}): React.Node => <CustomTabBar {...props} />}>
<Tab.Screen <Tab.Screen
name="services" name="services"
option
component={ServicesStackComponent} component={ServicesStackComponent}
options={{title: i18n.t('screens.services.title')}} options={{title: i18n.t('screens.services.title')}}
/> />
@ -299,7 +296,6 @@ export default class TabNavigator extends React.Component<PropsType> {
component={PlanningStackComponent} component={PlanningStackComponent}
options={{title: i18n.t('screens.planning.title')}} options={{title: i18n.t('screens.planning.title')}}
/> />
<Tab.Screen <Tab.Screen
name="planex" name="planex"
component={PlanexStackComponent} component={PlanexStackComponent}

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {List} from 'react-native-paper'; import {List} from 'react-native-paper';
import {View} from 'react-native-animatable'; import {View} from 'react-native-animatable';
@ -26,8 +24,8 @@ import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatLis
import packageJson from '../../../package.json'; import packageJson from '../../../package.json';
type ListItemType = { type ListItemType = {
name: string, name: string;
version: string, version: string;
}; };
/** /**
@ -37,9 +35,9 @@ type ListItemType = {
* @return {Array<ListItemType>} * @return {Array<ListItemType>}
*/ */
function generateListFromObject(object: { function generateListFromObject(object: {
[key: string]: string, [key: string]: string;
}): Array<ListItemType> { }): Array<ListItemType> {
const list = []; const list: Array<ListItemType> = [];
const keys = Object.keys(object); const keys = Object.keys(object);
keys.forEach((key: string) => { keys.forEach((key: string) => {
list.push({name: key, version: object[key]}); list.push({name: key, version: object[key]});
@ -52,17 +50,17 @@ const LIST_ITEM_HEIGHT = 64;
/** /**
* Class defining a screen showing the list of libraries used by the app, taken from package.json * Class defining a screen showing the list of libraries used by the app, taken from package.json
*/ */
export default class AboutDependenciesScreen extends React.Component<null> { export default class AboutDependenciesScreen extends React.Component<{}> {
data: Array<ListItemType>; data: Array<ListItemType>;
constructor() { constructor(props: {}) {
super(); super(props);
this.data = generateListFromObject(packageJson.dependencies); this.data = generateListFromObject(packageJson.dependencies);
} }
keyExtractor = (item: ListItemType): string => item.name; keyExtractor = (item: ListItemType): string => item.name;
getRenderItem = ({item}: {item: ListItemType}): React.Node => ( getRenderItem = ({item}: {item: ListItemType}) => (
<List.Item <List.Item
title={item.name} title={item.name}
description={item.version.replace('^', '').replace('~', '')} description={item.version.replace('^', '').replace('~', '')}
@ -71,15 +69,15 @@ export default class AboutDependenciesScreen extends React.Component<null> {
); );
getItemLayout = ( getItemLayout = (
data: ListItemType, data: Array<ListItemType> | null | undefined,
index: number, index: number,
): {length: number, offset: number, index: number} => ({ ): {length: number; offset: number; index: number} => ({
length: LIST_ITEM_HEIGHT, length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index, offset: LIST_ITEM_HEIGHT * index,
index, index,
}); });
render(): React.Node { render() {
return ( return (
<View> <View>
<CollapsibleFlatList <CollapsibleFlatList

View file

@ -17,37 +17,32 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import {FlatList, Linking, Platform, Image, View} from 'react-native'; import {FlatList, Linking, Platform, Image, View} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {Avatar, Card, List, withTheme} from 'react-native-paper'; import {Avatar, Card, List} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import packageJson from '../../../package.json'; import packageJson from '../../../package.json';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import APP_LOGO from '../../../assets/android.icon.round.png';
import type {
CardTitleIconPropsType,
ListIconPropsType,
} from '../../constants/PaperStyles';
import OptionsDialog from '../../components/Dialogs/OptionsDialog'; import OptionsDialog from '../../components/Dialogs/OptionsDialog';
import type {OptionsDialogButtonType} from '../../components/Dialogs/OptionsDialog'; import type {OptionsDialogButtonType} from '../../components/Dialogs/OptionsDialog';
const APP_LOGO = require('../../../assets/android.icon.round.png');
type ListItemType = { type ListItemType = {
onPressCallback: () => void, onPressCallback: () => void;
icon: string, icon: string;
text: string, text: string;
showChevron: boolean, showChevron: boolean;
}; };
type MemberItemType = { type MemberItemType = {
name: string, name: string;
message: string, message: string;
icon: string, icon: string;
trollLink?: string, trollLink?: string;
linkedin?: string, linkedin?: string;
mail?: string, mail?: string;
}; };
const links = { const links = {
@ -64,14 +59,14 @@ const links = {
}; };
type PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
}; };
type StateType = { type StateType = {
dialogVisible: boolean, dialogVisible: boolean;
dialogTitle: string, dialogTitle: string;
dialogMessage: string, dialogMessage: string;
dialogButtons: Array<OptionsDialogButtonType>, dialogButtons: Array<OptionsDialogButtonType>;
}; };
/** /**
@ -86,11 +81,10 @@ function openWebLink(link: string) {
* Class defining an about screen. This screen shows the user information about the app and it's author. * Class defining an about screen. This screen shows the user information about the app and it's author.
*/ */
class AboutScreen extends React.Component<PropsType, StateType> { class AboutScreen extends React.Component<PropsType, StateType> {
/** /**
* Object containing data relative to major contributors * Object containing data relative to major contributors
*/ */
static majorContributors: {[key: string]: MemberItemType} = { majorContributors: {[key: string]: MemberItemType} = {
arnaud: { arnaud: {
name: 'Arnaud Vergnet', name: 'Arnaud Vergnet',
message: i18n.t('screens.about.user.arnaud'), message: i18n.t('screens.about.user.arnaud'),
@ -109,7 +103,8 @@ class AboutScreen extends React.Component<PropsType, StateType> {
message: i18n.t('screens.about.user.yohan'), message: i18n.t('screens.about.user.yohan'),
icon: 'xml', icon: 'xml',
linkedin: 'https://www.linkedin.com/in/yohan-simard', linkedin: 'https://www.linkedin.com/in/yohan-simard',
mail: 'mailto:ysimard@etud.insa-toulouse.fr?' + mail:
'mailto:ysimard@etud.insa-toulouse.fr?' +
'subject=' + 'subject=' +
'Application Amicale INSA Toulouse' + 'Application Amicale INSA Toulouse' +
'&body=' + '&body=' +
@ -120,7 +115,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
/** /**
* Object containing data relative to users who helped during development * Object containing data relative to users who helped during development
*/ */
static helpfulUsers: {[key: string]: MemberItemType} = { helpfulUsers: {[key: string]: MemberItemType} = {
beranger: { beranger: {
name: 'Béranger Quintana Y Arciosana', name: 'Béranger Quintana Y Arciosana',
message: i18n.t('screens.about.user.beranger'), message: i18n.t('screens.about.user.beranger'),
@ -204,18 +199,18 @@ class AboutScreen extends React.Component<PropsType, StateType> {
teamData: Array<ListItemType> = [ teamData: Array<ListItemType> = [
{ {
onPressCallback: () => { onPressCallback: () => {
this.onContributorListItemPress(AboutScreen.majorContributors.arnaud); this.onContributorListItemPress(this.majorContributors.arnaud);
}, },
icon: AboutScreen.majorContributors.arnaud.icon, icon: this.majorContributors.arnaud.icon,
text: AboutScreen.majorContributors.arnaud.name, text: this.majorContributors.arnaud.name,
showChevron: false, showChevron: false,
}, },
{ {
onPressCallback: () => { onPressCallback: () => {
this.onContributorListItemPress(AboutScreen.majorContributors.yohan); this.onContributorListItemPress(this.majorContributors.yohan);
}, },
icon: AboutScreen.majorContributors.yohan.icon, icon: this.majorContributors.yohan.icon,
text: AboutScreen.majorContributors.yohan.name, text: this.majorContributors.yohan.name,
showChevron: false, showChevron: false,
}, },
{ {
@ -235,42 +230,42 @@ class AboutScreen extends React.Component<PropsType, StateType> {
thanksData: Array<ListItemType> = [ thanksData: Array<ListItemType> = [
{ {
onPressCallback: () => { onPressCallback: () => {
this.onContributorListItemPress(AboutScreen.helpfulUsers.beranger); this.onContributorListItemPress(this.helpfulUsers.beranger);
}, },
icon: AboutScreen.helpfulUsers.beranger.icon, icon: this.helpfulUsers.beranger.icon,
text: AboutScreen.helpfulUsers.beranger.name, text: this.helpfulUsers.beranger.name,
showChevron: false, showChevron: false,
}, },
{ {
onPressCallback: () => { onPressCallback: () => {
this.onContributorListItemPress(AboutScreen.helpfulUsers.celine); this.onContributorListItemPress(this.helpfulUsers.celine);
}, },
icon: AboutScreen.helpfulUsers.celine.icon, icon: this.helpfulUsers.celine.icon,
text: AboutScreen.helpfulUsers.celine.name, text: this.helpfulUsers.celine.name,
showChevron: false, showChevron: false,
}, },
{ {
onPressCallback: () => { onPressCallback: () => {
this.onContributorListItemPress(AboutScreen.helpfulUsers.damien); this.onContributorListItemPress(this.helpfulUsers.damien);
}, },
icon: AboutScreen.helpfulUsers.damien.icon, icon: this.helpfulUsers.damien.icon,
text: AboutScreen.helpfulUsers.damien.name, text: this.helpfulUsers.damien.name,
showChevron: false, showChevron: false,
}, },
{ {
onPressCallback: () => { onPressCallback: () => {
this.onContributorListItemPress(AboutScreen.helpfulUsers.titouan); this.onContributorListItemPress(this.helpfulUsers.titouan);
}, },
icon: AboutScreen.helpfulUsers.titouan.icon, icon: this.helpfulUsers.titouan.icon,
text: AboutScreen.helpfulUsers.titouan.name, text: this.helpfulUsers.titouan.name,
showChevron: false, showChevron: false,
}, },
{ {
onPressCallback: () => { onPressCallback: () => {
this.onContributorListItemPress(AboutScreen.helpfulUsers.theo); this.onContributorListItemPress(this.helpfulUsers.theo);
}, },
icon: AboutScreen.helpfulUsers.theo.icon, icon: this.helpfulUsers.theo.icon,
text: AboutScreen.helpfulUsers.theo.name, text: this.helpfulUsers.theo.name,
showChevron: false, showChevron: false,
}, },
]; ];
@ -333,7 +328,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* @param user The member to show information for * @param user The member to show information for
*/ */
onContributorListItemPress(user: MemberItemType) { onContributorListItemPress(user: MemberItemType) {
const dialogBtn = [ const dialogBtn: Array<OptionsDialogButtonType> = [
{ {
title: 'OK', title: 'OK',
onPress: this.onDialogDismiss, onPress: this.onDialogDismiss,
@ -379,15 +374,14 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getAppCard(): React.Node { getAppCard() {
return ( return (
<Card style={{marginBottom: 10}}> <Card style={{marginBottom: 10}}>
<Card.Title <Card.Title
title="Campus" title="Campus"
subtitle={packageJson.version} subtitle={packageJson.version}
left={(iconProps: CardTitleIconPropsType): React.Node => ( left={(iconProps) => (
<Image <Image
size={iconProps.size}
source={APP_LOGO} source={APP_LOGO}
style={{width: iconProps.size, height: iconProps.size}} style={{width: iconProps.size, height: iconProps.size}}
/> />
@ -409,12 +403,12 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getTeamCard(): React.Node { getTeamCard() {
return ( return (
<Card style={{marginBottom: 10}}> <Card style={{marginBottom: 10}}>
<Card.Title <Card.Title
title={i18n.t('screens.about.team')} title={i18n.t('screens.about.team')}
left={(iconProps: CardTitleIconPropsType): React.Node => ( left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="account-multiple" /> <Avatar.Icon size={iconProps.size} icon="account-multiple" />
)} )}
/> />
@ -434,12 +428,12 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getThanksCard(): React.Node { getThanksCard() {
return ( return (
<Card style={{marginBottom: 10}}> <Card style={{marginBottom: 10}}>
<Card.Title <Card.Title
title={i18n.t('screens.about.thanks')} title={i18n.t('screens.about.thanks')}
left={(iconProps: CardTitleIconPropsType): React.Node => ( left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="hand-heart" /> <Avatar.Icon size={iconProps.size} icon="hand-heart" />
)} )}
/> />
@ -459,12 +453,12 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getTechnoCard(): React.Node { getTechnoCard() {
return ( return (
<Card style={{marginBottom: 10}}> <Card style={{marginBottom: 10}}>
<Card.Title <Card.Title
title={i18n.t('screens.about.technologies')} title={i18n.t('screens.about.technologies')}
left={(iconProps: CardTitleIconPropsType): React.Node => ( left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="wrench" /> <Avatar.Icon size={iconProps.size} icon="wrench" />
)} )}
/> />
@ -485,7 +479,13 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* @param props * @param props
* @return {*} * @return {*}
*/ */
static getChevronIcon(props: ListIconPropsType): React.Node { static getChevronIcon(props: {
color: string;
style?: {
marginRight: number;
marginVertical?: number;
};
}) {
return ( return (
<List.Icon color={props.color} style={props.style} icon="chevron-right" /> <List.Icon color={props.color} style={props.style} icon="chevron-right" />
); );
@ -498,7 +498,16 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* @param props * @param props
* @return {*} * @return {*}
*/ */
static getItemIcon(item: ListItemType, props: ListIconPropsType): React.Node { static getItemIcon(
item: ListItemType,
props: {
color: string;
style?: {
marginRight: number;
marginVertical?: number;
};
},
) {
return ( return (
<List.Icon color={props.color} style={props.style} icon={item.icon} /> <List.Icon color={props.color} style={props.style} icon={item.icon} />
); );
@ -509,9 +518,14 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* *
* @returns {*} * @returns {*}
*/ */
getCardItem = ({item}: {item: ListItemType}): React.Node => { getCardItem = ({item}: {item: ListItemType}) => {
const getItemIcon = (props: ListIconPropsType): React.Node => const getItemIcon = (props: {
AboutScreen.getItemIcon(item, props); color: string;
style?: {
marginRight: number;
marginVertical?: number;
};
}) => AboutScreen.getItemIcon(item, props);
if (item.showChevron) { if (item.showChevron) {
return ( return (
<List.Item <List.Item
@ -537,7 +551,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
* @param item The item to show * @param item The item to show
* @return {*} * @return {*}
*/ */
getMainCard = ({item}: {item: {id: string}}): React.Node => { getMainCard = ({item}: {item: {id: string}}) => {
switch (item.id) { switch (item.id) {
case 'app': case 'app':
return this.getAppCard(); return this.getAppCard();
@ -564,7 +578,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
*/ */
keyExtractor = (item: ListItemType): string => item.icon; keyExtractor = (item: ListItemType): string => item.icon;
render(): React.Node { render() {
const {state} = this; const {state} = this;
return ( return (
<View <View
@ -588,4 +602,4 @@ class AboutScreen extends React.Component<PropsType, StateType> {
} }
} }
export default withTheme(AboutScreen); export default AboutScreen;

Some files were not shown because too many files have changed in this diff Show more