forked from vergnet/application-amicale
Compare commits
110 commits
Author | SHA1 | Date | |
---|---|---|---|
|
aaf72d9122 | ||
|
1a696f0628 | ||
|
9acfbf00df | ||
|
9efd40e48c | ||
|
de8820eada | ||
|
dc944060e1 | ||
|
2c11addf40 | ||
|
8bacddc7b5 | ||
|
53ec2bb578 | ||
|
764296708c | ||
|
d3e94ac9b3 | ||
|
7c38ec0bdb | ||
|
cb3af52483 | ||
|
d5c6aa6b48 | ||
|
6104b88815 | ||
|
26f6518270 | ||
|
3b2776542a | ||
|
76f13f04d5 | ||
|
67b5a5fb4f | ||
|
c75b90d254 | ||
|
b9c99bf269 | ||
7f763dcbcb | |||
|
1f930223c4 | ||
|
ba62e5d3ec | ||
|
b127cca068 | ||
|
06dc9966ec | ||
|
53b3f00005 | ||
|
20aed5cc80 | ||
|
0be3a53747 | ||
|
bdffd01df4 | ||
|
c500ae05e6 | ||
|
b289a85b8a | ||
|
ffa4cfa376 | ||
|
541c002558 | ||
|
44aa52b3aa | ||
|
b15b200846 | ||
|
fe96d9f8a1 | ||
|
44d35090ac | ||
|
245e6c5cc8 | ||
|
8f9c02ff75 | ||
|
9ae585bdf8 | ||
|
14365a92a4 | ||
|
19f6dd3cf0 | ||
|
acbbd2d27d | ||
|
20d5e790d0 | ||
|
9e6fee467f | ||
|
94a0ca33a4 | ||
|
c3304c6f06 | ||
|
7d0df0e7ce | ||
|
00f9428972 | ||
|
b5d4ad83c3 | ||
|
1d2ec83619 | ||
|
d55c692bd3 | ||
|
a1cfb0385a | ||
|
52651ecf85 | ||
|
9675d329cc | ||
|
5795fca035 | ||
|
ae1e2fcdc0 | ||
|
742643b9e2 | ||
|
9b4caade00 | ||
|
50c62dd676 | ||
|
6516cf918d | ||
|
e7cffde198 | ||
|
c1dd69d0ed | ||
|
02135d64ff | ||
|
360023aea6 | ||
|
27199b85e5 | ||
|
e08fdc7c37 | ||
|
ed4bb216a0 | ||
|
c2fdda5588 | ||
|
8506d3d81f | ||
|
46944a4487 | ||
|
115534f1c6 | ||
|
92eedda98b | ||
|
35a4b377f8 | ||
|
3cb6ddd7f9 | ||
|
27f7a079b4 | ||
|
aac598a94a | ||
|
d3a48d95c3 | ||
|
f6f1a5519e | ||
|
aefeb8373a | ||
|
a8dde29654 | ||
|
63722c2417 | ||
|
2bbb3f60ce | ||
|
0a28cf16e3 | ||
|
0182d6118f | ||
|
2f1c64e6f9 | ||
|
a94006d18a | ||
|
aed58f8749 | ||
|
7a58ce6b70 | ||
|
128af0b813 | ||
|
8f06843ba6 | ||
|
286c1e6411 | ||
|
0b4f115a14 | ||
|
95a35038eb | ||
|
02f9241d28 | ||
|
18f7a6abbd | ||
b3bd429afa | |||
|
9c65aadfbd | ||
|
e33320da10 | ||
|
b5dcb00fce | ||
|
d42c719cf1 | ||
|
5d65d72418 | ||
|
b692b6e7f6 | ||
|
8dae8adfbe | ||
|
00ed963503 | ||
|
7672dd109d | ||
|
f1318c6aed | ||
|
25a12dad94 | ||
|
6e7b3d02cd |
208 changed files with 40625 additions and 16221 deletions
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: '@react-native-community',
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
};
|
3
.gitattributes
vendored
3
.gitattributes
vendored
|
@ -1 +1,4 @@
|
|||
*.pbxproj -text
|
||||
# Windows files should use crlf line endings
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
*.bat text eol=crlf
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
bracketSpacing: false,
|
||||
jsxBracketSameLine: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"i18n-ally.localesPaths": "locales",
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
256
App.tsx
256
App.tsx
|
@ -17,50 +17,64 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {LogBox, Platform, SafeAreaView, View} from 'react-native';
|
||||
import {NavigationContainer} from '@react-navigation/native';
|
||||
import {Provider as PaperProvider} from 'react-native-paper';
|
||||
import {setSafeBounceHeight} from 'react-navigation-collapsible';
|
||||
import React from 'react';
|
||||
import { LogBox, Platform } from 'react-native';
|
||||
import { setSafeBounceHeight } from 'react-navigation-collapsible';
|
||||
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 ThemeManager from './src/managers/ThemeManager';
|
||||
import MainNavigator from './src/navigation/MainNavigator';
|
||||
import AprilFoolsManager from './src/managers/AprilFoolsManager';
|
||||
import Update from './src/constants/Update';
|
||||
import ConnectionManager from './src/managers/ConnectionManager';
|
||||
import type {ParsedUrlDataType} from './src/utils/URLHandler';
|
||||
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';
|
||||
import { NavigationContainerRef } from '@react-navigation/core';
|
||||
import {
|
||||
defaultMascotPreferences,
|
||||
defaultPlanexPreferences,
|
||||
defaultPreferences,
|
||||
defaultProxiwashPreferences,
|
||||
GeneralPreferenceKeys,
|
||||
GeneralPreferencesType,
|
||||
MascotPreferenceKeys,
|
||||
MascotPreferencesType,
|
||||
PlanexPreferenceKeys,
|
||||
PlanexPreferencesType,
|
||||
ProxiwashPreferenceKeys,
|
||||
ProxiwashPreferencesType,
|
||||
retrievePreferences,
|
||||
} from './src/utils/asyncStorage';
|
||||
import {
|
||||
GeneralPreferencesProvider,
|
||||
MascotPreferencesProvider,
|
||||
PlanexPreferencesProvider,
|
||||
ProxiwashPreferencesProvider,
|
||||
} from './src/components/providers/PreferencesProvider';
|
||||
import MainApp from './src/screens/MainApp';
|
||||
import LoginProvider from './src/components/providers/LoginProvider';
|
||||
import { retrieveLoginToken } from './src/utils/loginToken';
|
||||
import { setupNotifications } from './src/utils/Notifications';
|
||||
import { TabRoutes } from './src/navigation/TabNavigator';
|
||||
|
||||
// Native optimizations https://reactnavigation.org/docs/react-native-screens
|
||||
// Crashes app when navigating away from webview on android 9+
|
||||
// enableScreens(true);
|
||||
initLocales();
|
||||
setupNotifications();
|
||||
|
||||
LogBox.ignoreLogs([
|
||||
// collapsible headers cause this warning, just ignore as it is not an issue
|
||||
'Non-serializable values were found in the navigation state',
|
||||
'Cannot update a component from inside the function body of a different component',
|
||||
'`new NativeEventEmitter()` was called with a non-null argument',
|
||||
]);
|
||||
|
||||
type StateType = {
|
||||
isLoading: boolean;
|
||||
showIntro: boolean;
|
||||
showUpdate: boolean;
|
||||
showAprilFools: boolean;
|
||||
currentTheme: ReactNativePaper.Theme | undefined;
|
||||
initialPreferences: {
|
||||
general: GeneralPreferencesType;
|
||||
planex: PlanexPreferencesType;
|
||||
proxiwash: ProxiwashPreferencesType;
|
||||
mascot: MascotPreferencesType;
|
||||
};
|
||||
loginToken?: string;
|
||||
};
|
||||
|
||||
export default class App extends React.Component<{}, StateType> {
|
||||
navigatorRef: {current: null | NavigationContainerRef};
|
||||
navigatorRef: { current: null | NavigationContainerRef<any> };
|
||||
|
||||
defaultHomeRoute: string | null;
|
||||
|
||||
defaultHomeData: {[key: string]: string};
|
||||
defaultData?: ParsedUrlDataType;
|
||||
|
||||
urlHandler: URLHandler;
|
||||
|
||||
|
@ -68,21 +82,20 @@ export default class App extends React.Component<{}, StateType> {
|
|||
super(props);
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
showIntro: true,
|
||||
showUpdate: true,
|
||||
showAprilFools: false,
|
||||
currentTheme: undefined,
|
||||
initialPreferences: {
|
||||
general: defaultPreferences,
|
||||
planex: defaultPlanexPreferences,
|
||||
proxiwash: defaultProxiwashPreferences,
|
||||
mascot: defaultMascotPreferences,
|
||||
},
|
||||
loginToken: undefined,
|
||||
};
|
||||
initLocales();
|
||||
this.navigatorRef = React.createRef();
|
||||
this.defaultHomeRoute = null;
|
||||
this.defaultHomeData = {};
|
||||
this.defaultData = undefined;
|
||||
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
|
||||
this.urlHandler.listen();
|
||||
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
|
||||
this.loadAssetsAsync().finally(() => {
|
||||
this.onLoadFinished();
|
||||
});
|
||||
this.loadAssetsAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -92,8 +105,7 @@ export default class App extends React.Component<{}, StateType> {
|
|||
* @param parsedData The data parsed from the url
|
||||
*/
|
||||
onInitialURLParsed = (parsedData: ParsedUrlDataType) => {
|
||||
this.defaultHomeRoute = parsedData.route;
|
||||
this.defaultHomeData = parsedData.data;
|
||||
this.defaultData = parsedData;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -106,128 +118,100 @@ export default class App extends React.Component<{}, StateType> {
|
|||
// Navigate to nested navigator and pass data to the index screen
|
||||
const nav = this.navigatorRef.current;
|
||||
if (nav != null) {
|
||||
nav.navigate('home', {
|
||||
screen: 'index',
|
||||
params: {nextScreen: parsedData.route, data: parsedData.data},
|
||||
nav.navigate(TabRoutes.Home, {
|
||||
nextScreen: parsedData.route,
|
||||
data: parsedData.data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the current theme
|
||||
*/
|
||||
onUpdateTheme = () => {
|
||||
this.setState({
|
||||
currentTheme: ThemeManager.getCurrentTheme(),
|
||||
});
|
||||
setupStatusBar();
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback when user ends the intro. Save in preferences to avoid showing back the introSlides
|
||||
*/
|
||||
onIntroDone = () => {
|
||||
this.setState({
|
||||
showIntro: false,
|
||||
showUpdate: false,
|
||||
showAprilFools: false,
|
||||
});
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.showIntro.key,
|
||||
false,
|
||||
);
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.updateNumber.key,
|
||||
Update.number,
|
||||
);
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Async loading is done, finish processing startup data
|
||||
*/
|
||||
onLoadFinished() {
|
||||
// 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();
|
||||
}
|
||||
|
||||
onLoadFinished = (
|
||||
values: Array<
|
||||
| GeneralPreferencesType
|
||||
| PlanexPreferencesType
|
||||
| ProxiwashPreferencesType
|
||||
| MascotPreferencesType
|
||||
| string
|
||||
| undefined
|
||||
>
|
||||
) => {
|
||||
const [general, planex, proxiwash, mascot, token] = values;
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
currentTheme: ThemeManager.getCurrentTheme(),
|
||||
showIntro: AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.showIntro.key,
|
||||
),
|
||||
showUpdate:
|
||||
AsyncStorageManager.getNumber(
|
||||
AsyncStorageManager.PREFERENCES.updateNumber.key,
|
||||
) !== Update.number,
|
||||
showAprilFools:
|
||||
AprilFoolsManager.getInstance().isAprilFoolsEnabled() &&
|
||||
AsyncStorageManager.getBool(
|
||||
AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key,
|
||||
),
|
||||
initialPreferences: {
|
||||
general: general as GeneralPreferencesType,
|
||||
planex: planex as PlanexPreferencesType,
|
||||
proxiwash: proxiwash as ProxiwashPreferencesType,
|
||||
mascot: mascot as MascotPreferencesType,
|
||||
},
|
||||
loginToken: token as string | undefined,
|
||||
});
|
||||
SplashScreen.hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads every async data
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadAssetsAsync = async () => {
|
||||
await AsyncStorageManager.getInstance().loadPreferences();
|
||||
await ConnectionManager.getInstance()
|
||||
.recoverLogin()
|
||||
.catch(() => {});
|
||||
};
|
||||
loadAssetsAsync() {
|
||||
Promise.all([
|
||||
retrievePreferences(
|
||||
Object.values(GeneralPreferenceKeys),
|
||||
defaultPreferences
|
||||
),
|
||||
retrievePreferences(
|
||||
Object.values(PlanexPreferenceKeys),
|
||||
defaultPlanexPreferences
|
||||
),
|
||||
retrievePreferences(
|
||||
Object.values(ProxiwashPreferenceKeys),
|
||||
defaultProxiwashPreferences
|
||||
),
|
||||
retrievePreferences(
|
||||
Object.values(MascotPreferenceKeys),
|
||||
defaultMascotPreferences
|
||||
),
|
||||
retrieveLoginToken(),
|
||||
])
|
||||
.then(this.onLoadFinished)
|
||||
.catch(this.onLoadFinished);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the app based on loading state
|
||||
*/
|
||||
render() {
|
||||
const {state} = this;
|
||||
const { state } = this;
|
||||
if (state.isLoading) {
|
||||
return null;
|
||||
}
|
||||
if (state.showIntro || state.showUpdate || state.showAprilFools) {
|
||||
return (
|
||||
<CustomIntroSlider
|
||||
onDone={this.onIntroDone}
|
||||
isUpdate={state.showUpdate && !state.showIntro}
|
||||
isAprilFools={state.showAprilFools && !state.showIntro}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PaperProvider theme={state.currentTheme}>
|
||||
<OverflowMenuProvider>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: ThemeManager.getCurrentTheme().colors.background,
|
||||
flex: 1,
|
||||
}}>
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<NavigationContainer
|
||||
theme={state.currentTheme}
|
||||
ref={this.navigatorRef}>
|
||||
<MainNavigator
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
<GeneralPreferencesProvider
|
||||
initialPreferences={this.state.initialPreferences.general}
|
||||
>
|
||||
<PlanexPreferencesProvider
|
||||
initialPreferences={this.state.initialPreferences.planex}
|
||||
>
|
||||
<ProxiwashPreferencesProvider
|
||||
initialPreferences={this.state.initialPreferences.proxiwash}
|
||||
>
|
||||
<MascotPreferencesProvider
|
||||
initialPreferences={this.state.initialPreferences.mascot}
|
||||
>
|
||||
<LoginProvider initialToken={this.state.loginToken}>
|
||||
<MainApp
|
||||
ref={this.navigatorRef}
|
||||
defaultData={this.defaultData}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</OverflowMenuProvider>
|
||||
</PaperProvider>
|
||||
</LoginProvider>
|
||||
</MascotPreferencesProvider>
|
||||
</ProxiwashPreferencesProvider>
|
||||
</PlanexPreferencesProvider>
|
||||
</GeneralPreferencesProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
37
Changelog.md
37
Changelog.md
|
@ -1,21 +1,24 @@
|
|||
# Version actuelle - v3.0.7 - 13/06/2020
|
||||
# Version actuelle - v4.1.0 - 11/10/2020
|
||||
|
||||
## 🎉 Nouveautés
|
||||
- Mise à jour des écrans d'intro pour mieux refléter l'appli actuelle
|
||||
- Déplacement du bouton *À propos* dans les paramètres
|
||||
- Mode sombre par défaut parce que voilà
|
||||
- Possibilité de sélectionner la laverie des Tripodes à la place de celle de l'INSA
|
||||
- Possibilité d'ouvrir les liens zoom depuis planex !
|
||||
- Ajout d'une icône adaptive pour Android 9+
|
||||
- Ajout des remerciements dans la page À propos
|
||||
- Amélioration des animations au clic de la barre d'onglets
|
||||
|
||||
## 🐛 Corrections de bugs
|
||||
- Correction de crash au démarrage sur certains appareils
|
||||
- Correction de l'affichage de certains sites web
|
||||
- Correction du démarrage très lent sur certains appareils Android
|
||||
- Correction du comportement inconsistant de la liste des groupes pour Planex
|
||||
|
||||
## 🖥️ Notes de développement
|
||||
- Force soloader 0.8.2
|
||||
- Migration de Flow vers TypeScript
|
||||
- Blocage de react-native-keychain à la version 4.0.5 en raison d'un bug dans la librairie
|
||||
|
||||
|
||||
# Prochainement - **v4.0.1**
|
||||
# Versions précédentes
|
||||
|
||||
<details><summary>**v4.0.1**</summary>
|
||||
<details><summary>**v4.0.1** - 30/09/2020</summary>
|
||||
|
||||
## 🎉 Nouveautés
|
||||
- Ajout d'une mascotte !
|
||||
|
@ -41,7 +44,21 @@
|
|||
|
||||
</details>
|
||||
|
||||
# Versions précédentes
|
||||
<details><summary>**v3.0.7** - 13/06/2020</summary>
|
||||
|
||||
## 🎉 Nouveautés
|
||||
- Mise à jour des écrans d'intro pour mieux refléter l'appli actuelle
|
||||
- Déplacement du bouton *À propos* dans les paramètres
|
||||
- Mode sombre par défaut parce que voilà
|
||||
|
||||
## 🐛 Corrections de bugs
|
||||
- Correction de crash au démarrage sur certains appareils
|
||||
- Correction de l'affichage de certains sites web
|
||||
|
||||
## 🖥️ Notes de développement
|
||||
- Force soloader 0.8.2
|
||||
|
||||
</details>
|
||||
|
||||
<details><summary>**v3.0.5** - 28/05/2020</summary>
|
||||
|
||||
|
|
10
__mocks__/react-native-keychain/index.js
vendored
10
__mocks__/react-native-keychain/index.js
vendored
|
@ -1,7 +1,7 @@
|
|||
const keychainMock = {
|
||||
SECURITY_LEVEL_ANY: "MOCK_SECURITY_LEVEL_ANY",
|
||||
SECURITY_LEVEL_SECURE_SOFTWARE: "MOCK_SECURITY_LEVEL_SECURE_SOFTWARE",
|
||||
SECURITY_LEVEL_SECURE_HARDWARE: "MOCK_SECURITY_LEVEL_SECURE_HARDWARE",
|
||||
}
|
||||
SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY',
|
||||
SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE',
|
||||
SECURITY_LEVEL_SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE',
|
||||
};
|
||||
|
||||
export default keychainMock;
|
||||
export default keychainMock;
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import ConnectionManager from '../../src/managers/ConnectionManager';
|
||||
import {ERROR_TYPE} from '../../src/utils/WebData';
|
||||
import { ERROR_TYPE } from '../../src/utils/WebData';
|
||||
|
||||
jest.mock('react-native-keychain');
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||
|
||||
const c = ConnectionManager.getInstance();
|
||||
|
@ -44,7 +42,7 @@ test('connect bad credentials', () => {
|
|||
});
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.BAD_CREDENTIALS,
|
||||
ERROR_TYPE.BAD_CREDENTIALS
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -54,7 +52,7 @@ test('connect good credentials', () => {
|
|||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'},
|
||||
data: { token: 'token' },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -79,7 +77,7 @@ test('connect good credentials no consent', () => {
|
|||
});
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.NO_CONSENT,
|
||||
ERROR_TYPE.NO_CONSENT
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -89,7 +87,7 @@ test('connect good credentials, fail save token', () => {
|
|||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'},
|
||||
data: { token: 'token' },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -100,7 +98,7 @@ test('connect good credentials, fail save token', () => {
|
|||
return Promise.reject(false);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.TOKEN_SAVE,
|
||||
ERROR_TYPE.TOKEN_SAVE
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -109,7 +107,7 @@ test('connect connection error', () => {
|
|||
return Promise.reject();
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.CONNECTION_ERROR,
|
||||
ERROR_TYPE.CONNECTION_ERROR
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -125,7 +123,7 @@ test('connect bogus response 1', () => {
|
|||
});
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.SERVER_ERROR,
|
||||
ERROR_TYPE.SERVER_ERROR
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -140,14 +138,14 @@ test('authenticatedRequest success', () => {
|
|||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {coucou: 'toi'},
|
||||
data: { coucou: 'toi' },
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).resolves.toStrictEqual({coucou: 'toi'});
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
|
||||
).resolves.toStrictEqual({ coucou: 'toi' });
|
||||
});
|
||||
|
||||
test('authenticatedRequest error wrong token', () => {
|
||||
|
@ -167,7 +165,7 @@ test('authenticatedRequest error wrong token', () => {
|
|||
});
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
|
||||
).rejects.toBe(ERROR_TYPE.BAD_TOKEN);
|
||||
});
|
||||
|
||||
|
@ -187,7 +185,7 @@ test('authenticatedRequest error bogus response', () => {
|
|||
});
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
|
||||
).rejects.toBe(ERROR_TYPE.SERVER_ERROR);
|
||||
});
|
||||
|
||||
|
@ -201,7 +199,7 @@ test('authenticatedRequest connection error', () => {
|
|||
return Promise.reject();
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
|
||||
).rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
});
|
||||
|
||||
|
@ -212,6 +210,6 @@ test('authenticatedRequest error no token', () => {
|
|||
return null;
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
|
||||
).rejects.toBe(ERROR_TYPE.TOKEN_RETRIEVE);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import * as EquipmentBooking from '../../src/utils/EquipmentBooking';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
|
@ -18,7 +15,7 @@ test('getCurrentDay', () => {
|
|||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14 14:50:35').getTime());
|
||||
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(
|
||||
new Date('2020-01-14').getTime(),
|
||||
new Date('2020-01-14').getTime()
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -30,19 +27,19 @@ test('isEquipmentAvailable', () => {
|
|||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
booked_at: [{ begin: '2020-07-07', end: '2020-07-10' }],
|
||||
};
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
|
||||
testDevice.booked_at = [{ begin: '2020-07-07', end: '2020-07-09' }];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-09', end: '2020-07-10'}];
|
||||
testDevice.booked_at = [{ begin: '2020-07-09', end: '2020-07-10' }];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-8'},
|
||||
{begin: '2020-07-10', end: '2020-07-12'},
|
||||
{ begin: '2020-07-07', end: '2020-07-8' },
|
||||
{ begin: '2020-07-10', end: '2020-07-12' },
|
||||
];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
|
||||
});
|
||||
|
@ -55,29 +52,29 @@ test('getFirstEquipmentAvailability', () => {
|
|||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
booked_at: [{ begin: '2020-07-07', end: '2020-07-10' }],
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()
|
||||
).toBe(new Date('2020-07-11').getTime());
|
||||
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
|
||||
testDevice.booked_at = [{ begin: '2020-07-07', end: '2020-07-09' }];
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()
|
||||
).toBe(new Date('2020-07-10').getTime());
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-09'},
|
||||
{begin: '2020-07-10', end: '2020-07-16'},
|
||||
{ begin: '2020-07-07', end: '2020-07-09' },
|
||||
{ begin: '2020-07-10', end: '2020-07-16' },
|
||||
];
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()
|
||||
).toBe(new Date('2020-07-17').getTime());
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-09'},
|
||||
{begin: '2020-07-10', end: '2020-07-12'},
|
||||
{begin: '2020-07-14', end: '2020-07-16'},
|
||||
{ begin: '2020-07-07', end: '2020-07-09' },
|
||||
{ begin: '2020-07-10', end: '2020-07-12' },
|
||||
{ begin: '2020-07-14', end: '2020-07-16' },
|
||||
];
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()
|
||||
).toBe(new Date('2020-07-13').getTime());
|
||||
});
|
||||
|
||||
|
@ -85,7 +82,7 @@ test('getRelativeDateString', () => {
|
|||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||
jest.spyOn(i18n, 't').mockImplementation((translationString: string) => {
|
||||
jest.spyOn(i18n, 't').mockImplementation((translationString) => {
|
||||
const prefix = 'screens.equipment.';
|
||||
if (translationString === prefix + 'otherYear') return '0';
|
||||
else if (translationString === prefix + 'otherMonth') return '1';
|
||||
|
@ -95,25 +92,25 @@ test('getRelativeDateString', () => {
|
|||
else return null;
|
||||
});
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-09'))).toBe(
|
||||
'4',
|
||||
'4'
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-10'))).toBe(
|
||||
'3',
|
||||
'3'
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-11'))).toBe(
|
||||
'2',
|
||||
'2'
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-30'))).toBe(
|
||||
'2',
|
||||
'2'
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-08-30'))).toBe(
|
||||
'1',
|
||||
'1'
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2020-11-10'))).toBe(
|
||||
'1',
|
||||
'1'
|
||||
);
|
||||
expect(EquipmentBooking.getRelativeDateString(new Date('2021-11-10'))).toBe(
|
||||
'0',
|
||||
'0'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -122,7 +119,7 @@ test('getValidRange', () => {
|
|||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
booked_at: [{ begin: '2020-07-07', end: '2020-07-10' }],
|
||||
};
|
||||
let start = new Date('2020-07-11');
|
||||
let end = new Date('2020-07-15');
|
||||
|
@ -134,62 +131,62 @@ test('getValidRange', () => {
|
|||
'2020-07-15',
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-10'},
|
||||
{begin: '2020-07-13', end: '2020-07-15'},
|
||||
{ begin: '2020-07-07', end: '2020-07-10' },
|
||||
{ begin: '2020-07-13', end: '2020-07-15' },
|
||||
];
|
||||
result = ['2020-07-11', '2020-07-12'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
|
||||
testDevice.booked_at = [{ begin: '2020-07-12', end: '2020-07-13' }];
|
||||
result = ['2020-07-11'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-12'}];
|
||||
testDevice.booked_at = [{ begin: '2020-07-07', end: '2020-07-12' }];
|
||||
result = ['2020-07-13', '2020-07-14', '2020-07-15'];
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
start = new Date('2020-07-14');
|
||||
end = new Date('2020-07-14');
|
||||
result = ['2020-07-14'];
|
||||
expect(
|
||||
EquipmentBooking.getValidRange(start, start, testDevice),
|
||||
EquipmentBooking.getValidRange(start, start, testDevice)
|
||||
).toStrictEqual(result);
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
|
||||
start = new Date('2020-07-14');
|
||||
end = new Date('2020-07-17');
|
||||
result = ['2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-17', end: '2020-07-17'}];
|
||||
testDevice.booked_at = [{ begin: '2020-07-17', end: '2020-07-17' }];
|
||||
result = ['2020-07-14', '2020-07-15', '2020-07-16'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-12', end: '2020-07-13'},
|
||||
{begin: '2020-07-15', end: '2020-07-20'},
|
||||
{ begin: '2020-07-12', end: '2020-07-13' },
|
||||
{ begin: '2020-07-15', end: '2020-07-20' },
|
||||
];
|
||||
start = new Date('2020-07-11');
|
||||
end = new Date('2020-07-23');
|
||||
result = ['2020-07-21', '2020-07-22', '2020-07-23'];
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
result
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -205,7 +202,7 @@ test('generateMarkedDates', () => {
|
|||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
booked_at: [{ begin: '2020-07-07', end: '2020-07-10' }],
|
||||
};
|
||||
let start = new Date('2020-07-11');
|
||||
let end = new Date('2020-07-13');
|
||||
|
@ -228,7 +225,7 @@ test('generateMarkedDates', () => {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range)
|
||||
).toStrictEqual(result);
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
|
@ -248,7 +245,7 @@ test('generateMarkedDates', () => {
|
|||
},
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(false, theme, range),
|
||||
EquipmentBooking.generateMarkedDates(false, theme, range)
|
||||
).toStrictEqual(result);
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
|
@ -269,10 +266,10 @@ test('generateMarkedDates', () => {
|
|||
};
|
||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(false, theme, range),
|
||||
EquipmentBooking.generateMarkedDates(false, theme, range)
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-13', end: '2020-07-15'}];
|
||||
testDevice.booked_at = [{ begin: '2020-07-13', end: '2020-07-15' }];
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
|
@ -287,10 +284,10 @@ test('generateMarkedDates', () => {
|
|||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range)
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
|
||||
testDevice.booked_at = [{ begin: '2020-07-12', end: '2020-07-13' }];
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
|
@ -300,12 +297,12 @@ test('generateMarkedDates', () => {
|
|||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range)
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-12', end: '2020-07-13'},
|
||||
{begin: '2020-07-15', end: '2020-07-20'},
|
||||
{ begin: '2020-07-12', end: '2020-07-13' },
|
||||
{ begin: '2020-07-15', end: '2020-07-20' },
|
||||
];
|
||||
start = new Date('2020-07-11');
|
||||
end = new Date('2020-07-23');
|
||||
|
@ -318,7 +315,7 @@ test('generateMarkedDates', () => {
|
|||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range)
|
||||
).toStrictEqual(result);
|
||||
|
||||
result = {
|
||||
|
@ -340,6 +337,6 @@ test('generateMarkedDates', () => {
|
|||
};
|
||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range)
|
||||
).toStrictEqual(result);
|
||||
});
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import * as Planning from '../../src/utils/Planning';
|
||||
|
||||
test('isDescriptionEmpty', () => {
|
||||
|
@ -24,7 +21,7 @@ test('isEventDateStringFormatValid', () => {
|
|||
expect(Planning.isEventDateStringFormatValid('3214-64-12 01:16')).toBeTrue();
|
||||
|
||||
expect(
|
||||
Planning.isEventDateStringFormatValid('3214-64-12 01:16:00'),
|
||||
Planning.isEventDateStringFormatValid('3214-64-12 01:16:00')
|
||||
).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('3214-64-12 1:16')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('3214-f4-12 01:16')).toBeFalse();
|
||||
|
@ -32,7 +29,7 @@ test('isEventDateStringFormatValid', () => {
|
|||
expect(Planning.isEventDateStringFormatValid('2020-03-21')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('2020-03-21 truc')).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventDateStringFormatValid('3214-64-12 1:16:65'),
|
||||
Planning.isEventDateStringFormatValid('3214-64-12 1:16:65')
|
||||
).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('garbage')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('')).toBeFalse();
|
||||
|
@ -65,17 +62,17 @@ test('getFormattedEventTime', () => {
|
|||
expect(Planning.getFormattedEventTime(undefined, undefined)).toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime('20:30', '23:00')).toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime('2020-03-30', '2020-03-31')).toBe(
|
||||
'/ - /',
|
||||
'/ - /'
|
||||
);
|
||||
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-21 09:00'),
|
||||
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-21 09:00')
|
||||
).toBe('09:00');
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-22 17:00'),
|
||||
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-22 17:00')
|
||||
).toBe('09:00 - 23:59');
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-30 20:30', '2020-03-30 23:00'),
|
||||
Planning.getFormattedEventTime('2020-03-30 20:30', '2020-03-30 23:00')
|
||||
).toBe('20:30 - 23:00');
|
||||
});
|
||||
|
||||
|
@ -90,38 +87,38 @@ test('getDateOnlyString', () => {
|
|||
|
||||
test('isEventBefore', () => {
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00'),
|
||||
Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00')
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:15'),
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:15')
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2021-03-21 10:15'),
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2021-03-21 10:15')
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-05-21 10:15'),
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-05-21 10:15')
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-03-30 10:15'),
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-03-30 10:15')
|
||||
).toBeTrue();
|
||||
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:00'),
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:00')
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 09:00'),
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 09:00')
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-03-21 10:00'),
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-03-21 10:00')
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2021-03-21 10:15', '2020-03-21 10:15'),
|
||||
Planning.isEventBefore('2021-03-21 10:15', '2020-03-21 10:15')
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-05-21 10:15', '2020-03-21 10:15'),
|
||||
Planning.isEventBefore('2020-05-21 10:15', '2020-03-21 10:15')
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-30 10:15', '2020-03-21 10:15'),
|
||||
Planning.isEventBefore('2020-03-30 10:15', '2020-03-21 10:15')
|
||||
).toBeFalse();
|
||||
|
||||
expect(Planning.isEventBefore('garbage', '2020-03-21 10:15')).toBeFalse();
|
||||
|
@ -162,25 +159,25 @@ test('generateEmptyCalendar', () => {
|
|||
|
||||
test('pushEventInOrder', () => {
|
||||
let eventArray = [];
|
||||
let event1 = {date_begin: '2020-01-14 09:15'};
|
||||
let event1 = { date_begin: '2020-01-14 09:15' };
|
||||
Planning.pushEventInOrder(eventArray, event1);
|
||||
expect(eventArray.length).toBe(1);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
|
||||
let event2 = {date_begin: '2020-01-14 10:15'};
|
||||
let event2 = { date_begin: '2020-01-14 10:15' };
|
||||
Planning.pushEventInOrder(eventArray, event2);
|
||||
expect(eventArray.length).toBe(2);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
|
||||
let event3 = {date_begin: '2020-01-14 10:15', title: 'garbage'};
|
||||
let event3 = { date_begin: '2020-01-14 10:15', title: 'garbage' };
|
||||
Planning.pushEventInOrder(eventArray, event3);
|
||||
expect(eventArray.length).toBe(3);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
expect(eventArray[2]).toBe(event3);
|
||||
|
||||
let event4 = {date_begin: '2020-01-13 09:00'};
|
||||
let event4 = { date_begin: '2020-01-13 09:00' };
|
||||
Planning.pushEventInOrder(eventArray, event4);
|
||||
expect(eventArray.length).toBe(4);
|
||||
expect(eventArray[0]).toBe(event4);
|
||||
|
@ -194,11 +191,11 @@ test('generateEventAgenda', () => {
|
|||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
|
||||
let eventList = [
|
||||
{date_begin: '2020-01-14 09:15'},
|
||||
{date_begin: '2020-02-01 09:15'},
|
||||
{date_begin: '2020-01-15 09:15'},
|
||||
{date_begin: '2020-02-01 09:30'},
|
||||
{date_begin: '2020-02-01 08:30'},
|
||||
{ date_begin: '2020-01-14 09:15' },
|
||||
{ date_begin: '2020-02-01 09:15' },
|
||||
{ date_begin: '2020-01-15 09:15' },
|
||||
{ date_begin: '2020-02-01 09:30' },
|
||||
{ date_begin: '2020-02-01 08:30' },
|
||||
];
|
||||
const calendar = Planning.generateEventAgenda(eventList, 2);
|
||||
expect(calendar['2020-01-14'].length).toBe(1);
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
getCleanedMachineWatched,
|
||||
getMachineEndDate,
|
||||
|
@ -15,19 +12,19 @@ test('getMachineEndDate', () => {
|
|||
let expectDate = new Date('2020-01-14T15:00:00.000Z');
|
||||
expectDate.setHours(23);
|
||||
expectDate.setMinutes(10);
|
||||
expect(getMachineEndDate({endTime: '23:10'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
expect(getMachineEndDate({ endTime: '23:10' }).getTime()).toBe(
|
||||
expectDate.getTime()
|
||||
);
|
||||
|
||||
expectDate.setHours(16);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: '16:30'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
expect(getMachineEndDate({ endTime: '16:30' }).getTime()).toBe(
|
||||
expectDate.getTime()
|
||||
);
|
||||
|
||||
expect(getMachineEndDate({endTime: '15:30'})).toBeNull();
|
||||
expect(getMachineEndDate({ endTime: '15:30' })).toBeNull();
|
||||
|
||||
expect(getMachineEndDate({endTime: '13:10'})).toBeNull();
|
||||
expect(getMachineEndDate({ endTime: '13:10' })).toBeNull();
|
||||
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
|
@ -35,8 +32,8 @@ test('getMachineEndDate', () => {
|
|||
expectDate = new Date('2020-01-14T23:00:00.000Z');
|
||||
expectDate.setHours(0);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: '00:30'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
expect(getMachineEndDate({ endTime: '00:30' }).getTime()).toBe(
|
||||
expectDate.getTime()
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -52,16 +49,16 @@ test('isMachineWatched', () => {
|
|||
},
|
||||
];
|
||||
expect(
|
||||
isMachineWatched({number: '0', endTime: '23:30'}, machineList),
|
||||
isMachineWatched({ number: '0', endTime: '23:30' }, machineList)
|
||||
).toBeTrue();
|
||||
expect(
|
||||
isMachineWatched({number: '1', endTime: '20:30'}, machineList),
|
||||
isMachineWatched({ number: '1', endTime: '20:30' }, machineList)
|
||||
).toBeTrue();
|
||||
expect(
|
||||
isMachineWatched({number: '3', endTime: '20:30'}, machineList),
|
||||
isMachineWatched({ number: '3', endTime: '20:30' }, machineList)
|
||||
).toBeFalse();
|
||||
expect(
|
||||
isMachineWatched({number: '1', endTime: '23:30'}, machineList),
|
||||
isMachineWatched({ number: '1', endTime: '23:30' }, machineList)
|
||||
).toBeFalse();
|
||||
});
|
||||
|
||||
|
@ -74,8 +71,8 @@ test('getMachineOfId', () => {
|
|||
number: '1',
|
||||
},
|
||||
];
|
||||
expect(getMachineOfId('0', machineList)).toStrictEqual({number: '0'});
|
||||
expect(getMachineOfId('1', machineList)).toStrictEqual({number: '1'});
|
||||
expect(getMachineOfId('0', machineList)).toStrictEqual({ number: '0' });
|
||||
expect(getMachineOfId('1', machineList)).toStrictEqual({ number: '1' });
|
||||
expect(getMachineOfId('3', machineList)).toBeNull();
|
||||
});
|
||||
|
||||
|
@ -110,7 +107,7 @@ test('getCleanedMachineWatched', () => {
|
|||
];
|
||||
let cleanedList = watchList;
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||
cleanedList,
|
||||
cleanedList
|
||||
);
|
||||
|
||||
watchList = [
|
||||
|
@ -138,7 +135,7 @@ test('getCleanedMachineWatched', () => {
|
|||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||
cleanedList,
|
||||
cleanedList
|
||||
);
|
||||
|
||||
watchList = [
|
||||
|
@ -162,6 +159,6 @@ test('getCleanedMachineWatched', () => {
|
|||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||
cleanedList,
|
||||
cleanedList
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import {isApiResponseValid} from '../../src/utils/WebData';
|
||||
import { isApiResponseValid } from '../../src/utils/WebData';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||
|
||||
test('isRequestResponseValid', () => {
|
||||
|
@ -23,7 +21,7 @@ test('isRequestResponseValid', () => {
|
|||
expect(isApiResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 50,
|
||||
data: {truc: 'machin'},
|
||||
data: { truc: 'machin' },
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
|
@ -32,7 +30,7 @@ test('isRequestResponseValid', () => {
|
|||
expect(isApiResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 'coucou',
|
||||
data: {truc: 'machin'},
|
||||
data: { truc: 'machin' },
|
||||
};
|
||||
expect(isApiResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
|
|
|
@ -137,19 +137,16 @@ if (keystorePropertiesFile.exists() && !keystorePropertiesFile.isDirectory()) {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'fr.amicaleinsat.application'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 42
|
||||
versionName "4.0.1"
|
||||
versionCode 49
|
||||
versionName "5.0.0-3"
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
splits {
|
||||
|
@ -192,11 +189,12 @@ android {
|
|||
variant.outputs.each { output ->
|
||||
// For each separate APK per architecture, set a unique version code as described here:
|
||||
// https://developer.android.com/studio/build/configure-apk-splits.html
|
||||
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
|
||||
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
|
||||
def abi = output.getFilter(OutputFile.ABI)
|
||||
if (abi != null) { // null for the universal-debug, universal-release variants
|
||||
output.versionCodeOverride =
|
||||
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
|
||||
defaultConfig.versionCode * 1000 + versionCodes.get(abi)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -235,7 +233,7 @@ dependencies {
|
|||
// Run this once to be able to run the application with BUCK
|
||||
// puts all compile dependencies into folder libs for BUCK to use
|
||||
task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.compile
|
||||
from configurations.implementation
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
|
|
|
@ -4,5 +4,10 @@
|
|||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
|
||||
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
|
||||
<application
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="28"
|
||||
tools:ignore="GoogleAppIndexingWarning">
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
@ -19,31 +18,33 @@
|
|||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
>
|
||||
<!-- NOTIFICATIONS -->
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
|
||||
android:value="reminders"/>
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
|
||||
android:value="reminders"/>
|
||||
<!-- Change the resource name to your App's accent color - or any other color you want -->
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
|
||||
android:resource="@color/colorPrimary"/> <!-- or @android:color/{name} to use a standard color -->
|
||||
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher"/>
|
||||
<!-- START NOTIFICATIONS -->
|
||||
|
||||
<!-- Change the value to true to enable pop-up for in foreground on receiving remote notifications (for prevent duplicating while showing local notifications set this to false) -->
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
|
||||
android:value="false"/>
|
||||
<!-- Change the resource name to your App's accent color - or any other color you want -->
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
|
||||
android:resource="@color/colorPrimary"/>
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
||||
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
|
||||
android:exported="false">
|
||||
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
|
||||
android:exported="false" >
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- END NOTIFICATIONS-->
|
||||
<!-- END NOTIFICATIONS -->
|
||||
|
||||
|
||||
<meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false"/>
|
||||
|
@ -67,6 +68,5 @@
|
|||
<data android:scheme="campus-insat"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -5,22 +5,11 @@ import com.facebook.react.ReactActivity;
|
|||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import org.devio.rn.splashscreen.SplashScreen;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
|
||||
// Added automatically by Expo Config
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
Intent intent = new Intent("onConfigurationChanged");
|
||||
intent.putExtra("newConfig", newConfig);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
SplashScreen.show(this, R.style.SplashScreenTheme);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="android:textColor">#000000</item>
|
||||
<item name="android:windowBackground">@color/activityBackground</item>
|
||||
<item name="android:navigationBarColor">@color/navigationBarColor</item>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<uses-permission tools:node="remove" android:name="android.permission.WRITE_CALENDAR"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.WRITE_SETTINGS"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.WRITE_SETTINGS"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -2,17 +2,18 @@
|
|||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "29.0.2"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
buildToolsVersion = "30.0.2"
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "20.1.5948944"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:3.5.3")
|
||||
classpath("com.android.tools.build:gradle:4.2.1")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -21,6 +22,7 @@ buildscript {
|
|||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
|
@ -35,7 +37,6 @@ allprojects {
|
|||
url "$rootDir/../node_modules/expo-camera/android/maven"
|
||||
}
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,8 @@ android.useAndroidX=true
|
|||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.37.0
|
||||
FLIPPER_VERSION=0.93.0
|
||||
# Increase Java heap size for compilation
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
echo "Removing node_modules..."
|
||||
rm -rf node_modules/
|
||||
echo -e "Done\n"
|
||||
|
||||
echo "Removing locks..."
|
||||
rm -f package-lock.json && rm -f yarn.lock
|
||||
echo -e "Done\n"
|
||||
|
||||
#echo "Verifying npm cache..."
|
||||
#npm cache verify
|
||||
#echo -e "Done\n"
|
||||
|
||||
echo "Installing dependencies..."
|
||||
npm install
|
||||
echo -e "Done\n"
|
||||
|
|
@ -4,6 +4,12 @@ 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-10-07_ | react-native-keychain
|
||||
|
||||
Bloquée en 4.0.5 à cause d'un problème de performances. Au dessus de cette version, la récupération du token prend plusieurs secondes, ce qui n'est pas acceptable.
|
||||
|
||||
[Référence](https://github.com/oblador/react-native-keychain/issues/337)
|
||||
|
||||
## _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 :
|
||||
|
|
5
index.js
5
index.js
|
@ -21,9 +21,8 @@
|
|||
* @format
|
||||
*/
|
||||
|
||||
import {AppRegistry} from 'react-native';
|
||||
import { AppRegistry } from 'react-native';
|
||||
import App from './App';
|
||||
import {name as appName} from './app.json';
|
||||
import { name as appName } from './app.json';
|
||||
|
||||
// eslint-disable-next-line flowtype/require-return-type
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
|
|
@ -126,6 +126,7 @@
|
|||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle Expo Assets */,
|
||||
58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */,
|
||||
2C1F7D7FCACF5494D140CFB7 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -199,6 +200,24 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "../node_modules/react-native/scripts/react-native-xcode.sh\n";
|
||||
};
|
||||
2C1F7D7FCACF5494D140CFB7 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Campus/Pods-Campus-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Campus/Pods-Campus-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -313,12 +332,12 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Campus/application.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = 6JA7CLNUV6;
|
||||
INFOPLIST_FILE = Campus/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 4.0.1;
|
||||
MARKETING_VERSION = 4.1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -339,11 +358,11 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Campus/application.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = 6JA7CLNUV6;
|
||||
INFOPLIST_FILE = Campus/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 4.0.1;
|
||||
MARKETING_VERSION = 4.1.0;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -388,6 +407,7 @@
|
|||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
|
@ -403,7 +423,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application;
|
||||
|
@ -444,6 +464,7 @@
|
|||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
|
@ -452,7 +473,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application;
|
||||
PRODUCT_NAME = application;
|
||||
|
|
|
@ -52,7 +52,11 @@ static void InitializeFlipper(UIApplication *application) {
|
|||
|
||||
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
|
||||
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Campus" initialProperties:nil];
|
||||
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
rootView.backgroundColor = [UIColor systemBackgroundColor];
|
||||
} else {
|
||||
rootView.backgroundColor = [UIColor whiteColor];
|
||||
}
|
||||
|
||||
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
UIViewController *rootViewController = [UIViewController new];
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<string>5.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
@ -30,25 +30,25 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>4</string>
|
||||
<key>FacebookAdvertiserIDCollectionEnabled</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>FacebookAutoInitEnabled</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>FacebookAutoLogAppEventsEnabled</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<true />
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
@ -65,7 +65,7 @@
|
|||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
@ -74,6 +74,6 @@
|
|||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Automatic</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<false />
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
23
ios/Podfile
23
ios/Podfile
|
@ -1,26 +1,31 @@
|
|||
require_relative '../node_modules/react-native/scripts/react_native_pods'
|
||||
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
|
||||
|
||||
platform :ios, '10.0'
|
||||
platform :ios, '11.0'
|
||||
|
||||
target 'Campus' do
|
||||
config = use_native_modules!
|
||||
|
||||
use_react_native!(:path => config["reactNativePath"])
|
||||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
# to enable hermes on iOS, change `false` to `true` and then install pods
|
||||
:hermes_enabled => true
|
||||
)
|
||||
|
||||
|
||||
# Permissions
|
||||
permissions_path = '../node_modules/react-native-permissions/ios'
|
||||
|
||||
pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications.podspec"
|
||||
pod 'Permission-Camera', :path => "#{permissions_path}/Camera.podspec"
|
||||
pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications"
|
||||
pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
|
||||
|
||||
# Enables Flipper.
|
||||
#
|
||||
# Note that if you have use_frameworks! enabled, Flipper will not work and
|
||||
# you should disable these next few lines.
|
||||
# use_flipper!
|
||||
# post_install do |installer|
|
||||
# flipper_post_install(installer)
|
||||
# end
|
||||
# you should disable the next line.
|
||||
# use_flipper!()
|
||||
|
||||
post_install do |installer|
|
||||
react_native_post_install(installer)
|
||||
end
|
||||
end
|
||||
|
|
117
locales/en.json
117
locales/en.json
|
@ -40,40 +40,45 @@
|
|||
"dryers": "Dryers",
|
||||
"washer": "Washer",
|
||||
"washers": "Washers",
|
||||
"updated": "Updated ",
|
||||
"switch": "Switch laundromat",
|
||||
"min": "min",
|
||||
"informationTab": "Information",
|
||||
"paymentTab": "Payment",
|
||||
"tariffs": "Tariffs",
|
||||
"paymentMethods": "Payment Methods",
|
||||
"washerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the command central, then press the START button on the machine.\n\nWhen the program is finished, the screen indicates 'Programme terminé', press the yellow button to open the lid and retrieve your laundry.",
|
||||
"washerProcedure": "Put your laundry in the tumble without tamping it and by respecting weight limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the central command, then press the START button on the machine.\n\nWhen the program is finished, the screen indicates 'Programme terminé', press the yellow button to open the lid and retrieve your laundry.",
|
||||
"washerTips": "Program 'blanc/couleur': 6kg of dry laundry (cotton linen, linen, underwear, sheets, jeans, towels).\n\nProgram 'non repassable': 3,5 kg of dry laundry (synthetic fibre linen, cotton and polyester mixed).\n\nProgram 'fin 30°C': 2,5 kg of dry laundry (delicate linen in synthetic fibres).\n\nProgram 'laine 30°C': 2,5 kg of dry laundry (wool textiles).",
|
||||
"dryerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the command central, then press the START button on the machine.",
|
||||
"dryerTips": "The advised dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.",
|
||||
"dryerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the central command , then press the START button on the machine.",
|
||||
"dryerTips": "The recommended dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.",
|
||||
"procedure": "Procedure",
|
||||
"tips": "Tips",
|
||||
"numAvailable": "available",
|
||||
"numAvailablePlural": "available",
|
||||
"errors": {
|
||||
"title": "Proxiwash message",
|
||||
"button": "More info"
|
||||
},
|
||||
"washinsa": {
|
||||
"title": "INSA laundromat",
|
||||
"subtitle": "Your favorite laundromat !!",
|
||||
"description": "This is the washing service operated by Promologis for INSA's residences (We don't mind if you do not live on the campus and you do your laundry here). The room is right next to the R2, with 3 dryers and 9 washers, is open 7d/7 24h/24 ! You can bring your own detergent, use the one given on site or buy it at the Proximo (cheaper than the one given by the machines ).",
|
||||
"tariff": "Washers 6kg: 3€ the washer + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.",
|
||||
"paymentMethods": "Cash up until 10€.\nCredit Card also accepted."
|
||||
"subtitle": "Your favorite laundromat!!",
|
||||
"description": "This is the washing service for INSA's residences (We don't mind if you do not live on the campus and do your laundry here). The room is right next to the R2, with 3 dryers and 9 washers. It is open 7d/7 24h/24! You can bring your own detergent, use the one given on site or buy it at the Proximo (cheaper than the one given by the machines).",
|
||||
"tariff": "Washers 6kg: 3€ per run + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.",
|
||||
"paymentMethods": "Cash up to 10€.\nCredit Cards also accepted."
|
||||
},
|
||||
"tripodeB": {
|
||||
"title": "Tripode B laundromat",
|
||||
"subtitle": "That of those who live near the metro.",
|
||||
"description": "This is the washing service operated by the CROUS for the Tripode B and C residences as well as Thalès and Pythagore. The room is at the foot of Tripod B in front of the Pythagore residence, with 2 dryers and 6 washers, is open 7d/7 from 7am to 11pm. In addition to the 6kg washers there is one 10kg washers",
|
||||
"tariff": "Washers 6kg: 2.60€ the washer + 0.90€ with detergent.\nWashers 10kg: 4.90€ the washer + 1.50€ with detergent.\nDryers 14kg: 0.40€ for 5min of dryer usage.",
|
||||
"paymentMethods": "Carte bancaire acceptée."
|
||||
"subtitle": "For those who live near the metro.",
|
||||
"description": "This is the washing service for Tripode B and C residences, as well as Thalès and Pythagore. The room is at the foot of Tripod B in front of the Pythagore residence, with 2 dryers and 6 washers. It is open 7d/7 from 7am to 11pm. In addition to the 6kg washers there is one 10kg washer.",
|
||||
"tariff": "Washers 6kg: 2.60€ per run + 0.90€ with detergent.\nWashers 10kg: 4.90€ per run + 1.50€ with detergent.\nDryers 14kg: 0.40€ for 5min of dryer usage.",
|
||||
"paymentMethods": "Credit Cards accepted."
|
||||
},
|
||||
"modal": {
|
||||
"enableNotifications": "Notify me",
|
||||
"disableNotifications": "Stop notifications",
|
||||
"ok": "OK",
|
||||
"cancel": "Cancel",
|
||||
"finished": "This machine is finished. If you started it, you can get back your laundry.",
|
||||
"ready": "This machine is empty and ready to use.",
|
||||
"finished": "This machine is finished. If you started it, you can pick up your laundry.",
|
||||
"ready": "This machine is empty and ready for use.",
|
||||
"running": "This machine has been started at %{start} and will end at %{end}.\n\nRemaining time: %{remaining} min.\nProgram: %{program}",
|
||||
"runningNotStarted": "This machine is ready but not started. Please make sure you pressed the start button.",
|
||||
"broken": "This machine is out of order and cannot be used. Thank you for your comprehension.",
|
||||
|
@ -92,14 +97,18 @@
|
|||
"unknown": "UNKNOWN"
|
||||
},
|
||||
"notifications": {
|
||||
"channel": {
|
||||
"title": "Laundry reminders",
|
||||
"description": "Get reminders for watched washers/dryers"
|
||||
},
|
||||
"machineFinishedTitle": "Laundry Ready",
|
||||
"machineFinishedBody": "The machine n°{{number}} is finished and your laundry is ready to pickup",
|
||||
"machineFinishedBody": "Machine n°{{number}} is finished and your laundry is ready for pickup",
|
||||
"machineRunningTitle": "Laundry running: {{time}} minutes left",
|
||||
"machineRunningBody": "The machine n°{{number}} is still running"
|
||||
"machineRunningBody": "Machine n°{{number}} is still running"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Small tips",
|
||||
"message": "No need for queues anymore, you will be notified when machines are ready !\n\nIf you have your head in the clouds, you can turn on notifications for your machine by clicking on it.\n\nIf you live off campus we have other laundromat available, check the settings !!!!",
|
||||
"message": "No need for queues anymore, you will be notified when machines are ready !\n\nIf you have your head in the clouds, you can turn on notifications for your machine by clicking on it.\n\nIf you live off campus we have another available laundromat, check the settings !!!!",
|
||||
"ok": "Settings",
|
||||
"cancel": "Later"
|
||||
}
|
||||
|
@ -136,8 +145,14 @@
|
|||
},
|
||||
"planex": {
|
||||
"title": "Planex",
|
||||
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button bellow.",
|
||||
"favorites": "Favorites",
|
||||
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button below.",
|
||||
"favorites": {
|
||||
"title": "Favorites",
|
||||
"empty": {
|
||||
"title": "No favorites",
|
||||
"subtitle": "Click on the star next to a group to add it to the favorites"
|
||||
}
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Don't skip class",
|
||||
"message": "Here is Planex! You can set your class and your crush's to favorites in order to find them back easily!\n\nIf you mainly use Campus for Planex, go to the settings to make the app directly start on it!",
|
||||
|
@ -149,7 +164,7 @@
|
|||
"amicaleAbout": {
|
||||
"title": "A question ?",
|
||||
"subtitle": "Ask the Amicale",
|
||||
"message": "You want to revive a club?\nYou want to start a new project?\nHere are al the contacts you need! Do not hesitate to write a mail or send a message to the Amicale's Facebook page!",
|
||||
"message": "Want to revive a club?\nWant to start a new project?\nHere are all the contacts you need! Don't hesitate to write a mail or send a message to the Amicale's Facebook page!",
|
||||
"roles": {
|
||||
"interSchools": "Inter Schools",
|
||||
"culture": "Culture",
|
||||
|
@ -174,8 +189,8 @@
|
|||
"sortPrice": "Price",
|
||||
"sortPriceReverse": "Price (reverse)",
|
||||
"inStock": "in stock",
|
||||
"description": "The Proximo is your small grocery store maintained by students directly on the campus. Open every day from 18h30 to 19h30, we welcome you when you are short on pastas or sodas ! Different products for different problems, everything at cost price. You can pay by Lydia or cash.",
|
||||
"openingHours": "Openning Hours",
|
||||
"description": "The Proximo is your small grocery store held by students directly on campus. Open every day from 18h30 to 19h30, we welcome you when you are short on pasta or soda ! Different products for different problems, everything is sold at cost. You can pay with Lydia or cash.",
|
||||
"openingHours": "Opening Hours",
|
||||
"paymentMethods": "Payment Methods",
|
||||
"paymentMethodsDescription": "Cash or Lydia",
|
||||
"search": "Search",
|
||||
|
@ -205,7 +220,7 @@
|
|||
"resetPassword": "Forgot Password",
|
||||
"mascotDialog": {
|
||||
"title": "An account?",
|
||||
"message": "An Amicale account allows you to take part in several activities around campus. You can join a club, or even create your own!\n\nLogging into your Amicale account on the app will allow you to see all available clubs on the campus, vote for the upcoming elections, and more to come!\n\nNo Account? Go to the Amicale's building during open hours to create one.",
|
||||
"message": "An Amicale account allows you to take part in several activities around campus. You can join a club, or even create your own!\n\nLogging into your Amicale account on the app will allow you to see all available clubs on the campus, vote for the upcoming elections, and more to come!\n\nNo Account? Go to the Amicale's building during opening hours to create one.",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
|
@ -223,8 +238,8 @@
|
|||
"membershipPayed": "Payed",
|
||||
"membershipNotPayed": "Not payed",
|
||||
"welcomeTitle": "Welcome %{name}!",
|
||||
"welcomeDescription": "This is your Amicale INSA Toulouse personal space. Bellow are the current services you can access thanks to your account. Feels empty? You're right and we plan on fixing that, so stay tuned!",
|
||||
"welcomeFeedback": "We plan on doing more! If you have any suggestions or found bugs, please tell us by clicking the button bellow."
|
||||
"welcomeDescription": "This is your Amicale INSA Toulouse personal space. Below are the services you can currently access thanks to your account. Feels empty? You're right and we plan on fixing that, so stay tuned!",
|
||||
"welcomeFeedback": "We plan on doing more! If you have any suggestions or found bugs, please tell us by clicking the button below."
|
||||
},
|
||||
"clubs": {
|
||||
"title": "Clubs",
|
||||
|
@ -238,10 +253,10 @@
|
|||
"amicaleContact": "Contact the Amicale",
|
||||
"invalidClub": "Could not find the club. Please make sure the club you are trying to access is valid.",
|
||||
"about": {
|
||||
"text": "The clubs, making the campus live, with more than sixty clubs offering various activities! From the philosophy club to the PABI (Production Artisanale de Bière Insaienne), without forgetting the multiple music and dance clubs, you will surely find an activity that suits you!",
|
||||
"text": "The clubs keep the campus alive, with more than sixty clubs offering various activities! From the philosophy club to the PABI (Production Artisanale de Bière Insalienne), without forgetting the multiple music and dance clubs, you will surely find an activity that suits you!",
|
||||
"title": "A question ?",
|
||||
"subtitle": "Ask the Amicale",
|
||||
"message": "You have a question concerning the clubs?\nYou want to revive or create a club?\nContact the Amicale at the following address:"
|
||||
"message": "Do you have a question regarding clubs?\nWant to revive or create a club?\nContact the Amicale at the following address:"
|
||||
}
|
||||
},
|
||||
"vote": {
|
||||
|
@ -250,14 +265,14 @@
|
|||
"select": {
|
||||
"title": "Elections open",
|
||||
"subtitle": "Vote now!",
|
||||
"sendButton": "Send Vote",
|
||||
"dialogTitle": "Send Vote?",
|
||||
"dialogTitleLoading": "Sending vote...",
|
||||
"dialogMessage": "Are you sure you want to send your vote? You will not be able to change it."
|
||||
"sendButton": "Cast Vote",
|
||||
"dialogTitle": "Cast Vote?",
|
||||
"dialogTitleLoading": "Casting vote...",
|
||||
"dialogMessage": "Are you sure you want to cast your vote? You will not be able to change it."
|
||||
},
|
||||
"tease": {
|
||||
"title": "Elections incoming",
|
||||
"subtitle": "Be ready to vote!",
|
||||
"subtitle": "Get ready to vote!",
|
||||
"message": "Vote start:"
|
||||
},
|
||||
"wait": {
|
||||
|
@ -277,7 +292,7 @@
|
|||
},
|
||||
"mascotDialog": {
|
||||
"title": "Why vote?",
|
||||
"message": "The Amicale's elections is the right moment for you to choose the next team, which will handle different projects on the campus, help organizing your favorite events, animate the campus life during the whole year, and relay your ideas to the administration, so that your campus life is the most enjoyable possible!\nYour turn to make a change!\uD83D\uDE09\n\nNote: If there is only one list, it is still important to vote to show your support, so that the administration knows the current list is supported by students. It is always a plus when taking difficult decisions! \uD83D\uDE09",
|
||||
"message": "The Amicale's elections are the right moment for you to choose the next team, which will handle different projects on the campus, help organizing your favorite events, animate the campus life during the whole year, and relay your ideas to the administration, so that your campus life is the most enjoyable possible!\nYour turn to make a change!\uD83D\uDE09\n\nNote: If there is only one list, it is still important to vote to show your support, so that the administration knows the current list is supported by students. It is always a plus when taking difficult decisions! \uD83D\uDE09",
|
||||
"button": "Ok"
|
||||
}
|
||||
},
|
||||
|
@ -302,7 +317,7 @@
|
|||
"bookingConfirmedMessage": "Do not forget to come by the Amicale to give your bail in exchange of the equipment.",
|
||||
"mascotDialog": {
|
||||
"title": "How does it work ?",
|
||||
"message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, click the equipment of your choice in the list bellow, enter your lend dates, then come around the Amicale to claim it and give your bail.",
|
||||
"message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, select the equipment of your choice in the list below, enter your lend dates, then come around the Amicale to claim it and give your bail.",
|
||||
"button": "Ok"
|
||||
}
|
||||
},
|
||||
|
@ -322,7 +337,7 @@
|
|||
},
|
||||
"mascotDialog": {
|
||||
"title": "Scano...what?",
|
||||
"message": "Scanotron 3000 allows you to scan Campus QR codes, created by clubs or event managers, to get more detailed info!\n\nThe camera will never be used for any other purposes.",
|
||||
"message": "Scanotron 3000 allows you to scan Campus QR codes, created by clubs or event managers, to get more detailed info!\n\nThe camera will never be used for any other purpose.",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
|
@ -333,11 +348,11 @@
|
|||
"nightModeSubOn": "Your eyes are at peace",
|
||||
"nightModeSubOff": "Your eyes are burning",
|
||||
"nightModeAuto": "Follow system dark mode",
|
||||
"nightModeAutoSub": "Follows the mode chosen by your system",
|
||||
"nightModeAutoSub": "Follows the mode set by your system",
|
||||
"startScreen": "Start Screen",
|
||||
"startScreenSub": "Select which screen to start the app on",
|
||||
"dashboard": "Dashboard",
|
||||
"dashboardSub": "Edit what services to display on the dashboard",
|
||||
"dashboardSub": "Edit which services to display on the dashboard",
|
||||
"proxiwashNotifReminder": "Machine running reminder",
|
||||
"proxiwashNotifReminderSub": "How many minutes before",
|
||||
"proxiwashChangeWash": "Laundromat selection",
|
||||
|
@ -345,7 +360,7 @@
|
|||
"information": "Information",
|
||||
"dashboardEdit": {
|
||||
"title": "Edit dashboard",
|
||||
"message": "The five items above represent your dashboard.\nYou can replace one of its services by selecting it, and then by clicking on the desired new service in the list bellow.",
|
||||
"message": "The five items above represent your dashboard.\nYou can replace one of its services by selecting it, and then by clicking on the desired new service in the list below.",
|
||||
"undo": "Undo changes"
|
||||
}
|
||||
},
|
||||
|
@ -364,23 +379,24 @@
|
|||
"thanks": "Thanks",
|
||||
"user": {
|
||||
"you": "You ?",
|
||||
"arnaud": "Student in IR (2020). He is the creator of this beautiful app you use everyday. Some say he is handsome as well.",
|
||||
"yohan": "Student in IR (2020). He helped to fix bugs. I think he is handsome as well but I don't know him personally.",
|
||||
"beranger": "Student in AE (2020) and president of the Amicale when the app was created. The app was his idea. He helped a lot to find bugs, new features and communication.",
|
||||
"celine": "Student in GPE (2020). Without her, everything would be less cute. She helped to write the text, for communication, and also to create the mascot 🦊.",
|
||||
"damien": "Student in IR (2020) and creator of the 2020 version of the Amicale's website. Thanks to his help, integrating Amicale's services into the app was child's play.",
|
||||
"titouan": "Student in IR (2020). He helped a lot in finding bugs and new features.",
|
||||
"theo": "Student in AE (2020). If the app works on iOS, this is all thanks to his help during his numerous tests."
|
||||
"arnaud": "Student in 4IR (2020). He is the creator of this app you use everyday.",
|
||||
"docjyj": "Student in 2MIC FAS (2020). He added some new features and fixed some bugs.",
|
||||
"yohan": "Student in 4IR (2020). He helped to fix bugs and gave some ideas.",
|
||||
"beranger": "Student in 4AE (2020) and president of the Amicale when the app was created. The app was his idea. He helped a lot to find bugs, new features and communication.",
|
||||
"celine": "Student in 4GPE (2020). Without her, everything wouldn't be as cute. She helped to write the text, for communication, and also to create the mascot 🦊.",
|
||||
"damien": "Student in 4IR (2020) and creator of the 2020 version of the Amicale's website. Thanks to his help, integrating Amicale's services into the app was child's play.",
|
||||
"titouan": "Student in 4IR (2020). He helped a lot in finding bugs and new features.",
|
||||
"theo": "Student in 4AE (2020). If the app works on iOS, this is all thanks to his help during his numerous tests."
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
"title": "Contribute",
|
||||
"feedback": "Contact the dev",
|
||||
"feedbackSubtitle": "A student like you!",
|
||||
"feedbackDescription": "Feedback or bugs, you are always welcome.\nChoose your preferred way from the buttons bellow.",
|
||||
"feedbackDescription": "Feedback or bugs, you are always welcome.\nChoose your preferred way from the buttons below.",
|
||||
"contribute": "Contribute to the project",
|
||||
"contributeSubtitle": "With a possible \"implication citoyenne\"!",
|
||||
"contributeDescription": "Everyone can help: communication, design or coding! You are free to contribute as you like.\nYou can find bellow a link to Trello for project organization, and a link to the source code on GitEtud.",
|
||||
"contributeDescription": "Everyone can help: communication, design or coding! You are free to contribute as you like.\nYou can find below a link to Trello for project organization, and a link to the source code on GitEtud.",
|
||||
"homeButtonTitle": "Contribute to the project",
|
||||
"homeButtonSubtitle": "Your help is important"
|
||||
},
|
||||
|
@ -418,11 +434,11 @@
|
|||
"intro": {
|
||||
"slideMain": {
|
||||
"title": "Welcome to CAMPUS!",
|
||||
"text": "The students app of the INSA Toulouse! Read along to see everything you can do."
|
||||
"text": "INSA Toulouse's student app! Read along to see everything you can do."
|
||||
},
|
||||
"slidePlanex": {
|
||||
"title": "Prettier Planex",
|
||||
"text": "Lookup your and your friends timetable with a mobile friendly Planex!"
|
||||
"text": "Lookup your friends' and your own timetables with a mobile friendly Planex!"
|
||||
},
|
||||
"slideEvents": {
|
||||
"title": "Events",
|
||||
|
@ -430,7 +446,7 @@
|
|||
},
|
||||
"slideServices": {
|
||||
"title": "And even more!",
|
||||
"text": "You can do much more with CAMPUS, but I can't explain everything here. Explore the app to find out!"
|
||||
"text": "You can do much more with CAMPUS, but I can't explain everything here. Explore the app to find out for yourself!"
|
||||
},
|
||||
"slideDone": {
|
||||
"title": "Contribute to the project!",
|
||||
|
@ -455,6 +471,7 @@
|
|||
"badToken": "You are not logged in. Please login and try again.",
|
||||
"noConsent": "You did not give your consent for data processing to the Amicale.",
|
||||
"tokenSave": "Could not save session token. Please contact support.",
|
||||
"tokenRetrieve": "Could not retrieve session token. Please contact support.",
|
||||
"badInput": "Invalid input. Please try again.",
|
||||
"forbidden": "You do not have access to this data.",
|
||||
"connectionError": "Network error. Please check your internet connection.",
|
||||
|
|
|
@ -40,6 +40,8 @@
|
|||
"dryers": "Sèche-Linges",
|
||||
"washer": "Lave-Linge",
|
||||
"washers": "Lave-Linges",
|
||||
"updated": "Mise à jour ",
|
||||
"switch": "Changer de laverie",
|
||||
"min": "min",
|
||||
"informationTab": "Informations",
|
||||
"paymentTab": "Paiement",
|
||||
|
@ -53,24 +55,27 @@
|
|||
"tips": "Conseils",
|
||||
"numAvailable": "disponible",
|
||||
"numAvailablePlural": "disponibles",
|
||||
"errors": {
|
||||
"title": "Message laverie",
|
||||
"button": "En savoir plus"
|
||||
},
|
||||
"washinsa": {
|
||||
"title": "Laverie INSA",
|
||||
"subtitle": "Ta laverie préférer !!",
|
||||
"description": "C'est le service de laverie proposé par Promologis pour les résidences INSA (On t'en voudra pas si tu loges pas sur le campus et que tu fais ta machine ici). Le local situé au pied du R2 avec ses 3 sèche-linges et 9 machines est ouvert 7J/7 24h/24 ! Tu peux amener ta lessive, la prendre sur place ou encore mieux l'acheter au Proximo (moins chère qu'à la laverie directement).",
|
||||
"subtitle": "Ta laverie préférée !!",
|
||||
"description": "C'est le service de laverie pour les résidences INSA (On t'en voudra pas si tu loges pas sur le campus et que tu fais ta machine ici). Le local situé au pied du R2 avec ses 3 sèche-linges et 9 machines, est ouvert 7J/7 24h/24 ! Tu peux amener ta lessive, la prendre sur place ou encore mieux l'acheter au Proximo (moins chère qu'à la laverie directement).",
|
||||
"tariff": "Lave-Linges 6kg: 3€ la machine + 0.80€ avec la lessive.\nSèche-Linges 14kg: 0.35€ pour 5min de sèche linge.",
|
||||
"paymentMethods": "Toute monnaie jusqu'à 10€.\nCarte bancaire acceptée."
|
||||
},
|
||||
"tripodeB": {
|
||||
"title": "Laverie Tripode B",
|
||||
"subtitle": "Celle de ceux qui habite prés du métro.",
|
||||
"description": "C'est le service de laverie proposé par le CROUS pour les résidences Tripode B et C ainsi que Thalès et Pythagore. Le local situé au pied du Tripode B en face de de la résidence Pythagore avec ses 2 sèche-linges et 6 machines est ouvert 7J/7 de 7h à 23h. En plus des machine 6kg il y as une machine de 10kg.",
|
||||
"subtitle": "Pour ceux qui habitent proche du métro.",
|
||||
"description": "C'est le service de laverie pour les résidences Tripode B et C ainsi que Thalès et Pythagore. Le local situé au pied du Tripode B, en face de de la résidence Pythagore, avec ses 2 sèche-linges et 6 machines est ouvert 7J/7 de 7h à 23h. En plus des machine 6kg il y a une machine de 10kg.",
|
||||
"tariff": "Lave-Linges 6kg: 2.60€ la machine + 0.90€ avec la lessive.\nLave-Linges 10kg: 4.90€ la machine + 1.50€ avec la lessive.\nSèche-Linges 14kg: 0.40€ pour 5min de sèche linge.",
|
||||
"paymentMethods": "Carte bancaire acceptée."
|
||||
},
|
||||
"modal": {
|
||||
"enableNotifications": "Me Notifier",
|
||||
"disableNotifications": "Désactiver les notifications",
|
||||
"ok": "OK",
|
||||
"cancel": "Annuler",
|
||||
"finished": "Cette machine est terminée. Si tu l'as démarrée, tu peux récupérer ton linge.",
|
||||
"ready": "Cette machine est vide et prête à être utilisée.",
|
||||
|
@ -92,6 +97,10 @@
|
|||
"unknown": "INCONNU"
|
||||
},
|
||||
"notifications": {
|
||||
"channel": {
|
||||
"title": "Rappels laverie",
|
||||
"description": "Recevoir des rappels pour les machines demandées"
|
||||
},
|
||||
"machineFinishedTitle": "Linge prêt",
|
||||
"machineFinishedBody": "La machine n°{{number}} est terminée et ton linge est prêt à être récupéré",
|
||||
"machineRunningTitle": "Machine en cours: {{time}} minutes restantes",
|
||||
|
@ -137,7 +146,13 @@
|
|||
"planex": {
|
||||
"title": "Planex",
|
||||
"noGroupSelected": "Pas de groupe sélectionné. Choisis un groupe avec le beau bouton rouge ci-dessous.",
|
||||
"favorites": "Favoris",
|
||||
"favorites": {
|
||||
"title": "Favoris",
|
||||
"empty": {
|
||||
"title": "Aucun favoris",
|
||||
"subtitle": "Cliquez sur l'étoile à côté d'un groupe pour l'ajouter aux favoris"
|
||||
}
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Sécher c'est mal",
|
||||
"message": "Ici c'est Planex ! Tu peux mettre en favoris ta classe et celle de ton crush pour l'espio... les retrouver facilement !\n\nSi tu utilises Campus surtout pour Planex, vas dans les paramètres pour faire démarrer l'appli direct dessus !",
|
||||
|
@ -322,7 +337,7 @@
|
|||
},
|
||||
"mascotDialog": {
|
||||
"title": "Scano...quoi ?",
|
||||
"message": "Scanotron 3000 te permet de scanner des QR codes Campus, affichés par des clubs ou des respo d'évenements, pour avoir plus d'infos !\n\nL'appareil photo ne sera jamais utilisé pour d'autres raisons.",
|
||||
"message": "Scanotron 3000 te permet de scanner des QR codes Campus, affichés par des clubs ou des respo d'événements, pour avoir plus d'infos !\n\nL'appareil photo ne sera jamais utilisé pour d'autres raisons.",
|
||||
"button": "Oké"
|
||||
}
|
||||
},
|
||||
|
@ -341,11 +356,11 @@
|
|||
"proxiwashNotifReminder": "Rappel de machine en cours",
|
||||
"proxiwashNotifReminderSub": "Combien de minutes avant",
|
||||
"proxiwashChangeWash": "Sélection de la laverie",
|
||||
"proxiwashChangeWashSub": "Quel laverie à afficher",
|
||||
"proxiwashChangeWashSub": "Quelle laverie afficher",
|
||||
"information": "Informations",
|
||||
"dashboardEdit": {
|
||||
"title": "Modifier la dashboard",
|
||||
"message": "Les 5 icones ci-dessus représentent ta dashboard.\nTu peux remplacer un de ses services en cliquant dessus, puis en sélectionnant le nouveau service de ton choix dans la liste ci-dessous.",
|
||||
"message": "Les 5 icônes ci-dessus représentent ta dashboard.\nTu peux remplacer un de ses services en cliquant dessus, puis en sélectionnant le nouveau service de ton choix dans la liste ci-dessous.",
|
||||
"undo": "Annuler les changements"
|
||||
}
|
||||
},
|
||||
|
@ -364,13 +379,14 @@
|
|||
"thanks": "Remerciements",
|
||||
"user": {
|
||||
"you": "Toi ?",
|
||||
"arnaud": "Étudiant en IR (2020). C'est le créateur de cette magnifique application que t'utilises tous les jour. Et il est vraiment BG aussi.",
|
||||
"yohan": "Étudiant en IR (2020). Il a aidé à corriger des bug. Et j'imagine aussi qu'il est BG mais je le connait pas.",
|
||||
"beranger": "Étudiant en AE (2020) et Président de l’Amicale au moment de la création et du lancement du projet. L’application, c’était son idée. Il a beaucoup aidé pour trouver des bugs, de nouvelles fonctionnalités et faire de la com.",
|
||||
"celine": "Étudiante en GPE (2020). Sans elle, tout serait moins mignon. Elle a aidé pour écrire le texte, faire de la com, et aussi à créer la mascotte 🦊.",
|
||||
"damien": "Étudiant en IR (2020) et créateur de la dernière version du site de l’Amicale. Grâce à son aide, intégrer les services de l’Amicale à l’application a été très simple.",
|
||||
"titouan": "Étudiant en IR (2020). Il a beaucoup aidé pour trouver des bugs et proposer des nouvelles fonctionnalités.",
|
||||
"theo": "Étudiant en AE (2020). Si l’application marche sur iOS, c’est grâce à son aide lors de ses nombreux tests."
|
||||
"arnaud": "Étudiant en 4IR (2020). C'est le créateur de cette application que t' utilises tous les jours.",
|
||||
"docjyj": "Étudiant en 2MIC FAS (2020). Il a ajouté quelques nouvelles fonctionnalités et corrigé des bugs.",
|
||||
"yohan": "Étudiant en 4IR (2020). Il a aidé à corriger des bug et a proposé quelques idées.",
|
||||
"beranger": "Étudiant en 4AE (2020) et Président de l’Amicale au moment de la création et du lancement du projet. L’application, c’était son idée. Il a beaucoup aidé pour trouver des bugs, de nouvelles fonctionnalités et faire de la com.",
|
||||
"celine": "Étudiante en 4GPE (2020). Sans elle, tout serait moins mignon. Elle a aidé pour écrire le texte, faire de la com, et aussi à créer la mascotte 🦊.",
|
||||
"damien": "Étudiant en 4IR (2020) et créateur de la dernière version du site de l’Amicale. Grâce à son aide, intégrer les services de l’Amicale à l’application a été très simple.",
|
||||
"titouan": "Étudiant en 4IR (2020). Il a beaucoup aidé pour trouver des bugs et proposer des nouvelles fonctionnalités.",
|
||||
"theo": "Étudiant en 4AE (2020). Si l’application marche sur iOS, c’est grâce à son aide lors de ses nombreux tests."
|
||||
}
|
||||
},
|
||||
"feedback": {
|
||||
|
@ -455,6 +471,7 @@
|
|||
"badToken": "Tu n'est pas connecté. Merci de te connecter puis réessayes.",
|
||||
"noConsent": "Tu n'as pas donné ton consentement pour l'utilisation de tes données personnelles.",
|
||||
"tokenSave": "Impossible de sauvegarder le token de session. Merci de contacter le support.",
|
||||
"tokenRetrieve": "Impossible de récupérer le token de session. Merci de contacter le support.",
|
||||
"badInput": "Entrée invalide. Merci de réessayer.",
|
||||
"forbidden": "Tu n'as pas accès à cette information.",
|
||||
"connectionError": "Erreur de réseau. Merci de vérifier ta connexion Internet.",
|
||||
|
|
|
@ -7,11 +7,10 @@
|
|||
|
||||
module.exports = {
|
||||
transformer: {
|
||||
// eslint-disable-next-line flowtype/require-return-type
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: false,
|
||||
inlineRequires: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
35251
package-lock.json
generated
35251
package-lock.json
generated
File diff suppressed because it is too large
Load diff
186
package.json
186
package.json
|
@ -1,14 +1,128 @@
|
|||
{
|
||||
"name": "campus",
|
||||
"version": "4.0.1",
|
||||
"version": "5.0.0-3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"android-release": "react-native run-android --variant=release",
|
||||
"ios": "react-native run-ios",
|
||||
"start": "react-native start",
|
||||
"start-no-cache": "react-native start --reset-cache",
|
||||
"test": "jest",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx"
|
||||
"typescript": "tsc --noEmit",
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
||||
"lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"full-check": "npm run typescript && npm run lint && npm run test",
|
||||
"pod": "cd ios && pod install && cd ..",
|
||||
"bundle": "cd android && ./gradlew bundleRelease",
|
||||
"clean": "react-native-clean-project",
|
||||
"postversion": "react-native-version"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nartc/react-native-barcode-mask": "1.2.0",
|
||||
"@react-native-async-storage/async-storage": "1.15.7",
|
||||
"@react-native-community/masked-view": "0.1.11",
|
||||
"@react-native-community/push-notification-ios": "1.10.1",
|
||||
"@react-native-community/slider": "4.1.6",
|
||||
"@react-navigation/bottom-tabs": "6.0.5",
|
||||
"@react-navigation/native": "6.0.2",
|
||||
"@react-navigation/stack": "6.0.7",
|
||||
"i18n-js": "3.8.0",
|
||||
"moment": "2.29.1",
|
||||
"react": "17.0.2",
|
||||
"react-native": "0.65.1",
|
||||
"react-native-animatable": "1.3.3",
|
||||
"react-native-app-intro-slider": "4.0.4",
|
||||
"react-native-appearance": "0.3.4",
|
||||
"react-native-autolink": "4.0.0",
|
||||
"react-native-calendars": "1.1266.0",
|
||||
"react-native-camera": "4.1.1",
|
||||
"react-native-collapsible": "1.6.0",
|
||||
"react-native-gesture-handler": "1.10.3",
|
||||
"react-native-image-zoom-viewer": "3.0.1",
|
||||
"react-native-keychain": "4.0.5",
|
||||
"react-native-linear-gradient": "2.5.6",
|
||||
"react-native-localize": "2.1.4",
|
||||
"react-native-modalize": "2.0.8",
|
||||
"react-native-paper": "4.9.2",
|
||||
"react-native-permissions": "3.0.5",
|
||||
"react-native-push-notification": "8.1.0",
|
||||
"react-native-reanimated": "1.13.2",
|
||||
"react-native-render-html": "6.1.0",
|
||||
"react-native-safe-area-context": "3.3.2",
|
||||
"react-native-screens": "3.7.0",
|
||||
"react-native-splash-screen": "3.2.0",
|
||||
"react-native-timeago": "0.5.0",
|
||||
"react-native-vector-icons": "8.1.0",
|
||||
"react-native-webview": "11.13.0",
|
||||
"react-navigation-collapsible": "6.0.0",
|
||||
"react-navigation-header-buttons": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.12.9",
|
||||
"@babel/runtime": "7.12.5",
|
||||
"@react-native-community/eslint-config": "3.0.1",
|
||||
"@types/i18n-js": "3.8.2",
|
||||
"@types/jest": "26.0.24",
|
||||
"@types/react": "17.0.3",
|
||||
"@types/react-native": "0.65.0",
|
||||
"@types/react-native-calendars": "1.1264.2",
|
||||
"@types/react-native-push-notification": "7.3.2",
|
||||
"@types/react-native-vector-icons": "6.4.8",
|
||||
"@types/react-test-renderer": "17.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.31.0",
|
||||
"@typescript-eslint/parser": "4.31.0",
|
||||
"babel-jest": "26.6.3",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"jest": "26.6.3",
|
||||
"jest-extended": "0.11.5",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"metro-react-native-babel-preset": "0.66.0",
|
||||
"prettier": "2.4.0",
|
||||
"react-native-clean-project": "3.6.7",
|
||||
"react-native-codegen": "0.0.7",
|
||||
"react-native-version": "4.0.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"typescript": "4.4.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"@react-native-community",
|
||||
"prettier"
|
||||
],
|
||||
"rules": {
|
||||
"no-undef": 0,
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": [
|
||||
"error"
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"quoteProps": "consistent",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"node_modules/"
|
||||
],
|
||||
"prettier": {
|
||||
"quoteProps": "consistent",
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5",
|
||||
"useTabs": false
|
||||
},
|
||||
"jest": {
|
||||
"preset": "react-native",
|
||||
|
@ -23,71 +137,5 @@
|
|||
"setupFilesAfterEnv": [
|
||||
"jest-extended"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@nartc/react-native-barcode-mask": "^1.2.0",
|
||||
"@react-native-community/async-storage": "^1.12.0",
|
||||
"@react-native-community/masked-view": "^0.1.10",
|
||||
"@react-native-community/push-notification-ios": "^1.5.0",
|
||||
"@react-native-community/slider": "^3.0.3",
|
||||
"@react-navigation/bottom-tabs": "^5.8.0",
|
||||
"@react-navigation/native": "^5.7.3",
|
||||
"@react-navigation/stack": "^5.9.0",
|
||||
"i18n-js": "^3.7.1",
|
||||
"react": "16.13.1",
|
||||
"react-native": "0.63.2",
|
||||
"react-native-animatable": "^1.3.3",
|
||||
"react-native-app-intro-slider": "^4.0.4",
|
||||
"react-native-appearance": "^0.3.4",
|
||||
"react-native-autolink": "^3.0.0",
|
||||
"react-native-calendars": "^1.403.0",
|
||||
"react-native-camera": "^3.40.0",
|
||||
"react-native-collapsible": "^1.5.3",
|
||||
"react-native-gesture-handler": "^1.8.0",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
"react-native-keychain": "^6.2.0",
|
||||
"react-native-linear-gradient": "^2.5.6",
|
||||
"react-native-localize": "^1.4.1",
|
||||
"react-native-modalize": "^2.0.6",
|
||||
"react-native-paper": "^4.2.0",
|
||||
"react-native-permissions": "^2.2.1",
|
||||
"react-native-push-notification": "^5.1.1",
|
||||
"react-native-reanimated": "^1.13.0",
|
||||
"react-native-render-html": "^4.2.3",
|
||||
"react-native-safe-area-context": "^3.1.8",
|
||||
"react-native-screens": "^2.11.0",
|
||||
"react-native-splash-screen": "^3.2.0",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
"react-native-webview": "^10.9.0",
|
||||
"react-navigation-collapsible": "^5.6.4",
|
||||
"react-navigation-header-buttons": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.0",
|
||||
"@babel/runtime": "^7.11.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",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-flowtype": "^5.2.0",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-react": "^7.20.5",
|
||||
"eslint-plugin-react-hooks": "^4.0.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",
|
||||
"typescript": "^3.8.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,227 +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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import {ERROR_TYPE} from '../../utils/WebData';
|
||||
import ErrorView from '../Screens/ErrorView';
|
||||
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
|
||||
|
||||
type PropsType<T> = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
requests: Array<{
|
||||
link: string;
|
||||
params: object;
|
||||
mandatory: boolean;
|
||||
}>;
|
||||
renderFunction: (data: Array<T | null>) => React.ReactNode;
|
||||
errorViewOverride?: Array<{
|
||||
errorCode: number;
|
||||
message: string;
|
||||
icon: string;
|
||||
showRetryButton: boolean;
|
||||
}> | null;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
class AuthenticatedScreen<T> extends React.Component<PropsType<T>, StateType> {
|
||||
static defaultProps = {
|
||||
errorViewOverride: null,
|
||||
};
|
||||
|
||||
currentUserToken: string | null;
|
||||
|
||||
connectionManager: ConnectionManager;
|
||||
|
||||
errors: Array<number>;
|
||||
|
||||
fetchedData: Array<T | null>;
|
||||
|
||||
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);
|
||||
this.errors = new Array(props.requests.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes screen if user changed
|
||||
*/
|
||||
onScreenFocus = () => {
|
||||
if (this.currentUserToken !== this.connectionManager.getToken()) {
|
||||
this.currentUserToken = this.connectionManager.getToken();
|
||||
this.fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when a request finishes, successfully or not.
|
||||
* Saves data and error code.
|
||||
* If the token is invalid, logout the user and open the login screen.
|
||||
* If the last request was received, stop the loading screen.
|
||||
*
|
||||
* @param data The data fetched from the server
|
||||
* @param index The index for the data
|
||||
* @param error The error code received
|
||||
*/
|
||||
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 (this.allRequestsFinished()) {
|
||||
this.setState({loading: false});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error to render.
|
||||
* Non-mandatory requests are ignored.
|
||||
*
|
||||
*
|
||||
* @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
|
||||
*/
|
||||
getError(): number {
|
||||
const {props} = this;
|
||||
for (let i = 0; i < this.errors.length; i += 1) {
|
||||
if (
|
||||
this.errors[i] !== ERROR_TYPE.SUCCESS &&
|
||||
props.requests[i].mandatory
|
||||
) {
|
||||
return this.errors[i];
|
||||
}
|
||||
}
|
||||
return ERROR_TYPE.SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error view to display in case of error
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getErrorRender() {
|
||||
const {props} = this;
|
||||
const errorCode = this.getError();
|
||||
let shouldOverride = false;
|
||||
let override = null;
|
||||
const overrideList = props.errorViewOverride;
|
||||
if (overrideList != null) {
|
||||
for (let i = 0; i < overrideList.length; i += 1) {
|
||||
if (overrideList[i].errorCode === errorCode) {
|
||||
shouldOverride = true;
|
||||
override = overrideList[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldOverride && override != null) {
|
||||
return (
|
||||
<ErrorView
|
||||
icon={override.icon}
|
||||
message={override.message}
|
||||
showRetryButton={override.showRetryButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the data from the server.
|
||||
*
|
||||
* If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
|
||||
*
|
||||
* If the user is logged in, send all requests.
|
||||
*/
|
||||
fetchData = () => {
|
||||
const {state, props} = this;
|
||||
if (!state.loading) {
|
||||
this.setState({loading: true});
|
||||
}
|
||||
|
||||
if (this.connectionManager.isLoggedIn()) {
|
||||
for (let i = 0; i < props.requests.length; i += 1) {
|
||||
this.connectionManager
|
||||
.authenticatedRequest<T>(
|
||||
props.requests[i].link,
|
||||
props.requests[i].params,
|
||||
)
|
||||
.then((response: T): void => this.onRequestFinished(response, i))
|
||||
.catch((error: number): void =>
|
||||
this.onRequestFinished(null, i, error),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < props.requests.length; i += 1) {
|
||||
this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if all requests finished processing
|
||||
*
|
||||
* @return {boolean} True if all finished
|
||||
*/
|
||||
allRequestsFinished(): boolean {
|
||||
let finished = true;
|
||||
this.errors.forEach((error: number | null) => {
|
||||
if (error == null) {
|
||||
finished = false;
|
||||
}
|
||||
});
|
||||
return finished;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the data, to be called using ref by parent components
|
||||
*/
|
||||
reload() {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {state, props} = this;
|
||||
if (state.loading) {
|
||||
return <BasicLoadingScreen />;
|
||||
}
|
||||
if (this.getError() === ERROR_TYPE.SUCCESS) {
|
||||
return props.renderFunction(this.fetchedData);
|
||||
}
|
||||
return this.getErrorRender();
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatedScreen;
|
231
src/components/Amicale/Login/LoginForm.tsx
Normal file
231
src/components/Amicale/Login/LoginForm.tsx
Normal file
|
@ -0,0 +1,231 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import {
|
||||
Image,
|
||||
StyleSheet,
|
||||
View,
|
||||
TextInput as RNTextInput,
|
||||
} from 'react-native';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
HelperText,
|
||||
TextInput,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
onSubmit: (email: string, password: string) => void;
|
||||
onHelpPress: () => void;
|
||||
onResetPasswordPress: () => void;
|
||||
};
|
||||
|
||||
const ICON_AMICALE = require('../../../../assets/amicale.png');
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
header: {
|
||||
fontSize: 36,
|
||||
marginBottom: 48,
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
buttonContainer: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
lockButton: {
|
||||
marginRight: 'auto',
|
||||
marginBottom: 20,
|
||||
},
|
||||
sendButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
const emailRegex = /^.+@.+\..+$/;
|
||||
|
||||
/**
|
||||
* Checks if the entered email is valid (matches the regex)
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEmailValid(email: string): boolean {
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has entered a password
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPasswordValid(password: string): boolean {
|
||||
return password !== '';
|
||||
}
|
||||
|
||||
export default function LoginForm(props: Props) {
|
||||
const theme = useTheme();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isEmailValidated, setIsEmailValidated] = useState(false);
|
||||
const [isPasswordValidated, setIsPasswordValidated] = useState(false);
|
||||
const passwordRef = useRef<RNTextInput>(null);
|
||||
/**
|
||||
* Checks if we should tell the user his email is invalid.
|
||||
* We should only show this if his email is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
const shouldShowEmailError = () => {
|
||||
return isEmailValidated && !isEmailValid(email);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if we should tell the user his password is invalid.
|
||||
* We should only show this if his password is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
const shouldShowPasswordError = () => {
|
||||
return isPasswordValidated && !isPasswordValid(password);
|
||||
};
|
||||
|
||||
const onEmailSubmit = () => {
|
||||
if (passwordRef.current) {
|
||||
passwordRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his email is ready to be validated
|
||||
*/
|
||||
const validateEmail = () => setIsEmailValidated(true);
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his password is ready to be validated
|
||||
*/
|
||||
const validatePassword = () => setIsPasswordValidated(true);
|
||||
|
||||
const onEmailChange = (value: string) => {
|
||||
if (isEmailValidated) {
|
||||
setIsEmailValidated(false);
|
||||
}
|
||||
setEmail(value);
|
||||
};
|
||||
|
||||
const onPasswordChange = (value: string) => {
|
||||
if (isPasswordValidated) {
|
||||
setIsPasswordValidated(false);
|
||||
}
|
||||
setPassword(value);
|
||||
};
|
||||
|
||||
const shouldEnableLogin = () => {
|
||||
return isEmailValid(email) && isPasswordValid(password) && !props.loading;
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (shouldEnableLogin()) {
|
||||
props.onSubmit(email, password);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.login.title')}
|
||||
titleStyle={styles.text}
|
||||
subtitle={i18n.t('screens.login.subtitle')}
|
||||
subtitleStyle={styles.text}
|
||||
left={({ size }) => (
|
||||
<Image
|
||||
source={ICON_AMICALE}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<View>
|
||||
<TextInput
|
||||
label={i18n.t('screens.login.email')}
|
||||
mode={'outlined'}
|
||||
value={email}
|
||||
onChangeText={onEmailChange}
|
||||
onBlur={validateEmail}
|
||||
onSubmitEditing={onEmailSubmit}
|
||||
error={shouldShowEmailError()}
|
||||
textContentType={'emailAddress'}
|
||||
autoCapitalize={'none'}
|
||||
autoCompleteType={'email'}
|
||||
autoCorrect={false}
|
||||
keyboardType={'email-address'}
|
||||
returnKeyType={'next'}
|
||||
secureTextEntry={false}
|
||||
/>
|
||||
<HelperText type={'error'} visible={shouldShowEmailError()}>
|
||||
{i18n.t('screens.login.emailError')}
|
||||
</HelperText>
|
||||
<TextInput
|
||||
ref={passwordRef}
|
||||
label={i18n.t('screens.login.password')}
|
||||
mode={'outlined'}
|
||||
value={password}
|
||||
onChangeText={onPasswordChange}
|
||||
onBlur={validatePassword}
|
||||
onSubmitEditing={onSubmit}
|
||||
error={shouldShowPasswordError()}
|
||||
textContentType={'password'}
|
||||
autoCapitalize={'none'}
|
||||
autoCompleteType={'password'}
|
||||
autoCorrect={false}
|
||||
keyboardType={'default'}
|
||||
returnKeyType={'done'}
|
||||
secureTextEntry={true}
|
||||
/>
|
||||
<HelperText type={'error'} visible={shouldShowPasswordError()}>
|
||||
{i18n.t('screens.login.passwordError')}
|
||||
</HelperText>
|
||||
</View>
|
||||
<Card.Actions style={styles.buttonContainer}>
|
||||
<Button
|
||||
icon="lock-question"
|
||||
mode="contained"
|
||||
onPress={props.onResetPasswordPress}
|
||||
color={theme.colors.warning}
|
||||
style={styles.lockButton}
|
||||
>
|
||||
{i18n.t('screens.login.resetPassword')}
|
||||
</Button>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
disabled={!shouldEnableLogin()}
|
||||
loading={props.loading}
|
||||
onPress={onSubmit}
|
||||
style={styles.sendButton}
|
||||
>
|
||||
{i18n.t('screens.login.title')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="help-circle"
|
||||
mode="contained"
|
||||
onPress={props.onHelpPress}
|
||||
style={GENERAL_STYLES.centerHorizontal}
|
||||
>
|
||||
{i18n.t('screens.login.mascotDialog.title')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</View>
|
||||
);
|
||||
}
|
|
@ -20,8 +20,7 @@
|
|||
import * as React from 'react';
|
||||
import i18n from 'i18n-js';
|
||||
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
import { useLogout } from '../../utils/logout';
|
||||
|
||||
type PropsType = {
|
||||
visible: boolean;
|
||||
|
@ -29,19 +28,13 @@ type PropsType = {
|
|||
};
|
||||
|
||||
function LogoutDialog(props: PropsType) {
|
||||
const navigation = useNavigation();
|
||||
const onLogout = useLogout();
|
||||
// Use a loading dialog as it can take some time to update the context
|
||||
const onClickAccept = async (): Promise<void> => {
|
||||
return new Promise((resolve: () => void) => {
|
||||
ConnectionManager.getInstance()
|
||||
.disconnect()
|
||||
.then(() => {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{name: 'main'}],
|
||||
});
|
||||
props.onDismiss();
|
||||
resolve();
|
||||
});
|
||||
onLogout();
|
||||
props.onDismiss();
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
|
|
104
src/components/Amicale/Profile/ProfileClubCard.tsx
Normal file
104
src/components/Amicale/Profile/ProfileClubCard.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import React from 'react';
|
||||
import { Card, Avatar, Divider, useTheme, List } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import { ProfileClubType } from '../../../screens/Amicale/ProfileScreen';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
clubs?: Array<ProfileClubType>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default function ProfileClubCard(props: Props) {
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const clubKeyExtractor = (item: ProfileClubType) => item.name;
|
||||
|
||||
const getClubListItem = ({ item }: { item: ProfileClubType }) => {
|
||||
const onPress = () =>
|
||||
navigation.navigate(MainRoutes.ClubInformation, {
|
||||
type: 'id',
|
||||
clubId: item.id,
|
||||
});
|
||||
let description = i18n.t('screens.profile.isMember');
|
||||
let icon = (leftProps: {
|
||||
color: string;
|
||||
style: {
|
||||
marginLeft: number;
|
||||
marginRight: number;
|
||||
marginVertical?: number;
|
||||
};
|
||||
}) => (
|
||||
<List.Icon
|
||||
color={leftProps.color}
|
||||
style={leftProps.style}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
);
|
||||
if (item.is_manager) {
|
||||
description = i18n.t('screens.profile.isManager');
|
||||
icon = (leftProps) => (
|
||||
<List.Icon
|
||||
style={leftProps.style}
|
||||
icon="star"
|
||||
color={theme.colors.primary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={description}
|
||||
left={icon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function getClubList(list: Array<ProfileClubType> | undefined) {
|
||||
if (!list) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list.sort((a) => (a.is_manager ? -1 : 1));
|
||||
return (
|
||||
<FlatList
|
||||
renderItem={getClubListItem}
|
||||
keyExtractor={clubKeyExtractor}
|
||||
data={list}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.clubs')}
|
||||
subtitle={i18n.t('screens.profile.clubsSubtitle')}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon
|
||||
size={iconProps.size}
|
||||
icon="account-group"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
{getClubList(props.clubs)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
56
src/components/Amicale/Profile/ProfileMembershipCard.tsx
Normal file
56
src/components/Amicale/Profile/ProfileMembershipCard.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { Avatar, Card, List, useTheme } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
valid?: boolean;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default function ProfileMembershipCard(props: Props) {
|
||||
const theme = useTheme();
|
||||
const state = props.valid === true;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.membership')}
|
||||
subtitle={i18n.t('screens.profile.membershipSubtitle')}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon
|
||||
size={iconProps.size}
|
||||
icon="credit-card"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title={
|
||||
state
|
||||
? i18n.t('screens.profile.membershipPayed')
|
||||
: i18n.t('screens.profile.membershipNotPayed')
|
||||
}
|
||||
left={(leftProps) => (
|
||||
<List.Icon
|
||||
style={leftProps.style}
|
||||
color={state ? theme.colors.success : theme.colors.danger}
|
||||
icon={state ? 'check' : 'close'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</List.Section>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
111
src/components/Amicale/Profile/ProfilePersonalCard.tsx
Normal file
111
src/components/Amicale/Profile/ProfilePersonalCard.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { useNavigation } from '@react-navigation/core';
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
List,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import Urls from '../../../constants/Urls';
|
||||
import { ProfileDataType } from '../../../screens/Amicale/ProfileScreen';
|
||||
import i18n from 'i18n-js';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
profile?: ProfileDataType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
editButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
mascot: {
|
||||
width: 60,
|
||||
},
|
||||
title: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
|
||||
function getFieldValue(field?: string): string {
|
||||
return field ? field : i18n.t('screens.profile.noData');
|
||||
}
|
||||
|
||||
export default function ProfilePersonalCard(props: Props) {
|
||||
const { profile } = props;
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation();
|
||||
|
||||
function getPersonalListItem(field: string | undefined, icon: string) {
|
||||
const title = field != null ? getFieldValue(field) : ':(';
|
||||
const subtitle = field != null ? '' : getFieldValue(field);
|
||||
return (
|
||||
<List.Item
|
||||
title={title}
|
||||
description={subtitle}
|
||||
left={(leftProps) => (
|
||||
<List.Icon
|
||||
style={leftProps.style}
|
||||
icon={icon}
|
||||
color={field != null ? leftProps.color : theme.colors.textDisabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={`${profile?.first_name} ${profile?.last_name}`}
|
||||
subtitle={profile?.email}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon
|
||||
size={iconProps.size}
|
||||
icon="account"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
<List.Section>
|
||||
<List.Subheader>
|
||||
{i18n.t('screens.profile.personalInformation')}
|
||||
</List.Subheader>
|
||||
{getPersonalListItem(profile?.birthday, 'cake-variant')}
|
||||
{getPersonalListItem(profile?.phone, 'phone')}
|
||||
{getPersonalListItem(profile?.email, 'email')}
|
||||
{getPersonalListItem(profile?.branch, 'school')}
|
||||
</List.Section>
|
||||
<Divider />
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="account-edit"
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
navigation.navigate(MainRoutes.Website, {
|
||||
host: Urls.websites.amicale,
|
||||
path: profile?.link,
|
||||
title: i18n.t('screens.websites.amicale'),
|
||||
});
|
||||
}}
|
||||
style={styles.editButton}
|
||||
>
|
||||
{i18n.t('screens.profile.editInformation')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
84
src/components/Amicale/Profile/ProfileWelcomeCard.tsx
Normal file
84
src/components/Amicale/Profile/ProfileWelcomeCard.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { useNavigation } from '@react-navigation/core';
|
||||
import React from 'react';
|
||||
import { Button, Card, Divider, Paragraph } from 'react-native-paper';
|
||||
import Mascot, { MASCOT_STYLE } from '../../Mascot/Mascot';
|
||||
import i18n from 'i18n-js';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import CardList from '../../Lists/CardList/CardList';
|
||||
import { getAmicaleServices, SERVICES_KEY } from '../../../utils/Services';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
firstname?: string;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
editButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
mascot: {
|
||||
width: 60,
|
||||
},
|
||||
title: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
|
||||
function ProfileWelcomeCard(props: Props) {
|
||||
const navigation = useNavigation();
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.welcomeTitle', {
|
||||
name: props.firstname,
|
||||
})}
|
||||
left={() => (
|
||||
<Mascot
|
||||
style={styles.mascot}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'bounceIn',
|
||||
duration: 1000,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
titleStyle={styles.title}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
|
||||
<CardList
|
||||
dataset={getAmicaleServices(
|
||||
(route) => navigation.navigate(route),
|
||||
true,
|
||||
[SERVICES_KEY.PROFILE]
|
||||
)}
|
||||
isHorizontal={true}
|
||||
/>
|
||||
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
|
||||
<Divider />
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="bug"
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
navigation.navigate(MainRoutes.Feedback);
|
||||
}}
|
||||
style={styles.editButton}
|
||||
>
|
||||
{i18n.t('screens.feedback.homeButtonTitle')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(
|
||||
ProfileWelcomeCard,
|
||||
(pp, np) => pp.firstname === np.firstname
|
||||
);
|
|
@ -18,24 +18,31 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {Headline, useTheme} from 'react-native-paper';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Headline, useTheme } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
headline: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
function VoteNotAvailable() {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
<View style={styles.container}>
|
||||
<Headline
|
||||
style={{
|
||||
color: theme.colors.textDisabled,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
...styles.headline,
|
||||
}}
|
||||
>
|
||||
{i18n.t('screens.vote.noVote')}
|
||||
</Headline>
|
||||
</View>
|
||||
|
|
|
@ -26,9 +26,9 @@ import {
|
|||
Subheading,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {FlatList, StyleSheet} from 'react-native';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
||||
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
|
||||
|
||||
type PropsType = {
|
||||
teams: Array<VoteTeamType>;
|
||||
|
@ -40,8 +40,11 @@ const styles = StyleSheet.create({
|
|||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
itemCard: {
|
||||
marginTop: 10,
|
||||
},
|
||||
item: {
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -86,16 +89,18 @@ class VoteResults extends React.Component<PropsType> {
|
|||
|
||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||
|
||||
resultRenderItem = ({item}: {item: VoteTeamType}) => {
|
||||
resultRenderItem = ({ item }: { item: VoteTeamType }) => {
|
||||
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
|
||||
const isDraw = this.winnerIds.length > 1;
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
const elevation = isWinner ? 5 : 3;
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
marginTop: 10,
|
||||
elevation: isWinner ? 5 : 3,
|
||||
}}>
|
||||
...styles.itemCard,
|
||||
elevation: elevation,
|
||||
}}
|
||||
>
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
|
||||
|
@ -113,7 +118,7 @@ class VoteResults extends React.Component<PropsType> {
|
|||
? props.theme.colors.primary
|
||||
: props.theme.colors.text,
|
||||
}}
|
||||
style={{padding: 0}}
|
||||
style={styles.item}
|
||||
/>
|
||||
<ProgressBar
|
||||
progress={item.votes / this.totalVotes}
|
||||
|
@ -124,7 +129,7 @@ class VoteResults extends React.Component<PropsType> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
|
|
|
@ -17,146 +17,127 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
|
||||
import {FlatList, StyleSheet, View} from 'react-native';
|
||||
import React, { useState } from 'react';
|
||||
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
|
||||
import { FlatList, StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
|
||||
import ErrorDialog from '../../Dialogs/ErrorDialog';
|
||||
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
||||
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
|
||||
import { ApiRejectType } from '../../../utils/WebData';
|
||||
import { REQUEST_STATUS } from '../../../utils/Requests';
|
||||
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||
|
||||
type PropsType = {
|
||||
type Props = {
|
||||
teams: Array<VoteTeamType>;
|
||||
onVoteSuccess: () => void;
|
||||
onVoteError: () => void;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
selectedTeam: string;
|
||||
voteDialogVisible: boolean;
|
||||
errorDialogVisible: boolean;
|
||||
currentError: number;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
button: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
export default class VoteSelect extends React.PureComponent<
|
||||
PropsType,
|
||||
StateType
|
||||
> {
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedTeam: 'none',
|
||||
voteDialogVisible: false,
|
||||
errorDialogVisible: false,
|
||||
currentError: 0,
|
||||
};
|
||||
}
|
||||
function VoteSelect(props: Props) {
|
||||
const [selectedTeam, setSelectedTeam] = useState('none');
|
||||
const [voteDialogVisible, setVoteDialogVisible] = useState(false);
|
||||
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
||||
status: REQUEST_STATUS.SUCCESS,
|
||||
});
|
||||
const request = useAuthenticatedRequest('elections/vote', {
|
||||
team: parseInt(selectedTeam, 10),
|
||||
});
|
||||
|
||||
onVoteSelectionChange = (teamName: string): void =>
|
||||
this.setState({selectedTeam: teamName});
|
||||
const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
|
||||
|
||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||
|
||||
voteRenderItem = ({item}: {item: VoteTeamType}) => (
|
||||
const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
||||
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
||||
);
|
||||
|
||||
showVoteDialog = (): void => this.setState({voteDialogVisible: true});
|
||||
const showVoteDialog = () => setVoteDialogVisible(true);
|
||||
|
||||
onVoteDialogDismiss = (): void => this.setState({voteDialogVisible: false});
|
||||
const onVoteDialogDismiss = () => setVoteDialogVisible(false);
|
||||
|
||||
onVoteDialogAccept = async (): Promise<void> => {
|
||||
const onVoteDialogAccept = async (): Promise<void> => {
|
||||
return new Promise((resolve: () => void) => {
|
||||
const {state} = this;
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest('elections/vote', {
|
||||
team: parseInt(state.selectedTeam, 10),
|
||||
})
|
||||
request()
|
||||
.then(() => {
|
||||
this.onVoteDialogDismiss();
|
||||
const {props} = this;
|
||||
onVoteDialogDismiss();
|
||||
props.onVoteSuccess();
|
||||
resolve();
|
||||
})
|
||||
.catch((error: number) => {
|
||||
this.onVoteDialogDismiss();
|
||||
this.showErrorDialog(error);
|
||||
.catch((error: ApiRejectType) => {
|
||||
onVoteDialogDismiss();
|
||||
setCurrentError(error);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showErrorDialog = (error: number): void =>
|
||||
this.setState({
|
||||
errorDialogVisible: true,
|
||||
currentError: error,
|
||||
});
|
||||
|
||||
onErrorDialogDismiss = () => {
|
||||
this.setState({errorDialogVisible: false});
|
||||
const {props} = this;
|
||||
const onErrorDialogDismiss = () => {
|
||||
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||
props.onVoteError();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {state, props} = this;
|
||||
return (
|
||||
<View>
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.vote.select.title')}
|
||||
subtitle={i18n.t('screens.vote.select.subtitle')}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<RadioButton.Group
|
||||
onValueChange={this.onVoteSelectionChange}
|
||||
value={state.selectedTeam}>
|
||||
<FlatList
|
||||
data={props.teams}
|
||||
keyExtractor={this.voteKeyExtractor}
|
||||
extraData={state.selectedTeam}
|
||||
renderItem={this.voteRenderItem}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
onPress={this.showVoteDialog}
|
||||
style={{marginLeft: 'auto'}}
|
||||
disabled={state.selectedTeam === 'none'}>
|
||||
{i18n.t('screens.vote.select.sendButton')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
<LoadingConfirmDialog
|
||||
visible={state.voteDialogVisible}
|
||||
onDismiss={this.onVoteDialogDismiss}
|
||||
onAccept={this.onVoteDialogAccept}
|
||||
title={i18n.t('screens.vote.select.dialogTitle')}
|
||||
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
||||
message={i18n.t('screens.vote.select.dialogMessage')}
|
||||
return (
|
||||
<View>
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.vote.select.title')}
|
||||
subtitle={i18n.t('screens.vote.select.subtitle')}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
||||
)}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={state.errorDialogVisible}
|
||||
onDismiss={this.onErrorDialogDismiss}
|
||||
errorCode={state.currentError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
<Card.Content>
|
||||
<RadioButton.Group
|
||||
onValueChange={setSelectedTeam}
|
||||
value={selectedTeam}
|
||||
>
|
||||
<FlatList
|
||||
data={props.teams}
|
||||
keyExtractor={voteKeyExtractor}
|
||||
extraData={selectedTeam}
|
||||
renderItem={voteRenderItem}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon={'send'}
|
||||
mode={'contained'}
|
||||
onPress={showVoteDialog}
|
||||
style={styles.button}
|
||||
disabled={selectedTeam === 'none'}
|
||||
>
|
||||
{i18n.t('screens.vote.select.sendButton')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
<LoadingConfirmDialog
|
||||
visible={voteDialogVisible}
|
||||
onDismiss={onVoteDialogDismiss}
|
||||
onAccept={onVoteDialogAccept}
|
||||
title={i18n.t('screens.vote.select.dialogTitle')}
|
||||
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
||||
message={i18n.t('screens.vote.select.dialogMessage')}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={
|
||||
currentError.status !== REQUEST_STATUS.SUCCESS ||
|
||||
currentError.code !== undefined
|
||||
}
|
||||
onDismiss={onErrorDialogDismiss}
|
||||
status={currentError.status}
|
||||
code={currentError.code}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default VoteSelect;
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Card, Paragraph} from 'react-native-paper';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import { Avatar, Card, Paragraph } from 'react-native-paper';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -30,9 +30,6 @@ const styles = StyleSheet.create({
|
|||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default function VoteTease(props: PropsType) {
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Card, Paragraph, useTheme} from 'react-native-paper';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import { Avatar, Card, Paragraph, useTheme } from 'react-native-paper';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -33,14 +33,11 @@ const styles = StyleSheet.create({
|
|||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default function VoteWait(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
const {startDate} = props;
|
||||
const { startDate } = props;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
|
@ -56,12 +53,12 @@ export default function VoteWait(props: PropsType) {
|
|||
/>
|
||||
<Card.Content>
|
||||
{props.justVoted ? (
|
||||
<Paragraph style={{color: theme.colors.success}}>
|
||||
<Paragraph style={{ color: theme.colors.success }}>
|
||||
{i18n.t('screens.vote.wait.messageSubmitted')}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{props.hasVoted ? (
|
||||
<Paragraph style={{color: theme.colors.success}}>
|
||||
<Paragraph style={{ color: theme.colors.success }}>
|
||||
{i18n.t('screens.vote.wait.messageVoted')}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {View, ViewStyle} from 'react-native';
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
import { List, useTheme } from 'react-native-paper';
|
||||
import Collapsible from 'react-native-collapsible';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
theme: ReactNativePaper.Theme;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
style?: ViewStyle;
|
||||
|
@ -37,99 +37,101 @@ type PropsType = {
|
|||
}) => React.ReactNode;
|
||||
opened?: boolean;
|
||||
unmountWhenCollapsed?: boolean;
|
||||
children?: React.ReactNode;
|
||||
enabled?: boolean;
|
||||
renderItem: () => React.ReactNode;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
expanded: boolean;
|
||||
};
|
||||
function AnimatedAccordion(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
|
||||
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
|
||||
const [expanded, setExpanded] = React.useState(props.opened);
|
||||
const lastOpenedProp = useRef(props.opened);
|
||||
const chevronIcon = useRef(props.opened ? 'chevron-up' : 'chevron-down');
|
||||
const animStart = useRef(props.opened ? '180deg' : '0deg');
|
||||
const animEnd = useRef(props.opened ? '0deg' : '180deg');
|
||||
const enabled = props.enabled !== false;
|
||||
|
||||
class AnimatedAccordion extends React.Component<PropsType, StateType> {
|
||||
chevronRef: {current: null | (typeof AnimatedListIcon & List.Icon)};
|
||||
|
||||
chevronIcon: string;
|
||||
|
||||
animStart: string;
|
||||
|
||||
animEnd: string;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.chevronIcon = '';
|
||||
this.animStart = '';
|
||||
this.animEnd = '';
|
||||
this.state = {
|
||||
expanded: props.opened != null ? props.opened : false,
|
||||
};
|
||||
this.chevronRef = React.createRef();
|
||||
this.setupChevron();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {state, props} = this;
|
||||
if (nextProps.opened != null && nextProps.opened !== props.opened) {
|
||||
state.expanded = nextProps.opened;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
setupChevron() {
|
||||
const {expanded} = this.state;
|
||||
const getAccordionAnimation = ():
|
||||
| Animatable.Animation
|
||||
| string
|
||||
| Animatable.CustomAnimation => {
|
||||
// I don't knwo why ts is complaining
|
||||
// The type definitions must be broken because this is a valid style and it works
|
||||
if (expanded) {
|
||||
this.chevronIcon = 'chevron-up';
|
||||
this.animStart = '180deg';
|
||||
this.animEnd = '0deg';
|
||||
return {
|
||||
from: {
|
||||
// @ts-ignore
|
||||
rotate: animStart.current,
|
||||
},
|
||||
to: {
|
||||
// @ts-ignore
|
||||
rotate: animEnd.current,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
this.chevronIcon = 'chevron-down';
|
||||
this.animStart = '0deg';
|
||||
this.animEnd = '180deg';
|
||||
}
|
||||
}
|
||||
|
||||
toggleAccordion = () => {
|
||||
const {expanded} = this.state;
|
||||
if (this.chevronRef.current != null) {
|
||||
this.chevronRef.current.transitionTo({
|
||||
rotate: expanded ? this.animStart : this.animEnd,
|
||||
});
|
||||
this.setState((prevState: StateType): {expanded: boolean} => ({
|
||||
expanded: !prevState.expanded,
|
||||
}));
|
||||
return {
|
||||
from: {
|
||||
// @ts-ignore
|
||||
rotate: animEnd.current,
|
||||
},
|
||||
to: {
|
||||
// @ts-ignore
|
||||
rotate: animStart.current,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
const {colors} = props.theme;
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<List.Item
|
||||
title={props.title}
|
||||
description={props.subtitle}
|
||||
titleStyle={state.expanded ? {color: colors.primary} : null}
|
||||
onPress={this.toggleAccordion}
|
||||
right={(iconProps) => (
|
||||
<AnimatedListIcon
|
||||
ref={this.chevronRef}
|
||||
style={iconProps.style}
|
||||
icon={this.chevronIcon}
|
||||
color={state.expanded ? colors.primary : iconProps.color}
|
||||
useNativeDriver
|
||||
/>
|
||||
)}
|
||||
left={props.left}
|
||||
/>
|
||||
<Collapsible collapsed={!state.expanded}>
|
||||
{!props.unmountWhenCollapsed ||
|
||||
(props.unmountWhenCollapsed && state.expanded)
|
||||
? props.children
|
||||
: null}
|
||||
useEffect(() => {
|
||||
// Force the expanded state to follow the prop when changing
|
||||
if (!enabled) {
|
||||
setExpanded(false);
|
||||
} else if (
|
||||
props.opened !== undefined &&
|
||||
props.opened !== lastOpenedProp.current
|
||||
) {
|
||||
setExpanded(props.opened);
|
||||
}
|
||||
}, [enabled, props.opened]);
|
||||
|
||||
const toggleAccordion = () => setExpanded(!expanded);
|
||||
|
||||
const renderChildren =
|
||||
!props.unmountWhenCollapsed || (props.unmountWhenCollapsed && expanded);
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<List.Item
|
||||
title={props.title}
|
||||
description={props.subtitle}
|
||||
descriptionNumberOfLines={2}
|
||||
titleStyle={expanded ? { color: theme.colors.primary } : null}
|
||||
onPress={enabled ? toggleAccordion : undefined}
|
||||
right={
|
||||
enabled
|
||||
? (iconProps) => (
|
||||
<Animatable.View
|
||||
animation={getAccordionAnimation()}
|
||||
duration={300}
|
||||
useNativeDriver={true}
|
||||
>
|
||||
<List.Icon
|
||||
style={{ ...iconProps.style, ...GENERAL_STYLES.center }}
|
||||
icon={chevronIcon.current}
|
||||
color={expanded ? theme.colors.primary : iconProps.color}
|
||||
/>
|
||||
</Animatable.View>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
left={props.left}
|
||||
/>
|
||||
{enabled ? (
|
||||
<Collapsible collapsed={!expanded}>
|
||||
{renderChildren ? props.renderItem() : null}
|
||||
</Collapsible>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(AnimatedAccordion);
|
||||
export default AnimatedAccordion;
|
||||
|
|
|
@ -1,203 +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/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {FAB, IconButton, Surface, withTheme} from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
|
||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
onPress: (action: string, data?: string) => void;
|
||||
seekAttention: boolean;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
currentMode: string;
|
||||
};
|
||||
|
||||
const DISPLAY_MODES = {
|
||||
DAY: 'agendaDay',
|
||||
WEEK: 'agendaWeek',
|
||||
MONTH: 'month',
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
left: '5%',
|
||||
width: '90%',
|
||||
},
|
||||
surface: {
|
||||
position: 'relative',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderRadius: 50,
|
||||
elevation: 2,
|
||||
},
|
||||
fabContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
top: '-25%',
|
||||
},
|
||||
});
|
||||
|
||||
class AnimatedBottomBar extends React.Component<PropsType, StateType> {
|
||||
ref: {current: null | (Animatable.View & View)};
|
||||
|
||||
hideHandler: AutoHideHandler;
|
||||
|
||||
displayModeIcons: {[key: string]: string};
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
currentMode: DISPLAY_MODES.WEEK,
|
||||
};
|
||||
this.ref = React.createRef();
|
||||
this.hideHandler = new AutoHideHandler(false);
|
||||
this.hideHandler.addListener(this.onHideChange);
|
||||
|
||||
this.displayModeIcons = {};
|
||||
this.displayModeIcons[DISPLAY_MODES.DAY] = 'calendar-text';
|
||||
this.displayModeIcons[DISPLAY_MODES.WEEK] = 'calendar-week';
|
||||
this.displayModeIcons[DISPLAY_MODES.MONTH] = 'calendar-range';
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType, nextState: StateType): boolean {
|
||||
const {props, state} = this;
|
||||
return (
|
||||
nextProps.seekAttention !== props.seekAttention ||
|
||||
nextState.currentMode !== state.currentMode
|
||||
);
|
||||
}
|
||||
|
||||
onHideChange = (shouldHide: boolean) => {
|
||||
const ref = this.ref;
|
||||
if (ref && ref.current && ref.current.fadeOutDown && ref.current.fadeInUp) {
|
||||
if (shouldHide) {
|
||||
ref.current.fadeOutDown(500);
|
||||
} else {
|
||||
ref.current.fadeInUp(500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
this.hideHandler.onScroll(event);
|
||||
};
|
||||
|
||||
changeDisplayMode = () => {
|
||||
const {props, state} = this;
|
||||
let newMode;
|
||||
switch (state.currentMode) {
|
||||
case DISPLAY_MODES.DAY:
|
||||
newMode = DISPLAY_MODES.WEEK;
|
||||
break;
|
||||
case DISPLAY_MODES.WEEK:
|
||||
newMode = DISPLAY_MODES.MONTH;
|
||||
break;
|
||||
case DISPLAY_MODES.MONTH:
|
||||
newMode = DISPLAY_MODES.DAY;
|
||||
break;
|
||||
default:
|
||||
newMode = DISPLAY_MODES.WEEK;
|
||||
break;
|
||||
}
|
||||
this.setState({currentMode: newMode});
|
||||
props.onPress('changeView', newMode);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
const buttonColor = props.theme.colors.primary;
|
||||
return (
|
||||
<Animatable.View
|
||||
ref={this.ref}
|
||||
useNativeDriver
|
||||
style={{
|
||||
...styles.container,
|
||||
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}>
|
||||
<Surface style={styles.surface}>
|
||||
<View style={styles.fabContainer}>
|
||||
<AnimatedFAB
|
||||
animation={props.seekAttention ? 'bounce' : undefined}
|
||||
easing="ease-out"
|
||||
iterationDelay={500}
|
||||
iterationCount="infinite"
|
||||
useNativeDriver
|
||||
style={styles.fab}
|
||||
icon="account-clock"
|
||||
onPress={(): void => props.navigation.navigate('group-select')}
|
||||
/>
|
||||
</View>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<IconButton
|
||||
icon={this.displayModeIcons[state.currentMode]}
|
||||
color={buttonColor}
|
||||
onPress={this.changeDisplayMode}
|
||||
/>
|
||||
<IconButton
|
||||
icon="clock-in"
|
||||
color={buttonColor}
|
||||
style={{marginLeft: 5}}
|
||||
onPress={(): void => props.onPress('today')}
|
||||
/>
|
||||
</View>
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<IconButton
|
||||
icon="chevron-left"
|
||||
color={buttonColor}
|
||||
onPress={(): void => props.onPress('prev')}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
color={buttonColor}
|
||||
style={{marginLeft: 5}}
|
||||
onPress={(): void => props.onPress('next')}
|
||||
/>
|
||||
</View>
|
||||
</Surface>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(AnimatedBottomBar);
|
|
@ -24,10 +24,10 @@ import {
|
|||
StyleSheet,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import {FAB} from 'react-native-paper';
|
||||
import { FAB } from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
|
||||
type PropsType = {
|
||||
icon: string;
|
||||
|
@ -43,7 +43,7 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
export default class AnimatedFAB extends React.Component<PropsType> {
|
||||
ref: {current: null | (Animatable.View & View)};
|
||||
ref: { current: null | (Animatable.View & View) };
|
||||
|
||||
hideHandler: AutoHideHandler;
|
||||
|
||||
|
@ -75,15 +75,16 @@ export default class AnimatedFAB extends React.Component<PropsType> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
return (
|
||||
<Animatable.View
|
||||
ref={this.ref}
|
||||
useNativeDriver={true}
|
||||
style={{
|
||||
...styles.fab,
|
||||
bottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}>
|
||||
bottom: TAB_BAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<FAB icon={props.icon} onPress={props.onPress} />
|
||||
</Animatable.View>
|
||||
);
|
||||
|
|
177
src/components/Animations/PlanexBottomBar.tsx
Normal file
177
src/components/Animations/PlanexBottomBar.tsx
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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, { useState } from 'react';
|
||||
import { StyleSheet, View, Animated } from 'react-native';
|
||||
import { FAB, IconButton, Surface, useTheme } from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { useCollapsible } from '../../context/CollapsibleContext';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
onPress: (action: string, data?: string) => void;
|
||||
seekAttention: boolean;
|
||||
};
|
||||
|
||||
const DISPLAY_MODES = {
|
||||
DAY: 'agendaDay',
|
||||
WEEK: 'agendaWeek',
|
||||
MONTH: 'month',
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
left: '5%',
|
||||
width: '90%',
|
||||
},
|
||||
surface: {
|
||||
position: 'relative',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderRadius: 50,
|
||||
elevation: 2,
|
||||
},
|
||||
fabContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
top: '-25%',
|
||||
},
|
||||
side: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
icon: {
|
||||
marginLeft: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const DISPLAY_MODE_ICONS = {
|
||||
[DISPLAY_MODES.DAY]: 'calendar-text',
|
||||
[DISPLAY_MODES.WEEK]: 'calendar-week',
|
||||
[DISPLAY_MODES.MONTH]: 'calendar-range',
|
||||
};
|
||||
|
||||
function PlanexBottomBar(props: Props) {
|
||||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
const [currentMode, setCurrentMode] = useState(DISPLAY_MODES.WEEK);
|
||||
|
||||
const { collapsible } = useCollapsible();
|
||||
|
||||
const changeDisplayMode = () => {
|
||||
let newMode;
|
||||
switch (currentMode) {
|
||||
case DISPLAY_MODES.DAY:
|
||||
newMode = DISPLAY_MODES.WEEK;
|
||||
break;
|
||||
case DISPLAY_MODES.WEEK:
|
||||
newMode = DISPLAY_MODES.MONTH;
|
||||
break;
|
||||
case DISPLAY_MODES.MONTH:
|
||||
newMode = DISPLAY_MODES.DAY;
|
||||
break;
|
||||
default:
|
||||
newMode = DISPLAY_MODES.WEEK;
|
||||
break;
|
||||
}
|
||||
setCurrentMode(newMode);
|
||||
props.onPress('changeView', newMode);
|
||||
};
|
||||
|
||||
let translateY: number | Animated.AnimatedInterpolation = 0;
|
||||
let opacity: number | Animated.AnimatedInterpolation = 1;
|
||||
let scale: number | Animated.AnimatedInterpolation = 1;
|
||||
if (collapsible) {
|
||||
translateY = Animated.multiply(-3, collapsible.translateY);
|
||||
opacity = Animated.subtract(1, collapsible.progress);
|
||||
scale = Animated.add(
|
||||
0.5,
|
||||
Animated.multiply(0.5, Animated.subtract(1, collapsible.progress))
|
||||
);
|
||||
}
|
||||
|
||||
const buttonColor = theme.colors.primary;
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
...styles.container,
|
||||
bottom: 10 + TAB_BAR_HEIGHT,
|
||||
transform: [{ translateY: translateY }, { scale: scale }],
|
||||
opacity: opacity,
|
||||
}}
|
||||
>
|
||||
<Surface style={styles.surface}>
|
||||
<View style={styles.fabContainer}>
|
||||
<Animatable.View
|
||||
style={styles.fab}
|
||||
animation={props.seekAttention ? 'bounce' : undefined}
|
||||
easing={'ease-out'}
|
||||
iterationDelay={500}
|
||||
iterationCount={'infinite'}
|
||||
useNativeDriver={true}
|
||||
>
|
||||
<FAB
|
||||
icon={'account-clock'}
|
||||
onPress={() => navigation.navigate(MainRoutes.GroupSelect)}
|
||||
/>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
<View style={styles.side}>
|
||||
<IconButton
|
||||
icon={DISPLAY_MODE_ICONS[currentMode]}
|
||||
color={buttonColor}
|
||||
onPress={changeDisplayMode}
|
||||
/>
|
||||
<IconButton
|
||||
icon="clock-in"
|
||||
color={buttonColor}
|
||||
style={styles.icon}
|
||||
onPress={() => props.onPress('today')}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.side}>
|
||||
<IconButton
|
||||
icon="chevron-left"
|
||||
color={buttonColor}
|
||||
onPress={() => props.onPress('prev')}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
color={buttonColor}
|
||||
style={styles.icon}
|
||||
onPress={() => props.onPress('next')}
|
||||
/>
|
||||
</View>
|
||||
</Surface>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlanexBottomBar;
|
|
@ -17,44 +17,87 @@
|
|||
* 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';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useCollapsibleHeader } from 'react-navigation-collapsible';
|
||||
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { useCollapsible } from '../../context/CollapsibleContext';
|
||||
import { useFocusEffect } from '@react-navigation/core';
|
||||
|
||||
export interface CollapsibleComponentPropsType {
|
||||
export type CollapsibleComponentPropsType = {
|
||||
children?: React.ReactNode;
|
||||
hasTab?: boolean;
|
||||
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
}
|
||||
paddedProps?: (paddingTop: number) => Record<string, any>;
|
||||
headerColors?: string;
|
||||
};
|
||||
|
||||
interface PropsType extends CollapsibleComponentPropsType {
|
||||
type Props = CollapsibleComponentPropsType & {
|
||||
component: React.ComponentType<any>;
|
||||
}
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
main: {
|
||||
minHeight: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
function CollapsibleComponent(props: Props) {
|
||||
const { paddedProps, headerColors } = props;
|
||||
const Comp = props.component;
|
||||
const theme = useTheme();
|
||||
|
||||
const { setCollapsible } = useCollapsible();
|
||||
|
||||
const collapsible = useCollapsibleHeader({
|
||||
config: {
|
||||
collapsedColor: headerColors ? headerColors : theme.colors.surface,
|
||||
useNativeDriver: true,
|
||||
},
|
||||
navigationOptions: {
|
||||
headerStyle: {
|
||||
backgroundColor: headerColors ? headerColors : theme.colors.surface,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setCollapsible(collapsible);
|
||||
}, [collapsible, setCollapsible])
|
||||
);
|
||||
|
||||
const { containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener } =
|
||||
collapsible;
|
||||
|
||||
const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;
|
||||
|
||||
function CollapsibleComponent(props: PropsType) {
|
||||
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (props.onScroll) {
|
||||
props.onScroll(event);
|
||||
}
|
||||
};
|
||||
const Comp = props.component;
|
||||
const {
|
||||
containerPaddingTop,
|
||||
scrollIndicatorInsetTop,
|
||||
onScrollWithListener,
|
||||
} = useCollapsibleStack();
|
||||
|
||||
const pprops =
|
||||
paddedProps !== undefined ? paddedProps(containerPaddingTop) : undefined;
|
||||
|
||||
return (
|
||||
<Comp
|
||||
{...props}
|
||||
{...pprops}
|
||||
onScroll={onScrollWithListener(onScroll)}
|
||||
contentContainerStyle={{
|
||||
paddingTop: containerPaddingTop,
|
||||
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
|
||||
minHeight: '100%',
|
||||
paddingBottom: paddingBottom,
|
||||
...styles.main,
|
||||
}}
|
||||
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
|
||||
scrollIndicatorInsets={{ top: scrollIndicatorInsetTop }}
|
||||
>
|
||||
{props.children}
|
||||
</Comp>
|
||||
);
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated, FlatListProps} from 'react-native';
|
||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||
import { Animated, FlatListProps } from 'react-native';
|
||||
import type { CollapsibleComponentPropsType } from './CollapsibleComponent';
|
||||
import CollapsibleComponent from './CollapsibleComponent';
|
||||
|
||||
type Props<T> = FlatListProps<T> & CollapsibleComponentPropsType;
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated, ScrollViewProps} from 'react-native';
|
||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||
import { Animated, ScrollViewProps } from 'react-native';
|
||||
import type { CollapsibleComponentPropsType } from './CollapsibleComponent';
|
||||
import CollapsibleComponent from './CollapsibleComponent';
|
||||
|
||||
type Props = ScrollViewProps & CollapsibleComponentPropsType;
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated, SectionListProps} from 'react-native';
|
||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||
import { Animated, SectionListProps } from 'react-native';
|
||||
import type { CollapsibleComponentPropsType } from './CollapsibleComponent';
|
||||
import CollapsibleComponent from './CollapsibleComponent';
|
||||
|
||||
type Props<T> = SectionListProps<T> & CollapsibleComponentPropsType;
|
||||
|
|
|
@ -18,20 +18,26 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
||||
import { Button, Dialog, Paragraph, Portal } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { ViewStyle } from 'react-native';
|
||||
|
||||
type PropsType = {
|
||||
visible: boolean;
|
||||
onDismiss: () => void;
|
||||
title: string | React.ReactNode;
|
||||
message: string | React.ReactNode;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
function AlertDialog(props: PropsType) {
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
|
||||
<Dialog
|
||||
visible={props.visible}
|
||||
onDismiss={props.onDismiss}
|
||||
style={props.style}
|
||||
>
|
||||
<Dialog.Title>{props.title}</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Paragraph>{props.message}</Paragraph>
|
||||
|
|
|
@ -19,60 +19,27 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import i18n from 'i18n-js';
|
||||
import {ERROR_TYPE} from '../../utils/WebData';
|
||||
import AlertDialog from './AlertDialog';
|
||||
import {
|
||||
API_REQUEST_CODES,
|
||||
getErrorMessage,
|
||||
REQUEST_STATUS,
|
||||
} from '../../utils/Requests';
|
||||
|
||||
type PropsType = {
|
||||
visible: boolean;
|
||||
onDismiss: () => void;
|
||||
errorCode: number;
|
||||
status?: REQUEST_STATUS;
|
||||
code?: API_REQUEST_CODES;
|
||||
};
|
||||
|
||||
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}
|
||||
title={i18n.t('errors.title')}
|
||||
message={getErrorMessage(props).message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
Portal,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
type PropsType = {
|
||||
visible: boolean;
|
||||
|
@ -41,6 +42,12 @@ type StateType = {
|
|||
loading: boolean;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
marginRight: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default class LoadingConfirmDialog extends React.PureComponent<
|
||||
PropsType,
|
||||
StateType
|
||||
|
@ -70,8 +77,8 @@ export default class LoadingConfirmDialog extends React.PureComponent<
|
|||
* Set the dialog into loading state and closes it when operation finishes
|
||||
*/
|
||||
onClickAccept = () => {
|
||||
const {props} = this;
|
||||
this.setState({loading: true});
|
||||
const { props } = this;
|
||||
this.setState({ loading: true });
|
||||
if (props.onAccept != null) {
|
||||
props.onAccept().then(this.hideLoading);
|
||||
}
|
||||
|
@ -83,21 +90,21 @@ export default class LoadingConfirmDialog extends React.PureComponent<
|
|||
*/
|
||||
hideLoading = (): NodeJS.Timeout =>
|
||||
setTimeout(() => {
|
||||
this.setState({loading: false});
|
||||
this.setState({ loading: false });
|
||||
}, 200);
|
||||
|
||||
/**
|
||||
* Hide the dialog if it is not loading
|
||||
*/
|
||||
onDismiss = () => {
|
||||
const {state, props} = this;
|
||||
const { state, props } = this;
|
||||
if (!state.loading && props.onDismiss != null) {
|
||||
props.onDismiss();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {state, props} = this;
|
||||
const { state, props } = this;
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog visible={props.visible} onDismiss={this.onDismiss}>
|
||||
|
@ -113,7 +120,7 @@ export default class LoadingConfirmDialog extends React.PureComponent<
|
|||
</Dialog.Content>
|
||||
{state.loading ? null : (
|
||||
<Dialog.Actions>
|
||||
<Button onPress={this.onDismiss} style={{marginRight: 10}}>
|
||||
<Button onPress={this.onDismiss} style={styles.button}>
|
||||
{i18n.t('dialog.cancel')}
|
||||
</Button>
|
||||
<Button onPress={this.onClickAccept}>
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
||||
import {FlatList} from 'react-native';
|
||||
import { Button, Dialog, Paragraph, Portal } from 'react-native-paper';
|
||||
import { FlatList } from 'react-native';
|
||||
|
||||
export type OptionsDialogButtonType = {
|
||||
title: string;
|
||||
|
@ -36,7 +36,7 @@ type PropsType = {
|
|||
};
|
||||
|
||||
function OptionsDialog(props: PropsType) {
|
||||
const getButtonRender = ({item}: {item: OptionsDialogButtonType}) => {
|
||||
const getButtonRender = ({ item }: { item: OptionsDialogButtonType }) => {
|
||||
return (
|
||||
<Button onPress={item.onPress} icon={item.icon}>
|
||||
{item.title}
|
||||
|
|
|
@ -18,10 +18,20 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {List} from 'react-native-paper';
|
||||
import {View} from 'react-native';
|
||||
import { List } from 'react-native-paper';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
},
|
||||
});
|
||||
|
||||
function ActionsDashBoardItem() {
|
||||
const navigation = useNavigation();
|
||||
|
@ -44,13 +54,8 @@ function ActionsDashBoardItem() {
|
|||
icon="chevron-right"
|
||||
/>
|
||||
)}
|
||||
onPress={(): void => navigation.navigate('feedback')}
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}
|
||||
onPress={(): void => navigation.navigate(MainRoutes.Feedback)}
|
||||
style={styles.item}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -25,8 +25,9 @@ import {
|
|||
TouchableRipple,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
eventNumber: number;
|
||||
|
@ -45,6 +46,9 @@ const styles = StyleSheet.create({
|
|||
avatar: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
text: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -61,7 +65,7 @@ function EventDashBoardItem(props: PropsType) {
|
|||
if (isAvailable) {
|
||||
subtitle = (
|
||||
<Text>
|
||||
<Text style={{fontWeight: 'bold'}}>{props.eventNumber}</Text>
|
||||
<Text style={styles.text}>{props.eventNumber}</Text>
|
||||
<Text>
|
||||
{props.eventNumber > 1
|
||||
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
|
||||
|
@ -74,13 +78,13 @@ function EventDashBoardItem(props: PropsType) {
|
|||
}
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
|
||||
<TouchableRipple style={GENERAL_STYLES.flex} onPress={props.clickAction}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
|
||||
titleStyle={{color: textColor}}
|
||||
titleStyle={{ color: textColor }}
|
||||
subtitle={subtitle}
|
||||
subtitleStyle={{color: textColor}}
|
||||
subtitleStyle={{ color: textColor }}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon
|
||||
icon="calendar-range"
|
||||
|
|
|
@ -18,17 +18,19 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
|
||||
import {Image, View} from 'react-native';
|
||||
import { Button, Card, Text, TouchableRipple } from 'react-native-paper';
|
||||
import { Image, StyleSheet, View } from 'react-native';
|
||||
import Autolink from 'react-native-autolink';
|
||||
import i18n from 'i18n-js';
|
||||
import type {FeedItemType} from '../../screens/Home/HomeScreen';
|
||||
import type { FeedItemType } from '../../screens/Home/HomeScreen';
|
||||
import NewsSourcesConstants, {
|
||||
AvailablePages,
|
||||
} from '../../constants/NewsSourcesConstants';
|
||||
import type {NewsSourceType} from '../../constants/NewsSourcesConstants';
|
||||
import type { NewsSourceType } from '../../constants/NewsSourcesConstants';
|
||||
import ImageGalleryButton from '../Media/ImageGalleryButton';
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
item: FeedItemType;
|
||||
|
@ -46,19 +48,33 @@ function getFormattedDate(dateString: number): string {
|
|||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
},
|
||||
button: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
action: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display a feed item
|
||||
*/
|
||||
function FeedItem(props: PropsType) {
|
||||
const navigation = useNavigation();
|
||||
const onPress = () => {
|
||||
navigation.navigate('feed-information', {
|
||||
navigation.navigate(MainRoutes.FeedInformation, {
|
||||
data: item,
|
||||
date: getFormattedDate(props.item.time),
|
||||
});
|
||||
};
|
||||
|
||||
const {item, height} = props;
|
||||
const { item, height } = props;
|
||||
const image = item.image !== '' && item.image != null ? item.image : null;
|
||||
const pageSource: NewsSourceType =
|
||||
NewsSourcesConstants[item.page_id as AvailablePages];
|
||||
|
@ -76,46 +92,42 @@ function FeedItem(props: PropsType) {
|
|||
style={{
|
||||
margin: cardMargin,
|
||||
height: cardHeight,
|
||||
}}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={onPress}>
|
||||
}}
|
||||
>
|
||||
<TouchableRipple style={GENERAL_STYLES.flex} 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}}
|
||||
left={() => <Image source={pageSource.icon} style={styles.image} />}
|
||||
style={{ height: titleHeight }}
|
||||
/>
|
||||
{image != null ? (
|
||||
<ImageGalleryButton
|
||||
images={[{url: image}]}
|
||||
images={[{ url: image }]}
|
||||
style={{
|
||||
...styles.button,
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Card.Content>
|
||||
{item.message !== undefined ? (
|
||||
<Autolink<typeof Text>
|
||||
<Autolink
|
||||
text={item.message}
|
||||
hashtag="facebook"
|
||||
hashtag={'facebook'}
|
||||
component={Text}
|
||||
style={{height: textHeight}}
|
||||
style={{ height: textHeight }}
|
||||
truncate={32}
|
||||
email={true}
|
||||
url={true}
|
||||
phone={true}
|
||||
/>
|
||||
) : null}
|
||||
</Card.Content>
|
||||
<Card.Actions style={{height: actionsHeight}}>
|
||||
<Button onPress={onPress} icon="plus" style={{marginLeft: 'auto'}}>
|
||||
<Card.Actions style={{ height: actionsHeight }}>
|
||||
<Button onPress={onPress} icon="plus" style={styles.action}>
|
||||
{i18n.t('screens.home.dashboard.seeMore')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
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 { 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';
|
||||
import type { PlanningEventType } from '../../utils/Planning';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
event?: PlanningEventType | null;
|
||||
|
@ -52,19 +53,26 @@ const styles = StyleSheet.create({
|
|||
* Component used to display an event preview if an event is available
|
||||
*/
|
||||
function PreviewEventDashboardItem(props: PropsType) {
|
||||
const {event} = props;
|
||||
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} />
|
||||
<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}>
|
||||
<TouchableRipple
|
||||
style={GENERAL_STYLES.flex}
|
||||
onPress={props.clickAction}
|
||||
>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={event.title}
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Badge, TouchableRipple, useTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import { Badge, TouchableRipple, useTheme } from 'react-native-paper';
|
||||
import { Dimensions, Image, StyleSheet, View } from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -28,13 +28,32 @@ type PropsType = {
|
|||
badgeCount?: number;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
width: '80%',
|
||||
height: '80%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
badgeContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
},
|
||||
badge: {
|
||||
borderWidth: 2,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const { image } = props;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
|
@ -42,23 +61,18 @@ function SmallDashboardItem(props: PropsType) {
|
|||
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',
|
||||
}}
|
||||
source={typeof image === 'string' ? { uri: image } : image}
|
||||
style={styles.image}
|
||||
/>
|
||||
) : null}
|
||||
{props.badgeCount != null && props.badgeCount > 0 ? (
|
||||
|
@ -66,18 +80,16 @@ function SmallDashboardItem(props: PropsType) {
|
|||
animation="zoomIn"
|
||||
duration={300}
|
||||
useNativeDriver
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}>
|
||||
style={styles.badgeContainer}
|
||||
>
|
||||
<Badge
|
||||
visible={true}
|
||||
style={{
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderColor: theme.colors.background,
|
||||
borderWidth: 2,
|
||||
}}>
|
||||
...styles.badge,
|
||||
}}
|
||||
>
|
||||
{props.badgeCount}
|
||||
</Badge>
|
||||
</Animatable.View>
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
icon: string;
|
||||
|
@ -37,7 +38,7 @@ const styles = StyleSheet.create({
|
|||
|
||||
function IntroIcon(props: PropsType) {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<Animatable.View useNativeDriver style={styles.center} animation="fadeIn">
|
||||
<MaterialCommunityIcons name={props.icon} color="#fff" size={200} />
|
||||
</Animatable.View>
|
||||
|
|
|
@ -18,25 +18,23 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
...GENERAL_STYLES.center,
|
||||
width: '80%',
|
||||
},
|
||||
});
|
||||
|
||||
function MascotIntroEnd() {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<Mascot
|
||||
style={{
|
||||
...styles.center,
|
||||
width: '80%',
|
||||
}}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
animated
|
||||
|
|
|
@ -18,28 +18,40 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
mascot: {
|
||||
...GENERAL_STYLES.center,
|
||||
width: '80%',
|
||||
},
|
||||
text: {
|
||||
color: '#fff',
|
||||
textAlign: 'center',
|
||||
fontSize: 25,
|
||||
},
|
||||
container: {
|
||||
position: 'absolute',
|
||||
bottom: 30,
|
||||
right: '20%',
|
||||
width: 50,
|
||||
height: 50,
|
||||
},
|
||||
icon: {
|
||||
...GENERAL_STYLES.center,
|
||||
transform: [{ rotateZ: '70deg' }],
|
||||
},
|
||||
});
|
||||
|
||||
function MascotIntroWelcome() {
|
||||
return (
|
||||
<View style={{flex: 1}}>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<Mascot
|
||||
style={{
|
||||
...styles.center,
|
||||
width: '80%',
|
||||
}}
|
||||
style={styles.mascot}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
animated
|
||||
entryAnimation={{
|
||||
|
@ -51,11 +63,8 @@ function MascotIntroWelcome() {
|
|||
useNativeDriver
|
||||
animation="fadeInUp"
|
||||
duration={500}
|
||||
style={{
|
||||
color: '#fff',
|
||||
textAlign: 'center',
|
||||
fontSize: 25,
|
||||
}}>
|
||||
style={styles.text}
|
||||
>
|
||||
PABLO
|
||||
</Animatable.Text>
|
||||
<Animatable.View
|
||||
|
@ -63,18 +72,10 @@ function MascotIntroWelcome() {
|
|||
animation="fadeInUp"
|
||||
duration={500}
|
||||
delay={200}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 30,
|
||||
right: '20%',
|
||||
width: 50,
|
||||
height: 50,
|
||||
}}>
|
||||
style={styles.container}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
style={{
|
||||
...styles.center,
|
||||
transform: [{rotateZ: '70deg'}],
|
||||
}}
|
||||
style={styles.icon}
|
||||
name="undo"
|
||||
color="#fff"
|
||||
size={40}
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated, Dimensions, ViewStyle} from 'react-native';
|
||||
import { Animated, Dimensions, ViewStyle } from 'react-native';
|
||||
import ImageListItem from './ImageListItem';
|
||||
import CardListItem from './CardListItem';
|
||||
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||
import { ServiceItemType } from '../../../utils/Services';
|
||||
|
||||
type PropsType = {
|
||||
dataset: Array<ServiceItemType>;
|
||||
|
@ -45,8 +45,8 @@ export default class CardList extends React.Component<PropsType> {
|
|||
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}) => {
|
||||
const {props} = this;
|
||||
getRenderItem = ({ item }: { item: ServiceItemType }) => {
|
||||
const { props } = this;
|
||||
if (props.isHorizontal) {
|
||||
return (
|
||||
<ImageListItem
|
||||
|
@ -62,7 +62,7 @@ export default class CardList extends React.Component<PropsType> {
|
|||
keyExtractor = (item: ServiceItemType): string => item.key;
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
let containerStyle = {};
|
||||
if (props.isHorizontal) {
|
||||
containerStyle = {
|
||||
|
|
|
@ -18,29 +18,36 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
import { Caption, Card, Paragraph, TouchableRipple } from 'react-native-paper';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import { ServiceItemType } from '../../../utils/Services';
|
||||
|
||||
type PropsType = {
|
||||
item: ServiceItemType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: '40%',
|
||||
margin: 5,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
cover: {
|
||||
height: 80,
|
||||
},
|
||||
});
|
||||
|
||||
function CardListItem(props: PropsType) {
|
||||
const {item} = props;
|
||||
const { item } = props;
|
||||
const source =
|
||||
typeof item.image === 'number' ? item.image : {uri: item.image};
|
||||
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}>
|
||||
<Card style={styles.card}>
|
||||
<TouchableRipple style={GENERAL_STYLES.flex} onPress={item.onPress}>
|
||||
<View>
|
||||
<Card.Cover style={{height: 80}} source={source} />
|
||||
<Card.Cover style={styles.cover} source={source} />
|
||||
<Card.Content>
|
||||
<Paragraph>{item.title}</Paragraph>
|
||||
<Caption>{item.subtitle}</Caption>
|
||||
|
|
|
@ -18,46 +18,50 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Text, TouchableRipple} from 'react-native-paper';
|
||||
import {Image, View} from 'react-native';
|
||||
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||
import { Text, TouchableRipple } from 'react-native-paper';
|
||||
import { Image, StyleSheet, View } from 'react-native';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import { ServiceItemType } from '../../../utils/Services';
|
||||
|
||||
type PropsType = {
|
||||
item: ServiceItemType;
|
||||
width: number;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
ripple: {
|
||||
margin: 5,
|
||||
},
|
||||
text: {
|
||||
...GENERAL_STYLES.centerHorizontal,
|
||||
marginTop: 5,
|
||||
textAlign: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
function ImageListItem(props: PropsType) {
|
||||
const {item} = props;
|
||||
const { item } = props;
|
||||
const source =
|
||||
typeof item.image === 'number' ? item.image : {uri: item.image};
|
||||
typeof item.image === 'number' ? item.image : { uri: item.image };
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={{
|
||||
width: props.width,
|
||||
height: props.width + 40,
|
||||
margin: 5,
|
||||
...styles.ripple,
|
||||
}}
|
||||
onPress={item.onPress}>
|
||||
onPress={item.onPress}
|
||||
>
|
||||
<View>
|
||||
<Image
|
||||
style={{
|
||||
width: props.width - 20,
|
||||
height: props.width - 20,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
...GENERAL_STYLES.centerHorizontal,
|
||||
}}
|
||||
source={source}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 5,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text style={styles.text}>{item.title}</Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Card, Chip, List, Text} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import { Card, Chip, List, Text } from 'react-native-paper';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||
import {isItemInCategoryFilter} from '../../../utils/Search';
|
||||
import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||
import { isItemInCategoryFilter } from '../../../utils/Search';
|
||||
import type { ClubCategoryType } from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
categories: Array<ClubCategoryType>;
|
||||
|
@ -39,8 +40,7 @@ const styles = StyleSheet.create({
|
|||
paddingLeft: 0,
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
...GENERAL_STYLES.centerHorizontal,
|
||||
},
|
||||
chipContainer: {
|
||||
justifyContent: 'space-around',
|
||||
|
@ -49,6 +49,11 @@ const styles = StyleSheet.create({
|
|||
paddingLeft: 0,
|
||||
marginBottom: 5,
|
||||
},
|
||||
chip: {
|
||||
marginRight: 5,
|
||||
marginLeft: 5,
|
||||
marginBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
function ClubListHeader(props: PropsType) {
|
||||
|
@ -62,8 +67,9 @@ function ClubListHeader(props: PropsType) {
|
|||
])}
|
||||
mode="outlined"
|
||||
onPress={onPress}
|
||||
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
|
||||
key={key}>
|
||||
style={styles.chip}
|
||||
key={key}
|
||||
>
|
||||
{category.name}
|
||||
</Chip>
|
||||
);
|
||||
|
@ -88,12 +94,16 @@ function ClubListHeader(props: PropsType) {
|
|||
icon="star"
|
||||
/>
|
||||
)}
|
||||
opened>
|
||||
<Text style={styles.text}>
|
||||
{i18n.t('screens.clubs.categoriesFilterMessage')}
|
||||
</Text>
|
||||
<View style={styles.chipContainer}>{getCategoriesRender()}</View>
|
||||
</AnimatedAccordion>
|
||||
opened={true}
|
||||
renderItem={() => (
|
||||
<View>
|
||||
<Text style={styles.text}>
|
||||
{i18n.t('screens.clubs.categoriesFilterMessage')}
|
||||
</Text>
|
||||
<View style={styles.chipContainer}>{getCategoriesRender()}</View>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,12 +18,13 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
|
||||
import {View} from 'react-native';
|
||||
import { Avatar, Chip, List, withTheme } from 'react-native-paper';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import type {
|
||||
ClubCategoryType,
|
||||
ClubType,
|
||||
} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
onPress: () => void;
|
||||
|
@ -33,6 +34,28 @@ type PropsType = {
|
|||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chip: {
|
||||
marginRight: 5,
|
||||
marginBottom: 5,
|
||||
},
|
||||
chipContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent',
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
},
|
||||
icon: {
|
||||
...GENERAL_STYLES.centerVertical,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
item: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
class ClubListItem extends React.Component<PropsType> {
|
||||
hasManagers: boolean;
|
||||
|
||||
|
@ -46,30 +69,28 @@ class ClubListItem extends React.Component<PropsType> {
|
|||
}
|
||||
|
||||
getCategoriesRender(categories: Array<number | null>) {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
const final: Array<React.ReactNode> = [];
|
||||
categories.forEach((cat: number | null) => {
|
||||
if (cat != null) {
|
||||
const category = props.categoryTranslator(cat);
|
||||
if (category) {
|
||||
final.push(
|
||||
<Chip
|
||||
style={{marginRight: 5, marginBottom: 5}}
|
||||
key={`${props.item.id}:${category.id}`}>
|
||||
<Chip style={styles.chip} key={`${props.item.id}:${category.id}`}>
|
||||
{category.name}
|
||||
</Chip>,
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return <View style={{flexDirection: 'row'}}>{final}</View>;
|
||||
return <View style={styles.chipContainer}>{final}</View>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
const categoriesRender = () =>
|
||||
this.getCategoriesRender(props.item.category);
|
||||
const {colors} = props.theme;
|
||||
const { colors } = props.theme;
|
||||
return (
|
||||
<List.Item
|
||||
title={props.item.name}
|
||||
|
@ -77,22 +98,14 @@ class ClubListItem extends React.Component<PropsType> {
|
|||
onPress={props.onPress}
|
||||
left={() => (
|
||||
<Avatar.Image
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}
|
||||
style={styles.avatar}
|
||||
size={64}
|
||||
source={{uri: props.item.logo}}
|
||||
source={{ uri: props.item.logo }}
|
||||
/>
|
||||
)}
|
||||
right={() => (
|
||||
<Avatar.Icon
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
style={styles.icon}
|
||||
size={48}
|
||||
icon={
|
||||
this.hasManagers ? 'check-circle-outline' : 'alert-circle-outline'
|
||||
|
@ -102,7 +115,7 @@ class ClubListItem extends React.Component<PropsType> {
|
|||
)}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
...styles.item,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -18,15 +18,12 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {useTheme} from 'react-native-paper';
|
||||
import {FlatList, Image, View} from 'react-native';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { FlatList, Image, StyleSheet, 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 { ServiceCategoryType, ServiceItemType } from '../../../utils/Services';
|
||||
|
||||
type PropsType = {
|
||||
item: ServiceCategoryType;
|
||||
|
@ -34,12 +31,19 @@ type PropsType = {
|
|||
onPress: (service: ServiceItemType) => void;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
});
|
||||
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
|
||||
function DashboardEditAccordion(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
|
||||
const getRenderItem = ({item}: {item: ServiceItemType}) => {
|
||||
const getRenderItem = ({ item }: { item: ServiceItemType }) => {
|
||||
return (
|
||||
<DashboardEditItem
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
|
@ -53,28 +57,22 @@ function DashboardEditAccordion(props: PropsType) {
|
|||
};
|
||||
|
||||
const getItemLayout = (
|
||||
data: Array<ServiceItemType> | null | undefined,
|
||||
index: number,
|
||||
): {length: number; offset: number; index: number} => ({
|
||||
_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;
|
||||
const { item } = props;
|
||||
return (
|
||||
<View>
|
||||
<AnimatedAccordion
|
||||
title={item.title}
|
||||
left={() =>
|
||||
typeof item.image === 'number' ? (
|
||||
<Image
|
||||
source={item.image}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
}}
|
||||
/>
|
||||
<Image source={item.image} style={styles.image} />
|
||||
) : (
|
||||
<MaterialCommunityIcons
|
||||
name={item.image}
|
||||
|
@ -82,17 +80,19 @@ function DashboardEditAccordion(props: PropsType) {
|
|||
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>
|
||||
}
|
||||
renderItem={() => (
|
||||
<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={true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Image} from 'react-native';
|
||||
import {List, useTheme} from 'react-native-paper';
|
||||
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||
import { Image, StyleSheet } from 'react-native';
|
||||
import { List, useTheme } from 'react-native-paper';
|
||||
import { ServiceItemType } from '../../../utils/Services';
|
||||
|
||||
type PropsType = {
|
||||
item: ServiceItemType;
|
||||
|
@ -29,9 +29,23 @@ type PropsType = {
|
|||
onPress: () => void;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
item: {
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 30,
|
||||
},
|
||||
});
|
||||
|
||||
function DashboardEditItem(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
const {item, onPress, height, isActive} = props;
|
||||
const { item, onPress, height, isActive } = props;
|
||||
const backgroundColor = isActive
|
||||
? theme.colors.proxiwashFinishedColor
|
||||
: 'transparent';
|
||||
return (
|
||||
<List.Item
|
||||
title={item.title}
|
||||
|
@ -40,12 +54,9 @@ function DashboardEditItem(props: PropsType) {
|
|||
left={() => (
|
||||
<Image
|
||||
source={
|
||||
typeof item.image === 'string' ? {uri: item.image} : item.image
|
||||
typeof item.image === 'string' ? { uri: item.image } : item.image
|
||||
}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
}}
|
||||
style={styles.image}
|
||||
/>
|
||||
)}
|
||||
right={(iconProps) =>
|
||||
|
@ -58,12 +69,9 @@ function DashboardEditItem(props: PropsType) {
|
|||
) : null
|
||||
}
|
||||
style={{
|
||||
height,
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 30,
|
||||
backgroundColor: isActive
|
||||
? theme.colors.proxiwashFinishedColor
|
||||
: 'transparent',
|
||||
...styles.image,
|
||||
height: height,
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {TouchableRipple, useTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import { TouchableRipple, useTheme } from 'react-native-paper';
|
||||
import { Dimensions, Image, StyleSheet, View } from 'react-native';
|
||||
|
||||
type PropsType = {
|
||||
image?: string | number;
|
||||
|
@ -27,39 +27,50 @@ type PropsType = {
|
|||
onPress: () => void;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
ripple: {
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
borderRadius: 5,
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to render a small dashboard item
|
||||
*/
|
||||
function DashboardEditPreviewItem(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
const itemSize = Dimensions.get('window').width / 8;
|
||||
|
||||
const backgroundColor = props.isActive
|
||||
? theme.colors.textDisabled
|
||||
: 'transparent';
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
borderless
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
backgroundColor: props.isActive
|
||||
? theme.colors.textDisabled
|
||||
: 'transparent',
|
||||
borderRadius: 5,
|
||||
}}>
|
||||
...styles.ripple,
|
||||
backgroundColor: backgroundColor,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: itemSize,
|
||||
height: itemSize,
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{props.image ? (
|
||||
<Image
|
||||
source={
|
||||
typeof props.image === 'string' ? {uri: props.image} : props.image
|
||||
typeof props.image === 'string'
|
||||
? { uri: props.image }
|
||||
: props.image
|
||||
}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
style={styles.image}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
|
|
|
@ -18,26 +18,38 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, List, useTheme} from 'react-native-paper';
|
||||
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 type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
|
||||
import {
|
||||
getFirstEquipmentAvailability,
|
||||
getRelativeDateString,
|
||||
isEquipmentAvailable,
|
||||
} from '../../../utils/EquipmentBooking';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
userDeviceRentDates: [string, string] | null;
|
||||
item: DeviceType;
|
||||
height: number;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
item: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
function EquipmentListItem(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
const {item, userDeviceRentDates, navigation, height} = props;
|
||||
const navigation = useNavigation();
|
||||
const { item, userDeviceRentDates, height } = props;
|
||||
const isRented = userDeviceRentDates != null;
|
||||
const isAvailable = isEquipmentAvailable(item);
|
||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||
|
@ -45,14 +57,14 @@ function EquipmentListItem(props: PropsType) {
|
|||
let onPress;
|
||||
if (isRented) {
|
||||
onPress = () => {
|
||||
navigation.navigate('equipment-confirm', {
|
||||
navigation.navigate(MainRoutes.EquipmentConfirm, {
|
||||
item,
|
||||
dates: userDeviceRentDates,
|
||||
});
|
||||
};
|
||||
} else {
|
||||
onPress = () => {
|
||||
navigation.navigate('equipment-rent', {item});
|
||||
navigation.navigate(MainRoutes.EquipmentRent, { item });
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -71,7 +83,7 @@ function EquipmentListItem(props: PropsType) {
|
|||
});
|
||||
}
|
||||
} else if (isAvailable) {
|
||||
description = i18n.t('screens.equipment.bail', {cost: item.caution});
|
||||
description = i18n.t('screens.equipment.bail', { cost: item.caution });
|
||||
} else {
|
||||
description = i18n.t('screens.equipment.available', {
|
||||
date: getRelativeDateString(firstAvailability),
|
||||
|
@ -101,21 +113,12 @@ function EquipmentListItem(props: PropsType) {
|
|||
title={item.name}
|
||||
description={description}
|
||||
onPress={onPress}
|
||||
left={() => (
|
||||
<Avatar.Icon
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
icon={icon}
|
||||
color={color}
|
||||
/>
|
||||
)}
|
||||
left={() => <Avatar.Icon style={styles.icon} icon={icon} color={color} />}
|
||||
right={() => (
|
||||
<Avatar.Icon
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
backgroundColor: 'transparent',
|
||||
...GENERAL_STYLES.centerVertical,
|
||||
...styles.icon,
|
||||
}}
|
||||
size={48}
|
||||
icon="chevron-right"
|
||||
|
@ -123,7 +126,7 @@ function EquipmentListItem(props: PropsType) {
|
|||
)}
|
||||
style={{
|
||||
height,
|
||||
justifyContent: 'center',
|
||||
...styles.item,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -18,15 +18,15 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import {FlatList, View} from 'react-native';
|
||||
import {stringMatchQuery} from '../../../utils/Search';
|
||||
import { List, useTheme } from 'react-native-paper';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import GroupListItem from './GroupListItem';
|
||||
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||
import type {
|
||||
PlanexGroupType,
|
||||
PlanexGroupCategoryType,
|
||||
} from '../../../screens/Planex/GroupSelectionScreen';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
type PropsType = {
|
||||
item: PlanexGroupCategoryType;
|
||||
|
@ -34,99 +34,97 @@ type PropsType = {
|
|||
onGroupPress: (data: PlanexGroupType) => void;
|
||||
onFavoritePress: (data: PlanexGroupType) => void;
|
||||
currentSearchString: string;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
const REPLACE_REGEX = /_/g;
|
||||
// The minimum number of characters to type before expanding the accordion
|
||||
// This prevents expanding too many items at once
|
||||
const MIN_SEARCH_SIZE_EXPAND = 2;
|
||||
|
||||
class GroupListAccordion extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.currentSearchString !== props.currentSearchString ||
|
||||
nextProps.favorites.length !== props.favorites.length ||
|
||||
nextProps.item.content.length !== props.item.content.length
|
||||
);
|
||||
}
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
getRenderItem = ({item}: {item: PlanexGroupType}) => {
|
||||
const {props} = this;
|
||||
const onPress = () => {
|
||||
props.onGroupPress(item);
|
||||
};
|
||||
const onStarPress = () => {
|
||||
props.onFavoritePress(item);
|
||||
};
|
||||
function GroupListAccordion(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
|
||||
const getRenderItem = ({ item }: { item: PlanexGroupType }) => {
|
||||
return (
|
||||
<GroupListItem
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
item={item}
|
||||
favorites={props.favorites}
|
||||
onPress={onPress}
|
||||
onStarPress={onStarPress}
|
||||
isFav={props.favorites.some((f) => f.id === item.id)}
|
||||
onPress={() => props.onGroupPress(item)}
|
||||
onStarPress={() => props.onFavoritePress(item)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getData(): Array<PlanexGroupType> {
|
||||
const {props} = this;
|
||||
const originalData = props.item.content;
|
||||
const displayData: Array<PlanexGroupType> = [];
|
||||
originalData.forEach((data: PlanexGroupType) => {
|
||||
if (stringMatchQuery(data.name, props.currentSearchString)) {
|
||||
displayData.push(data);
|
||||
}
|
||||
});
|
||||
return displayData;
|
||||
}
|
||||
|
||||
itemLayout = (
|
||||
data: Array<PlanexGroupType> | null | undefined,
|
||||
index: number,
|
||||
): {length: number; offset: number; index: number} => ({
|
||||
const itemLayout = (
|
||||
_data: Array<PlanexGroupType> | null | undefined,
|
||||
index: number
|
||||
): { length: number; offset: number; index: number } => ({
|
||||
length: LIST_ITEM_HEIGHT,
|
||||
offset: LIST_ITEM_HEIGHT * index,
|
||||
index,
|
||||
});
|
||||
|
||||
keyExtractor = (item: PlanexGroupType): string => item.id.toString();
|
||||
const keyExtractor = (item: PlanexGroupType): string => item.id.toString();
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const {item} = this.props;
|
||||
return (
|
||||
<View>
|
||||
<AnimatedAccordion
|
||||
title={item.name.replace(REPLACE_REGEX, ' ')}
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
left={(iconProps) =>
|
||||
item.id === 0 ? (
|
||||
<List.Icon
|
||||
style={iconProps.style}
|
||||
icon="star"
|
||||
color={props.theme.colors.tetrisScore}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
unmountWhenCollapsed={item.id !== 0} // Only render list if expanded for increased performance
|
||||
opened={props.currentSearchString.length > 0}>
|
||||
<FlatList
|
||||
data={this.getData()}
|
||||
extraData={props.currentSearchString + props.favorites.length}
|
||||
renderItem={this.getRenderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
listKey={item.id.toString()}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
getItemLayout={this.itemLayout}
|
||||
removeClippedSubviews
|
||||
var isFavorite = props.item.id === 0;
|
||||
var isEmptyFavorite = isFavorite && props.favorites.length === 0;
|
||||
|
||||
return (
|
||||
<AnimatedAccordion
|
||||
title={
|
||||
isEmptyFavorite
|
||||
? i18n.t('screens.planex.favorites.empty.title')
|
||||
: props.item.name.replace(REPLACE_REGEX, ' ')
|
||||
}
|
||||
subtitle={
|
||||
isEmptyFavorite
|
||||
? i18n.t('screens.planex.favorites.empty.subtitle')
|
||||
: undefined
|
||||
}
|
||||
style={styles.container}
|
||||
left={(iconProps) =>
|
||||
isFavorite ? (
|
||||
<List.Icon
|
||||
style={iconProps.style}
|
||||
icon={'star'}
|
||||
color={theme.colors.tetrisScore}
|
||||
/>
|
||||
</AnimatedAccordion>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
) : undefined
|
||||
}
|
||||
unmountWhenCollapsed={!isFavorite} // Only render list if expanded for increased performance
|
||||
opened={
|
||||
props.currentSearchString.length >= MIN_SEARCH_SIZE_EXPAND ||
|
||||
(isFavorite && !isEmptyFavorite)
|
||||
}
|
||||
enabled={!isEmptyFavorite}
|
||||
renderItem={() => (
|
||||
<FlatList
|
||||
data={props.item.content}
|
||||
extraData={props.currentSearchString + props.favorites.length}
|
||||
renderItem={getRenderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
listKey={props.item.id.toString()}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
getItemLayout={itemLayout}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(GroupListAccordion);
|
||||
const propsEqual = (pp: PropsType, np: PropsType) =>
|
||||
pp.currentSearchString === np.currentSearchString &&
|
||||
pp.favorites.length === np.favorites.length &&
|
||||
pp.item.content.length === np.item.content.length &&
|
||||
pp.onFavoritePress === np.onFavoritePress;
|
||||
|
||||
export default React.memo(GroupListAccordion, propsEqual);
|
||||
|
|
|
@ -17,110 +17,82 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {List, TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import React, { useRef } from 'react';
|
||||
import { List, TouchableRipple, useTheme } from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
|
||||
import {View} from 'react-native';
|
||||
import type { PlanexGroupType } from '../../../screens/Planex/GroupSelectionScreen';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { getPrettierPlanexGroupName } from '../../../utils/Utils';
|
||||
|
||||
type PropsType = {
|
||||
theme: ReactNativePaper.Theme;
|
||||
type Props = {
|
||||
onPress: () => void;
|
||||
onStarPress: () => void;
|
||||
item: PlanexGroupType;
|
||||
favorites: Array<PlanexGroupType>;
|
||||
isFav: boolean;
|
||||
height: number;
|
||||
};
|
||||
|
||||
const REPLACE_REGEX = /_/g;
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
padding: 10,
|
||||
},
|
||||
iconContainer: {
|
||||
marginRight: 10,
|
||||
marginLeft: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
class GroupListItem extends React.Component<PropsType> {
|
||||
isFav: boolean;
|
||||
function GroupListItem(props: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
starRef: {current: null | (Animatable.View & View)};
|
||||
const starRef = useRef<Animatable.View & View>(null);
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.starRef = React.createRef();
|
||||
this.isFav = this.isGroupInFavorites(props.favorites);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {favorites} = this.props;
|
||||
const favChanged = favorites.length !== nextProps.favorites.length;
|
||||
let newFavState = this.isFav;
|
||||
if (favChanged) {
|
||||
newFavState = this.isGroupInFavorites(nextProps.favorites);
|
||||
}
|
||||
const shouldUpdate = this.isFav !== newFavState;
|
||||
this.isFav = newFavState;
|
||||
return shouldUpdate;
|
||||
}
|
||||
|
||||
onStarPress = () => {
|
||||
const {props} = this;
|
||||
const ref = this.starRef;
|
||||
if (ref.current && ref.current.rubberBand && ref.current.swing) {
|
||||
if (this.isFav) {
|
||||
ref.current.rubberBand();
|
||||
} else {
|
||||
ref.current.swing();
|
||||
}
|
||||
}
|
||||
props.onStarPress();
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const {colors} = props.theme;
|
||||
return (
|
||||
<List.Item
|
||||
title={props.item.name.replace(REPLACE_REGEX, ' ')}
|
||||
onPress={props.onPress}
|
||||
left={(iconProps) => (
|
||||
<List.Icon
|
||||
color={iconProps.color}
|
||||
style={iconProps.style}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
)}
|
||||
right={(iconProps) => (
|
||||
<Animatable.View ref={this.starRef} useNativeDriver>
|
||||
<TouchableRipple
|
||||
onPress={this.onStarPress}
|
||||
style={{
|
||||
marginRight: 10,
|
||||
marginLeft: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
size={30}
|
||||
style={{padding: 10}}
|
||||
name="star"
|
||||
color={this.isFav ? colors.tetrisScore : iconProps.color}
|
||||
/>
|
||||
</TouchableRipple>
|
||||
</Animatable.View>
|
||||
)}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<List.Item
|
||||
title={getPrettierPlanexGroupName(props.item.name)}
|
||||
onPress={props.onPress}
|
||||
left={(iconProps) => (
|
||||
<List.Icon
|
||||
color={iconProps.color}
|
||||
style={iconProps.style}
|
||||
icon={'chevron-right'}
|
||||
/>
|
||||
)}
|
||||
right={(iconProps) => (
|
||||
<Animatable.View
|
||||
ref={starRef}
|
||||
useNativeDriver={true}
|
||||
animation={props.isFav ? 'rubberBand' : undefined}
|
||||
>
|
||||
<TouchableRipple
|
||||
onPress={props.onStarPress}
|
||||
style={styles.iconContainer}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
size={30}
|
||||
style={styles.icon}
|
||||
name="star"
|
||||
color={props.isFav ? theme.colors.tetrisScore : iconProps.color}
|
||||
/>
|
||||
</TouchableRipple>
|
||||
</Animatable.View>
|
||||
)}
|
||||
style={{
|
||||
height: props.height,
|
||||
...styles.item,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(GroupListItem);
|
||||
export default React.memo(
|
||||
GroupListItem,
|
||||
(pp: Props, np: Props) =>
|
||||
pp.isFav === np.isFav && pp.onStarPress === np.onStarPress
|
||||
);
|
||||
|
|
|
@ -18,9 +18,12 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, List, Text} from 'react-native-paper';
|
||||
import { Avatar, List, Text } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import type {ProximoArticleType} from '../../../screens/Services/Proximo/ProximoMainScreen';
|
||||
import type { ProximoArticleType } from '../../../screens/Services/Proximo/ProximoMainScreen';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Urls from '../../../constants/Urls';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
onPress: () => void;
|
||||
|
@ -29,28 +32,45 @@ type PropsType = {
|
|||
height: number;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
avatar: {
|
||||
backgroundColor: 'transparent',
|
||||
marginRight: 5,
|
||||
},
|
||||
text: {
|
||||
marginLeft: 10,
|
||||
fontWeight: 'bold',
|
||||
fontSize: 15,
|
||||
...GENERAL_STYLES.centerVertical,
|
||||
},
|
||||
item: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
function ProximoListItem(props: PropsType) {
|
||||
return (
|
||||
<List.Item
|
||||
title={props.item.name}
|
||||
titleNumberOfLines={2}
|
||||
description={`${props.item.quantity} ${i18n.t(
|
||||
'screens.proximo.inStock',
|
||||
'screens.proximo.inStock'
|
||||
)}`}
|
||||
descriptionStyle={{color: props.color}}
|
||||
descriptionStyle={{ color: props.color }}
|
||||
onPress={props.onPress}
|
||||
left={() => (
|
||||
<Avatar.Image
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
style={styles.avatar}
|
||||
size={64}
|
||||
source={{uri: props.item.image}}
|
||||
source={{ uri: Urls.proximo.images + props.item.image }}
|
||||
/>
|
||||
)}
|
||||
right={() => (
|
||||
<Text style={{fontWeight: 'bold'}}>{props.item.price}€</Text>
|
||||
<Text style={styles.text}>{props.item.price.toFixed(2)}€</Text>
|
||||
)}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
...styles.item,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
129
src/components/Lists/Proxiwash/ProxiwashListHeader.tsx
Normal file
129
src/components/Lists/Proxiwash/ProxiwashListHeader.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
import React from 'react';
|
||||
import { Linking, StyleSheet } from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Paragraph,
|
||||
Text,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import TimeAgo from 'react-native-timeago';
|
||||
import i18n from 'i18n-js';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
|
||||
import { ProxiwashInfoType } from '../../../screens/Proxiwash/ProxiwashScreen';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
|
||||
let moment = require('moment'); //load moment module to set local language
|
||||
require('moment/locale/fr'); // import moment local language file during the application build
|
||||
moment.locale('fr');
|
||||
|
||||
type Props = {
|
||||
date?: Date;
|
||||
selectedWash: 'tripodeB' | 'washinsa';
|
||||
info?: ProxiwashInfoType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginHorizontal: 5,
|
||||
},
|
||||
messageCard: {
|
||||
marginTop: 50,
|
||||
marginBottom: 10,
|
||||
},
|
||||
actions: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
function ProxiwashListHeader(props: Props) {
|
||||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
const { date, selectedWash } = props;
|
||||
let title = i18n.t('screens.proxiwash.washinsa.title');
|
||||
let icon = ProxiwashConstants.washinsa.icon;
|
||||
if (selectedWash === 'tripodeB') {
|
||||
title = i18n.t('screens.proxiwash.tripodeB.title');
|
||||
icon = ProxiwashConstants.tripodeB.icon;
|
||||
}
|
||||
const message = props.info?.message;
|
||||
return (
|
||||
<>
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={title}
|
||||
subtitle={
|
||||
date ? (
|
||||
<Text>
|
||||
{i18n.t('screens.proxiwash.updated')}
|
||||
<TimeAgo time={date} interval={2000} />
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon icon={icon} size={iconProps.size} />
|
||||
)}
|
||||
/>
|
||||
<Card.Actions style={styles.actions}>
|
||||
<Button
|
||||
mode={'contained'}
|
||||
onPress={() => navigation.navigate(MainRoutes.Settings)}
|
||||
icon={'swap-horizontal'}
|
||||
>
|
||||
{i18n.t('screens.proxiwash.switch')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
{message ? (
|
||||
<Card
|
||||
style={{
|
||||
...styles.card,
|
||||
...styles.messageCard,
|
||||
}}
|
||||
>
|
||||
<Animatable.View
|
||||
useNativeDriver={false}
|
||||
animation={'flash'}
|
||||
iterationCount={'infinite'}
|
||||
duration={2000}
|
||||
>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.errors.title')}
|
||||
titleStyle={{
|
||||
color: theme.colors.primary,
|
||||
}}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon icon={'alert'} size={iconProps.size} />
|
||||
)}
|
||||
/>
|
||||
</Animatable.View>
|
||||
<Card.Content>
|
||||
<Paragraph
|
||||
style={{
|
||||
color: theme.colors.warning,
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
<Card.Actions style={styles.actions}>
|
||||
<Button
|
||||
mode={'contained'}
|
||||
onPress={() =>
|
||||
Linking.openURL(ProxiwashConstants[selectedWash].webPageUrl)
|
||||
}
|
||||
icon={'open-in-new'}
|
||||
>
|
||||
{i18n.t('screens.proxiwash.errors.button')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProxiwashListHeader;
|
|
@ -27,14 +27,14 @@ import {
|
|||
Text,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import ProxiwashConstants, {
|
||||
MachineStates,
|
||||
} from '../../../constants/ProxiwashConstants';
|
||||
import AprilFoolsManager from '../../../managers/AprilFoolsManager';
|
||||
import type {ProxiwashMachineType} from '../../../screens/Proxiwash/ProxiwashScreen';
|
||||
import type { ProxiwashMachineType } from '../../../screens/Proxiwash/ProxiwashScreen';
|
||||
|
||||
type PropsType = {
|
||||
item: ProxiwashMachineType;
|
||||
|
@ -42,7 +42,7 @@ type PropsType = {
|
|||
onPress: (
|
||||
title: string,
|
||||
item: ProxiwashMachineType,
|
||||
isDryer: boolean,
|
||||
isDryer: boolean
|
||||
) => void;
|
||||
isWatched: boolean;
|
||||
isDryer: boolean;
|
||||
|
@ -56,6 +56,7 @@ const styles = StyleSheet.create({
|
|||
margin: 5,
|
||||
justifyContent: 'center',
|
||||
elevation: 1,
|
||||
borderRadius: 4,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
|
@ -65,17 +66,29 @@ const styles = StyleSheet.create({
|
|||
left: 0,
|
||||
borderRadius: 4,
|
||||
},
|
||||
item: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
text: {
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
textRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
textContainer: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display a proxiwash item, showing machine progression and state
|
||||
*/
|
||||
class ProxiwashListItem extends React.Component<PropsType> {
|
||||
stateStrings: {[key in MachineStates]: 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',
|
||||
'screens.proxiwash.states.runningNotStarted'
|
||||
),
|
||||
[MachineStates.FINISHED]: i18n.t('screens.proxiwash.states.finished'),
|
||||
[MachineStates.UNAVAILABLE]: i18n.t('screens.proxiwash.states.broken'),
|
||||
|
@ -83,7 +96,7 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
[MachineStates.UNKNOWN]: i18n.t('screens.proxiwash.states.unknown'),
|
||||
};
|
||||
|
||||
stateColors: {[key: string]: string};
|
||||
stateColors: { [key: string]: string };
|
||||
|
||||
title: string;
|
||||
|
||||
|
@ -97,7 +110,7 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
const displayMaxWeight = props.item.maxWeight;
|
||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) {
|
||||
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(
|
||||
parseInt(props.item.number, 10),
|
||||
parseInt(props.item.number, 10)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -109,7 +122,7 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
return (
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.item.state !== props.item.state ||
|
||||
|
@ -119,13 +132,13 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
}
|
||||
|
||||
onListItemPress = () => {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
props.onPress(this.titlePopUp, props.item, props.isDryer);
|
||||
};
|
||||
|
||||
updateStateColors() {
|
||||
const {props} = this;
|
||||
const {colors} = props.theme;
|
||||
const { props } = this;
|
||||
const { colors } = props.theme;
|
||||
this.stateColors[MachineStates.AVAILABLE] = colors.proxiwashReadyColor;
|
||||
this.stateColors[MachineStates.RUNNING] = colors.proxiwashRunningColor;
|
||||
this.stateColors[MachineStates.RUNNING_NOT_STARTED] =
|
||||
|
@ -137,8 +150,8 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const {colors} = props.theme;
|
||||
const { props } = this;
|
||||
const { colors } = props.theme;
|
||||
const machineState = props.item.state;
|
||||
const isRunning = machineState === MachineStates.RUNNING;
|
||||
const isReady = machineState === MachineStates.AVAILABLE;
|
||||
|
@ -184,8 +197,8 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
style={{
|
||||
...styles.container,
|
||||
height: props.height,
|
||||
borderRadius: 4,
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
{!isReady ? (
|
||||
<ProgressBar
|
||||
style={{
|
||||
|
@ -201,26 +214,27 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
description={description}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
...styles.item,
|
||||
}}
|
||||
onPress={this.onListItemPress}
|
||||
left={() => icon}
|
||||
right={() => (
|
||||
<View style={{flexDirection: 'row'}}>
|
||||
<View style={{justifyContent: 'center'}}>
|
||||
<View style={styles.textRow}>
|
||||
<View style={styles.textContainer}>
|
||||
<Text
|
||||
style={
|
||||
machineState === MachineStates.FINISHED
|
||||
? {fontWeight: 'bold'}
|
||||
: {}
|
||||
}>
|
||||
? styles.text
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{stateString}
|
||||
</Text>
|
||||
{machineState === MachineStates.RUNNING ? (
|
||||
<Caption>{props.item.remainingTime} min</Caption>
|
||||
) : null}
|
||||
</View>
|
||||
<View style={{justifyContent: 'center'}}>
|
||||
<View style={styles.textContainer}>
|
||||
<Avatar.Icon
|
||||
icon={stateIcon}
|
||||
color={colors.text}
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Text, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import { Avatar, Text, withTheme } from 'react-native-paper';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -44,6 +44,9 @@ const styles = StyleSheet.create({
|
|||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
textContainer: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -51,7 +54,7 @@ const styles = StyleSheet.create({
|
|||
*/
|
||||
class ProxiwashListItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
return (
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.nbAvailable !== props.nbAvailable
|
||||
|
@ -59,7 +62,7 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const { props } = this;
|
||||
const subtitle = `${props.nbAvailable} ${
|
||||
props.nbAvailable <= 1
|
||||
? i18n.t('screens.proxiwash.numAvailable')
|
||||
|
@ -76,9 +79,9 @@ class ProxiwashListItem extends React.Component<PropsType> {
|
|||
color={iconColor}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<View style={{justifyContent: 'center'}}>
|
||||
<View style={styles.textContainer}>
|
||||
<Text style={styles.text}>{props.title}</Text>
|
||||
<Text style={{color: props.theme.colors.subtitle}}>{subtitle}</Text>
|
||||
<Text style={{ color: props.theme.colors.subtitle }}>{subtitle}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -19,8 +19,14 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {Image, TouchableWithoutFeedback, View, ViewStyle} from 'react-native';
|
||||
import {AnimatableProperties} from 'react-native-animatable';
|
||||
import {
|
||||
Image,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { AnimatableProperties } from 'react-native-animatable';
|
||||
|
||||
export type AnimatableViewRefType = {
|
||||
current: null | (typeof Animatable.View & View);
|
||||
|
@ -77,6 +83,34 @@ export enum MASCOT_STYLE {
|
|||
RANDOM = 999,
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
aspectRatio: 1,
|
||||
},
|
||||
mascot: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
glassesImage: {
|
||||
position: 'absolute',
|
||||
top: '15%',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
eyesImage: {
|
||||
position: 'absolute',
|
||||
top: '15%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
eyesContainer: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
class Mascot extends React.Component<PropsType, StateType> {
|
||||
static defaultProps = {
|
||||
emotion: MASCOT_STYLE.NORMAL,
|
||||
|
@ -100,9 +134,9 @@ class Mascot extends React.Component<PropsType, StateType> {
|
|||
|
||||
viewRef: AnimatableViewRefType;
|
||||
|
||||
eyeList: {[key in EYE_STYLE]: number};
|
||||
eyeList: { [key in EYE_STYLE]: number };
|
||||
|
||||
glassesList: {[key in GLASSES_STYLE]: number};
|
||||
glassesList: { [key in GLASSES_STYLE]: number };
|
||||
|
||||
onPress: (viewRef: AnimatableViewRefType) => void;
|
||||
|
||||
|
@ -141,9 +175,9 @@ class Mascot extends React.Component<PropsType, StateType> {
|
|||
this.onPress = (viewRef: AnimatableViewRefType) => {
|
||||
const ref = viewRef.current;
|
||||
if (ref && ref.rubberBand) {
|
||||
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
|
||||
this.setState({ currentEmotion: MASCOT_STYLE.LOVE });
|
||||
ref.rubberBand(1500).then(() => {
|
||||
this.setState({currentEmotion: this.initialEmotion});
|
||||
this.setState({ currentEmotion: this.initialEmotion });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -155,9 +189,9 @@ class Mascot extends React.Component<PropsType, StateType> {
|
|||
this.onLongPress = (viewRef: AnimatableViewRefType) => {
|
||||
const ref = viewRef.current;
|
||||
if (ref && ref.tada) {
|
||||
this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
|
||||
this.setState({ currentEmotion: MASCOT_STYLE.ANGRY });
|
||||
ref.tada(1000).then(() => {
|
||||
this.setState({currentEmotion: this.initialEmotion});
|
||||
this.setState({ currentEmotion: this.initialEmotion });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -174,30 +208,22 @@ class Mascot extends React.Component<PropsType, StateType> {
|
|||
source={
|
||||
glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]
|
||||
}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '15%',
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
style={styles.glassesImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getEye(style: EYE_STYLE, isRight: boolean, rotation: string = '0deg') {
|
||||
const eye = this.eyeList[style];
|
||||
const left = isRight ? '-11%' : '11%';
|
||||
return (
|
||||
<Image
|
||||
key={isRight ? 'right' : 'left'}
|
||||
source={eye != null ? eye : this.eyeList[EYE_STYLE.NORMAL]}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '15%',
|
||||
left: isRight ? '-11%' : '11%',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transform: [{rotateY: rotation}],
|
||||
...styles.eyesImage,
|
||||
left: left,
|
||||
transform: [{ rotateY: rotation }],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -205,16 +231,7 @@ class Mascot extends React.Component<PropsType, StateType> {
|
|||
|
||||
getEyes(emotion: MASCOT_STYLE) {
|
||||
const final = [];
|
||||
final.push(
|
||||
<View
|
||||
key="container"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
final.push(<View key="container" style={styles.eyesContainer} />);
|
||||
if (emotion === MASCOT_STYLE.CUTE) {
|
||||
final.push(this.getEye(EYE_STYLE.CUTE, true));
|
||||
final.push(this.getEye(EYE_STYLE.CUTE, false));
|
||||
|
@ -249,32 +266,28 @@ class Mascot extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
const { props, state } = this;
|
||||
const entryAnimation = props.animated ? props.entryAnimation : null;
|
||||
const loopAnimation = props.animated ? props.loopAnimation : null;
|
||||
return (
|
||||
<Animatable.View
|
||||
style={{
|
||||
aspectRatio: 1,
|
||||
...styles.container,
|
||||
...props.style,
|
||||
}}
|
||||
{...entryAnimation}>
|
||||
{...entryAnimation}
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => {
|
||||
this.onPress(this.viewRef);
|
||||
}}
|
||||
onLongPress={() => {
|
||||
this.onLongPress(this.viewRef);
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
<Animatable.View ref={this.viewRef}>
|
||||
<Animatable.View {...loopAnimation}>
|
||||
<Image
|
||||
source={MASCOT_IMAGE}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
<Image source={MASCOT_IMAGE} style={styles.mascot} />
|
||||
{this.getEyes(state.currentEmotion)}
|
||||
</Animatable.View>
|
||||
</Animatable.View>
|
||||
|
|
|
@ -17,315 +17,175 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Paragraph,
|
||||
Portal,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Portal } from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {
|
||||
BackHandler,
|
||||
Dimensions,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Mascot from './Mascot';
|
||||
import SpeechArrow from './SpeechArrow';
|
||||
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import MascotSpeechBubble, {
|
||||
MascotSpeechBubbleProps,
|
||||
} from './MascotSpeechBubble';
|
||||
import { useMountEffect } from '../../utils/customHooks';
|
||||
import { useRoute } from '@react-navigation/core';
|
||||
import { useShouldShowMascot } from '../../context/preferencesContext';
|
||||
|
||||
type PropsType = {
|
||||
theme: ReactNativePaper.Theme;
|
||||
icon: string;
|
||||
title: string;
|
||||
message: string;
|
||||
buttons: {
|
||||
action?: {
|
||||
message: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
onPress?: () => void;
|
||||
};
|
||||
cancel?: {
|
||||
message: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
onPress?: () => void;
|
||||
};
|
||||
};
|
||||
type PropsType = MascotSpeechBubbleProps & {
|
||||
emotion: number;
|
||||
visible?: boolean;
|
||||
prefKey?: string;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
shouldRenderDialog: boolean; // Used to stop rendering after hide animation
|
||||
dialogVisible: boolean;
|
||||
};
|
||||
const styles = StyleSheet.create({
|
||||
background: {
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
container: {
|
||||
marginTop: -80,
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
const MASCOT_SIZE = Dimensions.get('window').height / 6;
|
||||
const BUBBLE_HEIGHT = Dimensions.get('window').height / 3;
|
||||
|
||||
/**
|
||||
* Component used to display a popup with the mascot.
|
||||
*/
|
||||
class MascotPopup extends React.Component<PropsType, StateType> {
|
||||
mascotSize: number;
|
||||
function MascotPopup(props: PropsType) {
|
||||
const route = useRoute();
|
||||
const { shouldShow, setShouldShow } = useShouldShowMascot(route.name);
|
||||
|
||||
windowWidth: number;
|
||||
|
||||
windowHeight: number;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
this.windowWidth = Dimensions.get('window').width;
|
||||
this.windowHeight = Dimensions.get('window').height;
|
||||
|
||||
this.mascotSize = Dimensions.get('window').height / 6;
|
||||
|
||||
if (props.visible != null) {
|
||||
this.state = {
|
||||
shouldRenderDialog: props.visible,
|
||||
dialogVisible: props.visible,
|
||||
};
|
||||
} else if (props.prefKey != null) {
|
||||
const visible = AsyncStorageManager.getBool(props.prefKey);
|
||||
this.state = {
|
||||
shouldRenderDialog: visible,
|
||||
dialogVisible: visible,
|
||||
};
|
||||
const isVisible = () => {
|
||||
if (props.visible !== undefined) {
|
||||
return props.visible;
|
||||
} else {
|
||||
this.state = {
|
||||
shouldRenderDialog: false,
|
||||
dialogVisible: false,
|
||||
};
|
||||
return shouldShow;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid,
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType, nextState: StateType): boolean {
|
||||
const {props, state} = this;
|
||||
if (nextProps.visible) {
|
||||
this.state.shouldRenderDialog = true;
|
||||
this.state.dialogVisible = true;
|
||||
} else if (
|
||||
nextProps.visible !== props.visible ||
|
||||
(!nextState.dialogVisible &&
|
||||
nextState.dialogVisible !== state.dialogVisible)
|
||||
) {
|
||||
this.state.dialogVisible = false;
|
||||
setTimeout(this.onAnimationEnd, 300);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onAnimationEnd = () => {
|
||||
this.setState({
|
||||
shouldRenderDialog: false,
|
||||
});
|
||||
};
|
||||
|
||||
onBackButtonPressAndroid = (): boolean => {
|
||||
const {state, props} = this;
|
||||
if (state.dialogVisible) {
|
||||
const {cancel} = props.buttons;
|
||||
const {action} = props.buttons;
|
||||
if (cancel) {
|
||||
this.onDismiss(cancel.onPress);
|
||||
} else if (action) {
|
||||
this.onDismiss(action.onPress);
|
||||
} else {
|
||||
this.onDismiss();
|
||||
}
|
||||
const [shouldRenderDialog, setShouldRenderDialog] = useState(isVisible());
|
||||
const [dialogVisible, setDialogVisible] = useState(isVisible());
|
||||
const lastVisibleProps = useRef(props.visible);
|
||||
const lastVisibleState = useRef(dialogVisible);
|
||||
|
||||
useMountEffect(() => {
|
||||
BackHandler.addEventListener('hardwareBackPress', onBackButtonPressAndroid);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (props.visible && !dialogVisible) {
|
||||
setShouldRenderDialog(true);
|
||||
setDialogVisible(true);
|
||||
} else if (
|
||||
lastVisibleProps.current !== props.visible ||
|
||||
(!dialogVisible && dialogVisible !== lastVisibleState.current)
|
||||
) {
|
||||
setDialogVisible(false);
|
||||
setTimeout(onAnimationEnd, 400);
|
||||
}
|
||||
lastVisibleProps.current = props.visible;
|
||||
lastVisibleState.current = dialogVisible;
|
||||
}, [props.visible, dialogVisible]);
|
||||
|
||||
const onAnimationEnd = () => {
|
||||
setShouldRenderDialog(false);
|
||||
};
|
||||
|
||||
const onBackButtonPressAndroid = (): boolean => {
|
||||
if (dialogVisible) {
|
||||
const { cancel } = props.buttons;
|
||||
const { action } = props.buttons;
|
||||
if (cancel) {
|
||||
onDismiss(cancel.onPress);
|
||||
} else if (action) {
|
||||
onDismiss(action.onPress);
|
||||
} else {
|
||||
onDismiss();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
getSpeechBubble() {
|
||||
const {state, props} = this;
|
||||
const getSpeechBubble = () => {
|
||||
return (
|
||||
<Animatable.View
|
||||
style={{
|
||||
marginLeft: '10%',
|
||||
marginRight: '10%',
|
||||
}}
|
||||
useNativeDriver
|
||||
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
|
||||
duration={state.dialogVisible ? 1000 : 300}>
|
||||
<SpeechArrow
|
||||
style={{marginLeft: this.mascotSize / 3}}
|
||||
size={20}
|
||||
color={props.theme.colors.mascotMessageArrow}
|
||||
/>
|
||||
<Card
|
||||
style={{
|
||||
borderColor: props.theme.colors.mascotMessageArrow,
|
||||
borderWidth: 4,
|
||||
borderRadius: 10,
|
||||
}}>
|
||||
<Card.Title
|
||||
title={props.title}
|
||||
left={
|
||||
props.icon != null
|
||||
? () => (
|
||||
<Avatar.Icon
|
||||
size={48}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
color={props.theme.colors.primary}
|
||||
icon={props.icon}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<Card.Content
|
||||
style={{
|
||||
maxHeight: this.windowHeight / 3,
|
||||
}}>
|
||||
<ScrollView>
|
||||
<Paragraph style={{marginBottom: 10}}>{props.message}</Paragraph>
|
||||
</ScrollView>
|
||||
</Card.Content>
|
||||
|
||||
<Card.Actions style={{marginTop: 10, marginBottom: 10}}>
|
||||
{this.getButtons()}
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
</Animatable.View>
|
||||
<MascotSpeechBubble
|
||||
title={props.title}
|
||||
message={props.message}
|
||||
icon={props.icon}
|
||||
buttons={props.buttons}
|
||||
visible={dialogVisible}
|
||||
onDismiss={onDismiss}
|
||||
speechArrowPos={MASCOT_SIZE / 3}
|
||||
bubbleMaxHeight={BUBBLE_HEIGHT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getMascot() {
|
||||
const {props, state} = this;
|
||||
const getMascot = () => {
|
||||
return (
|
||||
<Animatable.View
|
||||
useNativeDriver
|
||||
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
|
||||
duration={state.dialogVisible ? 1500 : 200}>
|
||||
animation={dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
|
||||
duration={dialogVisible ? 1500 : 200}
|
||||
>
|
||||
<Mascot
|
||||
style={{width: this.mascotSize}}
|
||||
style={{ width: MASCOT_SIZE }}
|
||||
animated
|
||||
emotion={props.emotion}
|
||||
/>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getButtons() {
|
||||
const {props} = this;
|
||||
const {action} = props.buttons;
|
||||
const {cancel} = props.buttons;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
{action != null ? (
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginBottom: 10,
|
||||
}}
|
||||
mode="contained"
|
||||
icon={action.icon}
|
||||
color={action.color}
|
||||
onPress={() => {
|
||||
this.onDismiss(action.onPress);
|
||||
}}>
|
||||
{action.message}
|
||||
</Button>
|
||||
) : null}
|
||||
{cancel != null ? (
|
||||
<Button
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
mode="contained"
|
||||
icon={cancel.icon}
|
||||
color={cancel.color}
|
||||
onPress={() => {
|
||||
this.onDismiss(cancel.onPress);
|
||||
}}>
|
||||
{cancel.message}
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
getBackground() {
|
||||
const {props, state} = this;
|
||||
const getBackground = () => {
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => {
|
||||
this.onDismiss(props.buttons.cancel?.onPress);
|
||||
}}>
|
||||
onDismiss(props.buttons.cancel?.onPress);
|
||||
}}
|
||||
>
|
||||
<Animatable.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
style={styles.background}
|
||||
useNativeDriver
|
||||
animation={state.dialogVisible ? 'fadeIn' : 'fadeOut'}
|
||||
duration={state.dialogVisible ? 300 : 300}
|
||||
animation={dialogVisible ? 'fadeIn' : 'fadeOut'}
|
||||
duration={dialogVisible ? 300 : 300}
|
||||
/>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onDismiss = (callback?: () => void) => {
|
||||
const {prefKey} = this.props;
|
||||
if (prefKey != null) {
|
||||
AsyncStorageManager.set(prefKey, false);
|
||||
this.setState({dialogVisible: false});
|
||||
}
|
||||
if (callback != null) {
|
||||
const onDismiss = (callback?: () => void) => {
|
||||
setShouldShow(false);
|
||||
setDialogVisible(false);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {shouldRenderDialog} = this.state;
|
||||
if (shouldRenderDialog) {
|
||||
return (
|
||||
<Portal>
|
||||
{this.getBackground()}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
marginTop: -80,
|
||||
width: '100%',
|
||||
}}>
|
||||
{this.getMascot()}
|
||||
{this.getSpeechBubble()}
|
||||
</View>
|
||||
if (shouldRenderDialog) {
|
||||
return (
|
||||
<Portal>
|
||||
{getBackground()}
|
||||
<View style={GENERAL_STYLES.centerVertical}>
|
||||
<View style={styles.container}>
|
||||
{getMascot()}
|
||||
{getSpeechBubble()}
|
||||
</View>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
</View>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default withTheme(MascotPopup);
|
||||
export default MascotPopup;
|
||||
|
|
147
src/components/Mascot/MascotSpeechBubble.tsx
Normal file
147
src/components/Mascot/MascotSpeechBubble.tsx
Normal file
|
@ -0,0 +1,147 @@
|
|||
import React from 'react';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import { Avatar, Button, Card, Paragraph, useTheme } from 'react-native-paper';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import SpeechArrow from './SpeechArrow';
|
||||
|
||||
export type MascotSpeechBubbleProps = {
|
||||
icon: string;
|
||||
title: string;
|
||||
message: string;
|
||||
visible?: boolean;
|
||||
buttons: {
|
||||
action?: {
|
||||
message: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
onPress?: () => void;
|
||||
};
|
||||
cancel?: {
|
||||
message: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
onPress?: () => void;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type Props = MascotSpeechBubbleProps & {
|
||||
onDismiss: (callback?: () => void) => void;
|
||||
speechArrowPos: number;
|
||||
bubbleMaxHeight: number;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
speechBubbleContainer: {
|
||||
marginLeft: '10%',
|
||||
marginRight: '10%',
|
||||
},
|
||||
speechBubbleCard: {
|
||||
borderWidth: 4,
|
||||
borderRadius: 10,
|
||||
},
|
||||
speechBubbleIcon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
speechBubbleText: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
actionsContainer: {
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
},
|
||||
button: {
|
||||
...GENERAL_STYLES.centerHorizontal,
|
||||
marginBottom: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default function MascotSpeechBubble(props: Props) {
|
||||
const theme = useTheme();
|
||||
const getButtons = () => {
|
||||
const { action, cancel } = props.buttons;
|
||||
return (
|
||||
<View style={GENERAL_STYLES.center}>
|
||||
{action ? (
|
||||
<Button
|
||||
style={styles.button}
|
||||
mode="contained"
|
||||
icon={action.icon}
|
||||
color={action.color}
|
||||
onPress={() => {
|
||||
props.onDismiss(action.onPress);
|
||||
}}
|
||||
>
|
||||
{action.message}
|
||||
</Button>
|
||||
) : null}
|
||||
{cancel != null ? (
|
||||
<Button
|
||||
style={styles.button}
|
||||
mode="contained"
|
||||
icon={cancel.icon}
|
||||
color={cancel.color}
|
||||
onPress={() => {
|
||||
props.onDismiss(cancel.onPress);
|
||||
}}
|
||||
>
|
||||
{cancel.message}
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Animatable.View
|
||||
style={styles.speechBubbleContainer}
|
||||
useNativeDriver={true}
|
||||
animation={props.visible ? 'bounceInLeft' : 'bounceOutLeft'}
|
||||
duration={props.visible ? 1000 : 300}
|
||||
>
|
||||
<SpeechArrow
|
||||
style={{ marginLeft: props.speechArrowPos }}
|
||||
size={20}
|
||||
color={theme.colors.mascotMessageArrow}
|
||||
/>
|
||||
<Card
|
||||
style={{
|
||||
borderColor: theme.colors.mascotMessageArrow,
|
||||
...styles.speechBubbleCard,
|
||||
}}
|
||||
>
|
||||
<Card.Title
|
||||
title={props.title}
|
||||
left={
|
||||
props.icon
|
||||
? () => (
|
||||
<Avatar.Icon
|
||||
size={48}
|
||||
style={styles.speechBubbleIcon}
|
||||
color={theme.colors.primary}
|
||||
icon={props.icon}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<Card.Content
|
||||
style={{
|
||||
maxHeight: props.bubbleMaxHeight,
|
||||
}}
|
||||
>
|
||||
<ScrollView>
|
||||
<Paragraph style={styles.speechBubbleText}>
|
||||
{props.message}
|
||||
</Paragraph>
|
||||
</ScrollView>
|
||||
</Card.Content>
|
||||
|
||||
<Card.Actions style={styles.actionsContainer}>
|
||||
{getButtons()}
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {View, ViewStyle} from 'react-native';
|
||||
import { StyleSheet, View, ViewStyle } from 'react-native';
|
||||
|
||||
type PropsType = {
|
||||
style?: ViewStyle;
|
||||
|
@ -26,20 +26,26 @@ type PropsType = {
|
|||
color: string;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
arrow: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: 'transparent',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default function SpeechArrow(props: PropsType) {
|
||||
return (
|
||||
<View style={props.style}>
|
||||
<View
|
||||
style={{
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 0,
|
||||
...styles.arrow,
|
||||
borderRightWidth: props.size,
|
||||
borderBottomWidth: props.size,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: 'transparent',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
borderBottomColor: props.color,
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -18,32 +18,37 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {TouchableRipple} from 'react-native-paper';
|
||||
import {Image} from 'react-native-animatable';
|
||||
import {useNavigation} from '@react-navigation/native';
|
||||
import {ViewStyle} from 'react-native';
|
||||
import { TouchableRipple } from 'react-native-paper';
|
||||
import { Image } from 'react-native-animatable';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { StyleSheet, ViewStyle } from 'react-native';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
images: Array<{url: string}>;
|
||||
images: Array<{ url: string }>;
|
||||
style: ViewStyle;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
image: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
function ImageGalleryButton(props: PropsType) {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const onPress = () => {
|
||||
navigation.navigate('gallery', {images: props.images});
|
||||
navigation.navigate(MainRoutes.Gallery, { images: props.images });
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableRipple onPress={onPress} style={props.style}>
|
||||
<Image
|
||||
resizeMode="contain"
|
||||
source={{uri: props.images[0].url}}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
source={{ uri: props.images[0].url }}
|
||||
style={styles.image}
|
||||
/>
|
||||
</TouchableRipple>
|
||||
);
|
||||
|
|
|
@ -18,9 +18,10 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {useTheme} from 'react-native-paper';
|
||||
import {Agenda, AgendaProps} from 'react-native-calendars';
|
||||
import { View } from 'react-native';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { Agenda, AgendaProps } from 'react-native-calendars';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
onRef: (ref: Agenda<any>) => void;
|
||||
|
@ -67,7 +68,7 @@ function CustomAgenda(props: PropsType) {
|
|||
|
||||
// Completely recreate the component on theme change to force theme reload
|
||||
if (theme.dark) {
|
||||
return <View style={{flex: 1}}>{getAgenda()}</View>;
|
||||
return <View style={GENERAL_STYLES.flex}>{getAgenda()}</View>;
|
||||
}
|
||||
return getAgenda();
|
||||
}
|
||||
|
|
|
@ -18,9 +18,13 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Text} from 'react-native-paper';
|
||||
import HTML from 'react-native-render-html';
|
||||
import {GestureResponderEvent, Linking} from 'react-native';
|
||||
import { Text, useTheme } from 'react-native-paper';
|
||||
import HTML, {
|
||||
CustomRendererProps,
|
||||
TBlock,
|
||||
TText,
|
||||
} from 'react-native-render-html';
|
||||
import { Dimensions, GestureResponderEvent, Linking } from 'react-native';
|
||||
|
||||
type PropsType = {
|
||||
html: string;
|
||||
|
@ -30,37 +34,54 @@ type PropsType = {
|
|||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
function CustomHTML(props: PropsType) {
|
||||
const openWebLink = (event: GestureResponderEvent, link: string) => {
|
||||
const theme = useTheme();
|
||||
const openWebLink = (_event: GestureResponderEvent, link: string) => {
|
||||
Linking.openURL(link);
|
||||
};
|
||||
|
||||
const getBasicText = (
|
||||
htmlAttribs: any,
|
||||
children: any,
|
||||
convertedCSSStyles: any,
|
||||
passProps: any,
|
||||
) => {
|
||||
return <Text {...passProps}>{children}</Text>;
|
||||
// Why is this so complex?? I just want to replace the default Text element with the one
|
||||
// from react-native-paper
|
||||
// Might need to read the doc a bit more: https://meliorence.github.io/react-native-render-html/
|
||||
// For now this seems to work
|
||||
const getBasicText = (rendererProps: CustomRendererProps<TBlock>) => {
|
||||
let text: TText | undefined;
|
||||
if (rendererProps.tnode.children.length > 0) {
|
||||
const phrasing = rendererProps.tnode.children[0];
|
||||
if (phrasing.children.length > 0) {
|
||||
text = phrasing.children[0] as TText;
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
return <Text>{text.data}</Text>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
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>`}
|
||||
// Surround description with p to allow text styling if the description is not html
|
||||
source={{ html: `<p>${props.html}</p>` }}
|
||||
// Use Paper Text instead of React
|
||||
renderers={{
|
||||
p: getBasicText,
|
||||
li: getBasicText,
|
||||
}}
|
||||
listsPrefixesRenderers={{
|
||||
ul: getListBullet,
|
||||
// Sometimes we have images inside the text, just ignore them
|
||||
ignoredDomTags={['img']}
|
||||
// Ignore text color
|
||||
ignoredStyles={['color', 'backgroundColor']}
|
||||
contentWidth={Dimensions.get('window').width - 50}
|
||||
renderersProps={{
|
||||
a: {
|
||||
onPress: openWebLink,
|
||||
},
|
||||
ul: {
|
||||
markerTextStyle: {
|
||||
color: theme.colors.text,
|
||||
},
|
||||
},
|
||||
}}
|
||||
ignoredTags={['img']}
|
||||
ignoredStyles={['color', 'background-color']}
|
||||
onLinkPress={openWebLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
HeaderButtons,
|
||||
HeaderButtonsProps,
|
||||
} from 'react-navigation-header-buttons';
|
||||
import {useTheme} from 'react-native-paper';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
|
||||
const MaterialHeaderButton = (props: HeaderButtonProps) => {
|
||||
const theme = useTheme();
|
||||
|
@ -40,7 +40,7 @@ const MaterialHeaderButton = (props: HeaderButtonProps) => {
|
|||
};
|
||||
|
||||
const MaterialHeaderButtons = (
|
||||
props: HeaderButtonsProps & {children?: React.ReactNode},
|
||||
props: HeaderButtonsProps & { children?: React.ReactNode }
|
||||
) => {
|
||||
return (
|
||||
<HeaderButtons {...props} HeaderButtonComponent={MaterialHeaderButton} />
|
||||
|
@ -49,4 +49,4 @@ const MaterialHeaderButtons = (
|
|||
|
||||
export default MaterialHeaderButtons;
|
||||
|
||||
export {Item} from 'react-navigation-header-buttons';
|
||||
export { Item } from 'react-navigation-header-buttons';
|
||||
|
|
|
@ -30,13 +30,13 @@ import i18n from 'i18n-js';
|
|||
import AppIntroSlider from 'react-native-app-intro-slider';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {Card} from 'react-native-paper';
|
||||
import { Card } from 'react-native-paper';
|
||||
import Update from '../../constants/Update';
|
||||
import ThemeManager from '../../managers/ThemeManager';
|
||||
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
|
||||
import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot';
|
||||
import MascotIntroWelcome from '../Intro/MascotIntroWelcome';
|
||||
import IntroIcon from '../Intro/IconIntro';
|
||||
import MascotIntroEnd from '../Intro/MascotIntroEnd';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
|
||||
type PropsType = {
|
||||
onDone: () => void;
|
||||
|
@ -75,11 +75,42 @@ const styles = StyleSheet.create({
|
|||
textAlign: 'center',
|
||||
marginBottom: 16,
|
||||
},
|
||||
center: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginLeft: 'auto',
|
||||
mascot: {
|
||||
marginLeft: 30,
|
||||
marginBottom: 0,
|
||||
width: 100,
|
||||
marginTop: -30,
|
||||
},
|
||||
speechArrow: {
|
||||
marginLeft: 50,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 20,
|
||||
borderRightWidth: 0,
|
||||
borderBottomWidth: 20,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: 'transparent',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
borderBottomColor: 'rgba(0,0,0,0.60)',
|
||||
},
|
||||
card: {
|
||||
backgroundColor: 'rgba(0,0,0,0.38)',
|
||||
marginHorizontal: 20,
|
||||
borderColor: 'rgba(0,0,0,0.60)',
|
||||
borderWidth: 4,
|
||||
borderRadius: 10,
|
||||
elevation: 0,
|
||||
},
|
||||
nextButtonContainer: {
|
||||
borderRadius: 25,
|
||||
padding: 5,
|
||||
backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
},
|
||||
doneButtonContainer: {
|
||||
borderRadius: 25,
|
||||
padding: 5,
|
||||
backgroundColor: 'rgb(190,21,34)',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -90,7 +121,7 @@ export default class CustomIntroSlider extends React.Component<
|
|||
PropsType,
|
||||
StateType
|
||||
> {
|
||||
sliderRef: {current: null | AppIntroSlider};
|
||||
sliderRef: { current: null | AppIntroSlider };
|
||||
|
||||
introSlides: Array<IntroSlideType>;
|
||||
|
||||
|
@ -173,31 +204,27 @@ export default class CustomIntroSlider extends React.Component<
|
|||
getIntroRenderItem = (
|
||||
data:
|
||||
| (ListRenderItemInfo<IntroSlideType> & {
|
||||
dimensions: {width: number; height: number};
|
||||
dimensions: { width: number; height: number };
|
||||
})
|
||||
| ListRenderItemInfo<IntroSlideType>,
|
||||
| ListRenderItemInfo<IntroSlideType>
|
||||
) => {
|
||||
const item = data.item;
|
||||
const {state} = this;
|
||||
const { state } = this;
|
||||
const index = parseInt(item.key, 10);
|
||||
return (
|
||||
<LinearGradient
|
||||
style={[styles.mainContent]}
|
||||
colors={item.colors}
|
||||
start={{x: 0, y: 0.1}}
|
||||
end={{x: 0.1, y: 1}}>
|
||||
start={{ x: 0, y: 0.1 }}
|
||||
end={{ x: 0.1, y: 1 }}
|
||||
>
|
||||
{state.currentSlide === index ? (
|
||||
<View style={{height: '100%', flex: 1}}>
|
||||
<View style={{flex: 1}}>{item.view()}</View>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<View style={GENERAL_STYLES.flex}>{item.view()}</View>
|
||||
<Animatable.View useNativeDriver animation="fadeIn">
|
||||
{item.mascotStyle != null ? (
|
||||
<Mascot
|
||||
style={{
|
||||
marginLeft: 30,
|
||||
marginBottom: 0,
|
||||
width: 100,
|
||||
marginTop: -30,
|
||||
}}
|
||||
style={styles.mascot}
|
||||
emotion={item.mascotStyle}
|
||||
animated
|
||||
entryAnimation={{
|
||||
|
@ -211,43 +238,23 @@ export default class CustomIntroSlider extends React.Component<
|
|||
}}
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 50,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 20,
|
||||
borderRightWidth: 0,
|
||||
borderBottomWidth: 20,
|
||||
borderStyle: 'solid',
|
||||
backgroundColor: 'transparent',
|
||||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
borderBottomColor: 'rgba(0,0,0,0.60)',
|
||||
}}
|
||||
/>
|
||||
<Card
|
||||
style={{
|
||||
backgroundColor: 'rgba(0,0,0,0.38)',
|
||||
marginHorizontal: 20,
|
||||
borderColor: 'rgba(0,0,0,0.60)',
|
||||
borderWidth: 4,
|
||||
borderRadius: 10,
|
||||
elevation: 0,
|
||||
}}>
|
||||
<View style={styles.speechArrow} />
|
||||
<Card style={styles.card}>
|
||||
<Card.Content>
|
||||
<Animatable.Text
|
||||
useNativeDriver
|
||||
animation="fadeIn"
|
||||
delay={100}
|
||||
style={styles.title}>
|
||||
style={styles.title}
|
||||
>
|
||||
{item.title}
|
||||
</Animatable.Text>
|
||||
<Animatable.Text
|
||||
useNativeDriver
|
||||
animation="fadeIn"
|
||||
delay={200}
|
||||
style={styles.text}>
|
||||
style={styles.text}
|
||||
>
|
||||
{item.text}
|
||||
</Animatable.Text>
|
||||
</Card.Content>
|
||||
|
@ -267,12 +274,12 @@ export default class CustomIntroSlider extends React.Component<
|
|||
|
||||
onSlideChange = (index: number) => {
|
||||
CustomIntroSlider.setStatusBarColor(this.currentSlides[index].colors[0]);
|
||||
this.setState({currentSlide: index});
|
||||
this.setState({ currentSlide: index });
|
||||
};
|
||||
|
||||
onSkip = () => {
|
||||
CustomIntroSlider.setStatusBarColor(
|
||||
this.currentSlides[this.currentSlides.length - 1].colors[0],
|
||||
this.currentSlides[this.currentSlides.length - 1].colors[0]
|
||||
);
|
||||
if (this.sliderRef.current != null) {
|
||||
this.sliderRef.current.goToSlide(this.currentSlides.length - 1);
|
||||
|
@ -280,10 +287,7 @@ export default class CustomIntroSlider extends React.Component<
|
|||
};
|
||||
|
||||
onDone = () => {
|
||||
const {props} = this;
|
||||
CustomIntroSlider.setStatusBarColor(
|
||||
ThemeManager.getCurrentTheme().colors.surface,
|
||||
);
|
||||
const { props } = this;
|
||||
props.onDone();
|
||||
};
|
||||
|
||||
|
@ -292,11 +296,8 @@ export default class CustomIntroSlider extends React.Component<
|
|||
<Animatable.View
|
||||
useNativeDriver
|
||||
animation="fadeIn"
|
||||
style={{
|
||||
borderRadius: 25,
|
||||
padding: 5,
|
||||
backgroundColor: 'rgba(0,0,0,0.2)',
|
||||
}}>
|
||||
style={styles.nextButtonContainer}
|
||||
>
|
||||
<MaterialCommunityIcons name="arrow-right" color="#fff" size={40} />
|
||||
</Animatable.View>
|
||||
);
|
||||
|
@ -307,18 +308,15 @@ export default class CustomIntroSlider extends React.Component<
|
|||
<Animatable.View
|
||||
useNativeDriver
|
||||
animation="bounceIn"
|
||||
style={{
|
||||
borderRadius: 25,
|
||||
padding: 5,
|
||||
backgroundColor: 'rgb(190,21,34)',
|
||||
}}>
|
||||
style={styles.doneButtonContainer}
|
||||
>
|
||||
<MaterialCommunityIcons name="check" color="#fff" size={40} />
|
||||
</Animatable.View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
const { props, state } = this;
|
||||
this.currentSlides = this.introSlides;
|
||||
if (props.isUpdate) {
|
||||
this.currentSlides = this.updateSlides;
|
||||
|
|
|
@ -17,11 +17,15 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {useTheme} from 'react-native-paper';
|
||||
import {Modalize} from 'react-native-modalize';
|
||||
import {View} from 'react-native-animatable';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import React, { Ref } from 'react';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { Modalize } from 'react-native-modalize';
|
||||
import { View } from 'react-native-animatable';
|
||||
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
|
||||
type Props = {
|
||||
children?: React.ReactChild | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstraction layer for Modalize component, using custom configuration
|
||||
|
@ -29,27 +33,26 @@ import CustomTabBar from '../Tabbar/CustomTabBar';
|
|||
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
|
||||
* @return {*}
|
||||
*/
|
||||
function CustomModal(props: {
|
||||
onRef: (re: Modalize) => void;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
function CustomModal(props: Props, ref?: Ref<Modalize>) {
|
||||
const theme = useTheme();
|
||||
const {onRef, children} = props;
|
||||
const { children } = props;
|
||||
return (
|
||||
<Modalize
|
||||
ref={onRef}
|
||||
ref={ref}
|
||||
adjustToContentHeight
|
||||
handlePosition="inside"
|
||||
modalStyle={{backgroundColor: theme.colors.card}}
|
||||
handleStyle={{backgroundColor: theme.colors.primary}}>
|
||||
modalStyle={{ backgroundColor: theme.colors.card }}
|
||||
handleStyle={{ backgroundColor: theme.colors.primary }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}>
|
||||
paddingBottom: TAB_BAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Modalize>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomModal;
|
||||
export default React.forwardRef(CustomModal);
|
||||
|
|
|
@ -18,15 +18,28 @@
|
|||
*/
|
||||
|
||||
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';
|
||||
import { Text } from 'react-native-paper';
|
||||
import { View } from 'react-native-animatable';
|
||||
import Slider, { SliderProps } from '@react-native-community/slider';
|
||||
import { useState } from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
type PropsType = {
|
||||
valueSuffix?: string;
|
||||
} & SliderProps;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
text: {
|
||||
marginHorizontal: 10,
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Abstraction layer for Modalize component, using custom configuration
|
||||
*
|
||||
|
@ -44,15 +57,8 @@ function CustomSlider(props: PropsType) {
|
|||
};
|
||||
|
||||
return (
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
<Text
|
||||
style={{
|
||||
marginHorizontal: 10,
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
{currentValue}min
|
||||
</Text>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.text}>{currentValue}min</Text>
|
||||
<Slider {...props} ref={undefined} onValueChange={onValueChange} />
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -17,16 +17,24 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {ActivityIndicator, useTheme} from 'react-native-paper';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { ActivityIndicator, useTheme } from 'react-native-paper';
|
||||
|
||||
type Props = {
|
||||
isAbsolute?: boolean;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display a header button
|
||||
*
|
||||
|
@ -35,18 +43,16 @@ type Props = {
|
|||
*/
|
||||
export default function BasicLoadingScreen(props: Props) {
|
||||
const theme = useTheme();
|
||||
const {isAbsolute} = props;
|
||||
const { isAbsolute } = props;
|
||||
const position = isAbsolute ? 'absolute' : 'relative';
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.colors.background,
|
||||
position: isAbsolute ? 'absolute' : 'relative',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
position: position,
|
||||
...styles.container,
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator animating size="large" color={theme.colors.primary} />
|
||||
</View>
|
||||
);
|
||||
|
|
|
@ -18,28 +18,33 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Button, Subheading, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import { Button, Subheading, useTheme } from 'react-native-paper';
|
||||
import { StyleSheet, View, ViewStyle } from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
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 {
|
||||
API_REQUEST_CODES,
|
||||
getErrorMessage,
|
||||
REQUEST_STATUS,
|
||||
} from '../../utils/Requests';
|
||||
|
||||
type PropsType = {
|
||||
navigation?: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
route?: {name: string};
|
||||
onRefresh?: () => void;
|
||||
errorCode?: number;
|
||||
type Props = {
|
||||
status?: REQUEST_STATUS;
|
||||
code?: API_REQUEST_CODES;
|
||||
icon?: string;
|
||||
message?: string;
|
||||
showRetryButton?: boolean;
|
||||
loading?: boolean;
|
||||
button?: {
|
||||
text: string;
|
||||
icon: string;
|
||||
onPress: () => void;
|
||||
};
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
height: '100%',
|
||||
flex: 1,
|
||||
},
|
||||
inner: {
|
||||
marginTop: 'auto',
|
||||
|
@ -61,157 +66,52 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
class ErrorView extends React.PureComponent<PropsType> {
|
||||
static defaultProps = {
|
||||
onRefresh: () => {},
|
||||
errorCode: 0,
|
||||
icon: '',
|
||||
message: '',
|
||||
showRetryButton: true,
|
||||
};
|
||||
function ErrorView(props: Props) {
|
||||
const theme = useTheme();
|
||||
const fullMessage = getErrorMessage(props, props.message, props.icon);
|
||||
const { button } = props;
|
||||
|
||||
message: string;
|
||||
|
||||
icon: string;
|
||||
|
||||
showLoginButton: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.icon = '';
|
||||
this.showLoginButton = false;
|
||||
this.message = '';
|
||||
}
|
||||
|
||||
getRetryButton() {
|
||||
const {props} = this;
|
||||
return (
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="refresh"
|
||||
onPress={props.onRefresh}
|
||||
style={styles.button}>
|
||||
{i18n.t('general.retry')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
getLoginButton() {
|
||||
return (
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="login"
|
||||
onPress={this.goToLogin}
|
||||
style={styles.button}>
|
||||
{i18n.t('screens.login.title')}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
goToLogin = () => {
|
||||
const {props} = this;
|
||||
if (props.navigation) {
|
||||
props.navigation.navigate('login', {
|
||||
screen: 'login',
|
||||
params: {nextScreen: props.route ? props.route.name : undefined},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
generateMessage() {
|
||||
const {props} = this;
|
||||
this.showLoginButton = false;
|
||||
if (props.errorCode !== 0) {
|
||||
switch (props.errorCode) {
|
||||
case ERROR_TYPE.BAD_CREDENTIALS:
|
||||
this.message = i18n.t('errors.badCredentials');
|
||||
this.icon = 'account-alert-outline';
|
||||
break;
|
||||
case ERROR_TYPE.BAD_TOKEN:
|
||||
this.message = i18n.t('errors.badToken');
|
||||
this.icon = 'account-alert-outline';
|
||||
this.showLoginButton = true;
|
||||
break;
|
||||
case ERROR_TYPE.NO_CONSENT:
|
||||
this.message = i18n.t('errors.noConsent');
|
||||
this.icon = 'account-remove-outline';
|
||||
break;
|
||||
case ERROR_TYPE.TOKEN_SAVE:
|
||||
this.message = i18n.t('errors.tokenSave');
|
||||
this.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case ERROR_TYPE.BAD_INPUT:
|
||||
this.message = i18n.t('errors.badInput');
|
||||
this.icon = 'alert-circle-outline';
|
||||
break;
|
||||
case ERROR_TYPE.FORBIDDEN:
|
||||
this.message = i18n.t('errors.forbidden');
|
||||
this.icon = 'lock';
|
||||
break;
|
||||
case ERROR_TYPE.CONNECTION_ERROR:
|
||||
this.message = i18n.t('errors.connectionError');
|
||||
this.icon = 'access-point-network-off';
|
||||
break;
|
||||
case ERROR_TYPE.SERVER_ERROR:
|
||||
this.message = i18n.t('errors.serverError');
|
||||
this.icon = 'server-network-off';
|
||||
break;
|
||||
default:
|
||||
this.message = i18n.t('errors.unknown');
|
||||
this.icon = 'alert-circle-outline';
|
||||
break;
|
||||
}
|
||||
this.message += `\n\nCode ${
|
||||
props.errorCode != null ? props.errorCode : -1
|
||||
}`;
|
||||
} else {
|
||||
this.message = props.message != null ? props.message : '';
|
||||
this.icon = props.icon != null ? props.icon : '';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
this.generateMessage();
|
||||
let button;
|
||||
if (this.showLoginButton) {
|
||||
button = this.getLoginButton();
|
||||
} else if (props.showRetryButton) {
|
||||
button = this.getRetryButton();
|
||||
} else {
|
||||
button = null;
|
||||
}
|
||||
|
||||
return (
|
||||
return (
|
||||
<View style={{ ...styles.outer, ...props.style }}>
|
||||
<Animatable.View
|
||||
style={{
|
||||
...styles.outer,
|
||||
backgroundColor: props.theme.colors.background,
|
||||
backgroundColor: theme.colors.background,
|
||||
}}
|
||||
animation="zoomIn"
|
||||
duration={200}
|
||||
useNativeDriver>
|
||||
useNativeDriver
|
||||
>
|
||||
<View style={styles.inner}>
|
||||
<View style={styles.iconContainer}>
|
||||
<MaterialCommunityIcons
|
||||
// $FlowFixMe
|
||||
name={this.icon}
|
||||
name={fullMessage.icon}
|
||||
size={150}
|
||||
color={props.theme.colors.textDisabled}
|
||||
color={theme.colors.disabled}
|
||||
/>
|
||||
</View>
|
||||
<Subheading
|
||||
style={{
|
||||
...styles.subheading,
|
||||
color: props.theme.colors.textDisabled,
|
||||
}}>
|
||||
{this.message}
|
||||
color: theme.colors.disabled,
|
||||
}}
|
||||
>
|
||||
{fullMessage.message}
|
||||
</Subheading>
|
||||
{button}
|
||||
{button ? (
|
||||
<Button
|
||||
mode={'contained'}
|
||||
icon={button.icon}
|
||||
onPress={button.onPress}
|
||||
style={styles.button}
|
||||
>
|
||||
{button.text}
|
||||
</Button>
|
||||
) : null}
|
||||
</View>
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default withTheme(ErrorView);
|
||||
export default ErrorView;
|
||||
|
|
143
src/components/Screens/PlanexWebview.tsx
Normal file
143
src/components/Screens/PlanexWebview.tsx
Normal file
|
@ -0,0 +1,143 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
import DateManager from '../../managers/DateManager';
|
||||
import { PlanexGroupType } from '../../screens/Planex/GroupSelectionScreen';
|
||||
import ErrorView from './ErrorView';
|
||||
import WebViewScreen from './WebViewScreen';
|
||||
import i18n from 'i18n-js';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
|
||||
type Props = {
|
||||
currentGroup?: PlanexGroupType;
|
||||
injectJS: string;
|
||||
onMessage: (event: { nativeEvent: { data: string } }) => void;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
error: {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
// Watch for changes in the calendar and call the remove alpha function to prevent invisible events
|
||||
const OBSERVE_MUTATIONS_INJECTED =
|
||||
'function removeAlpha(node) {\n' +
|
||||
' let bg = node.css("background-color");\n' +
|
||||
' if (bg.match("^rgba")) {\n' +
|
||||
" let a = bg.slice(5).split(',');\n" +
|
||||
' // Fix for tooltips with broken background\n' +
|
||||
' if (parseInt(a[0]) === parseInt(a[1]) && parseInt(a[1]) === parseInt(a[2]) && parseInt(a[2]) === 0) {\n' +
|
||||
" a[0] = a[1] = a[2] = '255';\n" +
|
||||
' }\n' +
|
||||
" let newBg ='rgb(' + a[0] + ',' + a[1] + ',' + a[2] + ')';\n" +
|
||||
' node.css("background-color", newBg);\n' +
|
||||
' }\n' +
|
||||
'}\n' +
|
||||
'// Observe for planning DOM changes\n' +
|
||||
'let observer = new MutationObserver(function(mutations) {\n' +
|
||||
' for (let i = 0; i < mutations.length; i++) {\n' +
|
||||
" if (mutations[i]['addedNodes'].length > 0 &&\n" +
|
||||
' ($(mutations[i][\'addedNodes\'][0]).hasClass("fc-event") || $(mutations[i][\'addedNodes\'][0]).hasClass("tooltiptopicevent")))\n' +
|
||||
" removeAlpha($(mutations[i]['addedNodes'][0]))\n" +
|
||||
' }\n' +
|
||||
'});\n' +
|
||||
'// observer.observe(document.querySelector(".fc-body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' +
|
||||
'observer.observe(document.querySelector("body"), {attributes: false, childList: true, characterData: false, subtree:true});\n' +
|
||||
'// Run remove alpha a first time on whole planning. Useful when code injected after planning fully loaded.\n' +
|
||||
'$(".fc-event-container .fc-event").each(function(index) {\n' +
|
||||
' removeAlpha($(this));\n' +
|
||||
'});';
|
||||
|
||||
// Overrides default settings to send a message to the webview when clicking on an event
|
||||
const FULL_CALENDAR_SETTINGS = `
|
||||
let calendar = $('#calendar').fullCalendar('getCalendar');
|
||||
calendar.option({
|
||||
eventClick: function (data, event, view) {
|
||||
let message = {
|
||||
title: data.title,
|
||||
color: data.color,
|
||||
start: data.start._d,
|
||||
end: data.end._d,
|
||||
};
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify(message));
|
||||
}
|
||||
});`;
|
||||
|
||||
export const JS_LOADED_MESSAGE = '1';
|
||||
|
||||
const NOTIFY_JS_INJECTED = `
|
||||
function notifyJsInjected() {
|
||||
window.ReactNativeWebView.postMessage('${JS_LOADED_MESSAGE}');
|
||||
}
|
||||
`;
|
||||
|
||||
// Mobile friendly CSS
|
||||
const CUSTOM_CSS =
|
||||
'body>.container{padding-top:20px; padding-bottom: 50px}header,#entite,#groupe_visibility,#calendar .fc-left,#calendar .fc-right{display:none}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time{font-size:.5rem}#calendar .fc-month-view .fc-content-skeleton .fc-title{font-size:.6rem}#calendar .fc-month-view .fc-content-skeleton .fc-time{font-size:.7rem}.fc-axis{font-size:.8rem;width:15px!important}.fc-day-header{font-size:.8rem}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}';
|
||||
|
||||
// Dark mode CSS, to be used with the mobile friendly css
|
||||
const CUSTOM_CSS_DARK =
|
||||
'body{background-color:#121212}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#222}.fc-toolbar .fc-center>*,h2,table{color:#fff}.fc-event-container{color:#121212}.fc-event-container .fc-bg{opacity:0.2;background-color:#000}.fc-unthemed td.fc-today{background:#be1522; opacity:0.4}';
|
||||
|
||||
// Inject the custom css into the webpage
|
||||
const INJECT_STYLE = `$('head').append('<style>${CUSTOM_CSS}</style>');`;
|
||||
|
||||
// Inject the dark mode into the webpage, to call after the custom css inject above
|
||||
const INJECT_STYLE_DARK = `$('head').append('<style>${CUSTOM_CSS_DARK}</style>');`;
|
||||
|
||||
/**
|
||||
* Generates custom JavaScript to be injected into the webpage
|
||||
*
|
||||
* @param groupID The current group selected
|
||||
*/
|
||||
const generateInjectedJS = (
|
||||
group: PlanexGroupType | undefined,
|
||||
darkMode: boolean
|
||||
) => {
|
||||
let customInjectedJS = `$(document).ready(function() {
|
||||
${OBSERVE_MUTATIONS_INJECTED}
|
||||
${INJECT_STYLE}
|
||||
${FULL_CALENDAR_SETTINGS}
|
||||
${NOTIFY_JS_INJECTED}`;
|
||||
if (group) {
|
||||
customInjectedJS += `displayAde(${group.id});`;
|
||||
}
|
||||
if (DateManager.isWeekend(new Date())) {
|
||||
customInjectedJS += `calendar.next();`;
|
||||
}
|
||||
if (darkMode) {
|
||||
customInjectedJS += INJECT_STYLE_DARK;
|
||||
}
|
||||
customInjectedJS += `notifyJsInjected();});true;`; // Prevents crash on ios
|
||||
return customInjectedJS;
|
||||
};
|
||||
|
||||
function PlanexWebview(props: Props) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<WebViewScreen
|
||||
url={Urls.planex.planning}
|
||||
initialJS={generateInjectedJS(props.currentGroup, theme.dark)}
|
||||
injectJS={props.injectJS}
|
||||
onMessage={props.onMessage}
|
||||
showAdvancedControls={false}
|
||||
showControls={props.currentGroup !== undefined}
|
||||
incognito={true}
|
||||
/>
|
||||
{!props.currentGroup ? (
|
||||
<ErrorView
|
||||
icon={'account-clock'}
|
||||
message={i18n.t('screens.planex.noGroupSelected')}
|
||||
style={styles.error}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlanexWebview;
|
142
src/components/Screens/RequestScreen.tsx
Normal file
142
src/components/Screens/RequestScreen.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import ErrorView from './ErrorView';
|
||||
import { useRequestLogic } from '../../utils/customHooks';
|
||||
import {
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
useRoute,
|
||||
} from '@react-navigation/native';
|
||||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
import i18n from 'i18n-js';
|
||||
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
import { useLogout } from '../../utils/logout';
|
||||
|
||||
export type RequestScreenProps<T> = {
|
||||
request: () => Promise<T>;
|
||||
render: (
|
||||
data: T | undefined,
|
||||
loading: boolean,
|
||||
lastRefreshDate: Date | undefined,
|
||||
refreshData: (newRequest?: () => Promise<T>) => void,
|
||||
status: REQUEST_STATUS,
|
||||
code?: API_REQUEST_CODES
|
||||
) => React.ReactElement;
|
||||
cache?: T;
|
||||
onCacheUpdate?: (newCache: T) => void;
|
||||
onMajorError?: (status: number, code?: number) => void;
|
||||
showLoading?: boolean;
|
||||
showError?: boolean;
|
||||
refreshOnFocus?: boolean;
|
||||
autoRefreshTime?: number;
|
||||
refresh?: boolean;
|
||||
onFinish?: () => void;
|
||||
};
|
||||
|
||||
export type RequestProps = {
|
||||
refreshData: () => void;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
type Props<T> = RequestScreenProps<T>;
|
||||
|
||||
const MIN_REFRESH_TIME = 3 * 1000;
|
||||
|
||||
export default function RequestScreen<T>(props: Props<T>) {
|
||||
const onLogout = useLogout();
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const route = useRoute();
|
||||
const refreshInterval = useRef<number>();
|
||||
const [loading, lastRefreshDate, status, code, data, refreshData] =
|
||||
useRequestLogic<T>(
|
||||
props.request,
|
||||
props.cache,
|
||||
props.onCacheUpdate,
|
||||
props.refreshOnFocus,
|
||||
MIN_REFRESH_TIME
|
||||
);
|
||||
// Store last refresh prop value
|
||||
const lastRefresh = useRef<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Refresh data if refresh prop changed and we are not loading
|
||||
if (props.refresh && !lastRefresh.current && !loading) {
|
||||
refreshData();
|
||||
// Call finish callback if refresh prop was set and we finished loading
|
||||
} else if (lastRefresh.current && !loading && props.onFinish) {
|
||||
props.onFinish();
|
||||
}
|
||||
// Update stored refresh prop value
|
||||
if (props.refresh !== lastRefresh.current) {
|
||||
lastRefresh.current = props.refresh === true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props, loading]);
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
if (!props.cache && props.refreshOnFocus !== false) {
|
||||
refreshData();
|
||||
}
|
||||
if (props.autoRefreshTime && props.autoRefreshTime > 0) {
|
||||
refreshInterval.current = setInterval(
|
||||
refreshData,
|
||||
props.autoRefreshTime
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
if (refreshInterval.current) {
|
||||
clearInterval(refreshInterval.current);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.cache, props.refreshOnFocus, props.autoRefreshTime])
|
||||
);
|
||||
|
||||
const isErrorCritical = (e: API_REQUEST_CODES | undefined) => {
|
||||
return e === API_REQUEST_CODES.BAD_TOKEN;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isErrorCritical(code)) {
|
||||
onLogout();
|
||||
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
|
||||
}
|
||||
}, [code, navigation, route, onLogout]);
|
||||
|
||||
if (data === undefined && loading && props.showLoading !== false) {
|
||||
return <BasicLoadingScreen />;
|
||||
} else if (
|
||||
data === undefined &&
|
||||
(status !== REQUEST_STATUS.SUCCESS ||
|
||||
(status === REQUEST_STATUS.SUCCESS && code !== undefined)) &&
|
||||
props.showError !== false
|
||||
) {
|
||||
return (
|
||||
<ErrorView
|
||||
status={status}
|
||||
code={code}
|
||||
loading={loading}
|
||||
button={
|
||||
isErrorCritical(code)
|
||||
? undefined
|
||||
: {
|
||||
icon: 'refresh',
|
||||
text: i18n.t('general.retry'),
|
||||
onPress: () => refreshData(),
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return props.render(
|
||||
data,
|
||||
loading,
|
||||
lastRefreshDate,
|
||||
refreshData,
|
||||
status,
|
||||
code
|
||||
);
|
||||
}
|
||||
}
|
|
@ -17,24 +17,19 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import i18n from 'i18n-js';
|
||||
import {Snackbar} from 'react-native-paper';
|
||||
import {
|
||||
NativeSyntheticEvent,
|
||||
RefreshControl,
|
||||
SectionListData,
|
||||
View,
|
||||
SectionListProps,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import {Collapsible} from 'react-navigation-collapsible';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import ErrorView from './ErrorView';
|
||||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import {ERROR_TYPE, readData} from '../../utils/WebData';
|
||||
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
|
||||
import RequestScreen, { RequestScreenProps } from './RequestScreen';
|
||||
import { CollapsibleComponentPropsType } from '../Collapsible/CollapsibleComponent';
|
||||
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
|
||||
|
||||
export type SectionListDataType<ItemT> = Array<{
|
||||
title: string;
|
||||
|
@ -43,169 +38,53 @@ export type SectionListDataType<ItemT> = Array<{
|
|||
keyExtractor?: (data: ItemT) => string;
|
||||
}>;
|
||||
|
||||
type PropsType<ItemT, RawData> = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
fetchUrl: string;
|
||||
autoRefreshTime: number;
|
||||
refreshOnFocus: boolean;
|
||||
renderItem: (data: {item: ItemT}) => React.ReactNode;
|
||||
createDataset: (
|
||||
data: RawData | null,
|
||||
isLoading?: boolean,
|
||||
) => SectionListDataType<ItemT>;
|
||||
onScroll: (event: NativeSyntheticEvent<EventTarget>) => void;
|
||||
collapsibleStack: Collapsible;
|
||||
type Props<ItemT, RawData> = Omit<
|
||||
CollapsibleComponentPropsType,
|
||||
'children' | 'paddedProps'
|
||||
> &
|
||||
Omit<
|
||||
RequestScreenProps<RawData>,
|
||||
'render' | 'showLoading' | 'showError' | 'onMajorError'
|
||||
> &
|
||||
Omit<
|
||||
SectionListProps<ItemT>,
|
||||
'sections' | 'getItemLayout' | 'ListHeaderComponent' | 'ListEmptyComponent'
|
||||
> & {
|
||||
createDataset: (
|
||||
data: RawData | undefined,
|
||||
loading: boolean,
|
||||
lastRefreshDate: Date | undefined,
|
||||
refreshData: (newRequest?: () => Promise<RawData>) => void,
|
||||
status: REQUEST_STATUS,
|
||||
code?: API_REQUEST_CODES
|
||||
) => SectionListDataType<ItemT>;
|
||||
renderListHeaderComponent?: (
|
||||
data: RawData | undefined,
|
||||
loading: boolean,
|
||||
lastRefreshDate: Date | undefined,
|
||||
refreshData: (newRequest?: () => Promise<RawData>) => void,
|
||||
status: REQUEST_STATUS,
|
||||
code?: API_REQUEST_CODES
|
||||
) => React.ComponentType<any> | React.ReactElement | null;
|
||||
itemHeight?: number | null;
|
||||
};
|
||||
|
||||
showError?: boolean;
|
||||
itemHeight?: number | null;
|
||||
updateData?: number;
|
||||
renderListHeaderComponent?: (
|
||||
data: RawData | null,
|
||||
) => React.ComponentType<any> | React.ReactElement | null;
|
||||
renderSectionHeader?: (
|
||||
data: {section: SectionListData<ItemT>},
|
||||
isLoading?: boolean,
|
||||
) => React.ReactElement | null;
|
||||
stickyHeader?: boolean;
|
||||
};
|
||||
|
||||
type StateType<RawData> = {
|
||||
refreshing: boolean;
|
||||
fetchedData: RawData | null;
|
||||
snackbarVisible: boolean;
|
||||
};
|
||||
|
||||
const MIN_REFRESH_TIME = 5 * 1000;
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
minHeight: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to render a SectionList with data fetched from the web
|
||||
*
|
||||
* 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<ItemT, RawData> extends React.PureComponent<
|
||||
PropsType<ItemT, RawData>,
|
||||
StateType<RawData>
|
||||
> {
|
||||
static defaultProps = {
|
||||
showError: true,
|
||||
itemHeight: null,
|
||||
updateData: 0,
|
||||
renderListHeaderComponent: () => null,
|
||||
renderSectionHeader: () => null,
|
||||
stickyHeader: false,
|
||||
};
|
||||
|
||||
refreshInterval: NodeJS.Timeout | undefined;
|
||||
|
||||
lastRefresh: Date | undefined;
|
||||
|
||||
constructor(props: PropsType<ItemT, RawData>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
refreshing: false,
|
||||
fetchedData: null,
|
||||
snackbarVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers react navigation events on first screen load.
|
||||
* Allows to detect when the screen is focused
|
||||
*/
|
||||
componentDidMount() {
|
||||
const {navigation} = this.props;
|
||||
navigation.addListener('focus', this.onScreenFocus);
|
||||
navigation.addListener('blur', this.onScreenBlur);
|
||||
this.lastRefresh = undefined;
|
||||
this.onRefresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes data when focusing the screen and setup a refresh interval if asked to
|
||||
*/
|
||||
onScreenFocus = () => {
|
||||
const {props} = this;
|
||||
if (props.refreshOnFocus && this.lastRefresh) {
|
||||
setTimeout(this.onRefresh, 200);
|
||||
}
|
||||
if (props.autoRefreshTime > 0) {
|
||||
this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes any interval on un-focus
|
||||
*/
|
||||
onScreenBlur = () => {
|
||||
if (this.refreshInterval) {
|
||||
clearInterval(this.refreshInterval);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when fetch is successful.
|
||||
* It will update the displayed data and stop the refresh animation
|
||||
*
|
||||
* @param fetchedData The newly fetched data
|
||||
*/
|
||||
onFetchSuccess = (fetchedData: RawData) => {
|
||||
this.setState({
|
||||
fetchedData,
|
||||
refreshing: false,
|
||||
});
|
||||
this.lastRefresh = new Date();
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when fetch encountered an error.
|
||||
* It will reset the displayed data and show an error.
|
||||
*/
|
||||
onFetchError = () => {
|
||||
this.setState({
|
||||
fetchedData: null,
|
||||
refreshing: false,
|
||||
});
|
||||
this.showSnackBar();
|
||||
};
|
||||
|
||||
/**
|
||||
* Refreshes data and shows an animations while doing it
|
||||
*/
|
||||
onRefresh = () => {
|
||||
const {fetchUrl} = this.props;
|
||||
let canRefresh;
|
||||
if (this.lastRefresh != null) {
|
||||
const last = this.lastRefresh;
|
||||
canRefresh = new Date().getTime() - last.getTime() > MIN_REFRESH_TIME;
|
||||
} else {
|
||||
canRefresh = true;
|
||||
}
|
||||
if (canRefresh) {
|
||||
this.setState({refreshing: true});
|
||||
readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the error popup
|
||||
*/
|
||||
showSnackBar = () => {
|
||||
this.setState({snackbarVisible: true});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the error popup
|
||||
*/
|
||||
hideSnackBar = () => {
|
||||
this.setState({snackbarVisible: false});
|
||||
};
|
||||
|
||||
getItemLayout = (
|
||||
function WebSectionList<ItemT, RawData>(props: Props<ItemT, RawData>) {
|
||||
const getItemLayout = (
|
||||
height: number,
|
||||
data: Array<SectionListData<ItemT>> | null,
|
||||
index: number,
|
||||
): {length: number; offset: number; index: number} => {
|
||||
_data: Array<SectionListData<ItemT>> | null,
|
||||
index: number
|
||||
): { length: number; offset: number; index: number } => {
|
||||
return {
|
||||
length: height,
|
||||
offset: height * index,
|
||||
|
@ -213,103 +92,88 @@ class WebSectionList<ItemT, RawData> extends React.PureComponent<
|
|||
};
|
||||
};
|
||||
|
||||
getRenderSectionHeader = (data: {section: SectionListData<ItemT>}) => {
|
||||
const {renderSectionHeader} = this.props;
|
||||
const {refreshing} = this.state;
|
||||
if (renderSectionHeader != null) {
|
||||
return (
|
||||
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
|
||||
{renderSectionHeader(data, refreshing)}
|
||||
</Animatable.View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
getRenderItem = (data: {item: ItemT}) => {
|
||||
const {renderItem} = this.props;
|
||||
return (
|
||||
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
|
||||
{renderItem(data)}
|
||||
</Animatable.View>
|
||||
const render = (
|
||||
data: RawData | undefined,
|
||||
loading: boolean,
|
||||
lastRefreshDate: Date | undefined,
|
||||
refreshData: (newRequest?: () => Promise<RawData>) => void,
|
||||
status: REQUEST_STATUS,
|
||||
code?: API_REQUEST_CODES
|
||||
) => {
|
||||
const { itemHeight } = props;
|
||||
const dataset = props.createDataset(
|
||||
data,
|
||||
loading,
|
||||
lastRefreshDate,
|
||||
refreshData,
|
||||
status,
|
||||
code
|
||||
);
|
||||
};
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<EventTarget>) => {
|
||||
const {onScroll} = this.props;
|
||||
if (onScroll != null) {
|
||||
onScroll(event);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
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 (
|
||||
<View>
|
||||
<CollapsibleSectionList
|
||||
sections={dataset}
|
||||
extraData={props.updateData}
|
||||
refreshControl={
|
||||
<CollapsibleSectionList
|
||||
{...props}
|
||||
sections={dataset}
|
||||
paddedProps={(paddingTop) => ({
|
||||
refreshControl: (
|
||||
<RefreshControl
|
||||
progressViewOffset={containerPaddingTop}
|
||||
refreshing={state.refreshing}
|
||||
onRefresh={this.onRefresh}
|
||||
progressViewOffset={paddingTop}
|
||||
refreshing={loading}
|
||||
onRefresh={refreshData}
|
||||
/>
|
||||
}
|
||||
renderSectionHeader={this.getRenderSectionHeader}
|
||||
renderItem={this.getRenderItem}
|
||||
stickySectionHeadersEnabled={props.stickyHeader}
|
||||
style={{minHeight: '100%'}}
|
||||
ListHeaderComponent={
|
||||
props.renderListHeaderComponent != null
|
||||
? props.renderListHeaderComponent(state.fetchedData)
|
||||
: null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
state.refreshing ? (
|
||||
<BasicLoadingScreen />
|
||||
) : (
|
||||
<ErrorView
|
||||
navigation={props.navigation}
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={this.onRefresh}
|
||||
/>
|
||||
)
|
||||
}
|
||||
getItemLayout={
|
||||
itemHeight
|
||||
? (data, index) => this.getItemLayout(itemHeight, data, index)
|
||||
: undefined
|
||||
}
|
||||
onScroll={this.onScroll}
|
||||
hasTab
|
||||
/>
|
||||
<Snackbar
|
||||
visible={state.snackbarVisible}
|
||||
onDismiss={this.hideSnackBar}
|
||||
action={{
|
||||
label: 'OK',
|
||||
onPress: () => {},
|
||||
}}
|
||||
duration={4000}
|
||||
style={{
|
||||
bottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}>
|
||||
{i18n.t('general.listUpdateFail')}
|
||||
</Snackbar>
|
||||
</View>
|
||||
),
|
||||
})}
|
||||
renderItem={props.renderItem}
|
||||
style={styles.container}
|
||||
ListHeaderComponent={
|
||||
props.renderListHeaderComponent != null
|
||||
? props.renderListHeaderComponent(
|
||||
data,
|
||||
loading,
|
||||
lastRefreshDate,
|
||||
refreshData,
|
||||
status,
|
||||
code
|
||||
)
|
||||
: null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
loading ? undefined : (
|
||||
<ErrorView
|
||||
status={status}
|
||||
code={code}
|
||||
button={
|
||||
code !== API_REQUEST_CODES.BAD_TOKEN
|
||||
? {
|
||||
icon: 'refresh',
|
||||
text: i18n.t('general.retry'),
|
||||
onPress: () => refreshData(),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
getItemLayout={
|
||||
itemHeight ? (d, i) => getItemLayout(itemHeight, d, i) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RequestScreen<RawData>
|
||||
request={props.request}
|
||||
render={render}
|
||||
showError={false}
|
||||
showLoading={false}
|
||||
autoRefreshTime={props.autoRefreshTime}
|
||||
refreshOnFocus={props.refreshOnFocus}
|
||||
cache={props.cache}
|
||||
onCacheUpdate={props.onCacheUpdate}
|
||||
refresh={props.refresh}
|
||||
onFinish={props.onFinish}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withCollapsible(WebSectionList);
|
||||
export default WebSectionList;
|
||||
|
|
|
@ -17,8 +17,14 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import WebView from 'react-native-webview';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import WebView, { WebViewNavigation } from 'react-native-webview';
|
||||
import {
|
||||
Divider,
|
||||
HiddenItem,
|
||||
|
@ -31,161 +37,162 @@ import {
|
|||
Linking,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
} 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 withCollapsible from '../../utils/withCollapsible';
|
||||
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
|
||||
import {ERROR_TYPE} from '../../utils/WebData';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { useCollapsibleHeader } from 'react-navigation-collapsible';
|
||||
import MaterialHeaderButtons, { Item } from '../Overrides/CustomHeaderButton';
|
||||
import ErrorView from './ErrorView';
|
||||
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/core';
|
||||
import { useCollapsible } from '../../context/CollapsibleContext';
|
||||
import { REQUEST_STATUS } from '../../utils/Requests';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
type Props = {
|
||||
url: string;
|
||||
collapsibleStack: Collapsible;
|
||||
onMessage: (event: {nativeEvent: {data: string}}) => void;
|
||||
onScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
customJS?: string;
|
||||
onMessage?: (event: { nativeEvent: { data: string } }) => void;
|
||||
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
initialJS?: string;
|
||||
injectJS?: string;
|
||||
customPaddingFunction?: null | ((padding: number) => string);
|
||||
showAdvancedControls?: boolean;
|
||||
showControls?: boolean;
|
||||
incognito?: boolean;
|
||||
};
|
||||
|
||||
const AnimatedWebView = Animated.createAnimatedComponent(WebView);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overflow: {
|
||||
marginHorizontal: 10,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Class defining a webview screen.
|
||||
*/
|
||||
class WebViewScreen extends React.PureComponent<PropsType> {
|
||||
static defaultProps = {
|
||||
customJS: '',
|
||||
showAdvancedControls: true,
|
||||
customPaddingFunction: null,
|
||||
};
|
||||
function WebViewScreen(props: Props) {
|
||||
const [navState, setNavState] = useState<undefined | WebViewNavigation>({
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loading: true,
|
||||
url: props.url,
|
||||
lockIdentifier: 0,
|
||||
navigationType: 'click',
|
||||
title: '',
|
||||
});
|
||||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
const webviewRef = useRef<WebView>();
|
||||
|
||||
currentUrl: string;
|
||||
const { setCollapsible } = useCollapsible();
|
||||
const collapsible = useCollapsibleHeader({
|
||||
config: { collapsedColor: theme.colors.surface, useNativeDriver: false },
|
||||
});
|
||||
const { containerPaddingTop, onScrollWithListener } = collapsible;
|
||||
|
||||
webviewRef: {current: null | WebView};
|
||||
const [currentInjectedJS, setCurrentInjectedJS] = useState(props.injectJS);
|
||||
|
||||
canGoBack: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.webviewRef = React.createRef();
|
||||
this.canGoBack = false;
|
||||
this.currentUrl = props.url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates header buttons and listens to events after mounting
|
||||
*/
|
||||
componentDidMount() {
|
||||
const {props} = this;
|
||||
props.navigation.setOptions({
|
||||
headerRight: props.showAdvancedControls
|
||||
? this.getAdvancedButtons
|
||||
: this.getBasicButton,
|
||||
});
|
||||
props.navigation.addListener('focus', () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setCollapsible(collapsible);
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid,
|
||||
onBackButtonPressAndroid
|
||||
);
|
||||
});
|
||||
props.navigation.addListener('blur', () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid,
|
||||
);
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
onBackButtonPressAndroid
|
||||
);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [collapsible, setCollapsible])
|
||||
);
|
||||
|
||||
/**
|
||||
* Goes back on the webview or on the navigation stack if we cannot go back anymore
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
onBackButtonPressAndroid = (): boolean => {
|
||||
if (this.canGoBack) {
|
||||
this.onGoBackClicked();
|
||||
useLayoutEffect(() => {
|
||||
if (props.showControls !== false) {
|
||||
navigation.setOptions({
|
||||
headerRight: props.showAdvancedControls
|
||||
? getAdvancedButtons
|
||||
: getBasicButton,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
navigation,
|
||||
props.showAdvancedControls,
|
||||
navState?.url,
|
||||
props.showControls,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.injectJS && props.injectJS !== currentInjectedJS) {
|
||||
injectJavaScript(props.injectJS);
|
||||
setCurrentInjectedJS(props.injectJS);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.injectJS]);
|
||||
|
||||
const onBackButtonPressAndroid = () => {
|
||||
if (navState?.canGoBack) {
|
||||
onGoBackClicked();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets header refresh and open in browser buttons
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getBasicButton = () => {
|
||||
const getBasicButton = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="refresh"
|
||||
iconName="refresh"
|
||||
onPress={this.onRefreshClicked}
|
||||
title={'refresh'}
|
||||
iconName={'refresh'}
|
||||
onPress={onRefreshClicked}
|
||||
/>
|
||||
<Item
|
||||
title={i18n.t('general.openInBrowser')}
|
||||
iconName="open-in-new"
|
||||
onPress={this.onOpenClicked}
|
||||
iconName={'open-in-new'}
|
||||
onPress={onOpenClicked}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates advanced header control buttons.
|
||||
* These buttons allows the user to refresh, go back, go forward and open in the browser.
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getAdvancedButtons = () => {
|
||||
const {props} = this;
|
||||
const getAdvancedButtons = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="refresh"
|
||||
iconName="refresh"
|
||||
onPress={this.onRefreshClicked}
|
||||
/>
|
||||
<Item title="refresh" iconName="refresh" onPress={onRefreshClicked} />
|
||||
<OverflowMenu
|
||||
style={{marginHorizontal: 10}}
|
||||
style={styles.overflow}
|
||||
OverflowIcon={
|
||||
<MaterialCommunityIcons
|
||||
name="dots-vertical"
|
||||
size={26}
|
||||
color={props.theme.colors.text}
|
||||
color={theme.colors.text}
|
||||
/>
|
||||
}>
|
||||
}
|
||||
>
|
||||
<HiddenItem
|
||||
title={i18n.t('general.goBack')}
|
||||
onPress={this.onGoBackClicked}
|
||||
onPress={onGoBackClicked}
|
||||
/>
|
||||
<HiddenItem
|
||||
title={i18n.t('general.goForward')}
|
||||
onPress={this.onGoForwardClicked}
|
||||
onPress={onGoForwardClicked}
|
||||
/>
|
||||
<Divider />
|
||||
<HiddenItem
|
||||
title={i18n.t('general.openInBrowser')}
|
||||
onPress={this.onOpenClicked}
|
||||
onPress={onOpenClicked}
|
||||
/>
|
||||
</OverflowMenu>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the loading indicator
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getRenderLoading = () => <BasicLoadingScreen isAbsolute />;
|
||||
const getRenderLoading = () => <BasicLoadingScreen isAbsolute={true} />;
|
||||
|
||||
/**
|
||||
* Gets the javascript needed to generate a padding on top of the page
|
||||
|
@ -194,88 +201,81 @@ class WebViewScreen extends React.PureComponent<PropsType> {
|
|||
* @param padding The padding to add in pixels
|
||||
* @returns {string}
|
||||
*/
|
||||
getJavascriptPadding(padding: number): string {
|
||||
const {props} = this;
|
||||
const getJavascriptPadding = (padding: number) => {
|
||||
const customPadding =
|
||||
props.customPaddingFunction != null
|
||||
? props.customPaddingFunction(padding)
|
||||
: '';
|
||||
return `document.getElementsByTagName('body')[0].style.paddingTop = '${padding}px';${customPadding}true;`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback to use when refresh button is clicked. Reloads the webview.
|
||||
*/
|
||||
onRefreshClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.reload();
|
||||
const onRefreshClicked = () => {
|
||||
//@ts-ignore
|
||||
if (webviewRef.current) {
|
||||
//@ts-ignore
|
||||
webviewRef.current.reload();
|
||||
}
|
||||
};
|
||||
|
||||
onGoBackClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.goBack();
|
||||
const onGoBackClicked = () => {
|
||||
//@ts-ignore
|
||||
if (webviewRef.current) {
|
||||
//@ts-ignore
|
||||
webviewRef.current.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
onGoForwardClicked = () => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.goForward();
|
||||
const onGoForwardClicked = () => {
|
||||
//@ts-ignore
|
||||
if (webviewRef.current) {
|
||||
//@ts-ignore
|
||||
webviewRef.current.goForward();
|
||||
}
|
||||
};
|
||||
|
||||
onOpenClicked = () => {
|
||||
Linking.openURL(this.currentUrl);
|
||||
};
|
||||
const onOpenClicked = () =>
|
||||
navState ? Linking.openURL(navState.url) : undefined;
|
||||
|
||||
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const {onScroll} = this.props;
|
||||
if (onScroll) {
|
||||
onScroll(event);
|
||||
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (props.onScroll) {
|
||||
props.onScroll(event);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects the given javascript string into the web page
|
||||
*
|
||||
* @param script The script to inject
|
||||
*/
|
||||
injectJavaScript = (script: string) => {
|
||||
if (this.webviewRef.current != null) {
|
||||
this.webviewRef.current.injectJavaScript(script);
|
||||
const injectJavaScript = (script: string) => {
|
||||
//@ts-ignore
|
||||
if (webviewRef.current) {
|
||||
//@ts-ignore
|
||||
webviewRef.current.injectJavaScript(script);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack;
|
||||
return (
|
||||
<AnimatedWebView
|
||||
ref={this.webviewRef}
|
||||
source={{uri: props.url}}
|
||||
startInLoadingState
|
||||
injectedJavaScript={props.customJS}
|
||||
javaScriptEnabled
|
||||
renderLoading={this.getRenderLoading}
|
||||
renderError={() => (
|
||||
<ErrorView
|
||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||
onRefresh={this.onRefreshClicked}
|
||||
/>
|
||||
)}
|
||||
onNavigationStateChange={(navState) => {
|
||||
this.currentUrl = navState.url;
|
||||
this.canGoBack = navState.canGoBack;
|
||||
}}
|
||||
onMessage={props.onMessage}
|
||||
onLoad={() => {
|
||||
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
|
||||
}}
|
||||
// Animations
|
||||
onScroll={(event) => onScrollWithListener(this.onScroll)(event)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<AnimatedWebView
|
||||
ref={webviewRef}
|
||||
source={{ uri: props.url }}
|
||||
startInLoadingState={true}
|
||||
injectedJavaScript={props.initialJS}
|
||||
javaScriptEnabled={true}
|
||||
renderLoading={getRenderLoading}
|
||||
renderError={() => (
|
||||
<ErrorView
|
||||
status={REQUEST_STATUS.CONNECTION_ERROR}
|
||||
button={{
|
||||
icon: 'refresh',
|
||||
text: i18n.t('general.retry'),
|
||||
onPress: onRefreshClicked,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onNavigationStateChange={setNavState}
|
||||
onMessage={props.onMessage}
|
||||
onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
|
||||
// Animations
|
||||
onScroll={onScrollWithListener(onScroll)}
|
||||
incognito={props.incognito}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default withCollapsible(withTheme(WebViewScreen));
|
||||
export default WebViewScreen;
|
||||
|
|
|
@ -17,204 +17,85 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated} from 'react-native';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import {Collapsible} from 'react-navigation-collapsible';
|
||||
import React from 'react';
|
||||
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||
import { Animated, StyleSheet } from 'react-native';
|
||||
import TabIcon from './TabIcon';
|
||||
import TabHomeIcon from './TabHomeIcon';
|
||||
import {BottomTabBarProps} from '@react-navigation/bottom-tabs';
|
||||
import {NavigationState} from '@react-navigation/native';
|
||||
import {
|
||||
PartialState,
|
||||
Route,
|
||||
} from '@react-navigation/routers/lib/typescript/src/types';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { useCollapsible } from '../../context/CollapsibleContext';
|
||||
|
||||
type RouteType = Route<string> & {
|
||||
state?: NavigationState | PartialState<NavigationState>;
|
||||
};
|
||||
export const TAB_BAR_HEIGHT = 50;
|
||||
|
||||
interface PropsType extends BottomTabBarProps {
|
||||
theme: ReactNativePaper.Theme;
|
||||
function CustomTabBar(
|
||||
props: BottomTabBarProps & {
|
||||
icons: {
|
||||
[key: string]: {
|
||||
normal: string;
|
||||
focused: string;
|
||||
};
|
||||
};
|
||||
labels: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
) {
|
||||
const state = props.state;
|
||||
const theme = useTheme();
|
||||
|
||||
const { collapsible } = useCollapsible();
|
||||
let translateY: number | Animated.AnimatedInterpolation = 0;
|
||||
if (collapsible) {
|
||||
translateY = Animated.multiply(-1.5, collapsible.translateY);
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
...styles.bar,
|
||||
backgroundColor: theme.colors.surface,
|
||||
transform: [{ translateY: translateY }],
|
||||
}}
|
||||
>
|
||||
{state.routes.map(
|
||||
(
|
||||
route: {
|
||||
key: string;
|
||||
name: string;
|
||||
params?: object | undefined;
|
||||
},
|
||||
index: number
|
||||
) => {
|
||||
const iconData = props.icons[route.name];
|
||||
return (
|
||||
<TabIcon
|
||||
isMiddle={index === 2}
|
||||
onPress={() => props.navigation.navigate(route.name)}
|
||||
icon={iconData.normal}
|
||||
focusedIcon={iconData.focused}
|
||||
label={props.labels[route.name]}
|
||||
focused={state.index === index}
|
||||
key={route.key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
type StateType = {
|
||||
translateY: any;
|
||||
};
|
||||
const styles = StyleSheet.create({
|
||||
bar: {
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
height: 50,
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
});
|
||||
|
||||
type validRoutes = 'proxiwash' | 'services' | 'planning' | 'planex';
|
||||
|
||||
const TAB_ICONS = {
|
||||
proxiwash: 'tshirt-crew',
|
||||
services: 'account-circle',
|
||||
planning: 'calendar-range',
|
||||
planex: 'clock',
|
||||
};
|
||||
|
||||
class CustomTabBar extends React.Component<PropsType, StateType> {
|
||||
static TAB_BAR_HEIGHT = 48;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
translateY: new Animated.Value(0),
|
||||
};
|
||||
// @ts-ignore
|
||||
props.navigation.addListener('state', this.onRouteChange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to the given route if it is different from the current one
|
||||
*
|
||||
* @param route Destination route
|
||||
* @param currentIndex The current route index
|
||||
* @param destIndex The destination route index
|
||||
*/
|
||||
onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
|
||||
const {navigation} = this.props;
|
||||
if (currentIndex !== destIndex) {
|
||||
navigation.navigate(route.name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates to tetris screen on home button long press
|
||||
*
|
||||
* @param route
|
||||
*/
|
||||
onItemLongPress(route: RouteType) {
|
||||
const {navigation} = this.props;
|
||||
if (route.name === 'home') {
|
||||
navigation.navigate('game-start');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the active route and syncs the tab bar animation with the header bar
|
||||
*/
|
||||
onRouteChange = () => {
|
||||
const {props} = this;
|
||||
props.state.routes.map(this.syncTabBar);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an icon for the given route if it is not the home one as it uses a custom button
|
||||
*
|
||||
* @param route
|
||||
* @param focused
|
||||
* @returns {null}
|
||||
*/
|
||||
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 '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a tab icon render.
|
||||
* If the given route is focused, it syncs the tab bar and header bar animations together
|
||||
*
|
||||
* @param route The route for the icon
|
||||
* @param index The index of the current route
|
||||
* @returns {*}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
const onPress = () => {
|
||||
this.onItemPress(route, state.index, index);
|
||||
};
|
||||
const onLongPress = () => {
|
||||
this.onItemLongPress(route);
|
||||
};
|
||||
const isFocused = state.index === index;
|
||||
|
||||
const color = isFocused
|
||||
? props.theme.colors.primary
|
||||
: props.theme.colors.tabIcon;
|
||||
if (route.name !== 'home') {
|
||||
return (
|
||||
<TabIcon
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
icon={this.getTabBarIcon(route, isFocused)}
|
||||
color={color}
|
||||
label={label as string}
|
||||
focused={isFocused}
|
||||
extraData={state.index > index}
|
||||
key={route.key}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TabHomeIcon
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
focused={isFocused}
|
||||
key={route.key}
|
||||
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getIcons() {
|
||||
const {props} = this;
|
||||
return props.state.routes.map(this.getRenderIcon);
|
||||
}
|
||||
|
||||
syncTabBar = (route: RouteType, index: number) => {
|
||||
const {state} = this.props;
|
||||
const isFocused = state.index === index;
|
||||
if (isFocused) {
|
||||
const stackState = route.state;
|
||||
const stackRoute =
|
||||
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({
|
||||
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
const icons = this.getIcons();
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
height: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: props.theme.colors.surface,
|
||||
transform: [{translateY: state.translateY}],
|
||||
}}>
|
||||
{icons}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
function areEqual(prevProps: BottomTabBarProps, nextProps: BottomTabBarProps) {
|
||||
return prevProps.state.index === nextProps.state.index;
|
||||
}
|
||||
|
||||
export default withTheme(CustomTabBar);
|
||||
export default React.memo(CustomTabBar, areEqual);
|
||||
|
|
|
@ -1,127 +1,118 @@
|
|||
/*
|
||||
* 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, View} from 'react-native';
|
||||
import {FAB} from 'react-native-paper';
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, Image } from 'react-native';
|
||||
import { FAB } from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
interface Props {
|
||||
icon: string;
|
||||
focusedIcon: string;
|
||||
focused: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
fabFocusIn: {
|
||||
0: {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
0.4: {
|
||||
// @ts-ignore
|
||||
scale: 1.2,
|
||||
translateY: -9,
|
||||
},
|
||||
0.6: {
|
||||
// @ts-ignore
|
||||
scale: 1.05,
|
||||
translateY: -6,
|
||||
},
|
||||
0.8: {
|
||||
// @ts-ignore
|
||||
scale: 1.15,
|
||||
translateY: -6,
|
||||
},
|
||||
1: {
|
||||
// @ts-ignore
|
||||
scale: 1.1,
|
||||
translateY: -6,
|
||||
},
|
||||
},
|
||||
fabFocusOut: {
|
||||
0: {
|
||||
// @ts-ignore
|
||||
scale: 1.1,
|
||||
translateY: -6,
|
||||
},
|
||||
1: {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
inner: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: 60,
|
||||
},
|
||||
fab: {
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
/**
|
||||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
class TabHomeIcon extends React.Component<PropsType> {
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
fabFocusIn: {
|
||||
'0': {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
'0.9': {
|
||||
scale: 1.2,
|
||||
translateY: -9,
|
||||
},
|
||||
'1': {
|
||||
scale: 1.1,
|
||||
translateY: -7,
|
||||
},
|
||||
},
|
||||
fabFocusOut: {
|
||||
'0': {
|
||||
// @ts-ignore
|
||||
scale: 1.1,
|
||||
translateY: -6,
|
||||
},
|
||||
'1': {
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {focused} = this.props;
|
||||
return nextProps.focused !== focused;
|
||||
}
|
||||
|
||||
getIconRender = ({size, color}: {size: number; color: string}) => {
|
||||
const {focused} = this.props;
|
||||
function TabHomeIcon(props: Props) {
|
||||
const navigation = useNavigation();
|
||||
const getImage = (iconProps: { size: number; color: string }) => {
|
||||
return (
|
||||
<Image
|
||||
source={focused ? FOCUSED_ICON : UNFOCUSED_ICON}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
tintColor: color,
|
||||
}}
|
||||
/>
|
||||
<Animatable.View useNativeDriver={true} animation={'rubberBand'}>
|
||||
<Image
|
||||
source={props.focused ? FOCUSED_ICON : UNFOCUSED_ICON}
|
||||
style={{
|
||||
width: iconProps.size,
|
||||
height: iconProps.size,
|
||||
tintColor: iconProps.color,
|
||||
}}
|
||||
/>
|
||||
</Animatable.View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: props.tabBarHeight + 30,
|
||||
marginBottom: -15,
|
||||
}}>
|
||||
<AnimatedFAB
|
||||
duration={200}
|
||||
easing="ease-out"
|
||||
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
|
||||
icon={this.getIconRender}
|
||||
return (
|
||||
<View style={styles.outer}>
|
||||
<View style={styles.inner}>
|
||||
<Animatable.View
|
||||
style={styles.fab}
|
||||
useNativeDriver={true}
|
||||
duration={props.focused ? 500 : 200}
|
||||
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
|
||||
easing={'ease-out'}
|
||||
>
|
||||
<FAB
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
style={{
|
||||
marginTop: 15,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
onLongPress={() => navigation.navigate(MainRoutes.GameStart)}
|
||||
animated={false}
|
||||
icon={getImage}
|
||||
color={'#fff'}
|
||||
/>
|
||||
</View>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default TabHomeIcon;
|
||||
|
|
|
@ -1,135 +1,41 @@
|
|||
/*
|
||||
* 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 TabHomeIcon from './TabHomeIcon';
|
||||
import TabSideIcon from './TabSideIcon';
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
|
||||
type PropsType = {
|
||||
interface Props {
|
||||
isMiddle: boolean;
|
||||
focused: boolean;
|
||||
color: string;
|
||||
label: string;
|
||||
label: string | undefined;
|
||||
icon: string;
|
||||
focusedIcon: string;
|
||||
onPress: () => void;
|
||||
onLongPress: () => void;
|
||||
theme: ReactNativePaper.Theme;
|
||||
extraData: null | boolean | number | string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
class TabIcon extends React.Component<PropsType> {
|
||||
firstRender: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
Animatable.initializeRegistryWithDefinitions({
|
||||
focusIn: {
|
||||
'0': {
|
||||
// @ts-ignore
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
'0.9': {
|
||||
scale: 1.3,
|
||||
translateY: 7,
|
||||
},
|
||||
'1': {
|
||||
scale: 1.2,
|
||||
translateY: 6,
|
||||
},
|
||||
},
|
||||
focusOut: {
|
||||
'0': {
|
||||
// @ts-ignore
|
||||
scale: 1.2,
|
||||
translateY: 6,
|
||||
},
|
||||
'1': {
|
||||
scale: 1,
|
||||
translateY: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
this.firstRender = true;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.firstRender = false;
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
function TabIcon(props: Props) {
|
||||
if (props.isMiddle) {
|
||||
return (
|
||||
nextProps.focused !== props.focused ||
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.extraData !== props.extraData
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {props} = this;
|
||||
return (
|
||||
<TouchableRipple
|
||||
<TabHomeIcon
|
||||
icon={props.icon}
|
||||
focusedIcon={props.focusedIcon}
|
||||
focused={props.focused}
|
||||
onPress={props.onPress}
|
||||
onLongPress={props.onLongPress}
|
||||
rippleColor={props.theme.colors.primary}
|
||||
borderless
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
borderRadius: 10,
|
||||
}}>
|
||||
<View>
|
||||
<Animatable.View
|
||||
duration={200}
|
||||
easing="ease-out"
|
||||
animation={props.focused ? 'focusIn' : 'focusOut'}
|
||||
useNativeDriver>
|
||||
<MaterialCommunityIcons
|
||||
name={props.icon}
|
||||
color={props.color}
|
||||
size={26}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
</Animatable.View>
|
||||
<Animatable.Text
|
||||
animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
|
||||
useNativeDriver
|
||||
style={{
|
||||
color: props.color,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
fontSize: 10,
|
||||
}}>
|
||||
{props.label}
|
||||
</Animatable.Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TabSideIcon
|
||||
focused={props.focused}
|
||||
label={props.label}
|
||||
icon={props.icon}
|
||||
focusedIcon={props.focusedIcon}
|
||||
onPress={props.onPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(TabIcon);
|
||||
function areEqual(prevProps: Props, nextProps: Props) {
|
||||
return prevProps.focused === nextProps.focused;
|
||||
}
|
||||
|
||||
export default React.memo(TabIcon, areEqual);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue