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 = {
root: true,
extends: [
'airbnb',
'plugin:flowtype/recommended',
'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,
},
extends: '@react-native-community',
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
};

View file

View file

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

View file

@ -1,3 +1,8 @@
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

@ -8,14 +8,15 @@ 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 !
* [**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.
## 🤔 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.
* [**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 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.

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.
## _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
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...",
"retry": "Réessayer",
"networkError": "Impossible de contacter les serveurs. Assure-toi d'être connecté à Internet.",
"goBack": "Suivant",
"goForward": "Précédent",
"goBack": "Précédent",
"goForward": "Suivant",
"openInBrowser": "Ouvrir dans le navigateur",
"notAvailable": "Non disponible",
"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/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": {
"version": "7.10.5",
"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",
"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": {
"version": "0.1.10",
"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",
"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": {
"version": "4.1.3",
"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",
"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": {
"version": "2.0.3",
"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/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": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@ -2478,11 +2583,72 @@
"integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==",
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
"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": {
"version": "15.0.5",
"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",
"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": {
"version": "2.0.4",
"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": {
"version": "1.0.6",
"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": {
"version": "5.2.0",
"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": {
"version": "6.3.1",
"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": {
"version": "7.20.6",
"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==",
"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": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz",
@ -4708,6 +5008,12 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"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": {
"version": "2.1.0",
"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==",
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -10032,6 +10332,15 @@
"integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==",
"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": {
"version": "24.9.0",
"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",
"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": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -11956,6 +12274,12 @@
"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": {
"version": "0.7.21",
"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",
"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": {
"version": "4.0.2",
"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",
"ios": "react-native run-ios",
"test": "jest",
"lint": "eslint ."
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
},
"jest": {
"preset": "react-native",
"transformIgnorePatterns": [
"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)"
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"setupFilesAfterEnv": [
"jest-extended"
@ -59,9 +64,16 @@
},
"devDependencies": {
"@babel/core": "^7.11.0",
"@babel/preset-flow": "^7.10.4",
"@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",
"eslint": "^7.2.0",
"eslint-config-airbnb": "^18.2.0",
@ -71,11 +83,11 @@
"eslint-plugin-jsx-a11y": "^6.3.1",
"eslint-plugin-react": "^7.20.5",
"eslint-plugin-react-hooks": "^4.0.0",
"flow-bin": "^0.123.0",
"jest": "^25.1.0",
"jest-extended": "^0.11.5",
"metro-react-native-babel-preset": "^0.59.0",
"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/>.
*/
// @flow
import * as React from 'react';
import {StackNavigationProp} from '@react-navigation/stack';
import ConnectionManager from '../../managers/ConnectionManager';
import type {ApiGenericDataType} from '../../utils/WebData';
import {ERROR_TYPE} from '../../utils/WebData';
import ErrorView from '../Screens/ErrorView';
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
type PropsType = {
navigation: StackNavigationProp,
type PropsType<T> = {
navigation: StackNavigationProp<any>;
requests: Array<{
link: string,
params: {...},
mandatory: boolean,
}>,
renderFunction: (Array<ApiGenericDataType | null>) => React.Node,
link: string;
params: object;
mandatory: boolean;
}>;
renderFunction: (data: Array<T | null>) => React.ReactNode;
errorViewOverride?: Array<{
errorCode: number,
message: string,
icon: string,
showRetryButton: boolean,
}> | null,
errorCode: number;
message: string;
icon: string;
showRetryButton: boolean;
}> | null;
};
type StateType = {
loading: boolean,
loading: boolean;
};
class AuthenticatedScreen extends React.Component<PropsType, StateType> {
class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
static defaultProps = {
errorViewOverride: null,
};
@ -58,13 +55,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
errors: Array<number>;
fetchedData: Array<ApiGenericDataType | null>;
fetchedData: Array<T | null>;
constructor(props: PropsType) {
constructor(props: PropsType<T>) {
super(props);
this.state = {
loading: true,
};
this.currentUserToken = null;
this.connectionManager = ConnectionManager.getInstance();
props.navigation.addListener('focus', this.onScreenFocus);
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 error The error code received
*/
onRequestFinished(
data: ApiGenericDataType | null,
index: number,
error?: number,
) {
onRequestFinished(data: T | null, index: number, error?: number) {
const {props} = this;
if (index >= 0 && index < props.requests.length) {
this.fetchedData[index] = data;
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
}
// 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 {*}
*/
getErrorRender(): React.Node {
getErrorRender() {
const {props} = this;
const errorCode = this.getError();
let shouldOverride = false;
@ -169,18 +167,18 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
*/
fetchData = () => {
const {state, props} = this;
if (!state.loading) this.setState({loading: true});
if (!state.loading) {
this.setState({loading: true});
}
if (this.connectionManager.isLoggedIn()) {
for (let i = 0; i < props.requests.length; i += 1) {
this.connectionManager
.authenticatedRequest(
.authenticatedRequest<T>(
props.requests[i].link,
props.requests[i].params,
)
.then((response: ApiGenericDataType): void =>
this.onRequestFinished(response, i),
)
.then((response: T): void => this.onRequestFinished(response, i))
.catch((error: number): void =>
this.onRequestFinished(null, i, error),
);
@ -200,7 +198,9 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
allRequestsFinished(): boolean {
let finished = true;
this.errors.forEach((error: number | null) => {
if (error == null) finished = false;
if (error == null) {
finished = false;
}
});
return finished;
}
@ -212,11 +212,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
this.fetchData();
}
render(): React.Node {
render() {
const {state, props} = this;
if (state.loading) return <BasicLoadingScreen />;
if (this.getError() === ERROR_TYPE.SUCCESS)
if (state.loading) {
return <BasicLoadingScreen />;
}
if (this.getError() === ERROR_TYPE.SUCCESS) {
return props.renderFunction(this.fetchedData);
}
return this.getErrorRender();
}
}

View file

@ -17,28 +17,25 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
import ConnectionManager from '../../managers/ConnectionManager';
import {useNavigation} from '@react-navigation/native';
type PropsType = {
navigation: StackNavigationProp,
visible: boolean,
onDismiss: () => void,
visible: boolean;
onDismiss: () => void;
};
class LogoutDialog extends React.PureComponent<PropsType> {
onClickAccept = async (): Promise<void> => {
const {props} = this;
function LogoutDialog(props: PropsType) {
const navigation = useNavigation();
const onClickAccept = async (): Promise<void> => {
return new Promise((resolve: () => void) => {
ConnectionManager.getInstance()
.disconnect()
.then(() => {
props.navigation.reset({
navigation.reset({
index: 0,
routes: [{name: 'main'}],
});
@ -48,19 +45,16 @@ class LogoutDialog extends React.PureComponent<PropsType> {
});
};
render(): React.Node {
const {props} = this;
return (
<LoadingConfirmDialog
visible={props.visible}
onDismiss={props.onDismiss}
onAccept={this.onClickAccept}
title={i18n.t('dialog.disconnect.title')}
titleLoading={i18n.t('dialog.disconnect.titleLoading')}
message={i18n.t('dialog.disconnect.message')}
/>
);
}
return (
<LoadingConfirmDialog
visible={props.visible}
onDismiss={props.onDismiss}
onAccept={onClickAccept}
title={i18n.t('dialog.disconnect.title')}
titleLoading={i18n.t('dialog.disconnect.titleLoading')}
message={i18n.t('dialog.disconnect.message')}
/>
);
}
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/>.
*/
// @flow
import * as React from 'react';
import {
Avatar,
@ -31,16 +29,11 @@ import {
import {FlatList, StyleSheet} from 'react-native';
import i18n from 'i18n-js';
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {
CardTitleIconPropsType,
ListIconPropsType,
} from '../../../constants/PaperStyles';
type PropsType = {
teams: Array<VoteTeamType>,
dateEnd: string,
theme: CustomThemeType,
teams: Array<VoteTeamType>;
dateEnd: string;
theme: ReactNativePaper.Theme;
};
const styles = StyleSheet.create({
@ -58,10 +51,10 @@ class VoteResults extends React.Component<PropsType> {
winnerIds: Array<number>;
constructor(props: PropsType) {
super();
super(props);
props.teams.sort(this.sortByVotes);
this.getTotalVotes(props.teams);
this.getWinnerIds(props.teams);
this.totalVotes = this.getTotalVotes(props.teams);
this.winnerIds = this.getWinnerIds(props.teams);
}
shouldComponentUpdate(): boolean {
@ -69,26 +62,31 @@ class VoteResults extends React.Component<PropsType> {
}
getTotalVotes(teams: Array<VoteTeamType>) {
this.totalVotes = 0;
let totalVotes = 0;
for (let i = 0; i < teams.length; i += 1) {
this.totalVotes += teams[i].votes;
totalVotes += teams[i].votes;
}
return totalVotes;
}
getWinnerIds(teams: Array<VoteTeamType>) {
const max = teams[0].votes;
this.winnerIds = [];
let winnerIds = [];
for (let i = 0; i < teams.length; i += 1) {
if (teams[i].votes === max) this.winnerIds.push(teams[i].id);
else break;
if (teams[i].votes === max) {
winnerIds.push(teams[i].id);
} else {
break;
}
}
return winnerIds;
}
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
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 isDraw = this.winnerIds.length > 1;
const {props} = this;
@ -101,7 +99,7 @@ class VoteResults extends React.Component<PropsType> {
<List.Item
title={item.name}
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
left={(iconProps: ListIconPropsType): React.Node =>
left={(iconProps) =>
isWinner ? (
<List.Icon
style={iconProps.style}
@ -125,7 +123,7 @@ class VoteResults extends React.Component<PropsType> {
);
};
render(): React.Node {
render() {
const {props} = this;
return (
<Card style={styles.card}>
@ -134,15 +132,14 @@ class VoteResults extends React.Component<PropsType> {
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
props.dateEnd
}`}
left={(iconProps: CardTitleIconPropsType): React.Node => (
left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="podium-gold" />
)}
/>
<Card.Content>
<Subheading>{`${i18n.t('screens.vote.results.totalVotes')} ${
this.totalVotes
}`}</Subheading>
{/* $FlowFixMe */}
<Subheading>
{`${i18n.t('screens.vote.results.totalVotes')} ${this.totalVotes}`}
</Subheading>
<FlatList
data={props.teams}
keyExtractor={this.voteKeyExtractor}

View file

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

View file

@ -17,16 +17,13 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Avatar, Card, Paragraph} from 'react-native-paper';
import {StyleSheet} from 'react-native';
import i18n from 'i18n-js';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
startDate: string,
startDate: string;
};
const styles = StyleSheet.create({
@ -38,28 +35,19 @@ const styles = StyleSheet.create({
},
});
export default class VoteTease extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')}
left={(iconProps: CardTitleIconPropsType): React.Node => (
<Avatar.Icon size={iconProps.size} icon="vote" />
)}
/>
<Card.Content>
<Paragraph>
{`${i18n.t('screens.vote.tease.message')} ${props.startDate}`}
</Paragraph>
</Card.Content>
</Card>
);
}
export default function VoteTease(props: PropsType) {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')}
left={(iconProps) => <Avatar.Icon size={iconProps.size} icon="vote" />}
/>
<Card.Content>
<Paragraph>
{`${i18n.t('screens.vote.tease.message')} ${props.startDate}`}
</Paragraph>
</Card.Content>
</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/>.
*/
// @flow
import * as React from 'react';
import {View} from 'react-native';
import {View, ViewStyle} from 'react-native';
import {List, withTheme} from 'react-native-paper';
import Collapsible from 'react-native-collapsible';
import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {ListIconPropsType} from '../../constants/PaperStyles';
type PropsType = {
theme: CustomThemeType,
title: string,
subtitle?: string,
left?: () => React.Node,
opened?: boolean,
unmountWhenCollapsed?: boolean,
children?: React.Node,
theme: ReactNativePaper.Theme;
title: string;
subtitle?: string;
style?: ViewStyle;
left?: (props: {
color: string;
style?: {
marginRight: number;
marginVertical?: number;
};
}) => React.ReactNode;
opened?: boolean;
unmountWhenCollapsed?: boolean;
children?: React.ReactNode;
};
type StateType = {
expanded: boolean,
expanded: boolean;
};
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.Component<PropsType, StateType> {
static defaultProps = {
subtitle: '',
left: null,
opened: null,
unmountWhenCollapsed: false,
children: null,
};
chevronRef: {current: null | AnimatedListIcon};
chevronRef: {current: null | (typeof AnimatedListIcon & List.Icon)};
chevronIcon: string;
@ -62,6 +57,9 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
constructor(props: PropsType) {
super(props);
this.chevronIcon = '';
this.animStart = '';
this.animEnd = '';
this.state = {
expanded: props.opened != null ? props.opened : false,
};
@ -71,8 +69,9 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {state, props} = this;
if (nextProps.opened != null && nextProps.opened !== props.opened)
if (nextProps.opened != null && nextProps.opened !== props.opened) {
state.expanded = nextProps.opened;
}
return true;
}
@ -101,17 +100,17 @@ class AnimatedAccordion extends React.Component<PropsType, StateType> {
}
};
render(): React.Node {
render() {
const {props, state} = this;
const {colors} = props.theme;
return (
<View>
<View style={props.style}>
<List.Item
title={props.title}
subtitle={props.subtitle}
description={props.subtitle}
titleStyle={state.expanded ? {color: colors.primary} : null}
onPress={this.toggleAccordion}
right={(iconProps: ListIconPropsType): React.Node => (
right={(iconProps) => (
<AnimatedListIcon
ref={this.chevronRef}
style={iconProps.style}

View file

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

View file

@ -17,22 +17,23 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
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 * as Animatable from 'react-native-animatable';
import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar';
type PropsType = {
icon: string,
onPress: () => void,
icon: string;
onPress: () => void;
};
const AnimatedFab = Animatable.createAnimatableComponent(FAB);
const styles = StyleSheet.create({
fab: {
position: 'absolute',
@ -42,41 +43,49 @@ const styles = StyleSheet.create({
});
export default class AnimatedFAB extends React.Component<PropsType> {
ref: {current: null | Animatable.View};
ref: {current: null | (Animatable.View & View)};
hideHandler: AutoHideHandler;
constructor() {
super();
constructor(props: PropsType) {
super(props);
this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
this.hideHandler.onScroll(event);
};
onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) {
if (shouldHide) this.ref.current.bounceOutDown(1000);
else this.ref.current.bounceInUp(1000);
const ref = this.ref;
if (
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;
return (
<AnimatedFab
<Animatable.View
ref={this.ref}
useNativeDriver
icon={props.icon}
onPress={props.onPress}
useNativeDriver={true}
style={{
...styles.fab,
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/>.
*/
// @flow
import * as React from 'react';
import {Animated} from 'react-native';
import {Animated, FlatListProps} from 'react-native';
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent';
type PropsType = {
...CollapsibleComponentPropsType,
};
type Props<T> = FlatListProps<T> & CollapsibleComponentPropsType;
// eslint-disable-next-line react/prefer-stateless-function
class CollapsibleFlatList extends React.Component<PropsType> {
render(): React.Node {
const {props} = this;
return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
{...props}
component={Animated.FlatList}>
{props.children}
</CollapsibleComponent>
);
}
function CollapsibleFlatList<T>(props: Props<T>) {
return (
<CollapsibleComponent {...props} component={Animated.FlatList}>
{props.children}
</CollapsibleComponent>
);
}
export default CollapsibleFlatList;

View file

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

View file

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

View file

@ -17,36 +17,31 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import i18n from 'i18n-js';
type PropsType = {
visible: boolean,
onDismiss: () => void,
title: string | React.Node,
message: string | React.Node,
visible: boolean;
onDismiss: () => void;
title: string | React.ReactNode;
message: string | React.ReactNode;
};
class AlertDialog extends React.PureComponent<PropsType> {
render(): React.Node {
const {props} = this;
return (
<Portal>
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
<Dialog.Title>{props.title}</Dialog.Title>
<Dialog.Content>
<Paragraph>{props.message}</Paragraph>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={props.onDismiss}>{i18n.t('dialog.ok')}</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
}
function AlertDialog(props: PropsType) {
return (
<Portal>
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
<Dialog.Title>{props.title}</Dialog.Title>
<Dialog.Content>
<Paragraph>{props.message}</Paragraph>
</Dialog.Content>
<Dialog.Actions>
<Button onPress={props.onDismiss}>{i18n.t('dialog.ok')}</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
}
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/>.
*/
// @flow
import * as React from 'react';
import {
ActivityIndicator,
@ -30,20 +28,23 @@ import {
import i18n from 'i18n-js';
type PropsType = {
visible: boolean,
onDismiss?: () => void,
onAccept?: () => Promise<void>, // async function to be executed
title?: string,
titleLoading?: string,
message?: string,
startLoading?: boolean,
visible: boolean;
onDismiss?: () => void;
onAccept?: () => Promise<void>; // async function to be executed
title?: string;
titleLoading?: string;
message?: string;
startLoading?: boolean;
};
type StateType = {
loading: boolean,
loading: boolean;
};
class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
export default class LoadingConfirmDialog extends React.PureComponent<
PropsType,
StateType
> {
static defaultProps = {
onDismiss: () => {},
onAccept: (): Promise<void> => {
@ -71,14 +72,16 @@ class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
onClickAccept = () => {
const {props} = this;
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
* @returns {TimeoutID}
* @returns {NodeJS.Timeout}
*/
hideLoading = (): TimeoutID =>
hideLoading = (): NodeJS.Timeout =>
setTimeout(() => {
this.setState({loading: false});
}, 200);
@ -88,10 +91,12 @@ class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
*/
onDismiss = () => {
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;
return (
<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/>.
*/
// @flow
import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import {FlatList} from 'react-native';
export type OptionsDialogButtonType = {
title: string,
icon?: string,
onPress: () => void,
title: string;
icon?: string;
onPress: () => void;
};
type PropsType = {
visible: boolean,
title: string,
message: string,
buttons: Array<OptionsDialogButtonType>,
onDismiss: () => void,
visible: boolean;
title: string;
message: string;
buttons: Array<OptionsDialogButtonType>;
onDismiss: () => void;
};
class OptionsDialog extends React.PureComponent<PropsType> {
getButtonRender = ({item}: {item: OptionsDialogButtonType}): React.Node => {
function OptionsDialog(props: PropsType) {
const getButtonRender = ({item}: {item: OptionsDialogButtonType}) => {
return (
<Button onPress={item.onPress} icon={item.icon}>
{item.title}
@ -46,35 +44,32 @@ class OptionsDialog extends React.PureComponent<PropsType> {
);
};
keyExtractor = (item: OptionsDialogButtonType): string => {
const keyExtractor = (item: OptionsDialogButtonType): string => {
if (item.icon != null) {
return item.title + item.icon;
}
return item.title;
};
render(): React.Node {
const {props} = this;
return (
<Portal>
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
<Dialog.Title>{props.title}</Dialog.Title>
<Dialog.Content>
<Paragraph>{props.message}</Paragraph>
</Dialog.Content>
<Dialog.Actions>
<FlatList
data={props.buttons}
renderItem={this.getButtonRender}
keyExtractor={this.keyExtractor}
horizontal
inverted
/>
</Dialog.Actions>
</Dialog>
</Portal>
);
}
return (
<Portal>
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
<Dialog.Title>{props.title}</Dialog.Title>
<Dialog.Content>
<Paragraph>{props.message}</Paragraph>
</Dialog.Content>
<Dialog.Actions>
<FlatList
data={props.buttons}
renderItem={getButtonRender}
keyExtractor={keyExtractor}
horizontal
inverted
/>
</Dialog.Actions>
</Dialog>
</Portal>
);
}
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/>.
*/
// @flow
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
type PropsType = {
icon: string,
icon: string;
};
const styles = StyleSheet.create({
@ -37,24 +35,14 @@ const styles = StyleSheet.create({
},
});
class IntroIcon extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {icon} = this.props;
return (
<View style={{flex: 1}}>
<Animatable.View
useNativeDriver
style={styles.center}
animation="fadeIn">
<MaterialCommunityIcons name={icon} color="#fff" size={200} />
</Animatable.View>
</View>
);
}
function IntroIcon(props: PropsType) {
return (
<View style={{flex: 1}}>
<Animatable.View useNativeDriver style={styles.center} animation="fadeIn">
<MaterialCommunityIcons name={props.icon} color="#fff" size={200} />
</Animatable.View>
</View>
);
}
export default IntroIcon;

View file

@ -17,8 +17,6 @@
* 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 Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
@ -32,34 +30,28 @@ const styles = StyleSheet.create({
},
});
class MascotIntroEnd extends React.Component<null> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
return (
<View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
width: '80%',
}}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'slideInDown',
duration: 2000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
</View>
);
}
function MascotIntroEnd() {
return (
<View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
width: '80%',
}}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'slideInDown',
duration: 2000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
</View>
);
}
export default MascotIntroEnd;

View file

@ -17,8 +17,6 @@
* 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 * as Animatable from 'react-native-animatable';
@ -34,62 +32,56 @@ const styles = StyleSheet.create({
},
});
class MascotIntroWelcome extends React.Component<null> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
return (
<View style={{flex: 1}}>
<Mascot
function MascotIntroWelcome() {
return (
<View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
width: '80%',
}}
emotion={MASCOT_STYLE.NORMAL}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 2000,
}}
/>
<Animatable.Text
useNativeDriver
animation="fadeInUp"
duration={500}
style={{
color: '#fff',
textAlign: 'center',
fontSize: 25,
}}>
PABLO
</Animatable.Text>
<Animatable.View
useNativeDriver
animation="fadeInUp"
duration={500}
delay={200}
style={{
position: 'absolute',
bottom: 30,
right: '20%',
width: 50,
height: 50,
}}>
<MaterialCommunityIcons
style={{
...styles.center,
width: '80%',
}}
emotion={MASCOT_STYLE.NORMAL}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 2000,
transform: [{rotateZ: '70deg'}],
}}
name="undo"
color="#fff"
size={40}
/>
<Animatable.Text
useNativeDriver
animation="fadeInUp"
duration={500}
style={{
color: '#fff',
textAlign: 'center',
fontSize: 25,
}}>
PABLO
</Animatable.Text>
<Animatable.View
useNativeDriver
animation="fadeInUp"
duration={500}
delay={200}
style={{
position: 'absolute',
bottom: 30,
right: '20%',
width: 50,
height: 50,
}}>
<MaterialCommunityIcons
style={{
...styles.center,
transform: [{rotateZ: '70deg'}],
}}
name="undo"
color="#fff"
size={40}
/>
</Animatable.View>
</View>
);
}
</Animatable.View>
</View>
);
}
export default MascotIntroWelcome;

View file

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

View file

@ -17,45 +17,38 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
import {View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = {
item: ServiceItemType,
item: ServiceItemType;
};
export default class CardListItem 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 (
<Card
style={{
width: '40%',
margin: 5,
marginLeft: 'auto',
marginRight: 'auto',
}}>
<TouchableRipple style={{flex: 1}} onPress={item.onPress}>
<View>
<Card.Cover style={{height: 80}} source={source} />
<Card.Content>
<Paragraph>{item.title}</Paragraph>
<Caption>{item.subtitle}</Caption>
</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
function CardListItem(props: PropsType) {
const {item} = props;
const source =
typeof item.image === 'number' ? item.image : {uri: item.image};
return (
<Card
style={{
width: '40%',
margin: 5,
marginLeft: 'auto',
marginRight: 'auto',
}}>
<TouchableRipple style={{flex: 1}} onPress={item.onPress}>
<View>
<Card.Cover style={{height: 80}} source={source} />
<Card.Content>
<Paragraph>{item.title}</Paragraph>
<Caption>{item.subtitle}</Caption>
</Card.Content>
</View>
</TouchableRipple>
</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/>.
*/
// @flow
import * as React from 'react';
import {Card, Chip, List, Text} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
@ -26,12 +24,11 @@ import i18n from 'i18n-js';
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import {isItemInCategoryFilter} from '../../../utils/Search';
import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
categories: Array<ClubCategoryType>,
onChipSelect: (id: number) => void,
selectedCategories: Array<number>,
categories: Array<ClubCategoryType>;
onChipSelect: (id: number) => void;
selectedCategories: Array<number>;
};
const styles = StyleSheet.create({
@ -54,16 +51,8 @@ const styles = StyleSheet.create({
},
});
class ClubListHeader extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.selectedCategories.length !== props.selectedCategories.length
);
}
getChipRender = (category: ClubCategoryType, key: string): React.Node => {
const {props} = this;
function ClubListHeader(props: PropsType) {
const getChipRender = (category: ClubCategoryType, key: string) => {
const onPress = (): void => props.onChipSelect(category.id);
return (
<Chip
@ -80,32 +69,39 @@ class ClubListHeader extends React.Component<PropsType> {
);
};
getCategoriesRender(): React.Node {
const {props} = this;
const final = [];
const getCategoriesRender = () => {
const final: Array<React.ReactNode> = [];
props.categories.forEach((cat: ClubCategoryType) => {
final.push(this.getChipRender(cat, cat.id.toString()));
final.push(getChipRender(cat, cat.id.toString()));
});
return final;
}
};
render(): React.Node {
return (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t('screens.clubs.categories')}
left={(props: ListIconPropsType): React.Node => (
<List.Icon color={props.color} style={props.style} icon="star" />
)}
opened>
<Text style={styles.text}>
{i18n.t('screens.clubs.categoriesFilterMessage')}
</Text>
<View style={styles.chipContainer}>{this.getCategoriesRender()}</View>
</AnimatedAccordion>
</Card>
);
}
return (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t('screens.clubs.categories')}
left={(iconProps) => (
<List.Icon
color={iconProps.color}
style={iconProps.style}
icon="star"
/>
)}
opened>
<Text style={styles.text}>
{i18n.t('screens.clubs.categoriesFilterMessage')}
</Text>
<View style={styles.chipContainer}>{getCategoriesRender()}</View>
</AnimatedAccordion>
</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/>.
*/
// @flow
import * as React from 'react';
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
import {View} from 'react-native';
@ -26,14 +24,13 @@ import type {
ClubCategoryType,
ClubType,
} from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
onPress: () => void,
categoryTranslator: (id: number) => ClubCategoryType,
item: ClubType,
height: number,
theme: CustomThemeType,
onPress: () => void;
categoryTranslator: (id: number) => ClubCategoryType | null;
item: ClubType;
height: number;
theme: ReactNativePaper.Theme;
};
class ClubListItem extends React.Component<PropsType> {
@ -48,27 +45,29 @@ class ClubListItem extends React.Component<PropsType> {
return false;
}
getCategoriesRender(categories: Array<number | null>): React.Node {
getCategoriesRender(categories: Array<number | null>) {
const {props} = this;
const final = [];
const final: Array<React.ReactNode> = [];
categories.forEach((cat: number | null) => {
if (cat != null) {
const category: ClubCategoryType = props.categoryTranslator(cat);
final.push(
<Chip
style={{marginRight: 5, marginBottom: 5}}
key={`${props.item.id}:${category.id}`}>
{category.name}
</Chip>,
);
const category = props.categoryTranslator(cat);
if (category) {
final.push(
<Chip
style={{marginRight: 5, marginBottom: 5}}
key={`${props.item.id}:${category.id}`}>
{category.name}
</Chip>,
);
}
}
});
return <View style={{flexDirection: 'row'}}>{final}</View>;
}
render(): React.Node {
render() {
const {props} = this;
const categoriesRender = (): React.Node =>
const categoriesRender = () =>
this.getCategoriesRender(props.item.category);
const {colors} = props.theme;
return (
@ -76,7 +75,7 @@ class ClubListItem extends React.Component<PropsType> {
title={props.item.name}
description={categoriesRender}
onPress={props.onPress}
left={(): React.Node => (
left={() => (
<Avatar.Image
style={{
backgroundColor: 'transparent',
@ -87,7 +86,7 @@ class ClubListItem extends React.Component<PropsType> {
source={{uri: props.item.logo}}
/>
)}
right={(): React.Node => (
right={() => (
<Avatar.Icon
style={{
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,61 +17,54 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
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 type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
image: string,
isActive: boolean,
onPress: () => void,
theme: CustomThemeType,
image?: string | number;
isActive: boolean;
onPress: () => void;
};
/**
* Component used to render a small dashboard item
*/
class DashboardEditPreviewItem extends React.Component<PropsType> {
itemSize: number;
function DashboardEditPreviewItem(props: PropsType) {
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 (
<TouchableRipple
onPress={props.onPress}
borderless
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: 5,
marginRight: 5,
backgroundColor: props.isActive
? theme.colors.textDisabled
: 'transparent',
borderRadius: 5,
}}>
<View
style={{
marginLeft: 5,
marginRight: 5,
backgroundColor: props.isActive
? props.theme.colors.textDisabled
: 'transparent',
borderRadius: 5,
width: itemSize,
height: itemSize,
}}>
<View
style={{
width: this.itemSize,
height: this.itemSize,
}}>
{props.image ? (
<Image
source={{uri: props.image}}
source={
typeof props.image === 'string' ? {uri: props.image} : props.image
}
style={{
width: '100%',
height: '100%',
}}
/>
</View>
</TouchableRipple>
);
}
) : null}
</View>
</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/>.
*/
// @flow
import * as React from 'react';
import {List, withTheme} from 'react-native-paper';
import {FlatList, View} from 'react-native';
@ -29,17 +27,14 @@ import type {
PlanexGroupType,
PlanexGroupCategoryType,
} from '../../../screens/Planex/GroupSelectionScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
item: PlanexGroupCategoryType,
favorites: Array<PlanexGroupType>,
onGroupPress: (PlanexGroupType) => void,
onFavoritePress: (PlanexGroupType) => void,
currentSearchString: string,
height: number,
theme: CustomThemeType,
item: PlanexGroupCategoryType;
favorites: Array<PlanexGroupType>;
onGroupPress: (data: PlanexGroupType) => void;
onFavoritePress: (data: PlanexGroupType) => void;
currentSearchString: string;
theme: ReactNativePaper.Theme;
};
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 onPress = () => {
props.onGroupPress(item);
@ -77,18 +72,19 @@ class GroupListAccordion extends React.Component<PropsType> {
getData(): Array<PlanexGroupType> {
const {props} = this;
const originalData = props.item.content;
const displayData = [];
const displayData: Array<PlanexGroupType> = [];
originalData.forEach((data: PlanexGroupType) => {
if (stringMatchQuery(data.name, props.currentSearchString))
if (stringMatchQuery(data.name, props.currentSearchString)) {
displayData.push(data);
}
});
return displayData;
}
itemLayout = (
data: ?Array<PlanexGroupType>,
data: Array<PlanexGroupType> | null | undefined,
index: number,
): {length: number, offset: number, index: number} => ({
): {length: number; offset: number; index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
@ -96,7 +92,7 @@ class GroupListAccordion extends React.Component<PropsType> {
keyExtractor = (item: PlanexGroupType): string => item.id.toString();
render(): React.Node {
render() {
const {props} = this;
const {item} = this.props;
return (
@ -104,10 +100,9 @@ class GroupListAccordion extends React.Component<PropsType> {
<AnimatedAccordion
title={item.name.replace(REPLACE_REGEX, ' ')}
style={{
height: props.height,
justifyContent: 'center',
}}
left={(iconProps: ListIconPropsType): React.Node =>
left={(iconProps) =>
item.id === 0 ? (
<List.Icon
style={iconProps.style}

View file

@ -17,23 +17,20 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {List, TouchableRipple, withTheme} from 'react-native-paper';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
import type {ListIconPropsType} from '../../../constants/PaperStyles';
import {View} from 'react-native';
type PropsType = {
theme: CustomThemeType,
onPress: () => void,
onStarPress: () => void,
item: PlanexGroupType,
favorites: Array<PlanexGroupType>,
height: number,
theme: ReactNativePaper.Theme;
onPress: () => void;
onStarPress: () => void;
item: PlanexGroupType;
favorites: Array<PlanexGroupType>;
height: number;
};
const REPLACE_REGEX = /_/g;
@ -41,10 +38,11 @@ const REPLACE_REGEX = /_/g;
class GroupListItem extends React.Component<PropsType> {
isFav: boolean;
starRef: null | Animatable.View;
starRef: {current: null | (Animatable.View & View)};
constructor(props: PropsType) {
super(props);
this.starRef = React.createRef();
this.isFav = this.isGroupInFavorites(props.favorites);
}
@ -52,7 +50,9 @@ class GroupListItem extends React.Component<PropsType> {
const {favorites} = this.props;
const favChanged = favorites.length !== nextProps.favorites.length;
let newFavState = this.isFav;
if (favChanged) newFavState = this.isGroupInFavorites(nextProps.favorites);
if (favChanged) {
newFavState = this.isGroupInFavorites(nextProps.favorites);
}
const shouldUpdate = this.isFav !== newFavState;
this.isFav = newFavState;
return shouldUpdate;
@ -61,9 +61,12 @@ class GroupListItem extends React.Component<PropsType> {
onStarPress = () => {
const {props} = this;
const ref = this.starRef;
if (ref != null) {
if (this.isFav) ref.rubberBand();
else ref.swing();
if (ref.current && ref.current.rubberBand && ref.current.swing) {
if (this.isFav) {
ref.current.rubberBand();
} else {
ref.current.swing();
}
}
props.onStarPress();
};
@ -71,31 +74,29 @@ class GroupListItem extends React.Component<PropsType> {
isGroupInFavorites(favorites: Array<PlanexGroupType>): boolean {
const {item} = this.props;
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;
}
render(): React.Node {
render() {
const {props} = this;
const {colors} = props.theme;
return (
<List.Item
title={props.item.name.replace(REPLACE_REGEX, ' ')}
onPress={props.onPress}
left={(iconProps: ListIconPropsType): React.Node => (
left={(iconProps) => (
<List.Icon
color={iconProps.color}
style={iconProps.style}
icon="chevron-right"
/>
)}
right={(iconProps: ListIconPropsType): React.Node => (
<Animatable.View
ref={(ref: Animatable.View) => {
this.starRef = ref;
}}
useNativeDriver>
right={(iconProps) => (
<Animatable.View ref={this.starRef} useNativeDriver>
<TouchableRipple
onPress={this.onStarPress}
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/>.
*/
// @flow
import * as React from 'react';
import {
Avatar,
@ -32,22 +30,23 @@ import {
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable';
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
import ProxiwashConstants, {
MachineStates,
} from '../../../constants/ProxiwashConstants';
import AprilFoolsManager from '../../../managers/AprilFoolsManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ProxiwashMachineType} from '../../../screens/Proxiwash/ProxiwashScreen';
type PropsType = {
item: ProxiwashMachineType,
theme: CustomThemeType,
item: ProxiwashMachineType;
theme: ReactNativePaper.Theme;
onPress: (
title: string,
item: ProxiwashMachineType,
isDryer: boolean,
) => void,
isWatched: boolean,
isDryer: boolean,
height: number,
) => void;
isWatched: boolean;
isDryer: boolean;
height: number;
};
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
*/
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;
@ -83,16 +92,14 @@ class ProxiwashListItem extends React.Component<PropsType> {
constructor(props: PropsType) {
super(props);
this.stateColors = {};
this.stateStrings = {};
this.updateStateStrings();
let displayNumber = props.item.number;
const displayMaxWeight = props.item.maxWeight;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(
parseInt(props.item.number, 10),
);
}
this.title = props.isDryer
? i18n.t('screens.proxiwash.dryer')
@ -116,65 +123,38 @@ class ProxiwashListItem extends React.Component<PropsType> {
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() {
const {props} = this;
const {colors} = props.theme;
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] =
colors.proxiwashReadyColor;
this.stateColors[ProxiwashConstants.machineStates.RUNNING] =
colors.proxiwashRunningColor;
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] =
this.stateColors[MachineStates.AVAILABLE] = colors.proxiwashReadyColor;
this.stateColors[MachineStates.RUNNING] = colors.proxiwashRunningColor;
this.stateColors[MachineStates.RUNNING_NOT_STARTED] =
colors.proxiwashRunningNotStartedColor;
this.stateColors[ProxiwashConstants.machineStates.FINISHED] =
colors.proxiwashFinishedColor;
this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] =
colors.proxiwashBrokenColor;
this.stateColors[ProxiwashConstants.machineStates.ERROR] =
colors.proxiwashErrorColor;
this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] =
colors.proxiwashUnknownColor;
this.stateColors[MachineStates.FINISHED] = colors.proxiwashFinishedColor;
this.stateColors[MachineStates.UNAVAILABLE] = colors.proxiwashBrokenColor;
this.stateColors[MachineStates.ERROR] = colors.proxiwashErrorColor;
this.stateColors[MachineStates.UNKNOWN] = colors.proxiwashUnknownColor;
}
render(): React.Node {
render() {
const {props} = this;
const {colors} = props.theme;
const machineState = props.item.state;
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING;
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE;
const isRunning = machineState === MachineStates.RUNNING;
const isReady = machineState === MachineStates.AVAILABLE;
const description = isRunning
? `${props.item.startTime}/${props.item.endTime}`
: '';
const stateIcon = ProxiwashConstants.stateIcons[machineState];
const stateString = this.stateStrings[machineState];
let progress;
if (isRunning && props.item.donePercent !== '')
if (isRunning && props.item.donePercent !== '') {
progress = parseFloat(props.item.donePercent) / 100;
else if (isRunning) progress = 0;
else progress = 1;
} else if (isRunning) {
progress = 0;
} else {
progress = 1;
}
const icon = props.isWatched ? (
<AnimatedIcon
@ -224,19 +204,19 @@ class ProxiwashListItem extends React.Component<PropsType> {
justifyContent: 'center',
}}
onPress={this.onListItemPress}
left={(): React.Node => icon}
right={(): React.Node => (
left={() => icon}
right={() => (
<View style={{flexDirection: 'row'}}>
<View style={{justifyContent: 'center'}}>
<Text
style={
machineState === ProxiwashConstants.machineStates.FINISHED
machineState === MachineStates.FINISHED
? {fontWeight: 'bold'}
: {}
}>
{stateString}
</Text>
{machineState === ProxiwashConstants.machineStates.RUNNING ? (
{machineState === MachineStates.RUNNING ? (
<Caption>{props.item.remainingTime} min</Caption>
) : null}
</View>

View file

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

View file

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

View file

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

View file

@ -17,41 +17,36 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import {TouchableRipple} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack';
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 = {
navigation: StackNavigationProp,
images: Array<{url: string}>,
style: ViewStyleProp,
images: Array<{url: string}>;
style: ViewStyle;
};
class ImageGalleryButton extends React.Component<PropsType> {
onPress = () => {
const {navigation, images} = this.props;
navigation.navigate('gallery', {images});
function ImageGalleryButton(props: PropsType) {
const navigation = useNavigation();
const onPress = () => {
navigation.navigate('gallery', {images: props.images});
};
render(): React.Node {
const {style, images} = this.props;
return (
<TouchableRipple onPress={this.onPress} style={style}>
<Image
resizeMode="contain"
source={{uri: images[0].url}}
style={{
width: '100%',
height: '100%',
}}
/>
</TouchableRipple>
);
}
return (
<TouchableRipple onPress={onPress} style={props.style}>
<Image
resizeMode="contain"
source={{uri: props.images[0].url}}
style={{
width: '100%',
height: '100%',
}}
/>
</TouchableRipple>
);
}
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/>.
*/
// @flow
import * as React from 'react';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {HeaderButton, HeaderButtons} from 'react-navigation-header-buttons';
import {withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../managers/ThemeManager';
import {
HeaderButton,
HeaderButtonProps,
HeaderButtons,
HeaderButtonsProps,
} from 'react-navigation-header-buttons';
import {useTheme} from 'react-native-paper';
const MaterialHeaderButton = (props: {
theme: CustomThemeType,
color: string,
}): React.Node => {
const {color, theme} = props;
const MaterialHeaderButton = (props: HeaderButtonProps) => {
const theme = useTheme();
return (
// $FlowFixMe
<HeaderButton
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
IconComponent={MaterialCommunityIcons}
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 (
// $FlowFixMe
<HeaderButtons
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
/>
<HeaderButtons {...props} HeaderButtonComponent={MaterialHeaderButton} />
);
};

View file

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

View file

@ -17,14 +17,11 @@
* 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 {useTheme} from 'react-native-paper';
import {Modalize} from 'react-native-modalize';
import {View} from 'react-native-animatable';
import CustomTabBar from '../Tabbar/CustomTabBar';
import type {CustomThemeType} from '../../managers/ThemeManager';
/**
* Abstraction layer for Modalize component, using custom configuration
@ -33,11 +30,11 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
* @return {*}
*/
function CustomModal(props: {
theme: CustomThemeType,
onRef: (re: Modalize) => void,
children?: React.Node,
}): React.Node {
const {theme, onRef, children} = props;
onRef: (re: Modalize) => void;
children?: React.ReactNode;
}) {
const theme = useTheme();
const {onRef, children} = props;
return (
<Modalize
ref={onRef}
@ -55,6 +52,4 @@ function CustomModal(props: {
);
}
CustomModal.defaultProps = {children: null};
export default withTheme(CustomModal);
export default 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 {View} from 'react-native';
import {ActivityIndicator, withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../managers/ThemeManager';
import {ActivityIndicator, useTheme} from 'react-native-paper';
type Props = {
isAbsolute?: boolean;
};
/**
* 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
* @return {*}
*/
function BasicLoadingScreen(props: {
theme: CustomThemeType,
isAbsolute: boolean,
}): React.Node {
const {theme, isAbsolute} = props;
const {colors} = theme;
let position;
if (isAbsolute != null && isAbsolute) position = 'absolute';
export default function BasicLoadingScreen(props: Props) {
const theme = useTheme();
const {isAbsolute} = props;
return (
<View
style={{
backgroundColor: colors.background,
position,
backgroundColor: theme.colors.background,
position: isAbsolute ? 'absolute' : 'relative',
top: 0,
right: 0,
width: '100%',
height: '100%',
justifyContent: 'center',
}}>
<ActivityIndicator animating size="large" color={colors.primary} />
<ActivityIndicator animating size="large" color={theme.colors.primary} />
</View>
);
}
export default withTheme(BasicLoadingScreen);

View file

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

View file

@ -17,12 +17,15 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import i18n from 'i18n-js';
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 {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from '@react-navigation/stack';
@ -32,42 +35,44 @@ import withCollapsible from '../../utils/withCollapsible';
import CustomTabBar from '../Tabbar/CustomTabBar';
import {ERROR_TYPE, readData} from '../../utils/WebData';
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
import type {ApiGenericDataType} from '../../utils/WebData';
export type SectionListDataType<T> = Array<{
title: string,
data: Array<T>,
keyExtractor?: (T) => string,
export type SectionListDataType<ItemT> = Array<{
title: string;
icon?: string;
data: Array<ItemT>;
keyExtractor?: (data: ItemT) => string;
}>;
type PropsType<T> = {
navigation: StackNavigationProp,
fetchUrl: string,
autoRefreshTime: number,
refreshOnFocus: boolean,
renderItem: (data: {item: T}) => React.Node,
type PropsType<ItemT, RawData> = {
navigation: StackNavigationProp<any>;
fetchUrl: string;
autoRefreshTime: number;
refreshOnFocus: boolean;
renderItem: (data: {item: ItemT}) => React.ReactNode;
createDataset: (
data: ApiGenericDataType | null,
data: RawData | null,
isLoading?: boolean,
) => SectionListDataType<T>,
onScroll: (event: SyntheticEvent<EventTarget>) => void,
collapsibleStack: Collapsible,
) => SectionListDataType<ItemT>;
onScroll: (event: NativeSyntheticEvent<EventTarget>) => void;
collapsibleStack: Collapsible;
showError?: boolean,
itemHeight?: number | null,
updateData?: number,
renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node,
showError?: boolean;
itemHeight?: number | null;
updateData?: number;
renderListHeaderComponent?: (
data: RawData | null,
) => React.ComponentType<any> | React.ReactElement | null;
renderSectionHeader?: (
data: {section: {title: string}},
data: {section: SectionListData<ItemT>},
isLoading?: boolean,
) => React.Node,
stickyHeader?: boolean,
) => React.ReactElement | null;
stickyHeader?: boolean;
};
type StateType = {
refreshing: boolean,
fetchedData: ApiGenericDataType | null,
snackbarVisible: boolean,
type StateType<RawData> = {
refreshing: boolean;
fetchedData: RawData | null;
snackbarVisible: boolean;
};
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.
* 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 = {
showError: true,
itemHeight: null,
updateData: 0,
renderListHeaderComponent: (): React.Node => null,
renderSectionHeader: (): React.Node => null,
renderListHeaderComponent: () => null,
renderSectionHeader: () => null,
stickyHeader: false,
};
refreshInterval: IntervalID;
refreshInterval: NodeJS.Timeout | undefined;
lastRefresh: Date | null;
lastRefresh: Date | undefined;
constructor() {
super();
constructor(props: PropsType<ItemT, RawData>) {
super(props);
this.state = {
refreshing: false,
fetchedData: null,
@ -109,7 +117,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
const {navigation} = this.props;
navigation.addListener('focus', this.onScreenFocus);
navigation.addListener('blur', this.onScreenBlur);
this.lastRefresh = null;
this.lastRefresh = undefined;
this.onRefresh();
}
@ -121,15 +129,18 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
if (props.refreshOnFocus && this.lastRefresh) {
setTimeout(this.onRefresh, 200);
}
if (props.autoRefreshTime > 0)
if (props.autoRefreshTime > 0) {
this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
}
};
/**
* Removes any interval on un-focus
*/
onScreenBlur = () => {
clearInterval(this.refreshInterval);
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
};
/**
@ -138,7 +149,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
*
* @param fetchedData The newly fetched data
*/
onFetchSuccess = (fetchedData: ApiGenericDataType) => {
onFetchSuccess = (fetchedData: RawData) => {
this.setState({
fetchedData,
refreshing: false,
@ -167,7 +178,9 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
if (this.lastRefresh != null) {
const last = this.lastRefresh;
canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
} else canRefresh = true;
} else {
canRefresh = true;
}
if (canRefresh) {
this.setState({refreshing: true});
readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
@ -189,19 +202,18 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
};
getItemLayout = (
data: T,
height: number,
data: Array<SectionListData<ItemT>> | null,
index: number,
): {length: number, offset: number, index: number} | null => {
const {itemHeight} = this.props;
if (itemHeight == null) return null;
): {length: number; offset: number; index: number} => {
return {
length: itemHeight,
offset: itemHeight * index,
length: height,
offset: height * index,
index,
};
};
getRenderSectionHeader = (data: {section: {title: string}}): React.Node => {
getRenderSectionHeader = (data: {section: SectionListData<ItemT>}) => {
const {renderSectionHeader} = this.props;
const {refreshing} = this.state;
if (renderSectionHeader != null) {
@ -214,7 +226,7 @@ class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
return null;
};
getRenderItem = (data: {item: T}): React.Node => {
getRenderItem = (data: {item: ItemT}) => {
const {renderItem} = this.props;
return (
<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;
if (onScroll != null) onScroll(event);
if (onScroll != null) {
onScroll(event);
}
};
render(): React.Node {
render() {
const {props, state} = this;
let dataset = [];
const {itemHeight} = props;
let dataset: SectionListDataType<ItemT> = [];
if (
state.fetchedData != null ||
(state.fetchedData == null && !props.showError)
)
) {
dataset = props.createDataset(state.fetchedData, state.refreshing);
}
const {containerPaddingTop} = props.collapsibleStack;
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}
hasTab
/>

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import * as React from 'react';
import WebView from 'react-native-webview';
import {
@ -27,12 +25,17 @@ import {
OverflowMenu,
} from 'react-navigation-header-buttons';
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 {withTheme} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack';
import {Collapsible} from 'react-navigation-collapsible';
import type {CustomThemeType} from '../../managers/ThemeManager';
import withCollapsible from '../../utils/withCollapsible';
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
import {ERROR_TYPE} from '../../utils/WebData';
@ -40,15 +43,15 @@ import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen';
type PropsType = {
navigation: StackNavigationProp,
theme: CustomThemeType,
url: string,
collapsibleStack: Collapsible,
onMessage: (event: {nativeEvent: {data: string}}) => void,
onScroll: (event: SyntheticEvent<EventTarget>) => void,
customJS?: string,
customPaddingFunction?: null | ((padding: number) => string),
showAdvancedControls?: boolean,
navigation: StackNavigationProp<any>;
theme: ReactNativePaper.Theme;
url: string;
collapsibleStack: Collapsible;
onMessage: (event: {nativeEvent: {data: string}}) => void;
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
customJS?: string;
customPaddingFunction?: null | ((padding: number) => string);
showAdvancedControls?: boolean;
};
const AnimatedWebView = Animated.createAnimatedComponent(WebView);
@ -63,14 +66,17 @@ class WebViewScreen extends React.PureComponent<PropsType> {
customPaddingFunction: null,
};
currentUrl: string;
webviewRef: {current: null | WebView};
canGoBack: boolean;
constructor() {
super();
constructor(props: PropsType) {
super(props);
this.webviewRef = React.createRef();
this.canGoBack = false;
this.currentUrl = props.url;
}
/**
@ -115,7 +121,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
*
* @return {*}
*/
getBasicButton = (): React.Node => {
getBasicButton = () => {
return (
<MaterialHeaderButtons>
<Item
@ -138,7 +144,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
*
* @returns {*}
*/
getAdvancedButtons = (): React.Node => {
getAdvancedButtons = () => {
const {props} = this;
return (
<MaterialHeaderButtons>
@ -179,7 +185,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
*
* @return {*}
*/
getRenderLoading = (): React.Node => <BasicLoadingScreen isAbsolute />;
getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
/**
* 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.
*/
onRefreshClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.reload();
if (this.webviewRef.current != null) {
this.webviewRef.current.reload();
}
};
onGoBackClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goBack();
if (this.webviewRef.current != null) {
this.webviewRef.current.goBack();
}
};
onGoForwardClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goForward();
if (this.webviewRef.current != null) {
this.webviewRef.current.goForward();
}
};
onOpenClicked = () => {
const {url} = this.props;
Linking.openURL(url);
Linking.openURL(this.currentUrl);
};
onScroll = (event: SyntheticEvent<EventTarget>) => {
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
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
*/
injectJavaScript = (script: string) => {
if (this.webviewRef.current != null)
if (this.webviewRef.current != null) {
this.webviewRef.current.injectJavaScript(script);
}
};
render(): React.Node {
render() {
const {props} = this;
const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack;
return (
@ -243,13 +257,14 @@ class WebViewScreen extends React.PureComponent<PropsType> {
injectedJavaScript={props.customJS}
javaScriptEnabled
renderLoading={this.getRenderLoading}
renderError={(): React.Node => (
renderError={() => (
<ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked}
/>
)}
onNavigationStateChange={(navState: {canGoBack: boolean}) => {
onNavigationStateChange={(navState) => {
this.currentUrl = navState.url;
this.canGoBack = navState.canGoBack;
}}
onMessage={props.onMessage}
@ -257,7 +272,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
}}
// 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/>.
*/
// @flow
import * as React from 'react';
import {Animated} from 'react-native';
import {withTheme} from 'react-native-paper';
import {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from '@react-navigation/stack';
import TabIcon from './TabIcon';
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 = {
name: string,
key: string,
params: {collapsible: Collapsible},
state: {
index: number,
routes: Array<RouteType>,
},
type RouteType = Route<string> & {
state?: NavigationState | PartialState<NavigationState>;
};
type PropsType = {
state: {
index: number,
routes: Array<RouteType>,
},
descriptors: {
[key: string]: {
options: {
tabBarLabel: string,
title: string,
},
},
},
navigation: StackNavigationProp,
theme: CustomThemeType,
};
interface PropsType extends BottomTabBarProps {
theme: ReactNativePaper.Theme;
}
type StateType = {
// eslint-disable-next-line flowtype/no-weak-types
translateY: any,
translateY: any;
};
type validRoutes = 'proxiwash' | 'services' | 'planning' | 'planex';
const TAB_ICONS = {
proxiwash: 'tshirt-crew',
services: 'account-circle',
@ -70,11 +54,13 @@ const TAB_ICONS = {
class CustomTabBar extends React.Component<PropsType, StateType> {
static TAB_BAR_HEIGHT = 48;
constructor() {
super();
constructor(props: PropsType) {
super(props);
this.state = {
translateY: new Animated.Value(0),
};
// @ts-ignore
props.navigation.addListener('state', this.onRouteChange);
}
/**
@ -86,13 +72,9 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
*/
onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
const {navigation} = this.props;
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (currentIndex !== destIndex && !event.defaultPrevented)
if (currentIndex !== destIndex) {
navigation.navigate(route.name);
}
}
/**
@ -102,13 +84,9 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
*/
onItemLongPress(route: RouteType) {
const {navigation} = this.props;
const event = navigation.emit({
type: 'tabLongPress',
target: route.key,
canPreventDefault: true,
});
if (route.name === 'home' && !event.defaultPrevented)
if (route.name === 'home') {
navigation.navigate('game-start');
}
}
/**
@ -126,11 +104,13 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
* @param focused
* @returns {null}
*/
getTabBarIcon = (route: RouteType, focused: boolean): React.Node => {
let icon = TAB_ICONS[route.name];
getTabBarIcon = (route: RouteType, focused: boolean) => {
let icon = TAB_ICONS[route.name as validRoutes];
icon = focused ? icon : `${icon}-outline`;
if (route.name !== 'home') return icon;
return null;
if (route.name !== 'home') {
return icon;
}
return '';
};
/**
@ -141,14 +121,18 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
* @param index The index of the current route
* @returns {*}
*/
getRenderIcon = (route: RouteType, index: number): React.Node => {
getRenderIcon = (route: RouteType, index: number) => {
const {props} = this;
const {state} = props;
const {options} = props.descriptors[route.key];
let label;
if (options.tabBarLabel != null) label = options.tabBarLabel;
else if (options.title != null) label = options.title;
else label = route.name;
if (options.tabBarLabel != null) {
label = options.tabBarLabel;
} else if (options.title != null) {
label = options.title;
} else {
label = route.name;
}
const onPress = () => {
this.onItemPress(route, state.index, index);
@ -168,7 +152,7 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
onLongPress={onLongPress}
icon={this.getTabBarIcon(route, isFocused)}
color={color}
label={label}
label={label as string}
focused={isFocused}
extraData={state.index > index}
key={route.key}
@ -186,7 +170,7 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
);
};
getIcons(): React.Node {
getIcons() {
const {props} = this;
return props.state.routes.map(this.getRenderIcon);
}
@ -197,9 +181,12 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
if (isFocused) {
const stackState = route.state;
const stackRoute =
stackState != null ? stackState.routes[stackState.index] : null;
const params: {collapsible: Collapsible} | null =
stackRoute != null ? stackRoute.params : null;
stackState && stackState.index != null
? stackState.routes[stackState.index]
: null;
const params: {collapsible: Collapsible} | null | undefined = stackRoute
? (stackRoute.params as {collapsible: Collapsible})
: null;
const collapsible = params != null ? params.collapsible : null;
if (collapsible != null) {
this.setState({
@ -209,14 +196,11 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
}
};
render(): React.Node {
render() {
const {props, state} = this;
props.navigation.addListener('state', this.onRouteChange);
const icons = this.getIcons();
return (
// $FlowFixMe
<Animated.View
useNativeDriver
style={{
flexDirection: 'row',
height: CustomTabBar.TAB_BAR_HEIGHT,

View file

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

View file

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

View file

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

View file

@ -17,25 +17,26 @@
* 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 {
machineStates: {
AVAILABLE: 0,
RUNNING: 1,
RUNNING_NOT_STARTED: 2,
FINISHED: 3,
UNAVAILABLE: 4,
ERROR: 5,
UNKNOWN: 6,
},
stateIcons: {
0: 'radiobox-blank',
1: 'progress-check',
2: 'alert-circle-outline',
3: 'check-circle',
4: 'alert-octagram-outline',
5: 'alert',
6: 'help-circle-outline',
},
stateIcons: [
'radiobox-blank',
'progress-check',
'alert-circle-outline',
'check-circle',
'alert-octagram-outline',
'alert',
'help-circle-outline',
],
washinsa: {
id: 'washinsa',
title: 'screens.proxiwash.washinsa.title',

View file

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

View file

@ -17,10 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import type {ProxiwashMachineType} from '../screens/Proxiwash/ProxiwashScreen';
import type {CustomThemeType} from './ThemeManager';
import type {RuFoodCategoryType} from '../screens/Services/SelfMenuScreen';
/**
@ -57,8 +54,9 @@ export default class AprilFoolsManager {
* @returns {ThemeManager}
*/
static getInstance(): AprilFoolsManager {
if (AprilFoolsManager.instance == null)
if (AprilFoolsManager.instance == null) {
AprilFoolsManager.instance = new AprilFoolsManager();
}
return AprilFoolsManager.instance;
}
@ -130,7 +128,9 @@ export default class AprilFoolsManager {
* @param currentTheme
* @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 {
...currentTheme,
colors: {

View file

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

View file

@ -20,7 +20,7 @@
// @flow
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';
/**
@ -40,12 +40,10 @@ const AUTH_PATH = 'password';
export default class ConnectionManager {
static instance: ConnectionManager | null = null;
#email: string;
#token: string | null;
private token: string | null;
constructor() {
this.#token = null;
this.token = null;
}
/**
@ -54,8 +52,9 @@ export default class ConnectionManager {
* @returns {ConnectionManager}
*/
static getInstance(): ConnectionManager {
if (ConnectionManager.instance == null)
if (ConnectionManager.instance == null) {
ConnectionManager.instance = new ConnectionManager();
}
return ConnectionManager.instance;
}
@ -65,7 +64,7 @@ export default class ConnectionManager {
* @returns {string | null}
*/
getToken(): string | null {
return this.#token;
return this.token;
}
/**
@ -77,18 +76,17 @@ export default class ConnectionManager {
return new Promise(
(resolve: (token: string) => void, reject: () => void) => {
const token = this.getToken();
if (token != null) resolve(token);
else {
if (token != null) {
resolve(token);
} else {
Keychain.getInternetCredentials(SERVER_NAME)
.then((data: Keychain.UserCredentials | false) => {
if (
data != null &&
data.password != null &&
typeof data.password === 'string'
) {
this.#token = data.password;
resolve(this.#token);
} else reject();
if (data && data.password != null) {
this.token = data.password;
resolve(this.token);
} else {
reject();
}
})
.catch((): void => reject());
}
@ -116,8 +114,7 @@ export default class ConnectionManager {
return new Promise((resolve: () => void, reject: () => void) => {
Keychain.setInternetCredentials(SERVER_NAME, 'token', token)
.then(() => {
this.#token = token;
this.#email = email;
this.token = token;
resolve();
})
.catch((): void => reject());
@ -133,7 +130,7 @@ export default class ConnectionManager {
return new Promise((resolve: () => void, reject: () => void) => {
Keychain.resetInternetCredentials(SERVER_NAME)
.then(() => {
this.#token = null;
this.token = null;
resolve();
})
.catch((): void => reject());
@ -156,13 +153,15 @@ export default class ConnectionManager {
email,
password,
};
apiRequest(AUTH_PATH, 'POST', data)
apiRequest<ApiDataLoginType>(AUTH_PATH, 'POST', data)
.then((response: ApiDataLoginType) => {
if (response.token != null) {
this.saveLogin(email, response.token)
.then((): void => resolve())
.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));
},
@ -176,24 +175,23 @@ export default class ConnectionManager {
* @param params
* @returns Promise<ApiGenericDataType>
*/
async authenticatedRequest(
async authenticatedRequest<T>(
path: string,
params: {...},
): Promise<ApiGenericDataType> {
params: {[key: string]: any},
): Promise<T> {
return new Promise(
(
resolve: (response: ApiGenericDataType) => void,
reject: (error: number) => void,
) => {
(resolve: (response: T) => void, reject: (error: number) => void) => {
if (this.getToken() !== null) {
const data = {
...params,
token: this.getToken(),
};
apiRequest(path, 'POST', data)
.then((response: ApiGenericDataType): void => resolve(response))
apiRequest<T>(path, 'POST', data)
.then((response: T): void => resolve(response))
.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 ServicesManager from './ServicesManager';
import {getSublistWithIds} from '../utils/Utils';
import {getSublistWithIds} from '../utils/Services';
import AsyncStorageManager from './AsyncStorageManager';
export default class DashboardManager extends ServicesManager {
getCurrentDashboard(): Array<ServiceItemType | null> {
const dashboardIdList = AsyncStorageManager.getObject(
const dashboardIdList = AsyncStorageManager.getObject<Array<string>>(
AsyncStorageManager.PREFERENCES.dashboardItems.key,
);
const allDatasets = [

View file

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

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
// @flow
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import AvailableWebsites from '../constants/AvailableWebsites';
@ -93,24 +91,24 @@ export const SERVICES_CATEGORIES_KEY = {
};
export type ServiceItemType = {
key: string,
title: string,
subtitle: string,
image: string,
onPress: () => void,
badgeFunction?: (dashboard: FullDashboardType) => number,
key: string;
title: string;
subtitle: string;
image: string | number;
onPress: () => void;
badgeFunction?: (dashboard: FullDashboardType) => number;
};
export type ServiceCategoryType = {
key: string,
title: string,
subtitle: string,
image: string | number,
content: Array<ServiceItemType>,
key: string;
title: string;
subtitle: string;
image: string | number;
content: Array<ServiceItemType>;
};
export default class ServicesManager {
navigation: StackNavigationProp;
navigation: StackNavigationProp<any>;
amicaleDataset: Array<ServiceItemType>;
@ -122,7 +120,7 @@ export default class ServicesManager {
categoriesDataset: Array<ServiceCategoryType>;
constructor(nav: StackNavigationProp) {
constructor(nav: StackNavigationProp<any>) {
this.navigation = nav;
this.amicaleDataset = [
{
@ -336,9 +334,11 @@ export default class ServicesManager {
* @returns {null}
*/
onAmicaleServicePress(route: string) {
if (ConnectionManager.getInstance().isLoggedIn())
if (ConnectionManager.getInstance().isLoggedIn()) {
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>}
*/
getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null)
if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.amicaleDataset);
}
return this.amicaleDataset;
}
@ -360,8 +361,9 @@ export default class ServicesManager {
* @returns {Array<ServiceItemType>}
*/
getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null)
if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.studentsDataset);
}
return this.studentsDataset;
}
@ -372,8 +374,9 @@ export default class ServicesManager {
* @returns {Array<ServiceItemType>}
*/
getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null)
if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.insaDataset);
}
return this.insaDataset;
}
@ -384,8 +387,9 @@ export default class ServicesManager {
* @returns {Array<ServiceItemType>}
*/
getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null)
if (excludedItems != null) {
return getStrippedServicesList(excludedItems, this.specialDataset);
}
return this.specialDataset;
}
@ -396,8 +400,9 @@ export default class ServicesManager {
* @returns {Array<ServiceCategoryType>}
*/
getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> {
if (excludedItems != null)
if (excludedItems != null) {
return getStrippedServicesList(excludedItems, 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/>.
*/
// @flow
import * as React from 'react';
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
import i18n from 'i18n-js';
@ -40,18 +38,63 @@ import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
import {
createScreenCollapsibleStack,
CreateScreenCollapsibleStack,
getWebsiteStack,
} from '../utils/CollapsibleUtils';
import BugReportScreen from '../screens/Other/FeedbackScreen';
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 EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfirmScreen';
import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
import GameStartScreen from '../screens/Game/screens/GameStartScreen';
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 =
Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS
@ -63,19 +106,17 @@ const defaultScreenOptions = {
...TransitionPresets.SlideFromRightIOS,
};
const MainStack = createStackNavigator();
const MainStack = createStackNavigator<MainStackParamsList>();
function MainStackComponent(props: {
createTabNavigator: () => React.Node,
}): React.Node {
function MainStackComponent(props: {createTabNavigator: () => JSX.Element}) {
const {createTabNavigator} = props;
return (
<MainStack.Navigator
initialRouteName="main"
initialRouteName={MainRoutes.Main}
headerMode="screen"
screenOptions={defaultScreenOptions}>
<MainStack.Screen
name="main"
name={MainRoutes.Main}
component={createTabNavigator}
options={{
headerShown: false,
@ -83,62 +124,62 @@ function MainStackComponent(props: {
}}
/>
<MainStack.Screen
name="gallery"
name={MainRoutes.Gallery}
component={ImageGalleryScreen}
options={{
headerShown: false,
...modalTransition,
}}
/>
{createScreenCollapsibleStack(
'settings',
{CreateScreenCollapsibleStack(
MainRoutes.Settings,
MainStack,
SettingsScreen,
i18n.t('screens.settings.title'),
)}
{createScreenCollapsibleStack(
'dashboard-edit',
{CreateScreenCollapsibleStack(
MainRoutes.DashboardEdit,
MainStack,
DashboardEditScreen,
i18n.t('screens.settings.dashboardEdit.title'),
)}
{createScreenCollapsibleStack(
'about',
{CreateScreenCollapsibleStack(
MainRoutes.About,
MainStack,
AboutScreen,
i18n.t('screens.about.title'),
)}
{createScreenCollapsibleStack(
'dependencies',
{CreateScreenCollapsibleStack(
MainRoutes.Dependencies,
MainStack,
AboutDependenciesScreen,
i18n.t('screens.about.libs'),
)}
{createScreenCollapsibleStack(
'debug',
{CreateScreenCollapsibleStack(
MainRoutes.Debug,
MainStack,
DebugScreen,
i18n.t('screens.about.debug'),
)}
{createScreenCollapsibleStack(
'game-start',
{CreateScreenCollapsibleStack(
MainRoutes.GameStart,
MainStack,
GameStartScreen,
i18n.t('screens.game.title'),
true,
null,
undefined,
'transparent',
)}
<MainStack.Screen
name="game-main"
name={MainRoutes.GameMain}
component={GameMainScreen}
options={{
title: i18n.t('screens.game.title'),
}}
/>
{createScreenCollapsibleStack(
'login',
{CreateScreenCollapsibleStack(
MainRoutes.Login,
MainStack,
LoginScreen,
i18n.t('screens.login.title'),
@ -148,26 +189,26 @@ function MainStackComponent(props: {
)}
{getWebsiteStack('website', MainStack, WebsiteScreen, '')}
{createScreenCollapsibleStack(
'self-menu',
{CreateScreenCollapsibleStack(
MainRoutes.SelfMenu,
MainStack,
SelfMenuScreen,
i18n.t('screens.menu.title'),
)}
{createScreenCollapsibleStack(
'proximo',
{CreateScreenCollapsibleStack(
MainRoutes.Proximo,
MainStack,
ProximoMainScreen,
i18n.t('screens.proximo.title'),
)}
{createScreenCollapsibleStack(
'proximo-list',
{CreateScreenCollapsibleStack(
MainRoutes.ProximoList,
MainStack,
ProximoListScreen,
i18n.t('screens.proximo.articleList'),
)}
{createScreenCollapsibleStack(
'proximo-about',
{CreateScreenCollapsibleStack(
MainRoutes.ProximoAbout,
MainStack,
ProximoAboutScreen,
i18n.t('screens.proximo.title'),
@ -175,60 +216,60 @@ function MainStackComponent(props: {
{...modalTransition},
)}
{createScreenCollapsibleStack(
'profile',
{CreateScreenCollapsibleStack(
MainRoutes.Profile,
MainStack,
ProfileScreen,
i18n.t('screens.profile.title'),
)}
{createScreenCollapsibleStack(
'club-list',
{CreateScreenCollapsibleStack(
MainRoutes.ClubList,
MainStack,
ClubListScreen,
i18n.t('screens.clubs.title'),
)}
{createScreenCollapsibleStack(
'club-information',
{CreateScreenCollapsibleStack(
MainRoutes.ClubInformation,
MainStack,
ClubDisplayScreen,
i18n.t('screens.clubs.details'),
true,
{...modalTransition},
)}
{createScreenCollapsibleStack(
'club-about',
{CreateScreenCollapsibleStack(
MainRoutes.ClubAbout,
MainStack,
ClubAboutScreen,
i18n.t('screens.clubs.title'),
true,
{...modalTransition},
)}
{createScreenCollapsibleStack(
'equipment-list',
{CreateScreenCollapsibleStack(
MainRoutes.EquipmentList,
MainStack,
EquipmentScreen,
i18n.t('screens.equipment.title'),
)}
{createScreenCollapsibleStack(
'equipment-rent',
{CreateScreenCollapsibleStack(
MainRoutes.EquipmentRent,
MainStack,
EquipmentLendScreen,
i18n.t('screens.equipment.book'),
)}
{createScreenCollapsibleStack(
'equipment-confirm',
{CreateScreenCollapsibleStack(
MainRoutes.EquipmentConfirm,
MainStack,
EquipmentConfirmScreen,
i18n.t('screens.equipment.confirm'),
)}
{createScreenCollapsibleStack(
'vote',
{CreateScreenCollapsibleStack(
MainRoutes.Vote,
MainStack,
VoteScreen,
i18n.t('screens.vote.title'),
)}
{createScreenCollapsibleStack(
'feedback',
{CreateScreenCollapsibleStack(
MainRoutes.Feedback,
MainStack,
BugReportScreen,
i18n.t('screens.feedback.title'),
@ -238,25 +279,14 @@ function MainStackComponent(props: {
}
type PropsType = {
defaultHomeRoute: string | null,
// eslint-disable-next-line flowtype/no-weak-types
defaultHomeData: {[key: string]: string},
defaultHomeRoute: string | null;
defaultHomeData: {[key: string]: string};
};
export default class MainNavigator extends React.Component<PropsType> {
createTabNavigator: () => React.Node;
constructor(props: PropsType) {
super(props);
this.createTabNavigator = (): React.Node => (
<TabNavigator
defaultHomeRoute={props.defaultHomeRoute}
defaultHomeData={props.defaultHomeData}
/>
);
}
render(): React.Node {
return <MainStackComponent createTabNavigator={this.createTabNavigator} />;
}
export default function MainNavigator(props: PropsType) {
return (
<MainStackComponent
createTabNavigator={() => <TabNavigator {...props} />}
/>
);
}

View file

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

View file

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

View file

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