Compare commits

..

45 commits

Author SHA1 Message Date
b378473591 Add star press animation 2020-08-06 17:48:35 +02:00
327488a470 Improve planex group favorite handling 2020-08-06 17:34:53 +02:00
eef6f75414 Change default android modal transition 2020-08-06 14:16:22 +02:00
5166e0b879 Fix crash on collapsible click 2020-08-06 14:14:01 +02:00
98770611ff Fix crash on planex group screen open 2020-08-06 14:01:28 +02:00
ee9e225dae Fix game navigation issue
Fixed start game screen being replaced by end screen when exiting a runnnning game
2020-08-06 13:54:09 +02:00
05f769fe79 Fix crash on app start
Used animated from react native instead of react native reanimated in the tab bar
2020-08-06 13:46:48 +02:00
1be913c5aa Update tests to match new implementations 2020-08-06 12:09:17 +02:00
c86281cbd2 Change name 2020-08-05 21:09:37 +02:00
4cc9c61d72 Disable lint for test files 2020-08-05 21:09:04 +02:00
1e81b2cd7b Improve remaining files to match linter 2020-08-05 20:58:28 +02:00
cbe3777957 Improve Game files to match linter 2020-08-05 20:24:08 +02:00
569e659779 Improve utils files to match linter 2020-08-05 18:52:18 +02:00
fcbc70956b Improve Services screen components to match linter 2020-08-05 18:39:44 +02:00
3ce23726c2 Improve Planning screen components to match linter 2020-08-05 15:04:41 +02:00
a3299c19f7 Improve Settings screen components to match linter 2020-08-05 13:51:14 +02:00
0a64f5fcd7 Improve Amicale screen components to match linter 2020-08-05 11:54:13 +02:00
483970c9a8 Improve about components to match linter 2020-08-05 00:37:51 +02:00
3e4f2f4ac1 Improve navigators to match linter 2020-08-05 00:16:05 +02:00
7107a8eadf Improve constants to match linter 2020-08-05 00:06:05 +02:00
7ac62b99f4 Improve constants to match linter 2020-08-04 23:51:32 +02:00
aa992d20b2 Improve tab components to match linter 2020-08-04 23:49:18 +02:00
0117b25cd8 Improve basic screen components to match linter 2020-08-04 21:49:19 +02:00
4db4516296 Improve override components to match linter 2020-08-04 21:24:43 +02:00
7b94afadcc Improve Mascot components to match linter 2020-08-04 19:26:25 +02:00
1cc0802c12 Improve Proxiwash components to match linter 2020-08-04 18:53:10 +02:00
547af66977 Improve Proximo components to match linter 2020-08-04 18:00:45 +02:00
ab86c1c85c Improve planex components to match linter 2020-08-04 14:06:09 +02:00
11b5f2ac71 Improve equipment booking components to match linter 2020-08-04 10:57:19 +02:00
70365136ac Improve Dashboard edit components to match linter 2020-08-04 09:31:27 +02:00
93d12b27f8 Improve Clubs components to match linter 2020-08-03 21:53:53 +02:00
33d98b024b Improve Services components to match linter 2020-08-03 21:06:39 +02:00
6b12b4cde2 Improve Home components to match linter 2020-08-03 18:36:52 +02:00
34ccf9c4c9 Improve collapsible components to match linter 2020-08-03 17:18:50 +02:00
9d92a88627 Improve animated components to match linter 2020-08-03 16:45:10 +02:00
925bded69b Improve dialog components to match linter 2020-08-03 16:28:03 +02:00
3629c5730a Improve connection manager to match linter 2020-08-02 19:53:05 +02:00
3d9bfdea4c Improve vote screen to match linter 2020-08-02 19:52:19 +02:00
142b861ccb Improve vote screens to match linter 2020-08-02 19:51:19 +02:00
0a9e0eb0ca Improve requests handlers to match linter 2020-08-02 19:45:19 +02:00
9fc02baf6d Remove Expo launch config
Removed unused config
2020-08-02 19:44:06 +02:00
22eabf28d5 Update eslint rules
Changed rules to fix conflicts with flow
2020-08-02 19:43:06 +02:00
c0777511a6 Add flow babel preset 2020-08-01 21:00:28 +02:00
be1f61b671 Fix eslint errors
First files rewritten to match the new eslint config
2020-08-01 20:59:59 +02:00
b596f68abe Remove annoying eslint warning
This warning would have made me rename all my .js files to .jsx, which is pointless
2020-08-01 20:59:04 +02:00
145 changed files with 18112 additions and 16502 deletions

View file

@ -13,6 +13,8 @@ module.exports = {
jest: true, jest: true,
}, },
rules: { rules: {
'react/jsx-filename-extension': [1, {extensions: ['.js', '.jsx']}],
'react/static-property-placement': [2, 'static public field'],
'flowtype/define-flow-type': 1, 'flowtype/define-flow-type': 1,
'flowtype/no-mixed': 2, 'flowtype/no-mixed': 2,
'flowtype/no-primitive-constructor-types': 2, 'flowtype/no-primitive-constructor-types': 2,
@ -37,4 +39,8 @@ module.exports = {
onlyFilesWithFlowAnnotation: false, onlyFilesWithFlowAnnotation: false,
}, },
}, },
globals: {
fetch: false,
Headers: false,
},
}; };

View file

@ -1,16 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Expo" type="ReactNative" factoryName="React Native">
<node-interpreter value="project" />
<react-native value="$USER_HOME$/.nvm/versions/node/v12.4.0/lib/node_modules/react-native-cli" />
<platform value="ANDROID" />
<envs />
<only-packager />
<build-and-launch value="false" />
<browser value="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3" />
<debug-host value="127.0.0.1" />
<debug-port value="19001" />
<method v="2">
<option name="ReactNativePackager" enabled="true" />
</method>
</configuration>
</component>

369
App.js
View file

@ -1,210 +1,211 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {LogBox, Platform, SafeAreaView, StatusBar, View} from 'react-native'; import {LogBox, Platform, SafeAreaView, View} from 'react-native';
import LocaleManager from './src/managers/LocaleManager';
import AsyncStorageManager from "./src/managers/AsyncStorageManager";
import CustomIntroSlider from "./src/components/Overrides/CustomIntroSlider";
import type {CustomTheme} from "./src/managers/ThemeManager";
import ThemeManager from './src/managers/ThemeManager';
import {NavigationContainer} from '@react-navigation/native'; import {NavigationContainer} from '@react-navigation/native';
import MainNavigator from './src/navigation/MainNavigator';
import {Provider as PaperProvider} from 'react-native-paper'; import {Provider as PaperProvider} from 'react-native-paper';
import AprilFoolsManager from "./src/managers/AprilFoolsManager"; import {setSafeBounceHeight} from 'react-navigation-collapsible';
import Update from "./src/constants/Update"; import SplashScreen from 'react-native-splash-screen';
import ConnectionManager from "./src/managers/ConnectionManager"; import {OverflowMenuProvider} from 'react-navigation-header-buttons';
import URLHandler from "./src/utils/URLHandler"; import LocaleManager from './src/managers/LocaleManager';
import {setSafeBounceHeight} from "react-navigation-collapsible"; import AsyncStorageManager from './src/managers/AsyncStorageManager';
import SplashScreen from 'react-native-splash-screen' import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider';
import {OverflowMenuProvider} from "react-navigation-header-buttons"; import type {CustomThemeType} from './src/managers/ThemeManager';
import ThemeManager from './src/managers/ThemeManager';
import MainNavigator from './src/navigation/MainNavigator';
import AprilFoolsManager from './src/managers/AprilFoolsManager';
import Update from './src/constants/Update';
import ConnectionManager from './src/managers/ConnectionManager';
import type {ParsedUrlDataType} from './src/utils/URLHandler';
import URLHandler from './src/utils/URLHandler';
import {setupStatusBar} from './src/utils/Utils';
// Native optimizations https://reactnavigation.org/docs/react-native-screens // Native optimizations https://reactnavigation.org/docs/react-native-screens
// Crashes app when navigating away from webview on android 9+ // Crashes app when navigating away from webview on android 9+
// enableScreens(true); // enableScreens(true);
LogBox.ignoreLogs([
LogBox.ignoreLogs([ // collapsible headers cause this warning, just ignore as it is not an issue // collapsible headers cause this warning, just ignore as it is not an issue
'Non-serializable values were found in the navigation state', 'Non-serializable values were found in the navigation state',
'Cannot update a component from inside the function body of a different component', 'Cannot update a component from inside the function body of a different component',
]); ]);
type Props = {}; type StateType = {
isLoading: boolean,
type State = { showIntro: boolean,
isLoading: boolean, showUpdate: boolean,
showIntro: boolean, showAprilFools: boolean,
showUpdate: boolean, currentTheme: CustomThemeType | null,
showAprilFools: boolean,
currentTheme: CustomTheme | null,
}; };
export default class App extends React.Component<Props, State> { export default class App extends React.Component<null, StateType> {
navigatorRef: {current: null | NavigationContainer};
state = { defaultHomeRoute: string | null;
isLoading: true,
showIntro: true, defaultHomeData: {[key: string]: string};
showUpdate: true,
showAprilFools: false, urlHandler: URLHandler;
currentTheme: null,
constructor() {
super();
this.state = {
isLoading: true,
showIntro: true,
showUpdate: true,
showAprilFools: false,
currentTheme: null,
}; };
LocaleManager.initTranslations();
this.navigatorRef = React.createRef();
this.defaultHomeRoute = null;
this.defaultHomeData = {};
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
this.urlHandler.listen();
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
this.loadAssetsAsync().finally(() => {
this.onLoadFinished();
});
}
navigatorRef: { current: null | NavigationContainer }; /**
* The app has been started by an url, and it has been parsed.
* Set a new default start route based on the data parsed.
*
* @param parsedData The data parsed from the url
*/
onInitialURLParsed = (parsedData: ParsedUrlDataType) => {
this.defaultHomeRoute = parsedData.route;
this.defaultHomeData = parsedData.data;
};
defaultHomeRoute: string | null; /**
defaultHomeData: { [key: string]: any } * An url has been opened and parsed while the app was active.
* Redirect the user to the screen according to parsed data.
createDrawerNavigator: () => React.Node; *
* @param parsedData The data parsed from the url
urlHandler: URLHandler; */
onDetectURL = (parsedData: ParsedUrlDataType) => {
constructor() { // Navigate to nested navigator and pass data to the index screen
super(); const nav = this.navigatorRef.current;
LocaleManager.initTranslations(); if (nav != null) {
this.navigatorRef = React.createRef(); nav.navigate('home', {
this.defaultHomeRoute = null; screen: 'index',
this.defaultHomeData = {}; params: {nextScreen: parsedData.route, data: parsedData.data},
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL); });
this.urlHandler.listen();
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
this.loadAssetsAsync().then(() => {
this.onLoadFinished();
});
} }
};
/** /**
* The app has been started by an url, and it has been parsed. * Updates the current theme
* Set a new default start route based on the data parsed. */
* onUpdateTheme = () => {
* @param parsedData The data parsed from the url this.setState({
*/ currentTheme: ThemeManager.getCurrentTheme(),
onInitialURLParsed = (parsedData: { route: string, data: { [key: string]: any } }) => { });
this.defaultHomeRoute = parsedData.route; setupStatusBar();
this.defaultHomeData = parsedData.data; };
};
/** /**
* An url has been opened and parsed while the app was active. * Callback when user ends the intro. Save in preferences to avoid showing back the introSlides
* Redirect the user to the screen according to parsed data. */
* onIntroDone = () => {
* @param parsedData The data parsed from the url this.setState({
*/ showIntro: false,
onDetectURL = (parsedData: { route: string, data: { [key: string]: any } }) => { showUpdate: false,
// Navigate to nested navigator and pass data to the index screen showAprilFools: false,
if (this.navigatorRef.current != null) { });
this.navigatorRef.current.navigate('home', { AsyncStorageManager.set(
screen: 'index', AsyncStorageManager.PREFERENCES.showIntro.key,
params: {nextScreen: parsedData.route, data: parsedData.data} false,
}); );
} AsyncStorageManager.set(
}; AsyncStorageManager.PREFERENCES.updateNumber.key,
Update.number,
);
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key,
false,
);
};
/** /**
* Updates the current theme * Async loading is done, finish processing startup data
*/ */
onUpdateTheme = () => { onLoadFinished() {
this.setState({ // Only show intro if this is the first time starting the app
currentTheme: ThemeManager.getCurrentTheme() ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
}); // Status bar goes dark if set too fast on ios
this.setupStatusBar(); if (Platform.OS === 'ios') setTimeout(setupStatusBar, 1000);
}; else setupStatusBar();
/** this.setState({
* Updates status bar content color if on iOS only, isLoading: false,
* as the android status bar is always set to black. currentTheme: ThemeManager.getCurrentTheme(),
*/ showIntro: AsyncStorageManager.getBool(
setupStatusBar() { AsyncStorageManager.PREFERENCES.showIntro.key,
if (ThemeManager.getNightMode()) { ),
StatusBar.setBarStyle('light-content', true); showUpdate:
} else { AsyncStorageManager.getNumber(
StatusBar.setBarStyle('dark-content', true); AsyncStorageManager.PREFERENCES.updateNumber.key,
} ) !== Update.number,
if (Platform.OS === "android") showAprilFools:
StatusBar.setBackgroundColor(ThemeManager.getCurrentTheme().colors.surface, true); AprilFoolsManager.getInstance().isAprilFoolsEnabled() &&
AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key,
),
});
SplashScreen.hide();
}
/**
* Loads every async data
*
* @returns {Promise<void>}
*/
loadAssetsAsync = async () => {
await AsyncStorageManager.getInstance().loadPreferences();
await ConnectionManager.getInstance().recoverLogin();
};
/**
* Renders the app based on loading state
*/
render(): React.Node {
const {state} = this;
if (state.isLoading) {
return null;
} }
if (state.showIntro || state.showUpdate || state.showAprilFools) {
/** return (
* Callback when user ends the intro. Save in preferences to avoid showing back the introSlides <CustomIntroSlider
*/ onDone={this.onIntroDone}
onIntroDone = () => { isUpdate={state.showUpdate && !state.showIntro}
this.setState({ isAprilFools={state.showAprilFools && !state.showIntro}
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);
};
/**
* Loads every async data
*
* @returns {Promise<void>}
*/
loadAssetsAsync = async () => {
await AsyncStorageManager.getInstance().loadPreferences();
try {
await ConnectionManager.getInstance().recoverLogin();
} catch (e) {
}
}
/**
* Async loading is done, finish processing startup data
*/
onLoadFinished() {
// Only show intro if this is the first time starting the app
this.createDrawerNavigator = () => <MainNavigator
defaultHomeRoute={this.defaultHomeRoute}
defaultHomeData={this.defaultHomeData}
/>;
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
// Status bar goes dark if set too fast on ios
if (Platform.OS === 'ios')
setTimeout(this.setupStatusBar, 1000);
else
this.setupStatusBar();
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),
});
SplashScreen.hide();
}
/**
* Renders the app based on loading state
*/
render() {
if (this.state.isLoading) {
return null;
} else if (this.state.showIntro || this.state.showUpdate || this.state.showAprilFools) {
return <CustomIntroSlider
onDone={this.onIntroDone}
isUpdate={this.state.showUpdate && !this.state.showIntro}
isAprilFools={this.state.showAprilFools && !this.state.showIntro}
/>;
} else {
return (
<PaperProvider theme={this.state.currentTheme}>
<OverflowMenuProvider>
<View style={{backgroundColor: ThemeManager.getCurrentTheme().colors.background, flex: 1}}>
<SafeAreaView style={{flex: 1}}>
<NavigationContainer theme={this.state.currentTheme} ref={this.navigatorRef}>
<MainNavigator
defaultHomeRoute={this.defaultHomeRoute}
defaultHomeData={this.defaultHomeData}
/>
</NavigationContainer>
</SafeAreaView>
</View>
</OverflowMenuProvider>
</PaperProvider>
);
}
} }
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}
/>
</NavigationContainer>
</SafeAreaView>
</View>
</OverflowMenuProvider>
</PaperProvider>
);
}
} }

View file

@ -1,210 +1,217 @@
jest.mock('react-native-keychain'); /* eslint-disable */
import React from 'react'; import React from 'react';
import ConnectionManager from "../../src/managers/ConnectionManager"; import ConnectionManager from '../../src/managers/ConnectionManager';
import {ERROR_TYPE} from "../../src/utils/WebData"; import {ERROR_TYPE} from '../../src/utils/WebData';
let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native jest.mock('react-native-keychain');
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
const c = ConnectionManager.getInstance(); const c = ConnectionManager.getInstance();
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
test('isLoggedIn yes', () => { test('isLoggedIn yes', () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest
return 'token'; .spyOn(ConnectionManager.prototype, 'getToken')
.mockImplementationOnce(() => {
return 'token';
}); });
return expect(c.isLoggedIn()).toBe(true); return expect(c.isLoggedIn()).toBe(true);
}); });
test('isLoggedIn no', () => { test('isLoggedIn no', () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest
return null; .spyOn(ConnectionManager.prototype, 'getToken')
.mockImplementationOnce(() => {
return null;
}); });
return expect(c.isLoggedIn()).toBe(false); return expect(c.isLoggedIn()).toBe(false);
}); });
test("isConnectionResponseValid", () => { test('connect bad credentials', () => {
let json = { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
error: 0, return Promise.resolve({
data: {token: 'token'} json: () => {
}; return {
expect(c.isConnectionResponseValid(json)).toBeTrue(); error: ERROR_TYPE.BAD_CREDENTIALS,
json = { data: {},
error: 2, };
data: {} },
}; });
expect(c.isConnectionResponseValid(json)).toBeTrue(); });
json = { return expect(c.connect('email', 'password')).rejects.toBe(
error: 0, ERROR_TYPE.BAD_CREDENTIALS,
data: {token: ''} );
};
expect(c.isConnectionResponseValid(json)).toBeFalse();
json = {
error: 'prout',
data: {token: ''}
};
expect(c.isConnectionResponseValid(json)).toBeFalse();
}); });
test("connect bad credentials", () => { test('connect good credentials', () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.BAD_CREDENTIALS, error: ERROR_TYPE.SUCCESS,
data: {} data: {token: 'token'},
}; };
}, },
})
}); });
return expect(c.connect('email', 'password')) });
.rejects.toBe(ERROR_TYPE.BAD_CREDENTIALS); jest
.spyOn(ConnectionManager.prototype, 'saveLogin')
.mockImplementationOnce(() => {
return Promise.resolve(true);
});
return expect(c.connect('email', 'password')).resolves.toBe(undefined);
}); });
test("connect good credentials", () => { test('connect good credentials no consent', () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.SUCCESS, error: ERROR_TYPE.NO_CONSENT,
data: {token: 'token'} data: {},
}; };
}, },
})
}); });
jest.spyOn(ConnectionManager.prototype, 'saveLogin').mockImplementationOnce(() => { });
return Promise.resolve(true); return expect(c.connect('email', 'password')).rejects.toBe(
}); ERROR_TYPE.NO_CONSENT,
return expect(c.connect('email', 'password')).resolves.toBeTruthy(); );
}); });
test("connect good credentials no consent", () => { test('connect good credentials, fail save token', () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.NO_CONSENT, error: ERROR_TYPE.SUCCESS,
data: {} data: {token: 'token'},
}; };
}, },
})
}); });
return expect(c.connect('email', 'password')) });
.rejects.toBe(ERROR_TYPE.NO_CONSENT); jest
.spyOn(ConnectionManager.prototype, 'saveLogin')
.mockImplementationOnce(() => {
return Promise.reject(false);
});
return expect(c.connect('email', 'password')).rejects.toBe(
ERROR_TYPE.TOKEN_SAVE,
);
}); });
test("connect good credentials, fail save token", () => { test('connect connection error', () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.reject();
json: () => { });
return { return expect(c.connect('email', 'password')).rejects.toBe(
error: ERROR_TYPE.SUCCESS, ERROR_TYPE.CONNECTION_ERROR,
data: {token: 'token'} );
};
},
})
});
jest.spyOn(ConnectionManager.prototype, 'saveLogin').mockImplementationOnce(() => {
return Promise.reject(false);
});
return expect(c.connect('email', 'password')).rejects.toBe(ERROR_TYPE.UNKNOWN);
}); });
test("connect connection error", () => { test('connect bogus response 1', () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.reject(); return Promise.resolve({
json: () => {
return {
thing: true,
wrong: '',
};
},
}); });
return expect(c.connect('email', 'password')) });
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR); return expect(c.connect('email', 'password')).rejects.toBe(
ERROR_TYPE.SERVER_ERROR,
);
}); });
test("connect bogus response 1", () => { test('authenticatedRequest success', () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest
return Promise.resolve({ .spyOn(ConnectionManager.prototype, 'getToken')
json: () => { .mockImplementationOnce(() => {
return { return 'token';
thing: true,
wrong: '',
}
},
})
}); });
return expect(c.connect('email', 'password')) jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR); return Promise.resolve({
json: () => {
return {
error: ERROR_TYPE.SUCCESS,
data: {coucou: 'toi'},
};
},
});
});
return expect(
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
).resolves.toStrictEqual({coucou: 'toi'});
}); });
test('authenticatedRequest error wrong token', () => {
test("authenticatedRequest success", () => { jest
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { .spyOn(ConnectionManager.prototype, 'getToken')
return 'token'; .mockImplementationOnce(() => {
return 'token';
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.SUCCESS, error: ERROR_TYPE.BAD_TOKEN,
data: {coucou: 'toi'} data: {},
}; };
}, },
})
}); });
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')) });
.resolves.toStrictEqual({coucou: 'toi'}); return expect(
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
).rejects.toBe(ERROR_TYPE.BAD_TOKEN);
}); });
test("authenticatedRequest error wrong token", () => { test('authenticatedRequest error bogus response', () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest
return 'token'; .spyOn(ConnectionManager.prototype, 'getToken')
.mockImplementationOnce(() => {
return 'token';
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.resolve({
json: () => { json: () => {
return { return {
error: ERROR_TYPE.BAD_TOKEN, error: ERROR_TYPE.SUCCESS,
data: {} };
}; },
},
})
}); });
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')) });
.rejects.toBe(ERROR_TYPE.BAD_TOKEN); return expect(
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
).rejects.toBe(ERROR_TYPE.SERVER_ERROR);
}); });
test("authenticatedRequest error bogus response", () => { test('authenticatedRequest connection error', () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest
return 'token'; .spyOn(ConnectionManager.prototype, 'getToken')
.mockImplementationOnce(() => {
return 'token';
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({ return Promise.reject();
json: () => { });
return { return expect(
error: ERROR_TYPE.SUCCESS, c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
}; ).rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
},
})
});
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
}); });
test("authenticatedRequest connection error", () => { test('authenticatedRequest error no token', () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => { jest
return 'token'; .spyOn(ConnectionManager.prototype, 'getToken')
.mockImplementationOnce(() => {
return null;
}); });
jest.spyOn(global, 'fetch').mockImplementationOnce(() => { return expect(
return Promise.reject() c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
}); ).rejects.toBe(ERROR_TYPE.TOKEN_RETRIEVE);
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});
test("authenticatedRequest error no token", () => {
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
return null;
});
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
.rejects.toBe(ERROR_TYPE.UNKNOWN);
}); });

View file

@ -1,319 +1,345 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import * as EquipmentBooking from "../../src/utils/EquipmentBooking"; import * as EquipmentBooking from '../../src/utils/EquipmentBooking';
import i18n from "i18n-js"; import i18n from 'i18n-js';
test('getISODate', () => { test('getISODate', () => {
let date = new Date("2020-03-05 12:00"); let date = new Date('2020-03-05 12:00');
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05"); expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05');
date = new Date("2020-03-05"); date = new Date('2020-03-05');
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05"); expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05');
date = new Date("2020-03-05 00:00"); // Treated as local time date = new Date('2020-03-05 00:00'); // Treated as local time
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-04"); // Treated as UTC expect(EquipmentBooking.getISODate(date)).toBe('2020-03-04'); // Treated as UTC
}); });
test('getCurrentDay', () => { test('getCurrentDay', () => {
jest.spyOn(Date, 'now') jest
.mockImplementation(() => .spyOn(Date, 'now')
new Date('2020-01-14 14:50:35').getTime() .mockImplementation(() => new Date('2020-01-14 14:50:35').getTime());
); expect(EquipmentBooking.getCurrentDay().getTime()).toBe(
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(new Date("2020-01-14").getTime()); new Date('2020-01-14').getTime(),
);
}); });
test('isEquipmentAvailable', () => { test('isEquipmentAvailable', () => {
jest.spyOn(Date, 'now') jest
.mockImplementation(() => .spyOn(Date, 'now')
new Date('2020-07-09').getTime() .mockImplementation(() => new Date('2020-07-09').getTime());
); let testDevice = {
let testDevice = { id: 1,
id: 1, name: 'Petit barbecue',
name: "Petit barbecue", caution: 100,
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();
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(); 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(); expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
testDevice.booked_at = [ testDevice.booked_at = [
{begin: "2020-07-07", end: "2020-07-8"}, {begin: '2020-07-07', end: '2020-07-8'},
{begin: "2020-07-10", end: "2020-07-12"}, {begin: '2020-07-10', end: '2020-07-12'},
]; ];
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue(); expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
}); });
test('getFirstEquipmentAvailability', () => { test('getFirstEquipmentAvailability', () => {
jest.spyOn(Date, 'now') jest
.mockImplementation(() => .spyOn(Date, 'now')
new Date('2020-07-09').getTime() .mockImplementation(() => new Date('2020-07-09').getTime());
); let testDevice = {
let testDevice = { id: 1,
id: 1, name: 'Petit barbecue',
name: "Petit barbecue", caution: 100,
caution: 100, booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
booked_at: [{begin: "2020-07-07", end: "2020-07-10"}] };
}; expect(
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-11").getTime()); EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}]; ).toBe(new Date('2020-07-11').getTime());
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-10").getTime()); testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
testDevice.booked_at = [ expect(
{begin: "2020-07-07", end: "2020-07-09"}, EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
{begin: "2020-07-10", end: "2020-07-16"}, ).toBe(new Date('2020-07-10').getTime());
]; testDevice.booked_at = [
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-17").getTime()); {begin: '2020-07-07', end: '2020-07-09'},
testDevice.booked_at = [ {begin: '2020-07-10', end: '2020-07-16'},
{begin: "2020-07-07", end: "2020-07-09"}, ];
{begin: "2020-07-10", end: "2020-07-12"}, expect(
{begin: "2020-07-14", end: "2020-07-16"}, EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
]; ).toBe(new Date('2020-07-17').getTime());
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-13").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'},
];
expect(
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
).toBe(new Date('2020-07-13').getTime());
}); });
test('getRelativeDateString', () => { test('getRelativeDateString', () => {
jest.spyOn(Date, 'now') jest
.mockImplementation(() => .spyOn(Date, 'now')
new Date('2020-07-09').getTime() .mockImplementation(() => new Date('2020-07-09').getTime());
); jest.spyOn(i18n, 't').mockImplementation((translationString: string) => {
jest.spyOn(i18n, 't') const prefix = 'screens.equipment.';
.mockImplementation((translationString: string) => { if (translationString === prefix + 'otherYear') return '0';
const prefix = "screens.equipment."; else if (translationString === prefix + 'otherMonth') return '1';
if (translationString === prefix + "otherYear") else if (translationString === prefix + 'thisMonth') return '2';
return "0"; else if (translationString === prefix + 'tomorrow') return '3';
else if (translationString === prefix + "otherMonth") else if (translationString === prefix + 'today') return '4';
return "1"; else return null;
else if (translationString === prefix + "thisMonth") });
return "2"; expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-09'))).toBe(
else if (translationString === prefix + "tomorrow") '4',
return "3"; );
else if (translationString === prefix + "today") expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-10'))).toBe(
return "4"; '3',
else );
return null; expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-11'))).toBe(
} '2',
); );
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-09"))).toBe("4"); expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-30'))).toBe(
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-10"))).toBe("3"); '2',
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-11"))).toBe("2"); );
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-30"))).toBe("2"); expect(EquipmentBooking.getRelativeDateString(new Date('2020-08-30'))).toBe(
expect(EquipmentBooking.getRelativeDateString(new Date("2020-08-30"))).toBe("1"); '1',
expect(EquipmentBooking.getRelativeDateString(new Date("2020-11-10"))).toBe("1"); );
expect(EquipmentBooking.getRelativeDateString(new Date("2021-11-10"))).toBe("0"); expect(EquipmentBooking.getRelativeDateString(new Date('2020-11-10'))).toBe(
'1',
);
expect(EquipmentBooking.getRelativeDateString(new Date('2021-11-10'))).toBe(
'0',
);
}); });
test('getValidRange', () => { test('getValidRange', () => {
let testDevice = { let testDevice = {
id: 1, id: 1,
name: "Petit barbecue", name: 'Petit barbecue',
caution: 100, 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 start = new Date('2020-07-11');
let end = new Date("2020-07-15"); let end = new Date('2020-07-15');
let result = [ let result = [
"2020-07-11", '2020-07-11',
"2020-07-12", '2020-07-12',
"2020-07-13", '2020-07-13',
"2020-07-14", '2020-07-14',
"2020-07-15", '2020-07-15',
]; ];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result); expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
testDevice.booked_at = [ result,
{begin: "2020-07-07", end: "2020-07-10"}, );
{begin: "2020-07-13", end: "2020-07-15"}, testDevice.booked_at = [
]; {begin: '2020-07-07', end: '2020-07-10'},
result = [ {begin: '2020-07-13', end: '2020-07-15'},
"2020-07-11", ];
"2020-07-12", result = ['2020-07-11', '2020-07-12'];
]; expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
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"]; result = ['2020-07-11'];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result); expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-12"},]; result,
result = [ );
"2020-07-13", testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-12'}];
"2020-07-14", result = ['2020-07-13', '2020-07-14', '2020-07-15'];
"2020-07-15", expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
]; result,
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result); );
start = new Date("2020-07-14"); start = new Date('2020-07-14');
end = new Date("2020-07-14"); end = new Date('2020-07-14');
result = [ result = ['2020-07-14'];
"2020-07-14", expect(
]; EquipmentBooking.getValidRange(start, start, testDevice),
expect(EquipmentBooking.getValidRange(start, start, testDevice)).toStrictEqual(result); ).toStrictEqual(result);
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result); expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result); result,
);
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
result,
);
start = new Date("2020-07-14"); start = new Date('2020-07-14');
end = new Date("2020-07-17"); end = new Date('2020-07-17');
result = [ result = ['2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17'];
"2020-07-14", expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
"2020-07-15", result,
"2020-07-16", );
"2020-07-17",
];
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result);
testDevice.booked_at = [{begin: "2020-07-17", end: "2020-07-17"}]; testDevice.booked_at = [{begin: '2020-07-17', end: '2020-07-17'}];
result = [ result = ['2020-07-14', '2020-07-15', '2020-07-16'];
"2020-07-14", expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
"2020-07-15", result,
"2020-07-16", );
];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
testDevice.booked_at = [ testDevice.booked_at = [
{begin: "2020-07-12", end: "2020-07-13"}, {begin: '2020-07-12', end: '2020-07-13'},
{begin: "2020-07-15", end: "2020-07-20"}, {begin: '2020-07-15', end: '2020-07-20'},
]; ];
start = new Date("2020-07-11"); start = new Date('2020-07-11');
end = new Date("2020-07-23"); end = new Date('2020-07-23');
result = [ result = ['2020-07-21', '2020-07-22', '2020-07-23'];
"2020-07-21", expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
"2020-07-22", result,
"2020-07-23", );
];
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
}); });
test('generateMarkedDates', () => { test('generateMarkedDates', () => {
let theme = { let theme = {
colors: { colors: {
primary: "primary", primary: 'primary',
danger: "primary", danger: 'primary',
textDisabled: "primary", textDisabled: 'primary',
} },
} };
let testDevice = { let testDevice = {
id: 1, id: 1,
name: "Petit barbecue", name: 'Petit barbecue',
caution: 100, 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 start = new Date('2020-07-11');
let end = new Date("2020-07-13"); let end = new Date('2020-07-13');
let range = EquipmentBooking.getValidRange(start, end, testDevice); let range = EquipmentBooking.getValidRange(start, end, testDevice);
let result = { let result = {
"2020-07-11": { '2020-07-11': {
startingDay: true, startingDay: true,
endingDay: false, endingDay: false,
color: theme.colors.primary color: theme.colors.primary,
}, },
"2020-07-12": { '2020-07-12': {
startingDay: false, startingDay: false,
endingDay: false, endingDay: false,
color: theme.colors.danger color: theme.colors.danger,
}, },
"2020-07-13": { '2020-07-13': {
startingDay: false, startingDay: false,
endingDay: true, endingDay: true,
color: theme.colors.primary color: theme.colors.primary,
}, },
}; };
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); expect(
result = { EquipmentBooking.generateMarkedDates(true, theme, range),
"2020-07-11": { ).toStrictEqual(result);
startingDay: true, result = {
endingDay: false, '2020-07-11': {
color: theme.colors.textDisabled startingDay: true,
}, endingDay: false,
"2020-07-12": { color: theme.colors.textDisabled,
startingDay: false, },
endingDay: false, '2020-07-12': {
color: theme.colors.textDisabled startingDay: false,
}, endingDay: false,
"2020-07-13": { color: theme.colors.textDisabled,
startingDay: false, },
endingDay: true, '2020-07-13': {
color: theme.colors.textDisabled startingDay: false,
}, endingDay: true,
}; color: theme.colors.textDisabled,
expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result); },
result = { };
"2020-07-11": { expect(
startingDay: true, EquipmentBooking.generateMarkedDates(false, theme, range),
endingDay: false, ).toStrictEqual(result);
color: theme.colors.textDisabled result = {
}, '2020-07-11': {
"2020-07-12": { startingDay: true,
startingDay: false, endingDay: false,
endingDay: false, color: theme.colors.textDisabled,
color: theme.colors.textDisabled },
}, '2020-07-12': {
"2020-07-13": { startingDay: false,
startingDay: false, endingDay: false,
endingDay: true, color: theme.colors.textDisabled,
color: theme.colors.textDisabled },
}, '2020-07-13': {
}; startingDay: false,
range = EquipmentBooking.getValidRange(end, start, testDevice); endingDay: true,
expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result); color: theme.colors.textDisabled,
},
};
range = EquipmentBooking.getValidRange(end, start, testDevice);
expect(
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 = { result = {
"2020-07-11": { '2020-07-11': {
startingDay: true, startingDay: true,
endingDay: false, endingDay: false,
color: theme.colors.primary color: theme.colors.primary,
}, },
"2020-07-12": { '2020-07-12': {
startingDay: false, startingDay: false,
endingDay: true, endingDay: true,
color: theme.colors.primary color: theme.colors.primary,
}, },
}; };
range = EquipmentBooking.getValidRange(start, end, testDevice); range = EquipmentBooking.getValidRange(start, end, testDevice);
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); expect(
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 = { result = {
"2020-07-11": { '2020-07-11': {
startingDay: true, startingDay: true,
endingDay: true, endingDay: true,
color: theme.colors.primary color: theme.colors.primary,
}, },
}; };
range = EquipmentBooking.getValidRange(start, end, testDevice); range = EquipmentBooking.getValidRange(start, end, testDevice);
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); expect(
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
testDevice.booked_at = [ testDevice.booked_at = [
{begin: "2020-07-12", end: "2020-07-13"}, {begin: '2020-07-12', end: '2020-07-13'},
{begin: "2020-07-15", end: "2020-07-20"}, {begin: '2020-07-15', end: '2020-07-20'},
]; ];
start = new Date("2020-07-11"); start = new Date('2020-07-11');
end = new Date("2020-07-23"); end = new Date('2020-07-23');
result = { result = {
"2020-07-11": { '2020-07-11': {
startingDay: true, startingDay: true,
endingDay: true, endingDay: true,
color: theme.colors.primary color: theme.colors.primary,
}, },
}; };
range = EquipmentBooking.getValidRange(start, end, testDevice); range = EquipmentBooking.getValidRange(start, end, testDevice);
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); expect(
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
result = { result = {
"2020-07-21": { '2020-07-21': {
startingDay: true, startingDay: true,
endingDay: false, endingDay: false,
color: theme.colors.primary color: theme.colors.primary,
}, },
"2020-07-22": { '2020-07-22': {
startingDay: false, startingDay: false,
endingDay: false, endingDay: false,
color: theme.colors.danger color: theme.colors.danger,
}, },
"2020-07-23": { '2020-07-23': {
startingDay: false, startingDay: false,
endingDay: true, endingDay: true,
color: theme.colors.primary color: theme.colors.primary,
}, },
}; };
range = EquipmentBooking.getValidRange(end, start, testDevice); range = EquipmentBooking.getValidRange(end, start, testDevice);
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); expect(
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
}); });

View file

@ -1,210 +1,222 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import * as Planning from "../../src/utils/Planning"; import * as Planning from '../../src/utils/Planning';
test('isDescriptionEmpty', () => { test('isDescriptionEmpty', () => {
expect(Planning.isDescriptionEmpty("")).toBeTrue(); expect(Planning.isDescriptionEmpty('')).toBeTrue();
expect(Planning.isDescriptionEmpty(" ")).toBeTrue(); expect(Planning.isDescriptionEmpty(' ')).toBeTrue();
// noinspection CheckTagEmptyBody // noinspection CheckTagEmptyBody
expect(Planning.isDescriptionEmpty("<p></p>")).toBeTrue(); expect(Planning.isDescriptionEmpty('<p></p>')).toBeTrue();
expect(Planning.isDescriptionEmpty("<p> </p>")).toBeTrue(); expect(Planning.isDescriptionEmpty('<p> </p>')).toBeTrue();
expect(Planning.isDescriptionEmpty("<p><br></p>")).toBeTrue(); expect(Planning.isDescriptionEmpty('<p><br></p>')).toBeTrue();
expect(Planning.isDescriptionEmpty("<p><br></p><p><br></p>")).toBeTrue(); expect(Planning.isDescriptionEmpty('<p><br></p><p><br></p>')).toBeTrue();
expect(Planning.isDescriptionEmpty("<p><br><br><br></p>")).toBeTrue(); expect(Planning.isDescriptionEmpty('<p><br><br><br></p>')).toBeTrue();
expect(Planning.isDescriptionEmpty("<p><br>")).toBeTrue(); expect(Planning.isDescriptionEmpty('<p><br>')).toBeTrue();
expect(Planning.isDescriptionEmpty(null)).toBeTrue(); expect(Planning.isDescriptionEmpty(null)).toBeTrue();
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue(); expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
expect(Planning.isDescriptionEmpty("coucou")).toBeFalse(); expect(Planning.isDescriptionEmpty('coucou')).toBeFalse();
expect(Planning.isDescriptionEmpty("<p>coucou</p>")).toBeFalse(); expect(Planning.isDescriptionEmpty('<p>coucou</p>')).toBeFalse();
}); });
test('isEventDateStringFormatValid', () => { test('isEventDateStringFormatValid', () => {
expect(Planning.isEventDateStringFormatValid("2020-03-21 09:00")).toBeTrue(); expect(Planning.isEventDateStringFormatValid('2020-03-21 09:00')).toBeTrue();
expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16")).toBeTrue(); expect(Planning.isEventDateStringFormatValid('3214-64-12 01:16')).toBeTrue();
expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16:00")).toBeFalse(); expect(
expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16")).toBeFalse(); Planning.isEventDateStringFormatValid('3214-64-12 01:16:00'),
expect(Planning.isEventDateStringFormatValid("3214-f4-12 01:16")).toBeFalse(); ).toBeFalse();
expect(Planning.isEventDateStringFormatValid("sqdd 09:00")).toBeFalse(); expect(Planning.isEventDateStringFormatValid('3214-64-12 1:16')).toBeFalse();
expect(Planning.isEventDateStringFormatValid("2020-03-21")).toBeFalse(); expect(Planning.isEventDateStringFormatValid('3214-f4-12 01:16')).toBeFalse();
expect(Planning.isEventDateStringFormatValid("2020-03-21 truc")).toBeFalse(); expect(Planning.isEventDateStringFormatValid('sqdd 09:00')).toBeFalse();
expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16:65")).toBeFalse(); expect(Planning.isEventDateStringFormatValid('2020-03-21')).toBeFalse();
expect(Planning.isEventDateStringFormatValid("garbage")).toBeFalse(); expect(Planning.isEventDateStringFormatValid('2020-03-21 truc')).toBeFalse();
expect(Planning.isEventDateStringFormatValid("")).toBeFalse(); expect(
expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse(); Planning.isEventDateStringFormatValid('3214-64-12 1:16:65'),
expect(Planning.isEventDateStringFormatValid(null)).toBeFalse(); ).toBeFalse();
expect(Planning.isEventDateStringFormatValid('garbage')).toBeFalse();
expect(Planning.isEventDateStringFormatValid('')).toBeFalse();
expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse();
expect(Planning.isEventDateStringFormatValid(null)).toBeFalse();
}); });
test('stringToDate', () => { test('stringToDate', () => {
let testDate = new Date(); let testDate = new Date();
expect(Planning.stringToDate(undefined)).toBeNull(); expect(Planning.stringToDate(undefined)).toBeNull();
expect(Planning.stringToDate("")).toBeNull(); expect(Planning.stringToDate('')).toBeNull();
expect(Planning.stringToDate("garbage")).toBeNull(); expect(Planning.stringToDate('garbage')).toBeNull();
expect(Planning.stringToDate("2020-03-21")).toBeNull(); expect(Planning.stringToDate('2020-03-21')).toBeNull();
expect(Planning.stringToDate("09:00:00")).toBeNull(); expect(Planning.stringToDate('09:00:00')).toBeNull();
expect(Planning.stringToDate("2020-03-21 09:g0")).toBeNull(); expect(Planning.stringToDate('2020-03-21 09:g0')).toBeNull();
expect(Planning.stringToDate("2020-03-21 09:g0:")).toBeNull(); expect(Planning.stringToDate('2020-03-21 09:g0:')).toBeNull();
testDate.setFullYear(2020, 2, 21); testDate.setFullYear(2020, 2, 21);
testDate.setHours(9, 0, 0, 0); testDate.setHours(9, 0, 0, 0);
expect(Planning.stringToDate("2020-03-21 09:00")).toEqual(testDate); expect(Planning.stringToDate('2020-03-21 09:00')).toEqual(testDate);
testDate.setFullYear(2020, 0, 31); testDate.setFullYear(2020, 0, 31);
testDate.setHours(18, 30, 0, 0); testDate.setHours(18, 30, 0, 0);
expect(Planning.stringToDate("2020-01-31 18:30")).toEqual(testDate); expect(Planning.stringToDate('2020-01-31 18:30')).toEqual(testDate);
testDate.setFullYear(2020, 50, 50); testDate.setFullYear(2020, 50, 50);
testDate.setHours(65, 65, 0, 0); testDate.setHours(65, 65, 0, 0);
expect(Planning.stringToDate("2020-51-50 65:65")).toEqual(testDate); expect(Planning.stringToDate('2020-51-50 65:65')).toEqual(testDate);
}); });
test('getFormattedEventTime', () => { test('getFormattedEventTime', () => {
expect(Planning.getFormattedEventTime(null, null)) expect(Planning.getFormattedEventTime(null, null)).toBe('/ - /');
.toBe('/ - /'); expect(Planning.getFormattedEventTime(undefined, undefined)).toBe('/ - /');
expect(Planning.getFormattedEventTime(undefined, undefined)) expect(Planning.getFormattedEventTime('20:30', '23:00')).toBe('/ - /');
.toBe('/ - /'); expect(Planning.getFormattedEventTime('2020-03-30', '2020-03-31')).toBe(
expect(Planning.getFormattedEventTime("20:30", "23:00")) '/ - /',
.toBe('/ - /'); );
expect(Planning.getFormattedEventTime("2020-03-30", "2020-03-31"))
.toBe('/ - /');
expect(
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'); ).toBe('09:00');
expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-22 17:00")) expect(
.toBe('09:00 - 23:59'); Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-22 17:00'),
expect(Planning.getFormattedEventTime("2020-03-30 20:30", "2020-03-30 23:00")) ).toBe('09:00 - 23:59');
.toBe('20:30 - 23:00'); expect(
Planning.getFormattedEventTime('2020-03-30 20:30', '2020-03-30 23:00'),
).toBe('20:30 - 23:00');
}); });
test('getDateOnlyString', () => { test('getDateOnlyString', () => {
expect(Planning.getDateOnlyString("2020-03-21 09:00")).toBe("2020-03-21"); expect(Planning.getDateOnlyString('2020-03-21 09:00')).toBe('2020-03-21');
expect(Planning.getDateOnlyString("2021-12-15 09:00")).toBe("2021-12-15"); expect(Planning.getDateOnlyString('2021-12-15 09:00')).toBe('2021-12-15');
expect(Planning.getDateOnlyString("2021-12-o5 09:00")).toBeNull(); expect(Planning.getDateOnlyString('2021-12-o5 09:00')).toBeNull();
expect(Planning.getDateOnlyString("2021-12-15 09:")).toBeNull(); expect(Planning.getDateOnlyString('2021-12-15 09:')).toBeNull();
expect(Planning.getDateOnlyString("2021-12-15")).toBeNull(); expect(Planning.getDateOnlyString('2021-12-15')).toBeNull();
expect(Planning.getDateOnlyString("garbage")).toBeNull(); expect(Planning.getDateOnlyString('garbage')).toBeNull();
}); });
test('isEventBefore', () => { test('isEventBefore', () => {
expect(Planning.isEventBefore( expect(
"2020-03-21 09:00", "2020-03-21 10:00")).toBeTrue(); Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00'),
expect(Planning.isEventBefore( ).toBeTrue();
"2020-03-21 10:00", "2020-03-21 10:15")).toBeTrue(); expect(
expect(Planning.isEventBefore( Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:15'),
"2020-03-21 10:15", "2021-03-21 10:15")).toBeTrue(); ).toBeTrue();
expect(Planning.isEventBefore( expect(
"2020-03-21 10:15", "2020-05-21 10:15")).toBeTrue(); Planning.isEventBefore('2020-03-21 10:15', '2021-03-21 10:15'),
expect(Planning.isEventBefore( ).toBeTrue();
"2020-03-21 10:15", "2020-03-30 10:15")).toBeTrue(); expect(
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'),
).toBeTrue();
expect(Planning.isEventBefore( expect(
"2020-03-21 10:00", "2020-03-21 10:00")).toBeFalse(); Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:00'),
expect(Planning.isEventBefore( ).toBeFalse();
"2020-03-21 10:00", "2020-03-21 09:00")).toBeFalse(); expect(
expect(Planning.isEventBefore( Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 09:00'),
"2020-03-21 10:15", "2020-03-21 10:00")).toBeFalse(); ).toBeFalse();
expect(Planning.isEventBefore( expect(
"2021-03-21 10:15", "2020-03-21 10:15")).toBeFalse(); Planning.isEventBefore('2020-03-21 10:15', '2020-03-21 10:00'),
expect(Planning.isEventBefore( ).toBeFalse();
"2020-05-21 10:15", "2020-03-21 10:15")).toBeFalse(); expect(
expect(Planning.isEventBefore( Planning.isEventBefore('2021-03-21 10:15', '2020-03-21 10:15'),
"2020-03-30 10:15", "2020-03-21 10:15")).toBeFalse(); ).toBeFalse();
expect(
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'),
).toBeFalse();
expect(Planning.isEventBefore( expect(Planning.isEventBefore('garbage', '2020-03-21 10:15')).toBeFalse();
"garbage", "2020-03-21 10:15")).toBeFalse(); expect(Planning.isEventBefore(undefined, undefined)).toBeFalse();
expect(Planning.isEventBefore(
undefined, undefined)).toBeFalse();
}); });
test('dateToString', () => { test('dateToString', () => {
let testDate = new Date(); let testDate = new Date();
testDate.setFullYear(2020, 2, 21); testDate.setFullYear(2020, 2, 21);
testDate.setHours(9, 0, 0, 0); testDate.setHours(9, 0, 0, 0);
expect(Planning.dateToString(testDate)).toBe("2020-03-21 09:00"); expect(Planning.dateToString(testDate)).toBe('2020-03-21 09:00');
testDate.setFullYear(2021, 0, 12); testDate.setFullYear(2021, 0, 12);
testDate.setHours(9, 10, 0, 0); testDate.setHours(9, 10, 0, 0);
expect(Planning.dateToString(testDate)).toBe("2021-01-12 09:10"); expect(Planning.dateToString(testDate)).toBe('2021-01-12 09:10');
testDate.setFullYear(2022, 11, 31); testDate.setFullYear(2022, 11, 31);
testDate.setHours(9, 10, 15, 0); testDate.setHours(9, 10, 15, 0);
expect(Planning.dateToString(testDate)).toBe("2022-12-31 09:10"); expect(Planning.dateToString(testDate)).toBe('2022-12-31 09:10');
}); });
test('generateEmptyCalendar', () => { test('generateEmptyCalendar', () => {
jest.spyOn(Date, 'now') jest
.mockImplementation(() => .spyOn(Date, 'now')
new Date('2020-01-14T00:00:00.000Z').getTime() .mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
); let calendar = Planning.generateEmptyCalendar(1);
let calendar = Planning.generateEmptyCalendar(1); expect(calendar).toHaveProperty('2020-01-14');
expect(calendar).toHaveProperty("2020-01-14"); expect(calendar).toHaveProperty('2020-01-20');
expect(calendar).toHaveProperty("2020-01-20"); expect(calendar).toHaveProperty('2020-02-10');
expect(calendar).toHaveProperty("2020-02-10"); expect(Object.keys(calendar).length).toBe(32);
expect(Object.keys(calendar).length).toBe(32); calendar = Planning.generateEmptyCalendar(3);
calendar = Planning.generateEmptyCalendar(3); expect(calendar).toHaveProperty('2020-01-14');
expect(calendar).toHaveProperty("2020-01-14"); expect(calendar).toHaveProperty('2020-01-20');
expect(calendar).toHaveProperty("2020-01-20"); expect(calendar).toHaveProperty('2020-02-10');
expect(calendar).toHaveProperty("2020-02-10"); expect(calendar).toHaveProperty('2020-02-14');
expect(calendar).toHaveProperty("2020-02-14"); expect(calendar).toHaveProperty('2020-03-20');
expect(calendar).toHaveProperty("2020-03-20"); expect(calendar).toHaveProperty('2020-04-12');
expect(calendar).toHaveProperty("2020-04-12"); expect(Object.keys(calendar).length).toBe(92);
expect(Object.keys(calendar).length).toBe(92);
}); });
test('pushEventInOrder', () => { test('pushEventInOrder', () => {
let eventArray = []; let eventArray = [];
let event1 = {date_begin: "2020-01-14 09:15"}; let event1 = {date_begin: '2020-01-14 09:15'};
Planning.pushEventInOrder(eventArray, event1); Planning.pushEventInOrder(eventArray, event1);
expect(eventArray.length).toBe(1); expect(eventArray.length).toBe(1);
expect(eventArray[0]).toBe(event1); 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); Planning.pushEventInOrder(eventArray, event2);
expect(eventArray.length).toBe(2); expect(eventArray.length).toBe(2);
expect(eventArray[0]).toBe(event1); expect(eventArray[0]).toBe(event1);
expect(eventArray[1]).toBe(event2); 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); Planning.pushEventInOrder(eventArray, event3);
expect(eventArray.length).toBe(3); expect(eventArray.length).toBe(3);
expect(eventArray[0]).toBe(event1); expect(eventArray[0]).toBe(event1);
expect(eventArray[1]).toBe(event2); expect(eventArray[1]).toBe(event2);
expect(eventArray[2]).toBe(event3); 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); Planning.pushEventInOrder(eventArray, event4);
expect(eventArray.length).toBe(4); expect(eventArray.length).toBe(4);
expect(eventArray[0]).toBe(event4); expect(eventArray[0]).toBe(event4);
expect(eventArray[1]).toBe(event1); expect(eventArray[1]).toBe(event1);
expect(eventArray[2]).toBe(event2); expect(eventArray[2]).toBe(event2);
expect(eventArray[3]).toBe(event3); expect(eventArray[3]).toBe(event3);
}); });
test('generateEventAgenda', () => { test('generateEventAgenda', () => {
jest.spyOn(Date, 'now') jest
.mockImplementation(() => .spyOn(Date, 'now')
new Date('2020-01-14T00:00:00.000Z').getTime() .mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
); let eventList = [
let eventList = [ {date_begin: '2020-01-14 09:15'},
{date_begin: "2020-01-14 09:15"}, {date_begin: '2020-02-01 09:15'},
{date_begin: "2020-02-01 09:15"}, {date_begin: '2020-01-15 09:15'},
{date_begin: "2020-01-15 09:15"}, {date_begin: '2020-02-01 09:30'},
{date_begin: "2020-02-01 09:30"}, {date_begin: '2020-02-01 08:30'},
{date_begin: "2020-02-01 08:30"}, ];
]; const calendar = Planning.generateEventAgenda(eventList, 2);
const calendar = Planning.generateEventAgenda(eventList, 2); expect(calendar['2020-01-14'].length).toBe(1);
expect(calendar["2020-01-14"].length).toBe(1); expect(calendar['2020-01-14'][0]).toBe(eventList[0]);
expect(calendar["2020-01-14"][0]).toBe(eventList[0]); expect(calendar['2020-01-15'].length).toBe(1);
expect(calendar["2020-01-15"].length).toBe(1); expect(calendar['2020-01-15'][0]).toBe(eventList[2]);
expect(calendar["2020-01-15"][0]).toBe(eventList[2]); expect(calendar['2020-02-01'].length).toBe(3);
expect(calendar["2020-02-01"].length).toBe(3); expect(calendar['2020-02-01'][0]).toBe(eventList[4]);
expect(calendar["2020-02-01"][0]).toBe(eventList[4]); expect(calendar['2020-02-01'][1]).toBe(eventList[1]);
expect(calendar["2020-02-01"][1]).toBe(eventList[1]); expect(calendar['2020-02-01'][2]).toBe(eventList[3]);
expect(calendar["2020-02-01"][2]).toBe(eventList[3]);
}); });
test('getCurrentDateString', () => { test('getCurrentDateString', () => {
jest.spyOn(Date, 'now') jest.spyOn(Date, 'now').mockImplementation(() => {
.mockImplementation(() => { let date = new Date();
let date = new Date(); date.setFullYear(2020, 0, 14);
date.setFullYear(2020, 0, 14); date.setHours(15, 30, 54, 65);
date.setHours(15, 30, 54, 65); return date.getTime();
return date.getTime(); });
}); expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30');
expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30');
}); });

View file

@ -1,142 +1,167 @@
/* eslint-disable */
import React from 'react'; import React from 'react';
import {getCleanedMachineWatched, getMachineEndDate, getMachineOfId, isMachineWatched} from "../../src/utils/Proxiwash"; import {
getCleanedMachineWatched,
getMachineEndDate,
getMachineOfId,
isMachineWatched,
} from '../../src/utils/Proxiwash';
test('getMachineEndDate', () => { test('getMachineEndDate', () => {
jest.spyOn(Date, 'now') jest
.mockImplementation(() => .spyOn(Date, 'now')
new Date('2020-01-14T15:00:00.000Z').getTime() .mockImplementation(() => new Date('2020-01-14T15:00:00.000Z').getTime());
); let expectDate = new Date('2020-01-14T15:00:00.000Z');
let expectDate = new Date('2020-01-14T15:00:00.000Z'); expectDate.setHours(23);
expectDate.setHours(23); expectDate.setMinutes(10);
expectDate.setMinutes(10); expect(getMachineEndDate({endTime: '23:10'}).getTime()).toBe(
expect(getMachineEndDate({endTime: "23:10"}).getTime()).toBe(expectDate.getTime()); expectDate.getTime(),
);
expectDate.setHours(16); expectDate.setHours(16);
expectDate.setMinutes(30); 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') jest
.mockImplementation(() => .spyOn(Date, 'now')
new Date('2020-01-14T23:00:00.000Z').getTime() .mockImplementation(() => new Date('2020-01-14T23:00:00.000Z').getTime());
); expectDate = new Date('2020-01-14T23:00:00.000Z');
expectDate = new Date('2020-01-14T23:00:00.000Z'); expectDate.setHours(0);
expectDate.setHours(0); expectDate.setMinutes(30);
expectDate.setMinutes(30); expect(getMachineEndDate({endTime: '00:30'}).getTime()).toBe(
expect(getMachineEndDate({endTime: "00:30"}).getTime()).toBe(expectDate.getTime()); expectDate.getTime(),
);
}); });
test('isMachineWatched', () => { test('isMachineWatched', () => {
let machineList = [ let machineList = [
{ {
number: "0", number: '0',
endTime: "23:30", endTime: '23:30',
}, },
{ {
number: "1", number: '1',
endTime: "20:30", endTime: '20:30',
}, },
]; ];
expect(isMachineWatched({number: "0", endTime: "23:30"}, machineList)).toBeTrue(); expect(
expect(isMachineWatched({number: "1", endTime: "20:30"}, machineList)).toBeTrue(); isMachineWatched({number: '0', endTime: '23:30'}, machineList),
expect(isMachineWatched({number: "3", endTime: "20:30"}, machineList)).toBeFalse(); ).toBeTrue();
expect(isMachineWatched({number: "1", endTime: "23:30"}, machineList)).toBeFalse(); expect(
isMachineWatched({number: '1', endTime: '20:30'}, machineList),
).toBeTrue();
expect(
isMachineWatched({number: '3', endTime: '20:30'}, machineList),
).toBeFalse();
expect(
isMachineWatched({number: '1', endTime: '23:30'}, machineList),
).toBeFalse();
}); });
test('getMachineOfId', () => { test('getMachineOfId', () => {
let machineList = [ let machineList = [
{ {
number: "0", number: '0',
}, },
{ {
number: "1", number: '1',
}, },
]; ];
expect(getMachineOfId("0", machineList)).toStrictEqual({number: "0"}); expect(getMachineOfId('0', machineList)).toStrictEqual({number: '0'});
expect(getMachineOfId("1", machineList)).toStrictEqual({number: "1"}); expect(getMachineOfId('1', machineList)).toStrictEqual({number: '1'});
expect(getMachineOfId("3", machineList)).toBeNull(); expect(getMachineOfId('3', machineList)).toBeNull();
}); });
test('getCleanedMachineWatched', () => { test('getCleanedMachineWatched', () => {
let machineList = [ let machineList = [
{ {
number: "0", number: '0',
endTime: "23:30", endTime: '23:30',
}, },
{ {
number: "1", number: '1',
endTime: "20:30", endTime: '20:30',
}, },
{ {
number: "2", number: '2',
endTime: "", endTime: '',
}, },
]; ];
let watchList = [ let watchList = [
{ {
number: "0", number: '0',
endTime: "23:30", endTime: '23:30',
}, },
{ {
number: "1", number: '1',
endTime: "20:30", endTime: '20:30',
}, },
{ {
number: "2", number: '2',
endTime: "", endTime: '',
}, },
]; ];
let cleanedList = watchList; let cleanedList = watchList;
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList); expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
cleanedList,
);
watchList = [ watchList = [
{ {
number: "0", number: '0',
endTime: "23:30", endTime: '23:30',
}, },
{ {
number: "1", number: '1',
endTime: "20:30", endTime: '20:30',
}, },
{ {
number: "2", number: '2',
endTime: "15:30", endTime: '15:30',
}, },
]; ];
cleanedList = [ cleanedList = [
{ {
number: "0", number: '0',
endTime: "23:30", endTime: '23:30',
}, },
{ {
number: "1", number: '1',
endTime: "20:30", endTime: '20:30',
}, },
]; ];
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList); expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
cleanedList,
);
watchList = [ watchList = [
{ {
number: "0", number: '0',
endTime: "23:30", endTime: '23:30',
}, },
{ {
number: "1", number: '1',
endTime: "20:31", endTime: '20:31',
}, },
{ {
number: "3", number: '3',
endTime: "15:30", endTime: '15:30',
}, },
]; ];
cleanedList = [ cleanedList = [
{ {
number: "0", number: '0',
endTime: "23:30", endTime: '23:30',
}, },
]; ];
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList); expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
cleanedList,
);
}); });

View file

@ -1,45 +0,0 @@
import React from 'react';
import {isResponseValid} from "../../src/utils/WebData";
let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
test('isRequestResponseValid', () => {
let json = {
error: 0,
data: {}
};
expect(isResponseValid(json)).toBeTrue();
json = {
error: 1,
data: {}
};
expect(isResponseValid(json)).toBeTrue();
json = {
error: 50,
data: {}
};
expect(isResponseValid(json)).toBeTrue();
json = {
error: 50,
data: {truc: 'machin'}
};
expect(isResponseValid(json)).toBeTrue();
json = {
message: 'coucou'
};
expect(isResponseValid(json)).toBeFalse();
json = {
error: 'coucou',
data: {truc: 'machin'}
};
expect(isResponseValid(json)).toBeFalse();
json = {
error: 0,
data: 'coucou'
};
expect(isResponseValid(json)).toBeFalse();
json = {
error: 0,
};
expect(isResponseValid(json)).toBeFalse();
});

View file

@ -0,0 +1,47 @@
/* eslint-disable */
import React from 'react';
import {isApiResponseValid} from '../../src/utils/WebData';
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
test('isRequestResponseValid', () => {
let json = {
error: 0,
data: {},
};
expect(isApiResponseValid(json)).toBeTrue();
json = {
error: 1,
data: {},
};
expect(isApiResponseValid(json)).toBeTrue();
json = {
error: 50,
data: {},
};
expect(isApiResponseValid(json)).toBeTrue();
json = {
error: 50,
data: {truc: 'machin'},
};
expect(isApiResponseValid(json)).toBeTrue();
json = {
message: 'coucou',
};
expect(isApiResponseValid(json)).toBeFalse();
json = {
error: 'coucou',
data: {truc: 'machin'},
};
expect(isApiResponseValid(json)).toBeFalse();
json = {
error: 0,
data: 'coucou',
};
expect(isApiResponseValid(json)).toBeFalse();
json = {
error: 0,
};
expect(isApiResponseValid(json)).toBeFalse();
});

View file

@ -136,8 +136,8 @@ android {
applicationId 'fr.amicaleinsat.application' applicationId 'fr.amicaleinsat.application'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 32 versionCode 34
versionName "3.1.4" versionName "4.0.1"
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
splits { splits {

View file

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

View file

@ -6,4 +6,5 @@ import {AppRegistry} from 'react-native';
import App from './App'; 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); AppRegistry.registerComponent(appName, () => App);

View file

@ -6,12 +6,13 @@
*/ */
module.exports = { module.exports = {
transformer: { transformer: {
getTransformOptions: async () => ({ // eslint-disable-next-line flowtype/require-return-type
transform: { getTransformOptions: async () => ({
experimentalImportSupport: false, transform: {
inlineRequires: false, experimentalImportSupport: false,
}, inlineRequires: false,
}), },
}, }),
},
}; };

View file

@ -1,219 +1,205 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import ConnectionManager from "../../managers/ConnectionManager"; import {StackNavigationProp} from '@react-navigation/stack';
import {ERROR_TYPE} from "../../utils/WebData"; import ConnectionManager from '../../managers/ConnectionManager';
import ErrorView from "../Screens/ErrorView"; import type {ApiGenericDataType} from '../../utils/WebData';
import BasicLoadingScreen from "../Screens/BasicLoadingScreen"; import {ERROR_TYPE} from '../../utils/WebData';
import {StackNavigationProp} from "@react-navigation/stack"; import ErrorView from '../Screens/ErrorView';
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
requests: Array<{ requests: Array<{
link: string, link: string,
params: Object, params: {...},
mandatory: boolean mandatory: boolean,
}>, }>,
renderFunction: (Array<{ [key: string]: any } | null>) => React.Node, renderFunction: (Array<ApiGenericDataType | null>) => React.Node,
errorViewOverride?: Array<{ errorViewOverride?: Array<{
errorCode: number, errorCode: number,
message: string, message: string,
icon: string, icon: string,
showRetryButton: boolean showRetryButton: boolean,
}>, }> | null,
} };
type State = { type StateType = {
loading: boolean, loading: boolean,
} };
class AuthenticatedScreen extends React.Component<Props, State> { class AuthenticatedScreen extends React.Component<PropsType, StateType> {
static defaultProps = {
errorViewOverride: null,
};
state = { currentUserToken: string | null;
loading: true,
connectionManager: ConnectionManager;
errors: Array<number>;
fetchedData: Array<ApiGenericDataType | null>;
constructor(props: PropsType) {
super(props);
this.state = {
loading: true,
}; };
this.connectionManager = ConnectionManager.getInstance();
props.navigation.addListener('focus', this.onScreenFocus);
this.fetchedData = new Array(props.requests.length);
this.errors = new Array(props.requests.length);
}
currentUserToken: string | null; /**
connectionManager: ConnectionManager; * Refreshes screen if user changed
errors: Array<number>; */
fetchedData: Array<{ [key: string]: any } | null>; onScreenFocus = () => {
if (this.currentUserToken !== this.connectionManager.getToken()) {
this.currentUserToken = this.connectionManager.getToken();
this.fetchData();
}
};
constructor(props: Object) { /**
super(props); * Callback used when a request finishes, successfully or not.
this.connectionManager = ConnectionManager.getInstance(); * Saves data and error code.
this.props.navigation.addListener('focus', this.onScreenFocus); * If the token is invalid, logout the user and open the login screen.
this.fetchedData = new Array(this.props.requests.length); * If the last request was received, stop the loading screen.
this.errors = new Array(this.props.requests.length); *
* @param data The data fetched from the server
* @param index The index for the data
* @param error The error code received
*/
onRequestFinished(
data: ApiGenericDataType | 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(): React.Node {
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) {
* Refreshes screen if user changed return (
*/ <ErrorView
onScreenFocus = () => { icon={override.icon}
if (this.currentUserToken !== this.connectionManager.getToken()) { message={override.message}
this.currentUserToken = this.connectionManager.getToken(); showRetryButton={override.showRetryButton}
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 = () => {
if (!this.state.loading)
this.setState({loading: true});
if (this.connectionManager.isLoggedIn()) {
for (let i = 0; i < this.props.requests.length; i++) {
this.connectionManager.authenticatedRequest(
this.props.requests[i].link,
this.props.requests[i].params)
.then((data) => {
this.onRequestFinished(data, i, -1);
})
.catch((error) => {
this.onRequestFinished(null, i, error);
});
}
} else {
for (let i = 0; i < this.props.requests.length; i++) {
this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN);
}
}
};
/**
* 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: { [key: string]: any } | null, index: number, error: number) {
if (index >= 0 && index < this.props.requests.length) {
this.fetchedData[index] = data;
this.errors[index] = error;
}
if (error === ERROR_TYPE.BAD_TOKEN) // Token expired, logout user
this.connectionManager.disconnect();
if (this.allRequestsFinished())
this.setState({loading: false});
} }
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
}
/** /**
* Checks if all requests finished processing * Fetches the data from the server.
* *
* @return {boolean} True if all finished * If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
*/ *
allRequestsFinished() { * If the user is logged in, send all requests.
let finished = true; */
for (let i = 0; i < this.fetchedData.length; i++) { fetchData = () => {
if (this.fetchedData[i] === undefined) { const {state, props} = this;
finished = false; if (!state.loading) this.setState({loading: true});
break;
} if (this.connectionManager.isLoggedIn()) {
} for (let i = 0; i < props.requests.length; i += 1) {
return finished; this.connectionManager
.authenticatedRequest(
props.requests[i].link,
props.requests[i].params,
)
.then((response: ApiGenericDataType): 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 have finished successfully. * Checks if all requests finished processing
* This will return false only if a mandatory request failed. *
* All non-mandatory requests can fail without impacting the return value. * @return {boolean} True if all finished
* */
* @return {boolean} True if all finished successfully allRequestsFinished(): boolean {
*/ let finished = true;
allRequestsValid() { this.errors.forEach((error: number | null) => {
let valid = true; if (error == null) finished = false;
for (let i = 0; i < this.fetchedData.length; i++) { });
if (this.fetchedData[i] === null && this.props.requests[i].mandatory) { return finished;
valid = false; }
break;
}
}
return valid;
}
/** /**
* Gets the error to render. * Reloads the data, to be called using ref by parent components
* Non-mandatory requests are ignored. */
* reload() {
* this.fetchData();
* @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found }
*/
getError() {
for (let i = 0; i < this.errors.length; i++) {
if (this.errors[i] !== 0 && this.props.requests[i].mandatory) {
return this.errors[i];
}
}
return ERROR_TYPE.SUCCESS;
}
/** render(): React.Node {
* Gets the error view to display in case of error const {state, props} = this;
* if (state.loading) return <BasicLoadingScreen />;
* @return {*} if (this.getError() === ERROR_TYPE.SUCCESS)
*/ return props.renderFunction(this.fetchedData);
getErrorRender() { return this.getErrorRender();
const errorCode = this.getError(); }
let shouldOverride = false;
let override = null;
const overrideList = this.props.errorViewOverride;
if (overrideList != null) {
for (let i = 0; i < overrideList.length; i++) {
if (overrideList[i].errorCode === errorCode) {
shouldOverride = true;
override = overrideList[i];
break;
}
}
}
if (shouldOverride && override != null) {
return (
<ErrorView
{...this.props}
icon={override.icon}
message={override.message}
showRetryButton={override.showRetryButton}
/>
);
} else {
return (
<ErrorView
{...this.props}
errorCode={errorCode}
onRefresh={this.fetchData}
/>
);
}
}
/**
* Reloads the data, to be called using ref by parent components
*/
reload() {
this.fetchData();
}
render() {
return (
this.state.loading
? <BasicLoadingScreen/>
: (this.allRequestsValid()
? this.props.renderFunction(this.fetchedData)
: this.getErrorRender())
);
}
} }
export default AuthenticatedScreen; export default AuthenticatedScreen;

View file

@ -2,45 +2,46 @@
import * as React from 'react'; import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import LoadingConfirmDialog from "../Dialogs/LoadingConfirmDialog"; import {StackNavigationProp} from '@react-navigation/stack';
import ConnectionManager from "../../managers/ConnectionManager"; import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
import {StackNavigationProp} from "@react-navigation/stack"; import ConnectionManager from '../../managers/ConnectionManager';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
visible: boolean, visible: boolean,
onDismiss: () => void, onDismiss: () => void,
} };
class LogoutDialog extends React.PureComponent<Props> { class LogoutDialog extends React.PureComponent<PropsType> {
onClickAccept = async (): Promise<void> => {
onClickAccept = async () => { const {props} = this;
return new Promise((resolve) => { return new Promise((resolve: () => void) => {
ConnectionManager.getInstance().disconnect() ConnectionManager.getInstance()
.then(() => { .disconnect()
this.props.navigation.reset({ .then(() => {
index: 0, props.navigation.reset({
routes: [{name: 'main'}], index: 0,
}); routes: [{name: 'main'}],
this.props.onDismiss(); });
resolve(); props.onDismiss();
}); resolve();
}); });
}; });
};
render() { render(): React.Node {
return ( const {props} = this;
<LoadingConfirmDialog return (
{...this.props} <LoadingConfirmDialog
visible={this.props.visible} visible={props.visible}
onDismiss={this.props.onDismiss} onDismiss={props.onDismiss}
onAccept={this.onClickAccept} onAccept={this.onClickAccept}
title={i18n.t("dialog.disconnect.title")} title={i18n.t('dialog.disconnect.title')}
titleLoading={i18n.t("dialog.disconnect.titleLoading")} titleLoading={i18n.t('dialog.disconnect.titleLoading')}
message={i18n.t("dialog.disconnect.message")} message={i18n.t('dialog.disconnect.message')}
/> />
); );
} }
} }
export default LogoutDialog; export default LogoutDialog;

View file

@ -2,36 +2,38 @@
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from 'react-native';
import {Headline, withTheme} from "react-native-paper"; import {Headline, withTheme} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
type Props = { type PropsType = {
theme: CustomTheme theme: CustomThemeType,
} };
class VoteNotAvailable extends React.Component<Props> { class VoteNotAvailable extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
shouldComponentUpdate() { render(): React.Node {
return false; const {props} = this;
} return (
<View
render() { style={{
return ( width: '100%',
<View style={{ marginTop: 10,
width: "100%", marginBottom: 10,
marginTop: 10, }}>
marginBottom: 10, <Headline
}}> style={{
<Headline color: props.theme.colors.textDisabled,
style={{ textAlign: 'center',
color: this.props.theme.colors.textDisabled, }}>
textAlign: "center", {i18n.t('screens.vote.noVote')}
}} </Headline>
>{i18n.t("screens.vote.noVote")}</Headline> </View>
</View> );
); }
}
} }
export default withTheme(VoteNotAvailable); export default withTheme(VoteNotAvailable);

View file

@ -1,116 +1,134 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Card, List, ProgressBar, Subheading, withTheme} from "react-native-paper"; import {
import {FlatList, StyleSheet} from "react-native"; Avatar,
Card,
List,
ProgressBar,
Subheading,
withTheme,
} from 'react-native-paper';
import {FlatList, StyleSheet} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {team} from "../../../screens/Amicale/VoteScreen"; import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
type Props = { teams: Array<VoteTeamType>,
teams: Array<team>, dateEnd: string,
dateEnd: string, theme: CustomThemeType,
theme: CustomTheme, };
}
class VoteResults extends React.Component<Props> {
totalVotes: number;
winnerIds: Array<number>;
constructor(props) {
super();
props.teams.sort(this.sortByVotes);
this.getTotalVotes(props.teams);
this.getWinnerIds(props.teams);
}
shouldComponentUpdate() {
return false;
}
sortByVotes = (a: team, b: team) => b.votes - a.votes;
getTotalVotes(teams: Array<team>) {
this.totalVotes = 0;
for (let i = 0; i < teams.length; i++) {
this.totalVotes += teams[i].votes;
}
}
getWinnerIds(teams: Array<team>) {
let max = teams[0].votes;
this.winnerIds = [];
for (let i = 0; i < teams.length; i++) {
if (teams[i].votes === max)
this.winnerIds.push(teams[i].id);
else
break;
}
}
voteKeyExtractor = (item: team) => item.id.toString();
resultRenderItem = ({item}: { item: team }) => {
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
const isDraw = this.winnerIds.length > 1;
const colors = this.props.theme.colors;
return (
<Card style={{
marginTop: 10,
elevation: isWinner ? 5 : 3,
}}>
<List.Item
title={item.name}
description={item.votes + ' ' + i18n.t('screens.vote.results.votes')}
left={props => isWinner
? <List.Icon {...props} icon={isDraw ? "trophy-outline" : "trophy"} color={colors.primary}/>
: null}
titleStyle={{
color: isWinner
? colors.primary
: colors.text
}}
style={{padding: 0}}
/>
<ProgressBar progress={item.votes / this.totalVotes} color={colors.primary}/>
</Card>
);
};
render() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.results.title')}
subtitle={i18n.t('screens.vote.results.subtitle') + ' ' + this.props.dateEnd}
left={(props) => <Avatar.Icon
{...props}
icon={"podium-gold"}
/>}
/>
<Card.Content>
<Subheading>{i18n.t('screens.vote.results.totalVotes') + ' ' + this.totalVotes}</Subheading>
{/*$FlowFixMe*/}
<FlatList
data={this.props.teams}
keyExtractor={this.voteKeyExtractor}
renderItem={this.resultRenderItem}
/>
</Card.Content>
</Card>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
margin: 10, margin: 10,
}, },
icon: { icon: {
backgroundColor: 'transparent' backgroundColor: 'transparent',
}, },
}); });
class VoteResults extends React.Component<PropsType> {
totalVotes: number;
winnerIds: Array<number>;
constructor(props: PropsType) {
super();
props.teams.sort(this.sortByVotes);
this.getTotalVotes(props.teams);
this.getWinnerIds(props.teams);
}
shouldComponentUpdate(): boolean {
return false;
}
getTotalVotes(teams: Array<VoteTeamType>) {
this.totalVotes = 0;
for (let i = 0; i < teams.length; i += 1) {
this.totalVotes += teams[i].votes;
}
}
getWinnerIds(teams: Array<VoteTeamType>) {
const max = teams[0].votes;
this.winnerIds = [];
for (let i = 0; i < teams.length; i += 1) {
if (teams[i].votes === max) this.winnerIds.push(teams[i].id);
else break;
}
}
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
resultRenderItem = ({item}: {item: VoteTeamType}): React.Node => {
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
const isDraw = this.winnerIds.length > 1;
const {props} = this;
return (
<Card
style={{
marginTop: 10,
elevation: isWinner ? 5 : 3,
}}>
<List.Item
title={item.name}
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
left={({size}: {size: number}): React.Node =>
isWinner ? (
<List.Icon
size={size}
icon={isDraw ? 'trophy-outline' : 'trophy'}
color={props.theme.colors.primary}
/>
) : null
}
titleStyle={{
color: isWinner
? props.theme.colors.primary
: props.theme.colors.text,
}}
style={{padding: 0}}
/>
<ProgressBar
progress={item.votes / this.totalVotes}
color={props.theme.colors.primary}
/>
</Card>
);
};
render(): React.Node {
const {props} = this;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.results.title')}
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
props.dateEnd
}`}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon size={size} icon="podium-gold" />
)}
/>
<Card.Content>
<Subheading>{`${i18n.t('screens.vote.results.totalVotes')} ${
this.totalVotes
}`}</Subheading>
{/* $FlowFixMe */}
<FlatList
data={props.teams}
keyExtractor={this.voteKeyExtractor}
renderItem={this.resultRenderItem}
/>
</Card.Content>
</Card>
);
}
}
export default withTheme(VoteResults); export default withTheme(VoteResults);

View file

@ -1,137 +1,146 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Button, Card, RadioButton} from "react-native-paper"; import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
import {FlatList, StyleSheet, View} from "react-native"; import {FlatList, StyleSheet, View} from 'react-native';
import ConnectionManager from "../../../managers/ConnectionManager";
import LoadingConfirmDialog from "../../Dialogs/LoadingConfirmDialog";
import ErrorDialog from "../../Dialogs/ErrorDialog";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {team} from "../../../screens/Amicale/VoteScreen"; import ConnectionManager from '../../../managers/ConnectionManager';
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../Dialogs/ErrorDialog';
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
type Props = { type PropsType = {
teams: Array<team>, teams: Array<VoteTeamType>,
onVoteSuccess: () => void, onVoteSuccess: () => void,
onVoteError: () => void, onVoteError: () => void,
} };
type State = { type StateType = {
selectedTeam: string, selectedTeam: string,
voteDialogVisible: boolean, voteDialogVisible: boolean,
errorDialogVisible: boolean, errorDialogVisible: boolean,
currentError: number, currentError: number,
} };
export default class VoteSelect extends React.PureComponent<Props, State> {
state = {
selectedTeam: "none",
voteDialogVisible: false,
errorDialogVisible: false,
currentError: 0,
};
onVoteSelectionChange = (team: string) => this.setState({selectedTeam: team});
voteKeyExtractor = (item: team) => item.id.toString();
voteRenderItem = ({item}: { item: team }) => <RadioButton.Item label={item.name} value={item.id.toString()}/>;
showVoteDialog = () => this.setState({voteDialogVisible: true});
onVoteDialogDismiss = () => this.setState({voteDialogVisible: false,});
onVoteDialogAccept = async () => {
return new Promise((resolve) => {
ConnectionManager.getInstance().authenticatedRequest(
"elections/vote",
{"team": parseInt(this.state.selectedTeam)})
.then(() => {
this.onVoteDialogDismiss();
this.props.onVoteSuccess();
resolve();
})
.catch((error: number) => {
this.onVoteDialogDismiss();
this.showErrorDialog(error);
resolve();
});
});
};
showErrorDialog = (error: number) => this.setState({
errorDialogVisible: true,
currentError: error,
});
onErrorDialogDismiss = () => {
this.setState({errorDialogVisible: false});
this.props.onVoteError();
};
render() {
return (
<View>
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.select.title')}
subtitle={i18n.t('screens.vote.select.subtitle')}
left={(props) =>
<Avatar.Icon
{...props}
icon={"alert-decagram"}
/>}
/>
<Card.Content>
<RadioButton.Group
onValueChange={this.onVoteSelectionChange}
value={this.state.selectedTeam}
>
{/*$FlowFixMe*/}
<FlatList
data={this.props.teams}
keyExtractor={this.voteKeyExtractor}
extraData={this.state.selectedTeam}
renderItem={this.voteRenderItem}
/>
</RadioButton.Group>
</Card.Content>
<Card.Actions>
<Button
icon="send"
mode="contained"
onPress={this.showVoteDialog}
style={{marginLeft: 'auto'}}
disabled={this.state.selectedTeam === "none"}
>
{i18n.t('screens.vote.select.sendButton')}
</Button>
</Card.Actions>
</Card>
<LoadingConfirmDialog
visible={this.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')}
/>
<ErrorDialog
visible={this.state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={this.state.currentError}
/>
</View>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
margin: 10, margin: 10,
}, },
icon: { icon: {
backgroundColor: 'transparent' backgroundColor: 'transparent',
}, },
}); });
export default class VoteSelect extends React.PureComponent<
PropsType,
StateType,
> {
constructor() {
super();
this.state = {
selectedTeam: 'none',
voteDialogVisible: false,
errorDialogVisible: false,
currentError: 0,
};
}
onVoteSelectionChange = (teamName: string): void =>
this.setState({selectedTeam: teamName});
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
voteRenderItem = ({item}: {item: VoteTeamType}): React.Node => (
<RadioButton.Item label={item.name} value={item.id.toString()} />
);
showVoteDialog = (): void => this.setState({voteDialogVisible: true});
onVoteDialogDismiss = (): void => this.setState({voteDialogVisible: false});
onVoteDialogAccept = async (): Promise<void> => {
return new Promise((resolve: () => void) => {
const {state} = this;
ConnectionManager.getInstance()
.authenticatedRequest('elections/vote', {
team: parseInt(state.selectedTeam, 10),
})
.then(() => {
this.onVoteDialogDismiss();
const {props} = this;
props.onVoteSuccess();
resolve();
})
.catch((error: number) => {
this.onVoteDialogDismiss();
this.showErrorDialog(error);
resolve();
});
});
};
showErrorDialog = (error: number): void =>
this.setState({
errorDialogVisible: true,
currentError: error,
});
onErrorDialogDismiss = () => {
this.setState({errorDialogVisible: false});
const {props} = this;
props.onVoteError();
};
render(): React.Node {
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={({size}: {size: number}): React.Node => (
<Avatar.Icon size={size} icon="alert-decagram" />
)}
/>
<Card.Content>
<RadioButton.Group
onValueChange={this.onVoteSelectionChange}
value={state.selectedTeam}>
{/* $FlowFixMe */}
<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')}
/>
<ErrorDialog
visible={state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={state.currentError}
/>
</View>
);
}
}

View file

@ -1,45 +1,45 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Card, Paragraph} from "react-native-paper"; import {Avatar, Card, Paragraph} from 'react-native-paper';
import {StyleSheet} from "react-native"; import {StyleSheet} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
type Props = { type PropsType = {
startDate: string, startDate: string,
} };
export default class VoteTease extends React.Component<Props> {
shouldComponentUpdate() {
return false;
}
render() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')}
left={props => <Avatar.Icon
{...props}
icon="vote"/>}
/>
<Card.Content>
<Paragraph>
{i18n.t('screens.vote.tease.message') + ' ' + this.props.startDate}
</Paragraph>
</Card.Content>
</Card>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
margin: 10, margin: 10,
}, },
icon: { icon: {
backgroundColor: 'transparent' backgroundColor: 'transparent',
}, },
}); });
export default class VoteTease extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon size={size} icon="vote" />
)}
/>
<Card.Content>
<Paragraph>
{`${i18n.t('screens.vote.tease.message')} ${props.startDate}`}
</Paragraph>
</Card.Content>
</Card>
);
}
}

View file

@ -1,72 +1,78 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {ActivityIndicator, Card, Paragraph, withTheme} from "react-native-paper"; import {
import {StyleSheet} from "react-native"; ActivityIndicator,
Card,
Paragraph,
withTheme,
} from 'react-native-paper';
import {StyleSheet} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
type Props = { type PropsType = {
startDate: string | null, startDate: string | null,
justVoted: boolean, justVoted: boolean,
hasVoted: boolean, hasVoted: boolean,
isVoteRunning: boolean, isVoteRunning: boolean,
theme: CustomTheme, theme: CustomThemeType,
} };
class VoteWait extends React.Component<Props> {
shouldComponentUpdate() {
return false;
}
render() {
const colors = this.props.theme.colors;
const startDate = this.props.startDate;
return (
<Card style={styles.card}>
<Card.Title
title={this.props.isVoteRunning
? i18n.t('screens.vote.wait.titleSubmitted')
: i18n.t('screens.vote.wait.titleEnded')}
subtitle={i18n.t('screens.vote.wait.subtitle')}
left={(props) => <ActivityIndicator {...props}/>}
/>
<Card.Content>
{
this.props.justVoted
? <Paragraph style={{color: colors.success}}>
{i18n.t('screens.vote.wait.messageSubmitted')}
</Paragraph>
: null
}
{
this.props.hasVoted
? <Paragraph style={{color: colors.success}}>
{i18n.t('screens.vote.wait.messageVoted')}
</Paragraph>
: null
}
{
startDate != null
? <Paragraph>
{i18n.t('screens.vote.wait.messageDate') + ' ' + startDate}
</Paragraph>
: <Paragraph>{i18n.t('screens.vote.wait.messageDateUndefined')}</Paragraph>
}
</Card.Content>
</Card>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
margin: 10, margin: 10,
}, },
icon: { icon: {
backgroundColor: 'transparent' backgroundColor: 'transparent',
}, },
}); });
class VoteWait extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {props} = this;
const {startDate} = props;
return (
<Card style={styles.card}>
<Card.Title
title={
props.isVoteRunning
? i18n.t('screens.vote.wait.titleSubmitted')
: i18n.t('screens.vote.wait.titleEnded')
}
subtitle={i18n.t('screens.vote.wait.subtitle')}
left={({size}: {size: number}): React.Node => (
<ActivityIndicator size={size} />
)}
/>
<Card.Content>
{props.justVoted ? (
<Paragraph style={{color: props.theme.colors.success}}>
{i18n.t('screens.vote.wait.messageSubmitted')}
</Paragraph>
) : null}
{props.hasVoted ? (
<Paragraph style={{color: props.theme.colors.success}}>
{i18n.t('screens.vote.wait.messageVoted')}
</Paragraph>
) : null}
{startDate != null ? (
<Paragraph>
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
</Paragraph>
) : (
<Paragraph>
{i18n.t('screens.vote.wait.messageDateUndefined')}
</Paragraph>
)}
</Card.Content>
</Card>
);
}
}
export default withTheme(VoteWait); export default withTheme(VoteWait);

View file

@ -1,101 +1,117 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from "react-native"; import {View} from 'react-native';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import Collapsible from "react-native-collapsible"; import Collapsible from 'react-native-collapsible';
import * as Animatable from "react-native-animatable"; import * as Animatable from 'react-native-animatable';
import type {CustomTheme} from "../../managers/ThemeManager"; import type {CustomThemeType} from '../../managers/ThemeManager';
type Props = { type PropsType = {
theme: CustomTheme, theme: CustomThemeType,
title: string, title: string,
subtitle?: string, subtitle?: string,
left?: (props: { [keys: string]: any }) => React.Node, left?: () => React.Node,
opened?: boolean, opened?: boolean,
unmountWhenCollapsed: boolean, unmountWhenCollapsed?: boolean,
children?: React.Node, children?: React.Node,
} };
type State = { type StateType = {
expanded: boolean, expanded: boolean,
} };
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon); const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.Component<Props, State> { class AnimatedAccordion extends React.Component<PropsType, StateType> {
static defaultProps = {
subtitle: '',
left: null,
opened: null,
unmountWhenCollapsed: false,
children: null,
};
static defaultProps = { chevronRef: {current: null | AnimatedListIcon};
unmountWhenCollapsed: false,
}
chevronRef: { current: null | AnimatedListIcon };
chevronIcon: string;
animStart: string;
animEnd: string;
state = { chevronIcon: string;
expanded: this.props.opened != null ? this.props.opened : false,
}
constructor(props) { animStart: string;
super(props);
this.chevronRef = React.createRef();
this.setupChevron();
}
setupChevron() { animEnd: string;
if (this.state.expanded) {
this.chevronIcon = "chevron-up";
this.animStart = "180deg";
this.animEnd = "0deg";
} else {
this.chevronIcon = "chevron-down";
this.animStart = "0deg";
this.animEnd = "180deg";
}
}
toggleAccordion = () => { constructor(props: PropsType) {
if (this.chevronRef.current != null) { super(props);
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? this.animStart : this.animEnd}); this.state = {
this.setState({expanded: !this.state.expanded}) expanded: props.opened != null ? props.opened : false,
}
}; };
this.chevronRef = React.createRef();
this.setupChevron();
}
shouldComponentUpdate(nextProps: Props, nextState: State): boolean { shouldComponentUpdate(nextProps: PropsType): boolean {
if (nextProps.opened != null && nextProps.opened !== this.props.opened) const {state, props} = this;
this.state.expanded = nextProps.opened; if (nextProps.opened != null && nextProps.opened !== props.opened)
return true; state.expanded = nextProps.opened;
return true;
}
setupChevron() {
const {expanded} = this.state;
if (expanded) {
this.chevronIcon = 'chevron-up';
this.animStart = '180deg';
this.animEnd = '0deg';
} else {
this.chevronIcon = 'chevron-down';
this.animStart = '0deg';
this.animEnd = '180deg';
} }
}
render() { toggleAccordion = () => {
const colors = this.props.theme.colors; const {expanded} = this.state;
return ( if (this.chevronRef.current != null) {
<View> this.chevronRef.current.transitionTo({
<List.Item rotate: expanded ? this.animStart : this.animEnd,
{...this.props} });
title={this.props.title} this.setState((prevState: StateType): {expanded: boolean} => ({
subtitle={this.props.subtitle} expanded: !prevState.expanded,
titleStyle={this.state.expanded ? {color: colors.primary} : undefined} }));
onPress={this.toggleAccordion}
right={(props) => <AnimatedListIcon
ref={this.chevronRef}
{...props}
icon={this.chevronIcon}
color={this.state.expanded ? colors.primary : undefined}
useNativeDriver
/>}
left={this.props.left}
/>
<Collapsible collapsed={!this.state.expanded}>
{!this.props.unmountWhenCollapsed || (this.props.unmountWhenCollapsed && this.state.expanded)
? this.props.children
: null}
</Collapsible>
</View>
);
} }
};
render(): React.Node {
const {props, state} = this;
const {colors} = props.theme;
return (
<View>
<List.Item
title={props.title}
subtitle={props.subtitle}
titleStyle={state.expanded ? {color: colors.primary} : null}
onPress={this.toggleAccordion}
right={({size}: {size: number}): React.Node => (
<AnimatedListIcon
ref={this.chevronRef}
size={size}
icon={this.chevronIcon}
color={state.expanded ? colors.primary : null}
useNativeDriver
style={{rotate: '0deg'}}
/>
)}
left={props.left}
/>
<Collapsible collapsed={!state.expanded}>
{!props.unmountWhenCollapsed ||
(props.unmountWhenCollapsed && state.expanded)
? props.children
: null}
</Collapsible>
</View>
);
}
} }
export default withTheme(AnimatedAccordion); export default withTheme(AnimatedAccordion);

View file

@ -1,170 +1,179 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from 'react-native';
import {FAB, IconButton, Surface, withTheme} from "react-native-paper"; import {FAB, IconButton, Surface, withTheme} from 'react-native-paper';
import AutoHideHandler from "../../utils/AutoHideHandler";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import AutoHideHandler from '../../utils/AutoHideHandler';
import type {CustomTheme} from "../../managers/ThemeManager"; import CustomTabBar from '../Tabbar/CustomTabBar';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {OnScrollType} from '../../utils/AutoHideHandler';
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomThemeType,
onPress: (action: string, data: any) => void, onPress: (action: string, data?: string) => void,
seekAttention: boolean, seekAttention: boolean,
} };
type State = { type StateType = {
currentMode: string, currentMode: string,
} };
const DISPLAY_MODES = { const DISPLAY_MODES = {
DAY: "agendaDay", DAY: 'agendaDay',
WEEK: "agendaWeek", WEEK: 'agendaWeek',
MONTH: "month", MONTH: 'month',
} };
class AnimatedBottomBar extends React.Component<Props, State> {
ref: { current: null | Animatable.View };
hideHandler: AutoHideHandler;
displayModeIcons: { [key: string]: string };
state = {
currentMode: DISPLAY_MODES.WEEK,
}
constructor() {
super();
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: Props, nextState: State) {
return (nextProps.seekAttention !== this.props.seekAttention)
|| (nextState.currentMode !== this.state.currentMode);
}
onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) {
if (shouldHide)
this.ref.current.fadeOutDown(500);
else
this.ref.current.fadeInUp(500);
}
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event);
};
changeDisplayMode = () => {
let newMode;
switch (this.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;
}
this.setState({currentMode: newMode});
this.props.onPress("changeView", newMode);
};
render() {
const buttonColor = this.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={this.props.seekAttention ? "bounce" : undefined}
easing="ease-out"
iterationDelay={500}
iterationCount="infinite"
useNativeDriver
style={styles.fab}
icon="account-clock"
onPress={() => this.props.navigation.navigate('group-select')}
/>
</View>
<View style={{flexDirection: 'row'}}>
<IconButton
icon={this.displayModeIcons[this.state.currentMode]}
color={buttonColor}
onPress={this.changeDisplayMode}/>
<IconButton
icon="clock-in"
color={buttonColor}
style={{marginLeft: 5}}
onPress={() => this.props.onPress('today', undefined)}/>
</View>
<View style={{flexDirection: 'row'}}>
<IconButton
icon="chevron-left"
color={buttonColor}
onPress={() => this.props.onPress('prev', undefined)}/>
<IconButton
icon="chevron-right"
color={buttonColor}
style={{marginLeft: 5}}
onPress={() => this.props.onPress('next', undefined)}/>
</View>
</Surface>
</Animatable.View>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
position: 'absolute', position: 'absolute',
left: '5%', left: '5%',
width: '90%', width: '90%',
}, },
surface: { surface: {
position: 'relative', position: 'relative',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
borderRadius: 50, borderRadius: 50,
elevation: 2, elevation: 2,
}, },
fabContainer: { fabContainer: {
position: "absolute", position: 'absolute',
left: 0, left: 0,
right: 0, right: 0,
alignItems: "center", alignItems: 'center',
width: '100%', width: '100%',
height: '100%' height: '100%',
}, },
fab: { fab: {
position: 'absolute', position: 'absolute',
alignSelf: 'center', alignSelf: 'center',
top: '-25%', top: '-25%',
} },
}); });
class AnimatedBottomBar extends React.Component<PropsType, StateType> {
ref: {current: null | Animatable.View};
hideHandler: AutoHideHandler;
displayModeIcons: {[key: string]: string};
constructor() {
super();
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) => {
if (this.ref.current != null) {
if (shouldHide) this.ref.current.fadeOutDown(500);
else this.ref.current.fadeInUp(500);
}
};
onScroll = (event: OnScrollType) => {
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(): React.Node {
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); export default withTheme(AnimatedBottomBar);

View file

@ -1,66 +1,63 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet} from "react-native"; import {StyleSheet} from 'react-native';
import {FAB} from "react-native-paper"; import {FAB} from 'react-native-paper';
import AutoHideHandler from "../../utils/AutoHideHandler";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar"; import AutoHideHandler from '../../utils/AutoHideHandler';
import {StackNavigationProp} from "@react-navigation/stack"; import CustomTabBar from '../Tabbar/CustomTabBar';
type Props = { type PropsType = {
navigation: StackNavigationProp, icon: string,
icon: string, onPress: () => void,
onPress: () => void, };
}
const AnimatedFab = Animatable.createAnimatableComponent(FAB); const AnimatedFab = Animatable.createAnimatableComponent(FAB);
export default class AnimatedFAB extends React.Component<Props> {
ref: { current: null | Animatable.View };
hideHandler: AutoHideHandler;
constructor() {
super();
this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event);
};
onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) {
if (shouldHide)
this.ref.current.bounceOutDown(1000);
else
this.ref.current.bounceInUp(1000);
}
}
render() {
return (
<AnimatedFab
ref={this.ref}
useNativeDriver
icon={this.props.icon}
onPress={this.props.onPress}
style={{
...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT
}}
/>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
fab: { fab: {
position: 'absolute', position: 'absolute',
margin: 16, margin: 16,
right: 0, right: 0,
}, },
}); });
export default class AnimatedFAB extends React.Component<PropsType> {
ref: {current: null | Animatable.View};
hideHandler: AutoHideHandler;
constructor() {
super();
this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event);
};
onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) {
if (shouldHide) this.ref.current.bounceOutDown(1000);
else this.ref.current.bounceInUp(1000);
}
};
render(): React.Node {
const {props} = this;
return (
<AnimatedFab
ref={this.ref}
useNativeDriver
icon={props.icon}
onPress={props.onPress}
style={{
...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT,
}}
/>
);
}
}

View file

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

View file

@ -1,26 +1,26 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from "react-native"; import {Animated} from 'react-native';
import type {CollapsibleComponentProps} from "./CollapsibleComponent"; import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from "./CollapsibleComponent"; import CollapsibleComponent from './CollapsibleComponent';
type Props = { type PropsType = {
...CollapsibleComponentProps ...CollapsibleComponentPropsType,
} };
class CollapsibleFlatList extends React.Component<Props> { // eslint-disable-next-line react/prefer-stateless-function
class CollapsibleFlatList extends React.Component<PropsType> {
render() { render(): React.Node {
return ( const {props} = this;
<CollapsibleComponent return (
{...this.props} <CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
component={Animated.FlatList} {...props}
> component={Animated.FlatList}>
{this.props.children} {props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
} }
} }
export default CollapsibleFlatList; export default CollapsibleFlatList;

View file

@ -1,26 +1,26 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from "react-native"; import {Animated} from 'react-native';
import type {CollapsibleComponentProps} from "./CollapsibleComponent"; import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from "./CollapsibleComponent"; import CollapsibleComponent from './CollapsibleComponent';
type Props = { type PropsType = {
...CollapsibleComponentProps ...CollapsibleComponentPropsType,
} };
class CollapsibleScrollView extends React.Component<Props> { // eslint-disable-next-line react/prefer-stateless-function
class CollapsibleScrollView extends React.Component<PropsType> {
render() { render(): React.Node {
return ( const {props} = this;
<CollapsibleComponent return (
{...this.props} <CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
component={Animated.ScrollView} {...props}
> component={Animated.ScrollView}>
{this.props.children} {props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
} }
} }
export default CollapsibleScrollView; export default CollapsibleScrollView;

View file

@ -1,26 +1,26 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from "react-native"; import {Animated} from 'react-native';
import type {CollapsibleComponentProps} from "./CollapsibleComponent"; import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
import CollapsibleComponent from "./CollapsibleComponent"; import CollapsibleComponent from './CollapsibleComponent';
type Props = { type PropsType = {
...CollapsibleComponentProps ...CollapsibleComponentPropsType,
} };
class CollapsibleSectionList extends React.Component<Props> { // eslint-disable-next-line react/prefer-stateless-function
class CollapsibleSectionList extends React.Component<PropsType> {
render() { render(): React.Node {
return ( const {props} = this;
<CollapsibleComponent return (
{...this.props} <CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
component={Animated.SectionList} {...props}
> component={Animated.SectionList}>
{this.props.children} {props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
} }
} }
export default CollapsibleSectionList; export default CollapsibleSectionList;

View file

@ -2,34 +2,32 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import i18n from "i18n-js"; import i18n from 'i18n-js';
type Props = { type PropsType = {
visible: boolean, visible: boolean,
onDismiss: () => void, onDismiss: () => void,
title: string, title: string,
message: string, message: string,
} };
class AlertDialog extends React.PureComponent<Props> { class AlertDialog extends React.PureComponent<PropsType> {
render(): React.Node {
render() { const {props} = this;
return ( return (
<Portal> <Portal>
<Dialog <Dialog visible={props.visible} onDismiss={props.onDismiss}>
visible={this.props.visible} <Dialog.Title>{props.title}</Dialog.Title>
onDismiss={this.props.onDismiss}> <Dialog.Content>
<Dialog.Title>{this.props.title}</Dialog.Title> <Paragraph>{props.message}</Paragraph>
<Dialog.Content> </Dialog.Content>
<Paragraph>{this.props.message}</Paragraph> <Dialog.Actions>
</Dialog.Content> <Button onPress={props.onDismiss}>{i18n.t('dialog.ok')}</Button>
<Dialog.Actions> </Dialog.Actions>
<Button onPress={this.props.onDismiss}>{i18n.t("dialog.ok")}</Button> </Dialog>
</Dialog.Actions> </Portal>
</Dialog> );
</Portal> }
);
}
} }
export default AlertDialog; export default AlertDialog;

View file

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

View file

@ -1,92 +1,106 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {ActivityIndicator, Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {
import i18n from "i18n-js"; ActivityIndicator,
Button,
Dialog,
Paragraph,
Portal,
} from 'react-native-paper';
import i18n from 'i18n-js';
type Props = { type PropsType = {
visible: boolean, visible: boolean,
onDismiss: () => void, onDismiss?: () => void,
onAccept: () => Promise<void>, // async function to be executed onAccept?: () => Promise<void>, // async function to be executed
title: string, title?: string,
titleLoading: string, titleLoading?: string,
message: string, message?: string,
startLoading: boolean, startLoading?: boolean,
} };
type State = { type StateType = {
loading: boolean, loading: boolean,
} };
class LoadingConfirmDialog extends React.PureComponent<Props, State> { class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
static defaultProps = {
onDismiss: () => {},
onAccept: (): Promise<void> => {
return Promise.resolve();
},
title: '',
titleLoading: '',
message: '',
startLoading: false,
};
static defaultProps = { constructor(props: PropsType) {
title: '', super(props);
message: '', this.state = {
onDismiss: () => {}, loading:
onAccept: () => {return Promise.resolve()}, props.startLoading != null
startLoading: false, ? props.startLoading
} : LoadingConfirmDialog.defaultProps.startLoading,
state = {
loading: this.props.startLoading,
}; };
}
/** /**
* Set the dialog into loading state and closes it when operation finishes * Set the dialog into loading state and closes it when operation finishes
*/ */
onClickAccept = () => { onClickAccept = () => {
this.setState({loading: true}); const {props} = this;
this.props.onAccept().then(this.hideLoading); this.setState({loading: true});
}; if (props.onAccept != null) props.onAccept().then(this.hideLoading);
};
/** /**
* Waits for fade out animations to finish before hiding loading * Waits for fade out animations to finish before hiding loading
* @returns {TimeoutID} * @returns {TimeoutID}
*/ */
hideLoading = () => setTimeout(() => { hideLoading = (): TimeoutID =>
this.setState({loading: false}) setTimeout(() => {
this.setState({loading: false});
}, 200); }, 200);
/** /**
* Hide the dialog if it is not loading * Hide the dialog if it is not loading
*/ */
onDismiss = () => { onDismiss = () => {
if (!this.state.loading) const {state, props} = this;
this.props.onDismiss(); if (!state.loading && props.onDismiss != null) props.onDismiss();
}; };
render() { render(): React.Node {
return ( const {state, props} = this;
<Portal> return (
<Dialog <Portal>
visible={this.props.visible} <Dialog visible={props.visible} onDismiss={this.onDismiss}>
onDismiss={this.onDismiss}> <Dialog.Title>
<Dialog.Title> {state.loading ? props.titleLoading : props.title}
{this.state.loading </Dialog.Title>
? this.props.titleLoading <Dialog.Content>
: this.props.title} {state.loading ? (
</Dialog.Title> <ActivityIndicator animating size="large" />
<Dialog.Content> ) : (
{this.state.loading <Paragraph>{props.message}</Paragraph>
? <ActivityIndicator )}
animating={true} </Dialog.Content>
size={'large'}/> {state.loading ? null : (
: <Paragraph>{this.props.message}</Paragraph> <Dialog.Actions>
} <Button onPress={this.onDismiss} style={{marginRight: 10}}>
</Dialog.Content> {i18n.t('dialog.cancel')}
{this.state.loading </Button>
? null <Button onPress={this.onClickAccept}>
: <Dialog.Actions> {i18n.t('dialog.yes')}
<Button onPress={this.onDismiss} </Button>
style={{marginRight: 10}}>{i18n.t("dialog.cancel")}</Button> </Dialog.Actions>
<Button onPress={this.onClickAccept}>{i18n.t("dialog.yes")}</Button> )}
</Dialog.Actions> </Dialog>
} </Portal>
</Dialog> );
</Portal> }
);
}
} }
export default LoadingConfirmDialog; export default LoadingConfirmDialog;

View file

@ -2,55 +2,50 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import {FlatList} from "react-native"; import {FlatList} from 'react-native';
export type OptionsDialogButton = { export type OptionsDialogButtonType = {
title: string, title: string,
onPress: () => void, onPress: () => void,
} };
type Props = { type PropsType = {
visible: boolean, visible: boolean,
title: string, title: string,
message: string, message: string,
buttons: Array<OptionsDialogButton>, buttons: Array<OptionsDialogButtonType>,
onDismiss: () => void, onDismiss: () => void,
} };
class OptionsDialog extends React.PureComponent<Props> { class OptionsDialog extends React.PureComponent<PropsType> {
getButtonRender = ({item}: {item: OptionsDialogButtonType}): React.Node => {
return <Button onPress={item.onPress}>{item.title}</Button>;
};
getButtonRender = ({item}: { item: OptionsDialogButton }) => { keyExtractor = (item: OptionsDialogButtonType): string => item.title;
return <Button
onPress={item.onPress}>
{item.title}
</Button>;
}
keyExtractor = (item: OptionsDialogButton) => item.title; render(): React.Node {
const {props} = this;
render() { return (
return ( <Portal>
<Portal> <Dialog visible={props.visible} onDismiss={props.onDismiss}>
<Dialog <Dialog.Title>{props.title}</Dialog.Title>
visible={this.props.visible} <Dialog.Content>
onDismiss={this.props.onDismiss}> <Paragraph>{props.message}</Paragraph>
<Dialog.Title>{this.props.title}</Dialog.Title> </Dialog.Content>
<Dialog.Content> <Dialog.Actions>
<Paragraph>{this.props.message}</Paragraph> <FlatList
</Dialog.Content> data={props.buttons}
<Dialog.Actions> renderItem={this.getButtonRender}
<FlatList keyExtractor={this.keyExtractor}
data={this.props.buttons} horizontal
renderItem={this.getButtonRender} inverted
keyExtractor={this.keyExtractor} />
horizontal={true} </Dialog.Actions>
inverted={true} </Dialog>
/> </Portal>
</Dialog.Actions> );
</Dialog> }
</Portal>
);
}
} }
export default OptionsDialog; export default OptionsDialog;

View file

@ -2,37 +2,46 @@
import * as React from 'react'; import * as React from 'react';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import {View} from "react-native"; import {View} from 'react-native';
import type {CustomTheme} from "../../managers/ThemeManager";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../managers/ThemeManager';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomThemeType,
} };
class ActionsDashBoardItem extends React.Component<Props> { class ActionsDashBoardItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return nextProps.theme.dark !== props.theme.dark;
}
shouldComponentUpdate(nextProps: Props): boolean { render(): React.Node {
return (nextProps.theme.dark !== this.props.theme.dark); const {props} = this;
} return (
<View>
render() { <List.Item
return ( title={i18n.t('screens.feedback.homeButtonTitle')}
<View> description={i18n.t('screens.feedback.homeButtonSubtitle')}
<List.Item left={({size}: {size: number}): React.Node => (
title={i18n.t("screens.feedback.homeButtonTitle")} <List.Icon size={size} icon="comment-quote" />
description={i18n.t("screens.feedback.homeButtonSubtitle")} )}
left={props => <List.Icon {...props} icon={"comment-quote"}/>} right={({size}: {size: number}): React.Node => (
right={props => <List.Icon {...props} icon={"chevron-right"}/>} <List.Icon size={size} icon="chevron-right" />
onPress={() => this.props.navigation.navigate("feedback")} )}
style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}} onPress={(): void => props.navigation.navigate('feedback')}
/> style={{
</View> paddingTop: 0,
paddingBottom: 0,
); marginLeft: 10,
} marginRight: 10,
}}
/>
</View>
);
}
} }
export default withTheme(ActionsDashBoardItem); export default withTheme(ActionsDashBoardItem);

View file

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

View file

@ -2,126 +2,114 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Card, Text, TouchableRipple} from 'react-native-paper'; import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from "react-native"; import {Image, View} from 'react-native';
import Autolink from "react-native-autolink"; import Autolink from 'react-native-autolink';
import i18n from "i18n-js"; import i18n from 'i18n-js';
import ImageModal from 'react-native-image-modal'; import ImageModal from 'react-native-image-modal';
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomTheme} from "../../managers/ThemeManager"; import type {FeedItemType} from '../../screens/Home/HomeScreen';
import type {feedItem} from "../../screens/Home/HomeScreen";
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, item: FeedItemType,
item: feedItem, title: string,
title: string, subtitle: string,
subtitle: string, height: number,
height: number, };
}
/** /**
* Component used to display a feed item * Component used to display a feed item
*/ */
class FeedItem extends React.Component<Props> { class FeedItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
shouldComponentUpdate() { onPress = () => {
return false; const {props} = this;
} props.navigation.navigate('feed-information', {
data: props.item,
date: props.subtitle,
});
};
/** render(): React.Node {
* Gets the amicale INSAT logo const {props} = this;
* const {item} = props;
* @return {*} const hasImage =
*/ item.full_picture !== '' && item.full_picture !== undefined;
getAvatar() {
return ( const cardMargin = 10;
<Image const cardHeight = props.height - 2 * cardMargin;
size={48} const imageSize = 250;
source={ICON_AMICALE} const titleHeight = 80;
style={{ const actionsHeight = 60;
const textHeight = hasImage
? cardHeight - titleHeight - actionsHeight - imageSize
: cardHeight - titleHeight - actionsHeight;
return (
<Card
style={{
margin: cardMargin,
height: cardHeight,
}}>
<TouchableRipple style={{flex: 1}} onPress={this.onPress}>
<View>
<Card.Title
title={props.title}
subtitle={props.subtitle}
left={(): React.Node => (
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48, width: 48,
height: 48, height: 48,
}}/> }}
); />
} )}
style={{height: titleHeight}}
onPress = () => { />
this.props.navigation.navigate( {hasImage ? (
'feed-information', <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
{ <ImageModal
data: this.props.item, resizeMode="contain"
date: this.props.subtitle imageBackgroundColor="#000"
}); style={{
}; width: imageSize,
height: imageSize,
render() { }}
const item = this.props.item; source={{
const hasImage = item.full_picture !== '' && item.full_picture !== undefined; uri: item.full_picture,
}}
const cardMargin = 10; />
const cardHeight = this.props.height - 2 * cardMargin; </View>
const imageSize = 250; ) : null}
const titleHeight = 80; <Card.Content>
const actionsHeight = 60; {item.message !== undefined ? (
const textHeight = hasImage <Autolink
? cardHeight - titleHeight - actionsHeight - imageSize text={item.message}
: cardHeight - titleHeight - actionsHeight; hashtag="facebook"
return ( component={Text}
<Card style={{height: textHeight}}
style={{ />
margin: cardMargin, ) : null}
height: cardHeight, </Card.Content>
}} <Card.Actions style={{height: actionsHeight}}>
> <Button
<TouchableRipple onPress={this.onPress}
style={{flex: 1}} icon="plus"
onPress={this.onPress}> style={{marginLeft: 'auto'}}>
<View> {i18n.t('screens.home.dashboard.seeMore')}
<Card.Title </Button>
title={this.props.title} </Card.Actions>
subtitle={this.props.subtitle} </View>
left={this.getAvatar} </TouchableRipple>
style={{height: titleHeight}} </Card>
/> );
{hasImage ? }
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal
resizeMode="contain"
imageBackgroundColor={"#000"}
style={{
width: imageSize,
height: imageSize,
}}
source={{
uri: item.full_picture,
}}
/></View> : null}
<Card.Content>
{item.message !== undefined ?
<Autolink
text={item.message}
hashtag="facebook"
component={Text}
style={{height: textHeight}}
/> : null
}
</Card.Content>
<Card.Actions style={{height: actionsHeight}}>
<Button
onPress={this.onPress}
icon={'plus'}
style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
} }
export default FeedItem; export default FeedItem;

View file

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

View file

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

View file

@ -1,72 +1,73 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated, Dimensions} from "react-native"; import {Animated, Dimensions} from 'react-native';
import ImageListItem from "./ImageListItem"; import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
import CardListItem from "./CardListItem"; import ImageListItem from './ImageListItem';
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet"; import CardListItem from './CardListItem';
import type {ServiceItemType} from '../../../managers/ServicesManager';
type Props = { type PropsType = {
dataset: Array<cardItem>, dataset: Array<ServiceItemType>,
isHorizontal: boolean, isHorizontal?: boolean,
contentContainerStyle?: ViewStyle, contentContainerStyle?: ViewStyle | null,
}
export type cardItem = {
key: string,
title: string,
subtitle: string,
image: string | number,
onPress: () => void,
}; };
export type cardList = Array<cardItem>; export default class CardList extends React.Component<PropsType> {
static defaultProps = {
isHorizontal: false,
contentContainerStyle: null,
};
windowWidth: number;
export default class CardList extends React.Component<Props> { horizontalItemSize: number;
static defaultProps = { constructor(props: PropsType) {
isHorizontal: false, super(props);
this.windowWidth = Dimensions.get('window').width;
this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll
}
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => {
const {props} = this;
if (props.isHorizontal)
return (
<ImageListItem
item={item}
key={item.title}
width={this.horizontalItemSize}
/>
);
return <CardListItem item={item} key={item.title} />;
};
keyExtractor = (item: ServiceItemType): string => item.key;
render(): React.Node {
const {props} = this;
let containerStyle = {};
if (props.isHorizontal) {
containerStyle = {
height: this.horizontalItemSize + 50,
justifyContent: 'space-around',
};
} }
return (
windowWidth: number; <Animated.FlatList
horizontalItemSize: number; data={props.dataset}
renderItem={this.getRenderItem}
constructor(props: Props) { keyExtractor={this.keyExtractor}
super(props); numColumns={props.isHorizontal ? undefined : 2}
this.windowWidth = Dimensions.get('window').width; horizontal={props.isHorizontal}
this.horizontalItemSize = this.windowWidth/4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll contentContainerStyle={
} props.isHorizontal ? containerStyle : props.contentContainerStyle
renderItem = ({item}: { item: cardItem }) => {
if (this.props.isHorizontal)
return <ImageListItem item={item} key={item.title} width={this.horizontalItemSize}/>;
else
return <CardListItem item={item} key={item.title}/>;
};
keyExtractor = (item: cardItem) => item.key;
render() {
let containerStyle = {};
if (this.props.isHorizontal) {
containerStyle = {
height: this.horizontalItemSize + 50,
justifyContent: 'space-around',
};
} }
return ( pagingEnabled={props.isHorizontal}
<Animated.FlatList snapToInterval={
{...this.props} props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : null
data={this.props.dataset} }
renderItem={this.renderItem} />
keyExtractor={this.keyExtractor} );
numColumns={this.props.isHorizontal ? undefined : 2} }
horizontal={this.props.isHorizontal}
contentContainerStyle={this.props.isHorizontal ? containerStyle : this.props.contentContainerStyle}
pagingEnabled={this.props.isHorizontal}
snapToInterval={this.props.isHorizontal ? (this.horizontalItemSize+5)*3 : null}
/>
);
}
} }

View file

@ -2,50 +2,41 @@
import * as React from 'react'; import * as React from 'react';
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper'; import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
import {View} from "react-native"; import {View} from 'react-native';
import type {cardItem} from "./CardList"; import type {ServiceItemType} from '../../../managers/ServicesManager';
type Props = { type PropsType = {
item: cardItem, item: ServiceItemType,
} };
export default class CardListItem extends React.Component<Props> { export default class CardListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
shouldComponentUpdate() { return false;
return false; }
}
render(): React.Node {
render() { const {props} = this;
const props = this.props; const {item} = props;
const item = props.item; const source =
const source = typeof item.image === "number" typeof item.image === 'number' ? item.image : {uri: item.image};
? item.image return (
: {uri: item.image}; <Card
return ( style={{
<Card width: '40%',
style={{ margin: 5,
width: '40%', marginLeft: 'auto',
margin: 5, marginRight: 'auto',
marginLeft: 'auto', }}>
marginRight: 'auto', <TouchableRipple style={{flex: 1}} onPress={item.onPress}>
}} <View>
> <Card.Cover style={{height: 80}} source={source} />
<TouchableRipple <Card.Content>
style={{flex: 1}} <Paragraph>{item.title}</Paragraph>
onPress={item.onPress}> <Caption>{item.subtitle}</Caption>
<View> </Card.Content>
<Card.Cover </View>
style={{height: 80}} </TouchableRipple>
source={source} </Card>
/> );
<Card.Content> }
<Paragraph>{item.title}</Paragraph>
<Caption>{item.subtitle}</Caption>
</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
} }

View file

@ -3,53 +3,52 @@
import * as React from 'react'; import * as React from 'react';
import {Text, TouchableRipple} from 'react-native-paper'; import {Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native'; import {Image, View} from 'react-native';
import type {cardItem} from "./CardList"; import type {ServiceItemType} from '../../../managers/ServicesManager';
type Props = { type PropsType = {
item: cardItem, item: ServiceItemType,
width: number, width: number,
} };
export default class ImageListItem extends React.Component<Props> { export default class ImageListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
shouldComponentUpdate() { return false;
return false; }
}
render(): React.Node {
render() { const {props} = this;
const item = this.props.item; const {item} = props;
const source = typeof item.image === "number" const source =
? item.image typeof item.image === 'number' ? item.image : {uri: item.image};
: {uri: item.image}; return (
return ( <TouchableRipple
<TouchableRipple style={{
style={{ width: props.width,
width: this.props.width, height: props.width + 40,
height: this.props.width + 40, margin: 5,
margin: 5, }}
}} onPress={item.onPress}>
onPress={item.onPress} <View>
> <Image
<View> style={{
<Image width: props.width - 20,
style={{ height: props.width - 20,
width: this.props.width - 20, marginLeft: 'auto',
height: this.props.width - 20, marginRight: 'auto',
marginLeft: 'auto', }}
marginRight: 'auto', source={source}
}} />
source={source} <Text
/> style={{
<Text style={{ marginTop: 5,
marginTop: 5, marginLeft: 'auto',
marginLeft: 'auto', marginRight: 'auto',
marginRight: 'auto', textAlign: 'center',
textAlign: 'center' }}>
}}> {item.title}
{item.title} </Text>
</Text> </View>
</View> </TouchableRipple>
</TouchableRipple> );
); }
}
} }

View file

@ -2,82 +2,90 @@
import * as React from 'react'; import * as React from 'react';
import {Card, Chip, List, Text} from 'react-native-paper'; import {Card, Chip, List, Text} from 'react-native-paper';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AnimatedAccordion from "../../Animations/AnimatedAccordion"; import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import {isItemInCategoryFilter} from "../../../utils/Search"; import {isItemInCategoryFilter} from '../../../utils/Search';
import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen"; import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
type Props = { type PropsType = {
categories: Array<category>, categories: Array<ClubCategoryType>,
onChipSelect: (id: number) => void, onChipSelect: (id: number) => void,
selectedCategories: Array<number>, selectedCategories: Array<number>,
} };
class ClubListHeader extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props) {
return nextProps.selectedCategories.length !== this.props.selectedCategories.length;
}
getChipRender = (category: category, key: string) => {
const onPress = () => this.props.onChipSelect(category.id);
return <Chip
selected={isItemInCategoryFilter(this.props.selectedCategories, [category.id])}
mode={'outlined'}
onPress={onPress}
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
key={key}
>
{category.name}
</Chip>;
};
getCategoriesRender() {
let final = [];
for (let i = 0; i < this.props.categories.length; i++) {
final.push(this.getChipRender(this.props.categories[i], this.props.categories[i].id.toString()));
}
return final;
}
render() {
return (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t("screens.clubs.categories")}
left={props => <List.Icon {...props} icon="star"/>}
opened={true}
>
<Text style={styles.text}>{i18n.t("screens.clubs.categoriesFilterMessage")}</Text>
<View style={styles.chipContainer}>
{this.getCategoriesRender()}
</View>
</AnimatedAccordion>
</Card>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
margin: 5 margin: 5,
}, },
text: { text: {
paddingLeft: 0, paddingLeft: 0,
marginTop: 5, marginTop: 5,
marginBottom: 10, marginBottom: 10,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}, },
chipContainer: { chipContainer: {
justifyContent: 'space-around', justifyContent: 'space-around',
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',
paddingLeft: 0, paddingLeft: 0,
marginBottom: 5, marginBottom: 5,
}, },
}); });
class ClubListHeader extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.selectedCategories.length !== props.selectedCategories.length
);
}
getChipRender = (category: ClubCategoryType, key: string): React.Node => {
const {props} = this;
const onPress = (): void => props.onChipSelect(category.id);
return (
<Chip
selected={isItemInCategoryFilter(props.selectedCategories, [
category.id,
null,
])}
mode="outlined"
onPress={onPress}
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
key={key}>
{category.name}
</Chip>
);
};
getCategoriesRender(): React.Node {
const {props} = this;
const final = [];
props.categories.forEach((cat: ClubCategoryType) => {
final.push(this.getChipRender(cat, cat.id.toString()));
});
return final;
}
render(): React.Node {
return (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t('screens.clubs.categories')}
left={({size}: {size: number}): React.Node => (
<List.Icon size={size} icon="star" />
)}
opened>
<Text style={styles.text}>
{i18n.t('screens.clubs.categoriesFilterMessage')}
</Text>
<View style={styles.chipContainer}>{this.getCategoriesRender()}</View>
</AnimatedAccordion>
</Card>
);
}
}
export default ClubListHeader; export default ClubListHeader;

View file

@ -2,84 +2,93 @@
import * as React from 'react'; import * as React from 'react';
import {Avatar, Chip, List, withTheme} from 'react-native-paper'; import {Avatar, Chip, List, withTheme} from 'react-native-paper';
import {View} from "react-native"; import {View} from 'react-native';
import type {category, club} from "../../../screens/Amicale/Clubs/ClubListScreen"; import type {
import type {CustomTheme} from "../../../managers/ThemeManager"; ClubCategoryType,
ClubType,
} from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type Props = { type PropsType = {
onPress: () => void, onPress: () => void,
categoryTranslator: (id: number) => category, categoryTranslator: (id: number) => ClubCategoryType,
item: club, item: ClubType,
height: number, height: number,
theme: CustomTheme, theme: CustomThemeType,
} };
class ClubListItem extends React.Component<Props> { class ClubListItem extends React.Component<PropsType> {
hasManagers: boolean;
hasManagers: boolean; constructor(props: PropsType) {
super(props);
this.hasManagers = props.item.responsibles.length > 0;
}
constructor(props) { shouldComponentUpdate(): boolean {
super(props); return false;
this.hasManagers = props.item.responsibles.length > 0; }
}
shouldComponentUpdate() { getCategoriesRender(categories: Array<number | null>): React.Node {
return false; const {props} = this;
} const final = [];
categories.forEach((cat: number | null) => {
getCategoriesRender(categories: Array<number | null>) { if (cat != null) {
let final = []; const category: ClubCategoryType = props.categoryTranslator(cat);
for (let i = 0; i < categories.length; i++) { final.push(
if (categories[i] !== null) { <Chip
const category: category = this.props.categoryTranslator(categories[i]); style={{marginRight: 5, marginBottom: 5}}
final.push( key={`${props.item.id}:${category.id}`}>
<Chip {category.name}
style={{marginRight: 5, marginBottom: 5}} </Chip>,
key={this.props.item.id + ':' + category.id}
>
{category.name}
</Chip>
);
}
}
return <View style={{flexDirection: 'row'}}>{final}</View>;
}
render() {
const categoriesRender = this.getCategoriesRender.bind(this, this.props.item.category);
const colors = this.props.theme.colors;
return (
<List.Item
title={this.props.item.name}
description={categoriesRender}
onPress={this.props.onPress}
left={(props) => <Avatar.Image
{...props}
style={{
backgroundColor: 'transparent',
marginLeft: 10,
marginRight: 10,
}}
size={64}
source={{uri: this.props.item.logo}}/>}
right={(props) => <Avatar.Icon
{...props}
style={{
marginTop: 'auto',
marginBottom: 'auto',
backgroundColor: 'transparent',
}}
size={48}
icon={this.hasManagers ? "check-circle-outline" : "alert-circle-outline"}
color={this.hasManagers ? colors.success : colors.primary}
/>}
style={{
height: this.props.height,
justifyContent: 'center',
}}
/>
); );
} }
});
return <View style={{flexDirection: 'row'}}>{final}</View>;
}
render(): React.Node {
const {props} = this;
const categoriesRender = (): React.Node =>
this.getCategoriesRender(props.item.category);
const {colors} = props.theme;
return (
<List.Item
title={props.item.name}
description={categoriesRender}
onPress={props.onPress}
left={(): React.Node => (
<Avatar.Image
style={{
backgroundColor: 'transparent',
marginLeft: 10,
marginRight: 10,
}}
size={64}
source={{uri: props.item.logo}}
/>
)}
right={(): React.Node => (
<Avatar.Icon
style={{
marginTop: 'auto',
marginBottom: 'auto',
backgroundColor: 'transparent',
}}
size={48}
icon={
this.hasManagers ? 'check-circle-outline' : 'alert-circle-outline'
}
color={this.hasManagers ? colors.success : colors.primary}
/>
)}
style={{
height: props.height,
justifyContent: 'center',
}}
/>
);
}
} }
export default withTheme(ClubListItem); export default withTheme(ClubListItem);

View file

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

View file

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

View file

@ -2,57 +2,57 @@
import * as React from 'react'; import * as React from 'react';
import {TouchableRipple, withTheme} from 'react-native-paper'; import {TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from "react-native"; import {Dimensions, Image, View} from 'react-native';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
type Props = { type PropsType = {
image: string, image: string,
isActive: boolean, isActive: boolean,
onPress: () => void, onPress: () => void,
theme: CustomTheme, theme: CustomThemeType,
}; };
/** /**
* Component used to render a small dashboard item * Component used to render a small dashboard item
*/ */
class DashboardEditPreviewItem extends React.Component<Props> { class DashboardEditPreviewItem extends React.Component<PropsType> {
itemSize: number;
itemSize: number; constructor(props: PropsType) {
super(props);
constructor(props: Props) { this.itemSize = Dimensions.get('window').width / 8;
super(props); }
this.itemSize = Dimensions.get('window').width / 8;
}
render() {
const props = this.props;
return (
<TouchableRipple
onPress={this.props.onPress}
borderless={true}
style={{
marginLeft: 5,
marginRight: 5,
backgroundColor: this.props.isActive ? this.props.theme.colors.textDisabled : "transparent",
borderRadius: 5
}}
>
<View style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: "100%",
height: "100%",
}}
/>
</View>
</TouchableRipple>
);
}
render(): React.Node {
const {props} = this;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: 5,
marginRight: 5,
backgroundColor: props.isActive
? props.theme.colors.textDisabled
: 'transparent',
borderRadius: 5,
}}>
<View
style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: '100%',
height: '100%',
}}
/>
</View>
</TouchableRipple>
);
}
} }
export default withTheme(DashboardEditPreviewItem) export default withTheme(DashboardEditPreviewItem);

View file

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

View file

@ -2,96 +2,116 @@
import * as React from 'react'; import * as React from 'react';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import {FlatList, View} from "react-native"; import {FlatList, View} from 'react-native';
import {stringMatchQuery} from "../../../utils/Search"; import {stringMatchQuery} from '../../../utils/Search';
import GroupListItem from "./GroupListItem"; import GroupListItem from './GroupListItem';
import AnimatedAccordion from "../../Animations/AnimatedAccordion"; import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import type {group, groupCategory} from "../../../screens/Planex/GroupSelectionScreen"; import type {
import type {CustomTheme} from "../../../managers/ThemeManager"; PlanexGroupType,
PlanexGroupCategoryType,
} from '../../../screens/Planex/GroupSelectionScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type Props = { type PropsType = {
item: groupCategory, item: PlanexGroupCategoryType,
onGroupPress: (group) => void, favorites: Array<PlanexGroupType>,
onFavoritePress: (group) => void, onGroupPress: (PlanexGroupType) => void,
currentSearchString: string, onFavoritePress: (PlanexGroupType) => void,
favoriteNumber: number, currentSearchString: string,
height: number, height: number,
theme: CustomTheme, theme: CustomThemeType,
} };
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
const REPLACE_REGEX = /_/g;
class GroupListAccordion extends React.Component<Props> { 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
);
}
shouldComponentUpdate(nextProps: Props) { getRenderItem = ({item}: {item: PlanexGroupType}): React.Node => {
return (nextProps.currentSearchString !== this.props.currentSearchString) const {props} = this;
|| (nextProps.favoriteNumber !== this.props.favoriteNumber) const onPress = () => {
|| (nextProps.item.content.length !== this.props.item.content.length); props.onGroupPress(item);
} };
const onStarPress = () => {
props.onFavoritePress(item);
};
return (
<GroupListItem
height={LIST_ITEM_HEIGHT}
item={item}
favorites={props.favorites}
onPress={onPress}
onStarPress={onStarPress}
/>
);
};
keyExtractor = (item: group) => item.id.toString(); getData(): Array<PlanexGroupType> {
const {props} = this;
const originalData = props.item.content;
const displayData = [];
originalData.forEach((data: PlanexGroupType) => {
if (stringMatchQuery(data.name, props.currentSearchString))
displayData.push(data);
});
return displayData;
}
renderItem = ({item}: { item: group }) => { itemLayout = (
const onPress = () => this.props.onGroupPress(item); data: ?Array<PlanexGroupType>,
const onStarPress = () => this.props.onFavoritePress(item); index: number,
return ( ): {length: number, offset: number, index: number} => ({
<GroupListItem length: LIST_ITEM_HEIGHT,
height={LIST_ITEM_HEIGHT} offset: LIST_ITEM_HEIGHT * index,
item={item} index,
onPress={onPress} });
onStarPress={onStarPress}/>
);
}
getData() { keyExtractor = (item: PlanexGroupType): string => item.id.toString();
const originalData = this.props.item.content;
let displayData = [];
for (let i = 0; i < originalData.length; i++) {
if (stringMatchQuery(originalData[i].name, this.props.currentSearchString))
displayData.push(originalData[i]);
}
return displayData;
}
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); render(): React.Node {
const {props} = this;
const {item} = this.props;
render() { return (
const item = this.props.item; <View>
return ( <AnimatedAccordion
<View> title={item.name.replace(REPLACE_REGEX, ' ')}
<AnimatedAccordion style={{
title={item.name} height: props.height,
style={{ justifyContent: 'center',
height: this.props.height, }}
justifyContent: 'center', left={({size}: {size: number}): React.Node =>
}} item.id === 0 ? (
left={props => <List.Icon
item.id === 0 size={size}
? <List.Icon icon="star"
{...props} color={props.theme.colors.tetrisScore}
icon={"star"} />
color={this.props.theme.colors.tetrisScore} ) : null
/> }
: null} unmountWhenCollapsed={item.id !== 0} // Only render list if expanded for increased performance
unmountWhenCollapsed={true}// Only render list if expanded for increased performance opened={props.currentSearchString.length > 0}>
opened={this.props.item.id === 0 || this.props.currentSearchString.length > 0} <FlatList
> data={this.getData()}
{/*$FlowFixMe*/} extraData={props.currentSearchString + props.favorites.length}
<FlatList renderItem={this.getRenderItem}
data={this.getData()} keyExtractor={this.keyExtractor}
extraData={this.props.currentSearchString} listKey={item.id.toString()}
renderItem={this.renderItem} // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
keyExtractor={this.keyExtractor} getItemLayout={this.itemLayout}
listKey={item.id.toString()} removeClippedSubviews
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration />
getItemLayout={this.itemLayout} </AnimatedAccordion>
removeClippedSubviews={true} </View>
/> );
</AnimatedAccordion> }
</View>
);
}
} }
export default withTheme(GroupListAccordion) export default withTheme(GroupListAccordion);

View file

@ -2,65 +2,85 @@
import * as React from 'react'; import * as React from 'react';
import {IconButton, List, withTheme} from 'react-native-paper'; import {IconButton, List, withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager"; import * as Animatable from 'react-native-animatable';
import type {group} from "../../../screens/Planex/GroupSelectionScreen"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
type Props = { type PropsType = {
theme: CustomTheme, theme: CustomThemeType,
onPress: () => void, onPress: () => void,
onStarPress: () => void, onStarPress: () => void,
item: group, item: PlanexGroupType,
height: number, favorites: Array<PlanexGroupType>,
} height: number,
};
type State = { const REPLACE_REGEX = /_/g;
isFav: boolean,
}
class GroupListItem extends React.Component<Props, State> { class GroupListItem extends React.Component<PropsType> {
isFav: boolean;
constructor(props) { starRef = {current: null | IconButton};
super(props);
this.state = { constructor(props: PropsType) {
isFav: (props.item.isFav !== undefined && props.item.isFav), super(props);
} 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;
}
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;
}
shouldComponentUpdate(prevProps: Props, prevState: State) { onStarPress = () => {
return (prevState.isFav !== this.state.isFav); const {props} = this;
if (this.starRef.current != null) {
if (this.isFav) this.starRef.current.rubberBand();
else this.starRef.current.swing();
} }
props.onStarPress();
};
onStarPress = () => { render(): React.Node {
this.setState({isFav: !this.state.isFav}); const {props} = this;
this.props.onStarPress(); const {colors} = props.theme;
} return (
<List.Item
render() { title={props.item.name.replace(REPLACE_REGEX, ' ')}
const colors = this.props.theme.colors; onPress={props.onPress}
return ( left={({size}: {size: number}): React.Node => (
<List.Item <List.Icon size={size} icon="chevron-right" />
title={this.props.item.name} )}
onPress={this.props.onPress} right={({size, color}: {size: number, color: string}): React.Node => (
left={props => <Animatable.View ref={this.starRef} useNativeDriver>
<List.Icon <IconButton
{...props} size={size}
icon={"chevron-right"}/>} icon="star"
right={props => onPress={this.onStarPress}
<IconButton color={this.isFav ? colors.tetrisScore : color}
{...props}
icon={"star"}
onPress={this.onStarPress}
color={this.state.isFav
? colors.tetrisScore
: props.color}
/>}
style={{
height: this.props.height,
justifyContent: 'center',
}}
/> />
); </Animatable.View>
} )}
style={{
height: props.height,
justifyContent: 'center',
}}
/>
);
}
} }
export default withTheme(GroupListItem); export default withTheme(GroupListItem);

View file

@ -2,48 +2,48 @@
import * as React from 'react'; import * as React from 'react';
import {Avatar, List, Text, withTheme} from 'react-native-paper'; import {Avatar, List, Text, withTheme} from 'react-native-paper';
import i18n from "i18n-js"; import i18n from 'i18n-js';
import type {ProximoArticleType} from '../../../screens/Services/Proximo/ProximoMainScreen';
type Props = { type PropsType = {
onPress: Function, onPress: () => void,
color: string, color: string,
item: Object, item: ProximoArticleType,
height: number, height: number,
} };
class ProximoListItem extends React.Component<Props> { class ProximoListItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
colors: Object; render(): React.Node {
const {props} = this;
constructor(props) { return (
super(props); <List.Item
this.colors = props.theme.colors; title={props.item.name}
} description={`${props.item.quantity} ${i18n.t(
'screens.proximo.inStock',
shouldComponentUpdate() { )}`}
return false; descriptionStyle={{color: props.color}}
} onPress={props.onPress}
left={(): React.Node => (
render() { <Avatar.Image
return ( style={{backgroundColor: 'transparent'}}
<List.Item size={64}
title={this.props.item.name} source={{uri: props.item.image}}
description={this.props.item.quantity + ' ' + i18n.t('screens.proximo.inStock')} />
descriptionStyle={{color: this.props.color}} )}
onPress={this.props.onPress} right={(): React.Node => (
left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64} <Text style={{fontWeight: 'bold'}}>{props.item.price}</Text>
source={{uri: this.props.item.image}}/>} )}
right={() => style={{
<Text style={{fontWeight: "bold"}}> height: props.height,
{this.props.item.price} justifyContent: 'center',
</Text>} }}
style={{ />
height: this.props.height, );
justifyContent: 'center', }
}}
/>
);
}
} }
export default withTheme(ProximoListItem); export default withTheme(ProximoListItem);

View file

@ -1,194 +1,236 @@
import * as React from 'react'; // @flow
import {Avatar, Caption, List, ProgressBar, Surface, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native";
import ProxiwashConstants from "../../../constants/ProxiwashConstants";
import i18n from "i18n-js";
import AprilFoolsManager from "../../../managers/AprilFoolsManager";
import * as Animatable from "react-native-animatable";
import type {CustomTheme} from "../../../managers/ThemeManager";
import type {Machine} from "../../../screens/Proxiwash/ProxiwashScreen";
type Props = { import * as React from 'react';
item: Machine, import {
theme: CustomTheme, Avatar,
onPress: Function, Caption,
isWatched: boolean, List,
ProgressBar,
Surface,
Text,
withTheme,
} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import * as Animatable from 'react-native-animatable';
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
import AprilFoolsManager from '../../../managers/AprilFoolsManager';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ProxiwashMachineType} from '../../../screens/Proxiwash/ProxiwashScreen';
type PropsType = {
item: ProxiwashMachineType,
theme: CustomThemeType,
onPress: (
title: string,
item: ProxiwashMachineType,
isDryer: boolean, isDryer: boolean,
height: number, ) => void,
} isWatched: boolean,
isDryer: boolean,
height: number,
};
const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon); const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon);
const styles = StyleSheet.create({
container: {
margin: 5,
justifyContent: 'center',
elevation: 1,
},
icon: {
backgroundColor: 'transparent',
},
progressBar: {
position: 'absolute',
left: 0,
borderRadius: 4,
},
});
/** /**
* Component used to display a proxiwash item, showing machine progression and state * Component used to display a proxiwash item, showing machine progression and state
*/ */
class ProxiwashListItem extends React.Component<Props> { class ProxiwashListItem extends React.Component<PropsType> {
stateColors: {[key: string]: string};
stateColors: Object; stateStrings: {[key: string]: string};
stateStrings: Object;
title: string; title: string;
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
this.stateColors = {}; this.stateColors = {};
this.stateStrings = {}; this.stateStrings = {};
this.updateStateStrings(); this.updateStateStrings();
let displayNumber = props.item.number; let displayNumber = props.item.number;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(parseInt(props.item.number)); displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(
parseInt(props.item.number, 10),
);
this.title = props.isDryer this.title = props.isDryer
? i18n.t('screens.proxiwash.dryer') ? i18n.t('screens.proxiwash.dryer')
: i18n.t('screens.proxiwash.washer'); : i18n.t('screens.proxiwash.washer');
this.title += ' n°' + displayNumber; this.title += `${displayNumber}`;
} }
shouldComponentUpdate(nextProps: Props): boolean { shouldComponentUpdate(nextProps: PropsType): boolean {
const props = this.props; const {props} = this;
return (nextProps.theme.dark !== props.theme.dark) return (
|| (nextProps.item.state !== props.item.state) nextProps.theme.dark !== props.theme.dark ||
|| (nextProps.item.donePercent !== props.item.donePercent) nextProps.item.state !== props.item.state ||
|| (nextProps.isWatched !== props.isWatched); nextProps.item.donePercent !== props.item.donePercent ||
} nextProps.isWatched !== props.isWatched
);
}
updateStateStrings() { onListItemPress = () => {
this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t('screens.proxiwash.states.ready'); const {props} = this;
this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t('screens.proxiwash.states.running'); props.onPress(this.title, props.item, props.isDryer);
this.stateStrings[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = i18n.t('screens.proxiwash.states.runningNotStarted'); };
this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t('screens.proxiwash.states.finished');
this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t('screens.proxiwash.states.broken');
this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t('screens.proxiwash.states.error');
this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t('screens.proxiwash.states.unknown');
}
updateStateColors() { updateStateStrings() {
const colors = this.props.theme.colors; this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t(
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] = colors.proxiwashReadyColor; 'screens.proxiwash.states.ready',
this.stateColors[ProxiwashConstants.machineStates.RUNNING] = colors.proxiwashRunningColor; );
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = colors.proxiwashRunningNotStartedColor; this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t(
this.stateColors[ProxiwashConstants.machineStates.FINISHED] = colors.proxiwashFinishedColor; 'screens.proxiwash.states.running',
this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] = colors.proxiwashBrokenColor; );
this.stateColors[ProxiwashConstants.machineStates.ERROR] = colors.proxiwashErrorColor; this.stateStrings[
this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] = colors.proxiwashUnknownColor; ProxiwashConstants.machineStates.RUNNING_NOT_STARTED
} ] = i18n.t('screens.proxiwash.states.runningNotStarted');
this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t(
'screens.proxiwash.states.finished',
);
this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t(
'screens.proxiwash.states.broken',
);
this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t(
'screens.proxiwash.states.error',
);
this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t(
'screens.proxiwash.states.unknown',
);
}
onListItemPress = () => this.props.onPress(this.title, this.props.item, this.props.isDryer); updateStateColors() {
const {props} = this;
const {colors} = props.theme;
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] =
colors.proxiwashReadyColor;
this.stateColors[ProxiwashConstants.machineStates.RUNNING] =
colors.proxiwashRunningColor;
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] =
colors.proxiwashRunningNotStartedColor;
this.stateColors[ProxiwashConstants.machineStates.FINISHED] =
colors.proxiwashFinishedColor;
this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] =
colors.proxiwashBrokenColor;
this.stateColors[ProxiwashConstants.machineStates.ERROR] =
colors.proxiwashErrorColor;
this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] =
colors.proxiwashUnknownColor;
}
render() { render(): React.Node {
const props = this.props; const {props} = this;
const colors = props.theme.colors; const {colors} = props.theme;
const machineState = props.item.state; const machineState = props.item.state;
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING; const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING;
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE; const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE;
const description = isRunning ? props.item.startTime + '/' + props.item.endTime : ''; const description = isRunning
const stateIcon = ProxiwashConstants.stateIcons[machineState]; ? `${props.item.startTime}/${props.item.endTime}`
const stateString = this.stateStrings[machineState]; : '';
const progress = isRunning const stateIcon = ProxiwashConstants.stateIcons[machineState];
? props.item.donePercent !== '' const stateString = this.stateStrings[machineState];
? parseFloat(props.item.donePercent) / 100 let progress;
: 0 if (isRunning && props.item.donePercent !== '')
: 1; progress = parseFloat(props.item.donePercent) / 100;
else if (isRunning) progress = 0;
else progress = 1;
const icon = props.isWatched const icon = props.isWatched ? (
? <AnimatedIcon <AnimatedIcon
icon={'bell-ring'} icon="bell-ring"
animation={"rubberBand"} animation="rubberBand"
useNativeDriver useNativeDriver
size={50} size={50}
color={colors.primary} color={colors.primary}
style={styles.icon} style={styles.icon}
/> />
: <AnimatedIcon ) : (
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'} <AnimatedIcon
animation={isRunning ? "pulse" : undefined} icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
iterationCount={"infinite"} animation={isRunning ? 'pulse' : undefined}
easing={"linear"} iterationCount="infinite"
duration={1000} easing="linear"
useNativeDriver duration={1000}
size={40} useNativeDriver
color={colors.text} size={40}
style={styles.icon} color={colors.text}
/>; style={styles.icon}
this.updateStateColors(); />
return ( );
<Surface this.updateStateColors();
style={{ return (
...styles.container, <Surface
height: props.height, style={{
borderRadius: 4, ...styles.container,
}} height: props.height,
> borderRadius: 4,
{ }}>
!isReady {!isReady ? (
? <ProgressBar <ProgressBar
style={{ style={{
...styles.progressBar, ...styles.progressBar,
height: props.height height: props.height,
}} }}
progress={progress} progress={progress}
color={this.stateColors[machineState]} color={this.stateColors[machineState]}
/> />
: null ) : null}
} <List.Item
<List.Item title={this.title}
title={this.title} description={description}
description={description} style={{
style={{ height: props.height,
height: props.height, justifyContent: 'center',
justifyContent: 'center', }}
}} onPress={this.onListItemPress}
onPress={this.onListItemPress} left={(): React.Node => icon}
left={() => icon} right={(): React.Node => (
right={() => ( <View style={{flexDirection: 'row'}}>
<View style={{flexDirection: 'row',}}> <View style={{justifyContent: 'center'}}>
<View style={{justifyContent: 'center',}}> <Text
<Text style={ style={
machineState === ProxiwashConstants.machineStates.FINISHED ? machineState === ProxiwashConstants.machineStates.FINISHED
{fontWeight: 'bold',} : {} ? {fontWeight: 'bold'}
} : {}
> }>
{stateString} {stateString}
</Text> </Text>
{ {machineState === ProxiwashConstants.machineStates.RUNNING ? (
machineState === ProxiwashConstants.machineStates.RUNNING <Caption>{props.item.remainingTime} min</Caption>
? <Caption>{props.item.remainingTime} min</Caption> ) : null}
: null </View>
} <View style={{justifyContent: 'center'}}>
<Avatar.Icon
</View> icon={stateIcon}
<View style={{justifyContent: 'center',}}> color={colors.text}
<Avatar.Icon size={30}
icon={stateIcon} style={styles.icon}
color={colors.text}
size={30}
style={styles.icon}
/>
</View>
</View>)}
/> />
</Surface> </View>
); </View>
} )}
/>
</Surface>
);
}
} }
const styles = StyleSheet.create({
container: {
margin: 5,
justifyContent: 'center',
elevation: 1
},
icon: {
backgroundColor: 'transparent'
},
progressBar: {
position: 'absolute',
left: 0,
borderRadius: 4,
},
});
export default withTheme(ProxiwashListItem); export default withTheme(ProxiwashListItem);

View file

@ -1,72 +1,72 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Text, withTheme} from 'react-native-paper'; import {Avatar, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from 'react-native';
import i18n from "i18n-js"; import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type Props = { type PropsType = {
title: string, theme: CustomThemeType,
isDryer: boolean, title: string,
nbAvailable: number, isDryer: boolean,
} nbAvailable: number,
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginLeft: 5,
marginRight: 5,
marginBottom: 10,
marginTop: 20,
},
icon: {
backgroundColor: 'transparent',
},
text: {
fontSize: 20,
fontWeight: 'bold',
},
});
/** /**
* Component used to display a proxiwash item, showing machine progression and state * Component used to display a proxiwash item, showing machine progression and state
*/ */
class ProxiwashListItem extends React.Component<Props> { class ProxiwashListItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.nbAvailable !== props.nbAvailable
);
}
constructor(props) { render(): React.Node {
super(props); const {props} = this;
} const subtitle = `${props.nbAvailable} ${
props.nbAvailable <= 1
shouldComponentUpdate(nextProps: Props) { ? i18n.t('screens.proxiwash.numAvailable')
return (nextProps.theme.dark !== this.props.theme.dark) : i18n.t('screens.proxiwash.numAvailablePlural')
|| (nextProps.nbAvailable !== this.props.nbAvailable) }`;
} const iconColor =
props.nbAvailable > 0
render() { ? props.theme.colors.success
const props = this.props; : props.theme.colors.primary;
const subtitle = props.nbAvailable + ' ' + ( return (
(props.nbAvailable <= 1) <View style={styles.container}>
? i18n.t('screens.proxiwash.numAvailable') <Avatar.Icon
: i18n.t('screens.proxiwash.numAvailablePlural')); icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
const iconColor = props.nbAvailable > 0 color={iconColor}
? this.props.theme.colors.success style={styles.icon}
: this.props.theme.colors.primary; />
return ( <View style={{justifyContent: 'center'}}>
<View style={styles.container}> <Text style={styles.text}>{props.title}</Text>
<Avatar.Icon <Text style={{color: props.theme.colors.subtitle}}>{subtitle}</Text>
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'} </View>
color={iconColor} </View>
style={styles.icon} );
/> }
<View style={{justifyContent: 'center'}}>
<Text style={styles.text}>
{props.title}
</Text>
<Text style={{color: this.props.theme.colors.subtitle}}>
{subtitle}
</Text>
</View>
</View>
);
}
} }
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginLeft: 5,
marginRight: 5,
marginBottom: 10,
marginTop: 20,
},
icon: {
backgroundColor: 'transparent'
},
text: {
fontSize: 20,
fontWeight: 'bold',
}
});
export default withTheme(ProxiwashListItem); export default withTheme(ProxiwashListItem);

View file

@ -1,259 +1,269 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import * as Animatable from "react-native-animatable"; import * as Animatable from 'react-native-animatable';
import {Image, TouchableWithoutFeedback, View} from "react-native"; import {Image, TouchableWithoutFeedback, View} from 'react-native';
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet"; import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
type Props = { export type AnimatableViewRefType = {current: null | Animatable.View};
style?: ViewStyle,
emotion: number,
animated: boolean,
entryAnimation: Animatable.AnimatableProperties | null,
loopAnimation: Animatable.AnimatableProperties | null,
onPress?: (viewRef: AnimatableViewRef) => null,
onLongPress?: (viewRef: AnimatableViewRef) => null,
}
type State = { type PropsType = {
currentEmotion: number, emotion?: number,
} animated?: boolean,
style?: ViewStyle | null,
export type AnimatableViewRef = {current: null | Animatable.View}; entryAnimation?: Animatable.AnimatableProperties | null,
loopAnimation?: Animatable.AnimatableProperties | null,
const MASCOT_IMAGE = require("../../../assets/mascot/mascot.png"); onPress?: null | ((viewRef: AnimatableViewRefType) => void),
const MASCOT_EYES_NORMAL = require("../../../assets/mascot/mascot_eyes_normal.png"); onLongPress?: null | ((viewRef: AnimatableViewRefType) => void),
const MASCOT_EYES_GIRLY = require("../../../assets/mascot/mascot_eyes_girly.png");
const MASCOT_EYES_CUTE = require("../../../assets/mascot/mascot_eyes_cute.png");
const MASCOT_EYES_WINK = require("../../../assets/mascot/mascot_eyes_wink.png");
const MASCOT_EYES_HEART = require("../../../assets/mascot/mascot_eyes_heart.png");
const MASCOT_EYES_ANGRY = require("../../../assets/mascot/mascot_eyes_angry.png");
const MASCOT_GLASSES = require("../../../assets/mascot/mascot_glasses.png");
const MASCOT_SUNGLASSES = require("../../../assets/mascot/mascot_sunglasses.png");
export const EYE_STYLE = {
NORMAL: 0,
GIRLY: 2,
CUTE: 3,
WINK: 4,
HEART: 5,
ANGRY: 6,
}
const GLASSES_STYLE = {
NORMAL: 0,
COOl: 1
}
export const MASCOT_STYLE = {
NORMAL: 0,
HAPPY: 1,
GIRLY: 2,
WINK: 3,
CUTE: 4,
INTELLO: 5,
LOVE: 6,
COOL: 7,
ANGRY: 8,
RANDOM: 999,
}; };
type StateType = {
currentEmotion: number,
};
class Mascot extends React.Component<Props, State> { const MASCOT_IMAGE = require('../../../assets/mascot/mascot.png');
const MASCOT_EYES_NORMAL = require('../../../assets/mascot/mascot_eyes_normal.png');
const MASCOT_EYES_GIRLY = require('../../../assets/mascot/mascot_eyes_girly.png');
const MASCOT_EYES_CUTE = require('../../../assets/mascot/mascot_eyes_cute.png');
const MASCOT_EYES_WINK = require('../../../assets/mascot/mascot_eyes_wink.png');
const MASCOT_EYES_HEART = require('../../../assets/mascot/mascot_eyes_heart.png');
const MASCOT_EYES_ANGRY = require('../../../assets/mascot/mascot_eyes_angry.png');
const MASCOT_GLASSES = require('../../../assets/mascot/mascot_glasses.png');
const MASCOT_SUNGLASSES = require('../../../assets/mascot/mascot_sunglasses.png');
static defaultProps = { export const EYE_STYLE = {
animated: false, NORMAL: 0,
entryAnimation: { GIRLY: 2,
useNativeDriver: true, CUTE: 3,
animation: "rubberBand", WINK: 4,
duration: 2000, HEART: 5,
}, ANGRY: 6,
loopAnimation: { };
useNativeDriver: true,
animation: "swing",
duration: 2000,
iterationDelay: 250,
iterationCount: "infinite",
},
clickAnimation: {
useNativeDriver: true,
animation: "rubberBand",
duration: 2000,
},
}
viewRef: AnimatableViewRef; const GLASSES_STYLE = {
eyeList: { [key: number]: number | string }; NORMAL: 0,
glassesList: { [key: number]: number | string }; COOl: 1,
};
onPress: (viewRef: AnimatableViewRef) => null; export const MASCOT_STYLE = {
onLongPress: (viewRef: AnimatableViewRef) => null; NORMAL: 0,
HAPPY: 1,
GIRLY: 2,
WINK: 3,
CUTE: 4,
INTELLO: 5,
LOVE: 6,
COOL: 7,
ANGRY: 8,
RANDOM: 999,
};
initialEmotion: number; class Mascot extends React.Component<PropsType, StateType> {
static defaultProps = {
emotion: MASCOT_STYLE.NORMAL,
animated: false,
style: null,
entryAnimation: {
useNativeDriver: true,
animation: 'rubberBand',
duration: 2000,
},
loopAnimation: {
useNativeDriver: true,
animation: 'swing',
duration: 2000,
iterationDelay: 250,
iterationCount: 'infinite',
},
onPress: null,
onLongPress: null,
};
constructor(props: Props) { viewRef: AnimatableViewRefType;
super(props);
this.viewRef = React.createRef();
this.eyeList = {};
this.glassesList = {};
this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL;
this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY;
this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE;
this.eyeList[EYE_STYLE.WINK] = MASCOT_EYES_WINK;
this.eyeList[EYE_STYLE.HEART] = MASCOT_EYES_HEART;
this.eyeList[EYE_STYLE.ANGRY] = MASCOT_EYES_ANGRY;
this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES; eyeList: {[key: number]: number | string};
this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES;
this.initialEmotion = this.props.emotion; glassesList: {[key: number]: number | string};
if (this.initialEmotion === MASCOT_STYLE.RANDOM) onPress: (viewRef: AnimatableViewRefType) => void;
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
this.state = { onLongPress: (viewRef: AnimatableViewRefType) => void;
currentEmotion: this.initialEmotion
initialEmotion: number;
constructor(props: PropsType) {
super(props);
this.viewRef = React.createRef();
this.eyeList = {};
this.glassesList = {};
this.eyeList[EYE_STYLE.NORMAL] = MASCOT_EYES_NORMAL;
this.eyeList[EYE_STYLE.GIRLY] = MASCOT_EYES_GIRLY;
this.eyeList[EYE_STYLE.CUTE] = MASCOT_EYES_CUTE;
this.eyeList[EYE_STYLE.WINK] = MASCOT_EYES_WINK;
this.eyeList[EYE_STYLE.HEART] = MASCOT_EYES_HEART;
this.eyeList[EYE_STYLE.ANGRY] = MASCOT_EYES_ANGRY;
this.glassesList[GLASSES_STYLE.NORMAL] = MASCOT_GLASSES;
this.glassesList[GLASSES_STYLE.COOl] = MASCOT_SUNGLASSES;
this.initialEmotion =
props.emotion != null ? props.emotion : Mascot.defaultProps.emotion;
if (this.initialEmotion === MASCOT_STYLE.RANDOM)
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
this.state = {
currentEmotion: this.initialEmotion,
};
if (props.onPress == null) {
this.onPress = (viewRef: AnimatableViewRefType) => {
const ref = viewRef.current;
if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
ref.rubberBand(1500).then(() => {
this.setState({currentEmotion: this.initialEmotion});
});
} }
};
} else this.onPress = props.onPress;
if (this.props.onPress == null) { if (props.onLongPress == null) {
this.onPress = (viewRef: AnimatableViewRef) => { this.onLongPress = (viewRef: AnimatableViewRefType) => {
let ref = viewRef.current; const ref = viewRef.current;
if (ref != null) { if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.LOVE}); this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
ref.rubberBand(1500).then(() => { ref.tada(1000).then(() => {
this.setState({currentEmotion: this.initialEmotion}); this.setState({currentEmotion: this.initialEmotion});
}); });
}
return null;
}
} else
this.onPress = this.props.onPress;
if (this.props.onLongPress == null) {
this.onLongPress = (viewRef: AnimatableViewRef) => {
let ref = viewRef.current;
if (ref != null) {
this.setState({currentEmotion: MASCOT_STYLE.ANGRY});
ref.tada(1000).then(() => {
this.setState({currentEmotion: this.initialEmotion});
});
}
return null;
}
} else
this.onLongPress = this.props.onLongPress;
}
getGlasses(style: number) {
const glasses = this.glassesList[style];
return <Image
key={"glasses"}
source={glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]}
style={{
position: "absolute",
top: "15%",
left: 0,
width: "100%",
height: "100%",
}}
/>
}
getEye(style: number, isRight: boolean, rotation: string="0deg") {
const eye = this.eyeList[style];
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}]
}}
/>
}
getEyes(emotion: number) {
let final = [];
final.push(<View
key={"container"}
style={{
position: "absolute",
width: "100%",
height: "100%",
}}/>);
if (emotion === MASCOT_STYLE.CUTE) {
final.push(this.getEye(EYE_STYLE.CUTE, true));
final.push(this.getEye(EYE_STYLE.CUTE, false));
} else if (emotion === MASCOT_STYLE.GIRLY) {
final.push(this.getEye(EYE_STYLE.GIRLY, true));
final.push(this.getEye(EYE_STYLE.GIRLY, false));
} else if (emotion === MASCOT_STYLE.HAPPY) {
final.push(this.getEye(EYE_STYLE.WINK, true));
final.push(this.getEye(EYE_STYLE.WINK, false));
} else if (emotion === MASCOT_STYLE.WINK) {
final.push(this.getEye(EYE_STYLE.WINK, true));
final.push(this.getEye(EYE_STYLE.NORMAL, false));
} else if (emotion === MASCOT_STYLE.LOVE) {
final.push(this.getEye(EYE_STYLE.HEART, true));
final.push(this.getEye(EYE_STYLE.HEART, false));
} else if (emotion === MASCOT_STYLE.ANGRY) {
final.push(this.getEye(EYE_STYLE.ANGRY, true));
final.push(this.getEye(EYE_STYLE.ANGRY, false, "180deg"));
} else if (emotion === MASCOT_STYLE.COOL) {
final.push(this.getGlasses(GLASSES_STYLE.COOl));
} else {
final.push(this.getEye(EYE_STYLE.NORMAL, true));
final.push(this.getEye(EYE_STYLE.NORMAL, false));
} }
};
} else this.onLongPress = props.onLongPress;
}
if (emotion === MASCOT_STYLE.INTELLO) { // Needs to have normal eyes behind the glasses getGlasses(style: number): React.Node {
final.push(this.getGlasses(GLASSES_STYLE.NORMAL)); const glasses = this.glassesList[style];
return (
<Image
key="glasses"
source={
glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]
} }
final.push(<View key={"container2"}/>); style={{
return final; position: 'absolute',
top: '15%',
left: 0,
width: '100%',
height: '100%',
}}
/>
);
}
getEye(
style: number,
isRight: boolean,
rotation: string = '0deg',
): React.Node {
const eye = this.eyeList[style];
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}],
}}
/>
);
}
getEyes(emotion: number): React.Node {
const final = [];
final.push(
<View
key="container"
style={{
position: 'absolute',
width: '100%',
height: '100%',
}}
/>,
);
if (emotion === MASCOT_STYLE.CUTE) {
final.push(this.getEye(EYE_STYLE.CUTE, true));
final.push(this.getEye(EYE_STYLE.CUTE, false));
} else if (emotion === MASCOT_STYLE.GIRLY) {
final.push(this.getEye(EYE_STYLE.GIRLY, true));
final.push(this.getEye(EYE_STYLE.GIRLY, false));
} else if (emotion === MASCOT_STYLE.HAPPY) {
final.push(this.getEye(EYE_STYLE.WINK, true));
final.push(this.getEye(EYE_STYLE.WINK, false));
} else if (emotion === MASCOT_STYLE.WINK) {
final.push(this.getEye(EYE_STYLE.WINK, true));
final.push(this.getEye(EYE_STYLE.NORMAL, false));
} else if (emotion === MASCOT_STYLE.LOVE) {
final.push(this.getEye(EYE_STYLE.HEART, true));
final.push(this.getEye(EYE_STYLE.HEART, false));
} else if (emotion === MASCOT_STYLE.ANGRY) {
final.push(this.getEye(EYE_STYLE.ANGRY, true));
final.push(this.getEye(EYE_STYLE.ANGRY, false, '180deg'));
} else if (emotion === MASCOT_STYLE.COOL) {
final.push(this.getGlasses(GLASSES_STYLE.COOl));
} else {
final.push(this.getEye(EYE_STYLE.NORMAL, true));
final.push(this.getEye(EYE_STYLE.NORMAL, false));
} }
render() { if (emotion === MASCOT_STYLE.INTELLO) {
const entryAnimation = this.props.animated ? this.props.entryAnimation : null; // Needs to have normal eyes behind the glasses
const loopAnimation = this.props.animated ? this.props.loopAnimation : null; final.push(this.getGlasses(GLASSES_STYLE.NORMAL));
return ( }
final.push(<View key="container2" />);
return final;
}
render(): React.Node {
const {props, state} = this;
const entryAnimation = props.animated ? props.entryAnimation : null;
const loopAnimation = props.animated ? props.loopAnimation : null;
return (
<Animatable.View
style={{
aspectRatio: 1,
...props.style,
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...entryAnimation}>
<TouchableWithoutFeedback
onPress={() => {
this.onPress(this.viewRef);
}}
onLongPress={() => {
this.onLongPress(this.viewRef);
}}>
<Animatable.View ref={this.viewRef}>
<Animatable.View <Animatable.View
// eslint-disable-next-line react/jsx-props-no-spreading
{...loopAnimation}>
<Image
source={MASCOT_IMAGE}
style={{ style={{
aspectRatio: 1, width: '100%',
...this.props.style height: '100%',
}} }}
{...entryAnimation} />
> {this.getEyes(state.currentEmotion)}
<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%",
}}
/>
{this.getEyes(this.state.currentEmotion)}
</Animatable.View>
</Animatable.View>
</TouchableWithoutFeedback>
</Animatable.View> </Animatable.View>
); </Animatable.View>
} </TouchableWithoutFeedback>
</Animatable.View>
);
}
} }
export default Mascot; export default Mascot;

View file

@ -1,283 +1,312 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Button, Card, Paragraph, Portal, withTheme} from 'react-native-paper'; import {
import Mascot from "./Mascot"; Avatar,
import * as Animatable from "react-native-animatable"; Button,
import {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native"; Card,
import type {CustomTheme} from "../../managers/ThemeManager"; Paragraph,
import SpeechArrow from "./SpeechArrow"; Portal,
import AsyncStorageManager from "../../managers/AsyncStorageManager"; withTheme,
} from 'react-native-paper';
import * as Animatable from 'react-native-animatable';
import {
BackHandler,
Dimensions,
ScrollView,
TouchableWithoutFeedback,
View,
} from 'react-native';
import Mascot from './Mascot';
import type {CustomThemeType} from '../../managers/ThemeManager';
import SpeechArrow from './SpeechArrow';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
type Props = { type PropsType = {
theme: CustomTheme, theme: CustomThemeType,
icon: string, icon: string,
title: string, title: string,
message: string, message: string,
buttons: { buttons: {
action: { action: {
message: string, message: string,
icon: string | null, icon: string | null,
color: string | null, color: string | null,
onPress?: () => void, onPress?: () => void,
},
cancel: {
message: string,
icon: string | null,
color: string | null,
onPress?: () => void,
}
}, },
emotion: number, cancel: {
visible?: boolean, message: string,
prefKey?: string, icon: string | null,
} color: string | null,
onPress?: () => void,
},
},
emotion: number,
visible?: boolean,
prefKey?: string,
};
type State = { type StateType = {
shouldRenderDialog: boolean, // Used to stop rendering after hide animation shouldRenderDialog: boolean, // Used to stop rendering after hide animation
dialogVisible: boolean, dialogVisible: boolean,
} };
/** /**
* Component used to display a popup with the mascot. * Component used to display a popup with the mascot.
*/ */
class MascotPopup extends React.Component<Props, State> { class MascotPopup extends React.Component<PropsType, StateType> {
static defaultProps = {
visible: null,
prefKey: null,
};
mascotSize: number; mascotSize: number;
windowWidth: number;
windowHeight: number;
constructor(props: Props) { windowWidth: number;
super(props);
this.windowWidth = Dimensions.get('window').width; windowHeight: number;
this.windowHeight = Dimensions.get('window').height;
this.mascotSize = Dimensions.get('window').height / 6; constructor(props: PropsType) {
super(props);
if (this.props.visible != null) { this.windowWidth = Dimensions.get('window').width;
this.state = { this.windowHeight = Dimensions.get('window').height;
shouldRenderDialog: this.props.visible,
dialogVisible: this.props.visible,
};
} else if (this.props.prefKey != null) {
const visible = AsyncStorageManager.getBool(this.props.prefKey);
this.state = {
shouldRenderDialog: visible,
dialogVisible: visible,
};
} else {
this.state = {
shouldRenderDialog: false,
dialogVisible: false,
};
}
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,
};
} else {
this.state = {
shouldRenderDialog: false,
dialogVisible: false,
};
} }
}
onAnimationEnd = () => { componentDidMount(): * {
this.setState({ BackHandler.addEventListener(
shouldRenderDialog: false, '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;
}
shouldComponentUpdate(nextProps: Props, nextState: State): boolean { onAnimationEnd = () => {
if (nextProps.visible) { this.setState({
this.state.shouldRenderDialog = true; shouldRenderDialog: false,
this.state.dialogVisible = true; });
} else if (nextProps.visible !== this.props.visible };
|| (!nextState.dialogVisible && nextState.dialogVisible !== this.state.dialogVisible)) {
this.state.dialogVisible = false; onBackButtonPressAndroid = (): boolean => {
setTimeout(this.onAnimationEnd, 300); const {state, props} = this;
} if (state.dialogVisible) {
return true; const {cancel} = props.buttons;
const {action} = props.buttons;
if (cancel != null) this.onDismiss(cancel.onPress);
else this.onDismiss(action.onPress);
return true;
} }
return false;
};
componentDidMount(): * { getSpeechBubble(): React.Node {
BackHandler.addEventListener( const {state, props} = this;
'hardwareBackPress', return (
this.onBackButtonPressAndroid <Animatable.View
) style={{
} marginLeft: '10%',
marginRight: '10%',
onBackButtonPressAndroid = () => { }}
if (this.state.dialogVisible) { useNativeDriver
const cancel = this.props.buttons.cancel; animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
const action = this.props.buttons.action; duration={state.dialogVisible ? 1000 : 300}>
if (cancel != null) <SpeechArrow
this.onDismiss(cancel.onPress); style={{marginLeft: this.mascotSize / 3}}
else size={20}
this.onDismiss(action.onPress); color={props.theme.colors.mascotMessageArrow}
return true; />
} else { <Card
return false; style={{
} borderColor: props.theme.colors.mascotMessageArrow,
}; borderWidth: 4,
borderRadius: 10,
getSpeechBubble() { }}>
return ( <Card.Title
<Animatable.View title={props.title}
style={{ left={
marginLeft: "10%", props.icon != null
marginRight: "10%", ? (): React.Node => (
}} <Avatar.Icon
useNativeDriver={true} size={48}
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"} style={{backgroundColor: 'transparent'}}
duration={this.state.dialogVisible ? 1000 : 300} color={props.theme.colors.primary}
> icon={props.icon}
<SpeechArrow
style={{marginLeft: this.mascotSize / 3}}
size={20}
color={this.props.theme.colors.mascotMessageArrow}
/>
<Card style={{
borderColor: this.props.theme.colors.mascotMessageArrow,
borderWidth: 4,
borderRadius: 10,
}}>
<Card.Title
title={this.props.title}
left={this.props.icon != null ?
(props) => <Avatar.Icon
{...props}
size={48}
style={{backgroundColor: "transparent"}}
color={this.props.theme.colors.primary}
icon={this.props.icon}
/>
: null}
/> />
)
<Card.Content style={{ : null
maxHeight: this.windowHeight / 3 }
}}> />
<ScrollView> <Card.Content
<Paragraph style={{marginBottom: 10}}> style={{
{this.props.message} maxHeight: this.windowHeight / 3,
</Paragraph>
</ScrollView>
</Card.Content>
<Card.Actions style={{marginTop: 10, marginBottom: 10}}>
{this.getButtons()}
</Card.Actions>
</Card>
</Animatable.View>
);
}
getMascot() {
return (
<Animatable.View
useNativeDriver={true}
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
duration={this.state.dialogVisible ? 1500 : 200}
>
<Mascot
style={{width: this.mascotSize}}
animated={true}
emotion={this.props.emotion}
/>
</Animatable.View>
);
}
getButtons() {
const action = this.props.buttons.action;
const cancel = this.props.buttons.cancel;
return (
<View style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: "auto",
marginBottom: "auto",
}}> }}>
{action != null <ScrollView>
? <Button <Paragraph style={{marginBottom: 10}}>{props.message}</Paragraph>
style={{ </ScrollView>
marginLeft: 'auto', </Card.Content>
marginRight: 'auto',
marginBottom: 10, <Card.Actions style={{marginTop: 10, marginBottom: 10}}>
}} {this.getButtons()}
mode={"contained"} </Card.Actions>
icon={action.icon} </Card>
color={action.color} </Animatable.View>
onPress={() => this.onDismiss(action.onPress)} );
> }
{action.message}
</Button> getMascot(): React.Node {
: null} const {props, state} = this;
{cancel != null return (
? <Button <Animatable.View
style={{ useNativeDriver
marginLeft: 'auto', animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
marginRight: 'auto', duration={state.dialogVisible ? 1500 : 200}>
}} <Mascot
mode={"contained"} style={{width: this.mascotSize}}
icon={cancel.icon} animated
color={cancel.color} emotion={props.emotion}
onPress={() => this.onDismiss(cancel.onPress)} />
> </Animatable.View>
{cancel.message} );
</Button> }
: null}
getButtons(): React.Node {
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(): React.Node {
const {props, state} = this;
return (
<TouchableWithoutFeedback
onPress={() => {
this.onDismiss(props.buttons.cancel.onPress);
}}>
<Animatable.View
style={{
position: 'absolute',
backgroundColor: 'rgba(0,0,0,0.7)',
width: '100%',
height: '100%',
}}
useNativeDriver
animation={state.dialogVisible ? 'fadeIn' : 'fadeOut'}
duration={state.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) callback();
};
render(): React.Node {
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> </View>
); </View>
} </Portal>
);
getBackground() {
return (
<TouchableWithoutFeedback onPress={() => this.onDismiss(this.props.buttons.cancel.onPress)}>
<Animatable.View
style={{
position: "absolute",
backgroundColor: "rgba(0,0,0,0.7)",
width: "100%",
height: "100%",
}}
useNativeDriver={true}
animation={this.state.dialogVisible ? "fadeIn" : "fadeOut"}
duration={this.state.dialogVisible ? 300 : 300}
/>
</TouchableWithoutFeedback>
);
}
onDismiss = (callback?: ()=> void) => {
if (this.props.prefKey != null) {
AsyncStorageManager.set(this.props.prefKey, false);
this.setState({dialogVisible: false});
}
if (callback != null)
callback();
}
render() {
if (this.state.shouldRenderDialog) {
return (
<Portal>
{this.getBackground()}
<View style={{
marginTop: "auto",
marginBottom: "auto",
}}>
<View style={{
marginTop: -80,
width: "100%"
}}>
{this.getMascot()}
{this.getSpeechBubble()}
</View>
</View>
</Portal>
);
} else
return null;
} }
return null;
}
} }
export default withTheme(MascotPopup); export default withTheme(MascotPopup);

View file

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

View file

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

View file

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

View file

@ -1,27 +1,39 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {HeaderButton, HeaderButtons} from 'react-navigation-header-buttons'; import {HeaderButton, HeaderButtons} from 'react-navigation-header-buttons';
import {withTheme} from "react-native-paper"; import {withTheme} from 'react-native-paper';
import type {CustomThemeType} from '../../managers/ThemeManager';
const MaterialHeaderButton = (props: Object) => const MaterialHeaderButton = (props: {
theme: CustomThemeType,
color: string,
}): React.Node => {
const {color, theme} = props;
return (
// $FlowFixMe
<HeaderButton <HeaderButton
{...props} // eslint-disable-next-line react/jsx-props-no-spreading
IconComponent={MaterialCommunityIcons} {...props}
iconSize={26} IconComponent={MaterialCommunityIcons}
color={props.color != null ? props.color : props.theme.colors.text} iconSize={26}
/>; color={color != null ? color : theme.colors.text}
/>
const MaterialHeaderButtons = (props: Object) => { );
return (
<HeaderButtons
{...props}
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
/>
);
}; };
export default withTheme(MaterialHeaderButtons); const MaterialHeaderButtons = (props: {...}): React.Node => {
return (
// $FlowFixMe
<HeaderButtons
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
/>
);
};
export default MaterialHeaderButtons;
export {Item} from 'react-navigation-header-buttons'; export {Item} from 'react-navigation-header-buttons';

View file

@ -1,411 +1,403 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Platform, StatusBar, StyleSheet, View} from "react-native"; import {Platform, StatusBar, StyleSheet, View} from 'react-native';
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons"; import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AppIntroSlider from "react-native-app-intro-slider"; import AppIntroSlider from 'react-native-app-intro-slider';
import Update from "../../constants/Update";
import ThemeManager from "../../managers/ThemeManager";
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import Mascot, {MASCOT_STYLE} from "../Mascot/Mascot"; import * as Animatable from 'react-native-animatable';
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';
type Props = { type PropsType = {
onDone: Function, onDone: () => void,
isUpdate: boolean, isUpdate: boolean,
isAprilFools: boolean, isAprilFools: boolean,
}; };
type State = { type StateType = {
currentSlide: number, currentSlide: number,
}
type Slide = {
key: string,
title: string,
text: string,
view: () => React.Node,
mascotStyle: number,
colors: [string, string]
}; };
type IntroSlideType = {
key: string,
title: string,
text: string,
view: () => React.Node,
mascotStyle: number,
colors: [string, string],
};
const styles = StyleSheet.create({
mainContent: {
paddingBottom: 100,
},
text: {
color: 'rgba(255, 255, 255, 0.8)',
backgroundColor: 'transparent',
textAlign: 'center',
paddingHorizontal: 16,
},
title: {
fontSize: 22,
color: 'white',
backgroundColor: 'transparent',
textAlign: 'center',
marginBottom: 16,
},
center: {
marginTop: 'auto',
marginBottom: 'auto',
marginRight: 'auto',
marginLeft: 'auto',
},
});
/** /**
* Class used to create intro slides * Class used to create intro slides
*/ */
export default class CustomIntroSlider extends React.Component<Props, State> { export default class CustomIntroSlider extends React.Component<
PropsType,
StateType,
> {
sliderRef: {current: null | AppIntroSlider};
state = { introSlides: Array<IntroSlideType>;
currentSlide: 0,
}
sliderRef: { current: null | AppIntroSlider }; updateSlides: Array<IntroSlideType>;
introSlides: Array<Slide>; aprilFoolsSlides: Array<IntroSlideType>;
updateSlides: Array<Slide>;
aprilFoolsSlides: Array<Slide>;
currentSlides: Array<Slide>;
/** currentSlides: Array<IntroSlideType>;
* Generates intro slides
*/
constructor() {
super();
this.sliderRef = React.createRef();
this.introSlides = [
{
key: '0', // Mascot
title: i18n.t('intro.slideMain.title'),
text: i18n.t('intro.slideMain.text'),
view: this.getWelcomeView,
mascotStyle: MASCOT_STYLE.NORMAL,
colors: ['#be1522', '#57080e'],
},
{
key: '1',
title: i18n.t('intro.slidePlanex.title'),
text: i18n.t('intro.slidePlanex.text'),
view: () => this.getIconView("calendar-clock"),
mascotStyle: MASCOT_STYLE.INTELLO,
colors: ['#be1522', '#57080e'],
},
{
key: '2',
title: i18n.t('intro.slideEvents.title'),
text: i18n.t('intro.slideEvents.text'),
view: () => this.getIconView("calendar-star",),
mascotStyle: MASCOT_STYLE.HAPPY,
colors: ['#be1522', '#57080e'],
},
{
key: '3',
title: i18n.t('intro.slideServices.title'),
text: i18n.t('intro.slideServices.text'),
view: () => this.getIconView("view-dashboard-variant",),
mascotStyle: MASCOT_STYLE.CUTE,
colors: ['#be1522', '#57080e'],
},
{
key: '4',
title: i18n.t('intro.slideDone.title'),
text: i18n.t('intro.slideDone.text'),
view: () => this.getEndView(),
mascotStyle: MASCOT_STYLE.COOL,
colors: ['#9c165b', '#3e042b'],
},
];
this.updateSlides = [];
for (let i = 0; i < Update.slidesNumber; i++) {
this.updateSlides.push(
{
key: i.toString(),
title: Update.getInstance().titleList[i],
text: Update.getInstance().descriptionList[i],
icon: Update.iconList[i],
colors: Update.colorsList[i],
},
);
}
this.aprilFoolsSlides = [ /**
{ * Generates intro slides
key: '1', */
title: i18n.t('intro.aprilFoolsSlide.title'), constructor() {
text: i18n.t('intro.aprilFoolsSlide.text'), super();
view: () => <View/>, this.state = {
mascotStyle: MASCOT_STYLE.NORMAL, currentSlide: 0,
colors: ['#e01928', '#be1522'],
},
];
}
/**
* Render item to be used for the intro introSlides
*
* @param item The item to be displayed
* @param dimensions Dimensions of the item
*/
getIntroRenderItem = ({item, dimensions}: { item: Slide, dimensions: { width: number, height: number } }) => {
const index = parseInt(item.key);
return (
<LinearGradient
style={[
styles.mainContent,
dimensions
]}
colors={item.colors}
start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}
>
{this.state.currentSlide === index
? <View style={{height: "100%", flex: 1}}>
<View style={{flex: 1}}>
{item.view()}
</View>
<Animatable.View
animation={"fadeIn"}>
{index !== 0 && index !== this.introSlides.length - 1
?
<Mascot
style={{
marginLeft: 30,
marginBottom: 0,
width: 100,
marginTop: -30,
}}
emotion={item.mascotStyle}
animated={true}
entryAnimation={{
animation: "slideInLeft",
duration: 500
}}
loopAnimation={{
animation: "pulse",
iterationCount: "infinite",
duration: 2000,
}}
/> : 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,
}}>
<Card.Content>
<Animatable.Text
animation={"fadeIn"}
delay={100}
style={styles.title}>
{item.title}
</Animatable.Text>
<Animatable.Text
animation={"fadeIn"}
delay={200}
style={styles.text}>
{item.text}
</Animatable.Text>
</Card.Content>
</Card>
</Animatable.View>
</View> : null}
</LinearGradient>
);
}
getEndView = () => {
return (
<View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
height: "80%"
}}
emotion={MASCOT_STYLE.COOL}
animated={true}
entryAnimation={{
animation: "slideInDown",
duration: 2000,
}}
loopAnimation={{
animation: "pulse",
duration: 2000,
iterationCount: "infinite"
}}
/>
</View>
);
}
getWelcomeView = () => {
return (
<View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
height: "80%"
}}
emotion={MASCOT_STYLE.NORMAL}
animated={true}
entryAnimation={{
animation: "bounceIn",
duration: 2000,
}}
/>
<Animatable.Text
useNativeDriver={true}
animation={"fadeInUp"}
duration={500}
style={{
color: "#fff",
textAlign: "center",
fontSize: 25,
}}>
PABLO
</Animatable.Text>
<Animatable.View
useNativeDriver={true}
animation={"fadeInUp"}
duration={500}
delay={200}
style={{
position: "absolute",
bottom: 30,
right: "20%",
width: 50,
height: 50,
}}>
<MaterialCommunityIcons
style={{
...styles.center,
transform: [{rotateZ: "70deg"}],
}}
name={"undo"}
color={'#fff'}
size={40}/>
</Animatable.View>
</View>
)
}
getIconView(icon: MaterialCommunityIconsGlyphs) {
return (
<View style={{flex: 1}}>
<Animatable.View
style={styles.center}
animation={"fadeIn"}
>
<MaterialCommunityIcons
name={icon}
color={'#fff'}
size={200}/>
</Animatable.View>
</View>
)
}
setStatusBarColor(color: string) {
if (Platform.OS === 'android')
StatusBar.setBackgroundColor(color, true);
}
onSlideChange = (index: number, lastIndex: number) => {
this.setStatusBarColor(this.currentSlides[index].colors[0]);
this.setState({currentSlide: index});
}; };
this.sliderRef = React.createRef();
onSkip = () => { this.introSlides = [
this.setStatusBarColor(this.currentSlides[this.currentSlides.length - 1].colors[0]); {
if (this.sliderRef.current != null) key: '0', // Mascot
this.sliderRef.current.goToSlide(this.currentSlides.length - 1); title: i18n.t('intro.slideMain.title'),
text: i18n.t('intro.slideMain.text'),
view: this.getWelcomeView,
mascotStyle: MASCOT_STYLE.NORMAL,
colors: ['#be1522', '#57080e'],
},
{
key: '1',
title: i18n.t('intro.slidePlanex.title'),
text: i18n.t('intro.slidePlanex.text'),
view: (): React.Node => CustomIntroSlider.getIconView('calendar-clock'),
mascotStyle: MASCOT_STYLE.INTELLO,
colors: ['#be1522', '#57080e'],
},
{
key: '2',
title: i18n.t('intro.slideEvents.title'),
text: i18n.t('intro.slideEvents.text'),
view: (): React.Node => CustomIntroSlider.getIconView('calendar-star'),
mascotStyle: MASCOT_STYLE.HAPPY,
colors: ['#be1522', '#57080e'],
},
{
key: '3',
title: i18n.t('intro.slideServices.title'),
text: i18n.t('intro.slideServices.text'),
view: (): React.Node =>
CustomIntroSlider.getIconView('view-dashboard-variant'),
mascotStyle: MASCOT_STYLE.CUTE,
colors: ['#be1522', '#57080e'],
},
{
key: '4',
title: i18n.t('intro.slideDone.title'),
text: i18n.t('intro.slideDone.text'),
view: (): React.Node => this.getEndView(),
mascotStyle: MASCOT_STYLE.COOL,
colors: ['#9c165b', '#3e042b'],
},
];
// $FlowFixMe
this.updateSlides = [];
for (let i = 0; i < Update.slidesNumber; i += 1) {
this.updateSlides.push({
key: i.toString(),
title: Update.getInstance().titleList[i],
text: Update.getInstance().descriptionList[i],
icon: Update.iconList[i],
colors: Update.colorsList[i],
});
} }
onDone = () => { this.aprilFoolsSlides = [
this.setStatusBarColor(ThemeManager.getCurrentTheme().colors.surface); {
this.props.onDone(); key: '1',
} title: i18n.t('intro.aprilFoolsSlide.title'),
text: i18n.t('intro.aprilFoolsSlide.text'),
renderNextButton = () => { view: (): React.Node => <View />,
return ( mascotStyle: MASCOT_STYLE.NORMAL,
<Animatable.View colors: ['#e01928', '#be1522'],
animation={"fadeIn"} },
];
}
/**
* Render item to be used for the intro introSlides
*
* @param item The item to be displayed
* @param dimensions Dimensions of the item
*/
getIntroRenderItem = ({
item,
dimensions,
}: {
item: IntroSlideType,
dimensions: {width: number, height: number},
}): React.Node => {
const {state} = this;
const index = parseInt(item.key, 10);
return (
<LinearGradient
style={[styles.mainContent, dimensions]}
colors={item.colors}
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>
<Animatable.View animation="fadeIn">
{index !== 0 && index !== this.introSlides.length - 1 ? (
<Mascot
style={{
marginLeft: 30,
marginBottom: 0,
width: 100,
marginTop: -30,
}}
emotion={item.mascotStyle}
animated
entryAnimation={{
animation: 'slideInLeft',
duration: 500,
}}
loopAnimation={{
animation: 'pulse',
iterationCount: 'infinite',
duration: 2000,
}}
/>
) : null}
<View
style={{ style={{
borderRadius: 25, marginLeft: 50,
padding: 5, width: 0,
backgroundColor: "rgba(0,0,0,0.2)" height: 0,
}}> borderLeftWidth: 20,
<MaterialCommunityIcons borderRightWidth: 0,
name={"arrow-right"} borderBottomWidth: 20,
color={'#fff'} borderStyle: 'solid',
size={40}/> backgroundColor: 'transparent',
</Animatable.View> borderLeftColor: 'transparent',
) borderRightColor: 'transparent',
} borderBottomColor: 'rgba(0,0,0,0.60)',
}}
renderDoneButton = () => { />
return ( <Card
<Animatable.View
animation={"bounceIn"}
style={{ style={{
borderRadius: 25, backgroundColor: 'rgba(0,0,0,0.38)',
padding: 5, marginHorizontal: 20,
backgroundColor: "rgb(190,21,34)" borderColor: 'rgba(0,0,0,0.60)',
borderWidth: 4,
borderRadius: 10,
}}> }}>
<MaterialCommunityIcons <Card.Content>
name={"check"} <Animatable.Text
color={'#fff'} animation="fadeIn"
size={40}/> delay={100}
style={styles.title}>
{item.title}
</Animatable.Text>
<Animatable.Text
animation="fadeIn"
delay={200}
style={styles.text}>
{item.text}
</Animatable.Text>
</Card.Content>
</Card>
</Animatable.View> </Animatable.View>
) </View>
} ) : null}
</LinearGradient>
);
};
render() { getEndView = (): React.Node => {
this.currentSlides = this.introSlides; return (
if (this.props.isUpdate) <View style={{flex: 1}}>
this.currentSlides = this.updateSlides; <Mascot
else if (this.props.isAprilFools) style={{
this.currentSlides = this.aprilFoolsSlides; ...styles.center,
this.setStatusBarColor(this.currentSlides[0].colors[0]); height: '80%',
return ( }}
<AppIntroSlider emotion={MASCOT_STYLE.COOL}
ref={this.sliderRef} animated
data={this.currentSlides} entryAnimation={{
extraData={this.state.currentSlide} animation: 'slideInDown',
duration: 2000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
</View>
);
};
renderItem={this.getIntroRenderItem} getWelcomeView = (): React.Node => {
renderNextButton={this.renderNextButton} return (
renderDoneButton={this.renderDoneButton} <View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
height: '80%',
}}
emotion={MASCOT_STYLE.NORMAL}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 2000,
}}
/>
<Animatable.Text
useNativeDriver
animation="fadeInUp"
duration={500}
style={{
color: '#fff',
textAlign: 'center',
fontSize: 25,
}}>
PABLO
</Animatable.Text>
<Animatable.View
useNativeDriver
animation="fadeInUp"
duration={500}
delay={200}
style={{
position: 'absolute',
bottom: 30,
right: '20%',
width: 50,
height: 50,
}}>
<MaterialCommunityIcons
style={{
...styles.center,
transform: [{rotateZ: '70deg'}],
}}
name="undo"
color="#fff"
size={40}
/>
</Animatable.View>
</View>
);
};
onDone={this.onDone} static getIconView(icon: MaterialCommunityIconsGlyphs): React.Node {
onSlideChange={this.onSlideChange} return (
onSkip={this.onSkip} <View style={{flex: 1}}>
/> <Animatable.View style={styles.center} animation="fadeIn">
); <MaterialCommunityIcons name={icon} color="#fff" size={200} />
} </Animatable.View>
</View>
);
}
static setStatusBarColor(color: string) {
if (Platform.OS === 'android') StatusBar.setBackgroundColor(color, true);
}
onSlideChange = (index: number) => {
CustomIntroSlider.setStatusBarColor(this.currentSlides[index].colors[0]);
this.setState({currentSlide: index});
};
onSkip = () => {
CustomIntroSlider.setStatusBarColor(
this.currentSlides[this.currentSlides.length - 1].colors[0],
);
if (this.sliderRef.current != null)
this.sliderRef.current.goToSlide(this.currentSlides.length - 1);
};
onDone = () => {
const {props} = this;
CustomIntroSlider.setStatusBarColor(
ThemeManager.getCurrentTheme().colors.surface,
);
props.onDone();
};
getRenderNextButton = (): React.Node => {
return (
<Animatable.View
animation="fadeIn"
style={{
borderRadius: 25,
padding: 5,
backgroundColor: 'rgba(0,0,0,0.2)',
}}>
<MaterialCommunityIcons name="arrow-right" color="#fff" size={40} />
</Animatable.View>
);
};
getRenderDoneButton = (): React.Node => {
return (
<Animatable.View
animation="bounceIn"
style={{
borderRadius: 25,
padding: 5,
backgroundColor: 'rgb(190,21,34)',
}}>
<MaterialCommunityIcons name="check" color="#fff" size={40} />
</Animatable.View>
);
};
render(): React.Node {
const {props, state} = this;
this.currentSlides = this.introSlides;
if (props.isUpdate) this.currentSlides = this.updateSlides;
else if (props.isAprilFools) this.currentSlides = this.aprilFoolsSlides;
CustomIntroSlider.setStatusBarColor(this.currentSlides[0].colors[0]);
return (
<AppIntroSlider
ref={this.sliderRef}
data={this.currentSlides}
extraData={state.currentSlide}
renderItem={this.getIntroRenderItem}
renderNextButton={this.getRenderNextButton}
renderDoneButton={this.getRenderDoneButton}
onDone={this.onDone}
onSlideChange={this.onSlideChange}
onSkip={this.onSkip}
/>
);
}
} }
const styles = StyleSheet.create({
mainContent: {
paddingBottom: 100,
},
text: {
color: 'rgba(255, 255, 255, 0.8)',
backgroundColor: 'transparent',
textAlign: 'center',
paddingHorizontal: 16,
},
title: {
fontSize: 22,
color: 'white',
backgroundColor: 'transparent',
textAlign: 'center',
marginBottom: 16,
},
center: {
marginTop: 'auto',
marginBottom: 'auto',
marginRight: 'auto',
marginLeft: 'auto',
},
});

View file

@ -2,9 +2,10 @@
import * as React from 'react'; import * as React from 'react';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import {Modalize} from "react-native-modalize"; import {Modalize} from 'react-native-modalize';
import {View} from "react-native-animatable"; import {View} from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar"; import CustomTabBar from '../Tabbar/CustomTabBar';
import type {CustomThemeType} from '../../managers/ThemeManager';
/** /**
* Abstraction layer for Modalize component, using custom configuration * Abstraction layer for Modalize component, using custom configuration
@ -12,25 +13,29 @@ import CustomTabBar from "../Tabbar/CustomTabBar";
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref. * @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
* @return {*} * @return {*}
*/ */
function CustomModal(props) { function CustomModal(props: {
const {colors} = props.theme; theme: CustomThemeType,
return ( onRef: (re: Modalize) => void,
<Modalize children?: React.Node,
ref={props.onRef} }): React.Node {
adjustToContentHeight const {theme, onRef, children} = props;
handlePosition={'inside'} return (
modalStyle={{backgroundColor: colors.card}} <Modalize
handleStyle={{backgroundColor: colors.primary}} ref={onRef}
> adjustToContentHeight
<View style={{ handlePosition="inside"
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT modalStyle={{backgroundColor: theme.colors.card}}
}}> handleStyle={{backgroundColor: theme.colors.primary}}>
{props.children} <View
</View> style={{
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
</Modalize> }}>
); {children}
</View>
</Modalize>
);
} }
export default withTheme(CustomModal); CustomModal.defaultProps = {children: null};
export default withTheme(CustomModal);

View file

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

View file

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

View file

@ -2,191 +2,192 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Subheading, withTheme} from 'react-native-paper'; import {Button, Subheading, withTheme} from 'react-native-paper';
import {StyleSheet, View} from "react-native"; import {StyleSheet, View} from 'react-native';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {ERROR_TYPE} from "../../utils/WebData";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from '@react-navigation/stack';
import {ERROR_TYPE} from '../../utils/WebData';
import type {CustomThemeType} from '../../managers/ThemeManager';
type Props = { type PropsType = {
navigation: Object, navigation: StackNavigationProp,
route: Object, theme: CustomThemeType,
errorCode: number, route: {name: string},
onRefresh: Function, onRefresh?: () => void,
icon: string, errorCode?: number,
message: string, icon?: string,
showRetryButton: boolean, message?: string,
} showRetryButton?: boolean,
};
type State = {
refreshing: boolean,
}
class ErrorView extends React.PureComponent<Props, State> {
colors: Object;
message: string;
icon: string;
showLoginButton: boolean;
static defaultProps = {
errorCode: 0,
icon: '',
message: '',
showRetryButton: true,
}
state = {
refreshing: false,
};
constructor(props) {
super(props);
this.colors = props.theme.colors;
this.icon = "";
}
generateMessage() {
this.showLoginButton = false;
if (this.props.errorCode !== 0) {
switch (this.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 " + this.props.errorCode;
} else {
this.message = this.props.message;
this.icon = this.props.icon;
}
}
getRetryButton() {
return <Button
mode={'contained'}
icon={'refresh'}
onPress={this.props.onRefresh}
style={styles.button}
>
{i18n.t("general.retry")}
</Button>;
}
goToLogin = () => {
this.props.navigation.navigate("login",
{
screen: 'login',
params: {nextScreen: this.props.route.name}
})
};
getLoginButton() {
return <Button
mode={'contained'}
icon={'login'}
onPress={this.goToLogin}
style={styles.button}
>
{i18n.t("screens.login.title")}
</Button>;
}
render() {
this.generateMessage();
return (
<Animatable.View
style={{
...styles.outer,
backgroundColor: this.colors.background
}}
animation={"zoomIn"}
duration={200}
useNativeDriver
>
<View style={styles.inner}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons
name={this.icon}
size={150}
color={this.colors.textDisabled}/>
</View>
<Subheading style={{
...styles.subheading,
color: this.colors.textDisabled
}}>
{this.message}
</Subheading>
{this.props.showRetryButton
? (this.showLoginButton
? this.getLoginButton()
: this.getRetryButton())
: null}
</View>
</Animatable.View>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
outer: { outer: {
height: '100%', height: '100%',
}, },
inner: { inner: {
marginTop: 'auto', marginTop: 'auto',
marginBottom: 'auto', marginBottom: 'auto',
}, },
iconContainer: { iconContainer: {
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
marginBottom: 20 marginBottom: 20,
}, },
subheading: { subheading: {
textAlign: 'center', textAlign: 'center',
paddingHorizontal: 20 paddingHorizontal: 20,
}, },
button: { button: {
marginTop: 10, marginTop: 10,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
} },
}); });
class ErrorView extends React.PureComponent<PropsType> {
static defaultProps = {
onRefresh: () => {},
errorCode: 0,
icon: '',
message: '',
showRetryButton: true,
};
message: string;
icon: string;
showLoginButton: boolean;
constructor(props: PropsType) {
super(props);
this.icon = '';
}
getRetryButton(): React.Node {
const {props} = this;
return (
<Button
mode="contained"
icon="refresh"
onPress={props.onRefresh}
style={styles.button}>
{i18n.t('general.retry')}
</Button>
);
}
getLoginButton(): React.Node {
return (
<Button
mode="contained"
icon="login"
onPress={this.goToLogin}
style={styles.button}>
{i18n.t('screens.login.title')}
</Button>
);
}
goToLogin = () => {
const {props} = this;
props.navigation.navigate('login', {
screen: 'login',
params: {nextScreen: props.route.name},
});
};
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(): React.Node {
const {props} = this;
this.generateMessage();
let button;
if (this.showLoginButton) button = this.getLoginButton();
else if (props.showRetryButton) button = this.getRetryButton();
else button = null;
return (
<Animatable.View
style={{
...styles.outer,
backgroundColor: props.theme.colors.background,
}}
animation="zoomIn"
duration={200}
useNativeDriver>
<View style={styles.inner}>
<View style={styles.iconContainer}>
<MaterialCommunityIcons
// $FlowFixMe
name={this.icon}
size={150}
color={props.theme.colors.textDisabled}
/>
</View>
<Subheading
style={{
...styles.subheading,
color: props.theme.colors.textDisabled,
}}>
{this.message}
</Subheading>
{button}
</View>
</Animatable.View>
);
}
}
export default withTheme(ErrorView); export default withTheme(ErrorView);

View file

@ -1,44 +1,55 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {ERROR_TYPE, readData} from "../../utils/WebData"; import i18n from 'i18n-js';
import i18n from "i18n-js";
import {Snackbar} from 'react-native-paper'; import {Snackbar} from 'react-native-paper';
import {RefreshControl, View} from "react-native"; import {RefreshControl, View} from 'react-native';
import ErrorView from "./ErrorView";
import BasicLoadingScreen from "./BasicLoadingScreen";
import {withCollapsible} from "../../utils/withCollapsible";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import CustomTabBar from "../Tabbar/CustomTabBar"; import {Collapsible} from 'react-navigation-collapsible';
import {Collapsible} from "react-navigation-collapsible"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import ErrorView from './ErrorView';
import CollapsibleSectionList from "../Collapsible/CollapsibleSectionList"; 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 type {ApiGenericDataType} from '../../utils/WebData';
type Props = { export type SectionListDataType<T> = Array<{
navigation: StackNavigationProp, title: string,
fetchUrl: string, data: Array<T>,
autoRefreshTime: number, keyExtractor?: (T) => string,
refreshOnFocus: boolean, }>;
renderItem: (data: { [key: string]: any }) => React.Node,
createDataset: (data: { [key: string]: any } | null, isLoading?: boolean) => Array<Object>,
onScroll: (event: SyntheticEvent<EventTarget>) => void,
collapsibleStack: Collapsible,
showError: boolean, type PropsType<T> = {
itemHeight?: number, navigation: StackNavigationProp,
updateData?: number, fetchUrl: string,
renderListHeaderComponent?: (data: { [key: string]: any } | null) => React.Node, autoRefreshTime: number,
renderSectionHeader?: (data: { section: { [key: string]: any } }, isLoading?: boolean) => React.Node, refreshOnFocus: boolean,
stickyHeader?: boolean, renderItem: (data: {item: T}) => React.Node,
} createDataset: (
data: ApiGenericDataType | null,
isLoading?: boolean,
) => SectionListDataType<T>,
onScroll: (event: SyntheticEvent<EventTarget>) => void,
collapsibleStack: Collapsible,
type State = { showError?: boolean,
refreshing: boolean, itemHeight?: number | null,
firstLoading: boolean, updateData?: number,
fetchedData: { [key: string]: any } | null, renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node,
snackbarVisible: boolean renderSectionHeader?: (
data: {section: {title: string}},
isLoading?: boolean,
) => React.Node,
stickyHeader?: boolean,
}; };
type StateType = {
refreshing: boolean,
fetchedData: ApiGenericDataType | null,
snackbarVisible: boolean,
};
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
@ -48,211 +59,216 @@ const MIN_REFRESH_TIME = 5 * 1000;
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different. * This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
* To force the component to update, change the value of updateData. * To force the component to update, change the value of updateData.
*/ */
class WebSectionList extends React.PureComponent<Props, State> { class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
static defaultProps = {
showError: true,
itemHeight: null,
updateData: 0,
renderListHeaderComponent: (): React.Node => null,
renderSectionHeader: (): React.Node => null,
stickyHeader: false,
};
static defaultProps = { refreshInterval: IntervalID;
stickyHeader: false,
updateData: 0, lastRefresh: Date | null;
showError: true,
constructor() {
super();
this.state = {
refreshing: false,
fetchedData: null,
snackbarVisible: false,
}; };
}
refreshInterval: IntervalID; /**
lastRefresh: Date | null; * 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 = null;
this.onRefresh();
}
state = { /**
refreshing: false, * Refreshes data when focusing the screen and setup a refresh interval if asked to
firstLoading: true, */
fetchedData: null, onScreenFocus = () => {
snackbarVisible: false const {props} = this;
if (props.refreshOnFocus && this.lastRefresh) this.onRefresh();
if (props.autoRefreshTime > 0)
this.refreshInterval = setInterval(this.onRefresh, props.autoRefreshTime);
};
/**
* Removes any interval on un-focus
*/
onScreenBlur = () => {
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: ApiGenericDataType) => {
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 = (
data: T,
index: number,
): {length: number, offset: number, index: number} | null => {
const {itemHeight} = this.props;
if (itemHeight == null) return null;
return {
length: itemHeight,
offset: itemHeight * index,
index,
}; };
};
/** getRenderSectionHeader = (data: {section: {title: string}}): React.Node => {
* Registers react navigation events on first screen load. const {renderSectionHeader} = this.props;
* Allows to detect when the screen is focused const {refreshing} = this.state;
*/ if (renderSectionHeader != null) {
componentDidMount() { return (
this.props.navigation.addListener('focus', this.onScreenFocus); <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
this.props.navigation.addListener('blur', this.onScreenBlur); {renderSectionHeader(data, refreshing)}
this.lastRefresh = null; </Animatable.View>
this.onRefresh(); );
} }
return null;
};
/** getRenderItem = (data: {item: T}): React.Node => {
* Refreshes data when focusing the screen and setup a refresh interval if asked to const {renderItem} = this.props;
*/ return (
onScreenFocus = () => { <Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
if (this.props.refreshOnFocus && this.lastRefresh) {renderItem(data)}
this.onRefresh(); </Animatable.View>
if (this.props.autoRefreshTime > 0) );
this.refreshInterval = setInterval(this.onRefresh, this.props.autoRefreshTime) };
}
/** onScroll = (event: SyntheticEvent<EventTarget>) => {
* Removes any interval on un-focus const {onScroll} = this.props;
*/ if (onScroll != null) onScroll(event);
onScreenBlur = () => { };
clearInterval(this.refreshInterval);
}
render(): React.Node {
const {props, state} = this;
let dataset = [];
if (
state.fetchedData != null ||
(state.fetchedData == null && !props.showError)
)
dataset = props.createDataset(state.fetchedData, state.refreshing);
/** const {containerPaddingTop} = props.collapsibleStack;
* Callback used when fetch is successful. return (
* It will update the displayed data and stop the refresh animation <View>
* <CollapsibleSectionList
* @param fetchedData The newly fetched data sections={dataset}
*/ extraData={props.updateData}
onFetchSuccess = (fetchedData: { [key: string]: any }) => { refreshControl={
this.setState({ <RefreshControl
fetchedData: fetchedData, progressViewOffset={containerPaddingTop}
refreshing: false, refreshing={state.refreshing}
firstLoading: false onRefresh={this.onRefresh}
}); />
this.lastRefresh = new Date(); }
}; renderSectionHeader={this.getRenderSectionHeader}
renderItem={this.getRenderItem}
/** stickySectionHeadersEnabled={props.stickyHeader}
* Callback used when fetch encountered an error. style={{minHeight: '100%'}}
* It will reset the displayed data and show an error. ListHeaderComponent={
*/ props.renderListHeaderComponent != null
onFetchError = () => { ? props.renderListHeaderComponent(state.fetchedData)
this.setState({ : null
fetchedData: null, }
refreshing: false, ListEmptyComponent={
firstLoading: false state.refreshing ? (
}); <BasicLoadingScreen />
this.showSnackBar(); ) : (
}; <ErrorView
navigation={props.navigation}
/** errorCode={ERROR_TYPE.CONNECTION_ERROR}
* Refreshes data and shows an animations while doing it onRefresh={this.onRefresh}
*/ />
onRefresh = () => { )
let canRefresh; }
if (this.lastRefresh != null) { getItemLayout={props.itemHeight != null ? this.getItemLayout : null}
const last = this.lastRefresh; onScroll={this.onScroll}
canRefresh = (new Date().getTime() - last.getTime()) > MIN_REFRESH_TIME; hasTab
} else />
canRefresh = true; <Snackbar
if (canRefresh) { visible={state.snackbarVisible}
this.setState({refreshing: true}); onDismiss={this.hideSnackBar}
readData(this.props.fetchUrl) action={{
.then(this.onFetchSuccess) label: 'OK',
.catch(this.onFetchError); onPress: () => {},
} }}
}; duration={4000}
style={{
/** bottom: CustomTabBar.TAB_BAR_HEIGHT,
* Shows the error popup }}>
*/ {i18n.t('general.listUpdateFail')}
showSnackBar = () => this.setState({snackbarVisible: true}); </Snackbar>
</View>
/** );
* Hides the error popup }
*/
hideSnackBar = () => this.setState({snackbarVisible: false});
itemLayout = (data: { [key: string]: any }, index: number) => {
const height = this.props.itemHeight;
if (height == null)
return undefined;
return {
length: height,
offset: height * index,
index
}
};
renderSectionHeader = (data: { section: { [key: string]: any } }) => {
if (this.props.renderSectionHeader != null) {
return (
<Animatable.View
animation={"fadeInUp"}
duration={500}
useNativeDriver
>
{this.props.renderSectionHeader(data, this.state.refreshing)}
</Animatable.View>
);
} else
return null;
}
renderItem = (data: {
item: { [key: string]: any },
index: number,
section: { [key: string]: any },
separators: { [key: string]: any },
}) => {
return (
<Animatable.View
animation={"fadeInUp"}
duration={500}
useNativeDriver
>
{this.props.renderItem(data)}
</Animatable.View>
);
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
if (this.props.onScroll)
this.props.onScroll(event);
}
render() {
let dataset = [];
if (this.state.fetchedData != null || (this.state.fetchedData == null && !this.props.showError)) {
dataset = this.props.createDataset(this.state.fetchedData, this.state.refreshing);
}
const {containerPaddingTop} = this.props.collapsibleStack;
return (
<View>
<CollapsibleSectionList
sections={dataset}
extraData={this.props.updateData}
refreshControl={
<RefreshControl
progressViewOffset={containerPaddingTop}
refreshing={this.state.refreshing}
onRefresh={this.onRefresh}
/>
}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
stickySectionHeadersEnabled={this.props.stickyHeader}
style={{minHeight: '100%'}}
ListHeaderComponent={this.props.renderListHeaderComponent != null
? this.props.renderListHeaderComponent(this.state.fetchedData)
: null}
ListEmptyComponent={this.state.refreshing
? <BasicLoadingScreen/>
: <ErrorView
{...this.props}
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefresh}/>
}
getItemLayout={this.props.itemHeight != null ? this.itemLayout : undefined}
onScroll={this.onScroll}
hasTab={true}
/>
<Snackbar
visible={this.state.snackbarVisible}
onDismiss={this.hideSnackBar}
action={{
label: 'OK',
onPress: () => {
},
}}
duration={4000}
style={{
bottom: CustomTabBar.TAB_BAR_HEIGHT
}}
>
{i18n.t("general.listUpdateFail")}
</Snackbar>
</View>
);
}
} }
export default withCollapsible(WebSectionList); export default withCollapsible(WebSectionList);

View file

@ -1,233 +1,247 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import WebView from "react-native-webview"; import WebView from 'react-native-webview';
import BasicLoadingScreen from "./BasicLoadingScreen"; import {
import ErrorView from "./ErrorView"; Divider,
import {ERROR_TYPE} from "../../utils/WebData"; HiddenItem,
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton'; OverflowMenu,
import {Divider, HiddenItem, OverflowMenu} from "react-navigation-header-buttons"; } from 'react-navigation-header-buttons';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {Animated, BackHandler, Linking} from "react-native"; import {Animated, BackHandler, Linking} from 'react-native';
import {withCollapsible} from "../../utils/withCollapsible"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import {withTheme} from 'react-native-paper';
import {withTheme} from "react-native-paper"; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomTheme} from "../../managers/ThemeManager"; import {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from "@react-navigation/stack"; import type {CustomThemeType} from '../../managers/ThemeManager';
import {Collapsible} from "react-navigation-collapsible"; import withCollapsible from '../../utils/withCollapsible';
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
import {ERROR_TYPE} from '../../utils/WebData';
import ErrorView from './ErrorView';
import BasicLoadingScreen from './BasicLoadingScreen';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomThemeType,
url: string, url: string,
customJS: string, collapsibleStack: Collapsible,
customPaddingFunction: null | (padding: number) => string, onMessage: (event: {nativeEvent: {data: string}}) => void,
collapsibleStack: Collapsible, onScroll: (event: SyntheticEvent<EventTarget>) => void,
onMessage: Function, customJS?: string,
onScroll: Function, customPaddingFunction?: null | ((padding: number) => string),
showAdvancedControls: boolean, showAdvancedControls?: boolean,
} };
const AnimatedWebView = Animated.createAnimatedComponent(WebView); const AnimatedWebView = Animated.createAnimatedComponent(WebView);
/** /**
* Class defining a webview screen. * Class defining a webview screen.
*/ */
class WebViewScreen extends React.PureComponent<Props> { class WebViewScreen extends React.PureComponent<PropsType> {
static defaultProps = {
customJS: '',
showAdvancedControls: true,
customPaddingFunction: null,
};
static defaultProps = { webviewRef: {current: null | WebView};
customJS: '',
showAdvancedControls: true,
customPaddingFunction: null,
};
webviewRef: { current: null | WebView }; canGoBack: boolean;
canGoBack: boolean; constructor() {
super();
this.webviewRef = React.createRef();
this.canGoBack = false;
}
constructor() { /**
super(); * Creates header buttons and listens to events after mounting
this.webviewRef = React.createRef(); */
this.canGoBack = false; componentDidMount() {
const {props} = this;
props.navigation.setOptions({
headerRight: props.showAdvancedControls
? this.getAdvancedButtons
: this.getBasicButton,
});
props.navigation.addListener('focus', () => {
BackHandler.addEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
props.navigation.addListener('blur', () => {
BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
}
/**
* 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();
return true;
} }
return false;
};
/** /**
* Creates header buttons and listens to events after mounting * Gets header refresh and open in browser buttons
*/ *
componentDidMount() { * @return {*}
this.props.navigation.setOptions({ */
headerRight: this.props.showAdvancedControls getBasicButton = (): React.Node => {
? this.getAdvancedButtons return (
: this.getBasicButton, <MaterialHeaderButtons>
}); <Item
this.props.navigation.addListener( title="refresh"
'focus', iconName="refresh"
() => onPress={this.onRefreshClicked}
BackHandler.addEventListener( />
'hardwareBackPress', <Item
this.onBackButtonPressAndroid title={i18n.t('general.openInBrowser')}
) iconName="open-in-new"
); onPress={this.onOpenClicked}
this.props.navigation.addListener( />
'blur', </MaterialHeaderButtons>
() => );
BackHandler.removeEventListener( };
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
}
/** /**
* Goes back on the webview or on the navigation stack if we cannot go back anymore * Creates advanced header control buttons.
* * These buttons allows the user to refresh, go back, go forward and open in the browser.
* @returns {boolean} *
*/ * @returns {*}
onBackButtonPressAndroid = () => { */
if (this.canGoBack) { getAdvancedButtons = (): React.Node => {
this.onGoBackClicked(); const {props} = this;
return true; return (
} <MaterialHeaderButtons>
return false; <Item
}; title="refresh"
iconName="refresh"
/** onPress={this.onRefreshClicked}
* Gets header refresh and open in browser buttons />
* <OverflowMenu
* @return {*} style={{marginHorizontal: 10}}
*/ OverflowIcon={
getBasicButton = () => { <MaterialCommunityIcons
return ( name="dots-vertical"
<MaterialHeaderButtons> size={26}
<Item color={props.theme.colors.text}
title="refresh"
iconName="refresh"
onPress={this.onRefreshClicked}/>
<Item
title={i18n.t("general.openInBrowser")}
iconName="open-in-new"
onPress={this.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 = () => {
return (
<MaterialHeaderButtons>
<Item
title="refresh"
iconName="refresh"
onPress={this.onRefreshClicked}
/>
<OverflowMenu
style={{marginHorizontal: 10}}
OverflowIcon={
<MaterialCommunityIcons
name="dots-vertical"
size={26}
color={this.props.theme.colors.text}
/>}
>
<HiddenItem
title={i18n.t("general.goBack")}
onPress={this.onGoBackClicked}/>
<HiddenItem
title={i18n.t("general.goForward")}
onPress={this.onGoForwardClicked}/>
<Divider/>
<HiddenItem
title={i18n.t("general.openInBrowser")}
onPress={this.onOpenClicked}/>
</OverflowMenu>
</MaterialHeaderButtons>
);
}
/**
* Callback to use when refresh button is clicked. Reloads the webview.
*/
onRefreshClicked = () => {
if (this.webviewRef.current != null)
this.webviewRef.current.reload();
}
onGoBackClicked = () => {
if (this.webviewRef.current != null)
this.webviewRef.current.goBack();
}
onGoForwardClicked = () => {
if (this.webviewRef.current != null)
this.webviewRef.current.goForward();
}
onOpenClicked = () => Linking.openURL(this.props.url);
/**
* 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);
}
/**
* Gets the loading indicator
*
* @return {*}
*/
getRenderLoading = () => <BasicLoadingScreen isAbsolute={true}/>;
/**
* Gets the javascript needed to generate a padding on top of the page
* This adds padding to the body and runs the custom padding function given in props
*
* @param padding The padding to add in pixels
* @returns {string}
*/
getJavascriptPadding(padding: number) {
const customPadding = this.props.customPaddingFunction != null ? this.props.customPaddingFunction(padding) : "";
return (
"document.getElementsByTagName('body')[0].style.paddingTop = '" + padding + "px';" +
customPadding +
"true;"
);
}
onScroll = (event: Object) => {
if (this.props.onScroll)
this.props.onScroll(event);
}
render() {
const {containerPaddingTop, onScrollWithListener} = this.props.collapsibleStack;
return (
<AnimatedWebView
ref={this.webviewRef}
source={{uri: this.props.url}}
startInLoadingState={true}
injectedJavaScript={this.props.customJS}
javaScriptEnabled={true}
renderLoading={this.getRenderLoading}
renderError={() => <ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked}
/>}
onNavigationStateChange={navState => {
this.canGoBack = navState.canGoBack;
}}
onMessage={this.props.onMessage}
onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))}
// Animations
onScroll={onScrollWithListener(this.onScroll)}
/> />
); }>
} <HiddenItem
title={i18n.t('general.goBack')}
onPress={this.onGoBackClicked}
/>
<HiddenItem
title={i18n.t('general.goForward')}
onPress={this.onGoForwardClicked}
/>
<Divider />
<HiddenItem
title={i18n.t('general.openInBrowser')}
onPress={this.onOpenClicked}
/>
</OverflowMenu>
</MaterialHeaderButtons>
);
};
/**
* Gets the loading indicator
*
* @return {*}
*/
getRenderLoading = (): React.Node => <BasicLoadingScreen isAbsolute />;
/**
* Gets the javascript needed to generate a padding on top of the page
* This adds padding to the body and runs the custom padding function given in props
*
* @param padding The padding to add in pixels
* @returns {string}
*/
getJavascriptPadding(padding: number): string {
const {props} = this;
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();
};
onGoBackClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goBack();
};
onGoForwardClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goForward();
};
onOpenClicked = () => {
const {url} = this.props;
Linking.openURL(url);
};
onScroll = (event: SyntheticEvent<EventTarget>) => {
const {onScroll} = this.props;
if (onScroll) 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);
};
render(): React.Node {
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={(): React.Node => (
<ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked}
/>
)}
onNavigationStateChange={(navState: {canGoBack: boolean}) => {
this.canGoBack = navState.canGoBack;
}}
onMessage={props.onMessage}
onLoad={() => {
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
}}
// Animations
onScroll={onScrollWithListener(this.onScroll)}
/>
);
}
} }
export default withCollapsible(withTheme(WebViewScreen)); export default withCollapsible(withTheme(WebViewScreen));

View file

@ -1,180 +1,217 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {withTheme} from 'react-native-paper';
import TabIcon from "./TabIcon";
import TabHomeIcon from "./TabHomeIcon";
import {Animated} from 'react-native'; import {Animated} from 'react-native';
import {Collapsible} from "react-navigation-collapsible"; import {withTheme} from 'react-native-paper';
import {Collapsible} from 'react-navigation-collapsible';
import {StackNavigationProp} from '@react-navigation/stack';
import TabIcon from './TabIcon';
import TabHomeIcon from './TabHomeIcon';
import type {CustomThemeType} from '../../managers/ThemeManager';
type Props = { type RouteType = {
state: Object, name: string,
descriptors: Object, key: string,
navigation: Object, params: {collapsible: Collapsible},
theme: Object, state: {
collapsibleStack: Object, index: number,
} routes: Array<RouteType>,
},
type State = {
translateY: AnimatedValue,
barSynced: boolean,
}
const TAB_ICONS = {
proxiwash: 'tshirt-crew',
services: 'account-circle',
planning: 'calendar-range',
planex: 'clock',
}; };
class CustomTabBar extends React.Component<Props, State> { type PropsType = {
state: {
index: number,
routes: Array<RouteType>,
},
descriptors: {
[key: string]: {
options: {
tabBarLabel: string,
title: string,
},
},
},
navigation: StackNavigationProp,
theme: CustomThemeType,
};
static TAB_BAR_HEIGHT = 48; type StateType = {
// eslint-disable-next-line flowtype/no-weak-types
translateY: any,
};
state = { const TAB_ICONS = {
translateY: new Animated.Value(0), proxiwash: 'tshirt-crew',
} services: 'account-circle',
planning: 'calendar-range',
planex: 'clock',
};
syncTabBar = (route, index) => { class CustomTabBar extends React.Component<PropsType, StateType> {
const state = this.props.state; static TAB_BAR_HEIGHT = 48;
const isFocused = state.index === index;
if (isFocused) { constructor() {
const stackState = route.state; super();
const stackRoute = stackState ? stackState.routes[stackState.index] : undefined; this.state = {
const params: { collapsible: Collapsible } = stackRoute ? stackRoute.params : undefined; translateY: new Animated.Value(0),
const collapsible = params ? params.collapsible : undefined;
if (collapsible) {
this.setState({
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
});
}
}
}; };
}
/** /**
* Navigates to the given route if it is different from the current one * Navigates to the given route if it is different from the current one
* *
* @param route Destination route * @param route Destination route
* @param currentIndex The current route index * @param currentIndex The current route index
* @param destIndex The destination route index * @param destIndex The destination route index
*/ */
onItemPress(route: Object, currentIndex: number, destIndex: number) { onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
const event = this.props.navigation.emit({ const {navigation} = this.props;
type: 'tabPress', const event = navigation.emit({
target: route.key, type: 'tabPress',
canPreventDefault: true, target: route.key,
canPreventDefault: true,
});
if (currentIndex !== destIndex && !event.defaultPrevented)
navigation.navigate(route.name);
}
/**
* Navigates to tetris screen on home button long press
*
* @param route
*/
onItemLongPress(route: RouteType) {
const {navigation} = this.props;
const event = navigation.emit({
type: 'tabLongPress',
target: route.key,
canPreventDefault: true,
});
if (route.name === 'home' && !event.defaultPrevented)
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): React.Node => {
let icon = TAB_ICONS[route.name];
icon = focused ? icon : `${icon}-outline`;
if (route.name !== 'home') return icon;
return null;
};
/**
* 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): React.Node => {
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}
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(): React.Node {
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 != null ? stackState.routes[stackState.index] : null;
const params: {collapsible: Collapsible} | null =
stackRoute != null ? stackRoute.params : 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
}); });
if (currentIndex !== destIndex && !event.defaultPrevented) }
this.props.navigation.navigate(route.name);
} }
};
/** render(): React.Node {
* Navigates to tetris screen on home button long press const {props, state} = this;
* props.navigation.addListener('state', this.onRouteChange);
* @param route const icons = this.getIcons();
*/ return (
onItemLongPress(route: Object) { // $FlowFixMe
const event = this.props.navigation.emit({ <Animated.View
type: 'tabLongPress', useNativeDriver
target: route.key, style={{
canPreventDefault: true, flexDirection: 'row',
}); height: CustomTabBar.TAB_BAR_HEIGHT,
if (route.name === "home" && !event.defaultPrevented) width: '100%',
this.props.navigation.navigate('game-start'); position: 'absolute',
} bottom: 0,
left: 0,
/** backgroundColor: props.theme.colors.surface,
* Gets an icon for the given route if it is not the home one as it uses a custom button transform: [{translateY: state.translateY}],
* }}>
* @param route {icons}
* @param focused </Animated.View>
* @returns {null} );
*/ }
tabBarIcon = (route, focused) => {
let icon = TAB_ICONS[route.name];
icon = focused ? icon : icon + ('-outline');
if (route.name !== "home")
return icon;
else
return null;
};
/**
* Finds the active route and syncs the tab bar animation with the header bar
*/
onRouteChange = () => {
this.props.state.routes.map(this.syncTabBar)
}
/**
* 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 {*}
*/
renderIcon = (route, index) => {
const state = this.props.state;
const {options} = this.props.descriptors[route.key];
const label =
options.tabBarLabel != null
? options.tabBarLabel
: options.title != null
? options.title
: route.name;
const onPress = () => this.onItemPress(route, state.index, index);
const onLongPress = () => this.onItemLongPress(route);
const isFocused = state.index === index;
const color = isFocused ? this.props.theme.colors.primary : this.props.theme.colors.tabIcon;
if (route.name !== "home") {
return <TabIcon
onPress={onPress}
onLongPress={onLongPress}
icon={this.tabBarIcon(route, isFocused)}
color={color}
label={label}
focused={isFocused}
extraData={state.index > index}
key={route.key}
/>
} else
return <TabHomeIcon
onPress={onPress}
onLongPress={onLongPress}
focused={isFocused}
key={route.key}
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
/>
};
getIcons() {
return this.props.state.routes.map(this.renderIcon);
}
render() {
this.props.navigation.addListener('state', this.onRouteChange);
const icons = this.getIcons();
return (
<Animated.View
useNativeDriver
style={{
flexDirection: 'row',
height: CustomTabBar.TAB_BAR_HEIGHT,
width: '100%',
position: 'absolute',
bottom: 0,
left: 0,
backgroundColor: this.props.theme.colors.surface,
transform: [{translateY: this.state.translateY}],
}}
>
{icons}
</Animated.View>
);
}
} }
export default withTheme(CustomTabBar); export default withTheme(CustomTabBar);

View file

@ -1,106 +1,133 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Image, Platform, View} from "react-native"; import {Image, Platform, View} from 'react-native';
import {FAB, TouchableRipple, withTheme} from 'react-native-paper'; import {FAB, TouchableRipple, withTheme} from 'react-native-paper';
import * as Animatable from "react-native-animatable"; import * as Animatable from 'react-native-animatable';
import FOCUSED_ICON from '../../../assets/tab-icon.png';
import UNFOCUSED_ICON from '../../../assets/tab-icon-outline.png';
import type {CustomThemeType} from '../../managers/ThemeManager';
type Props = { type PropsType = {
focused: boolean, focused: boolean,
onPress: Function, onPress: () => void,
onLongPress: Function, onLongPress: () => void,
theme: Object, theme: CustomThemeType,
tabBarHeight: number, tabBarHeight: number,
} };
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class TabHomeIcon extends React.Component<Props> { class TabHomeIcon extends React.Component<PropsType> {
constructor(props: PropsType) {
super(props);
Animatable.initializeRegistryWithDefinitions({
fabFocusIn: {
'0': {
scale: 1,
translateY: 0,
},
'0.9': {
scale: 1.2,
translateY: -9,
},
'1': {
scale: 1.1,
translateY: -7,
},
},
fabFocusOut: {
'0': {
scale: 1.1,
translateY: -6,
},
'1': {
scale: 1,
translateY: 0,
},
},
});
}
focusedIcon = require('../../../assets/tab-icon.png'); shouldComponentUpdate(nextProps: PropsType): boolean {
unFocusedIcon = require('../../../assets/tab-icon-outline.png'); const {focused} = this.props;
return nextProps.focused !== focused;
}
constructor(props) { getIconRender = ({
super(props); size,
Animatable.initializeRegistryWithDefinitions({ color,
fabFocusIn: { }: {
"0": { size: number,
scale: 1, translateY: 0 color: string,
}, }): React.Node => {
"0.9": { const {focused} = this.props;
scale: 1.2, translateY: -9 if (focused)
}, return (
"1": { <Image
scale: 1.1, translateY: -7 source={FOCUSED_ICON}
}, style={{
}, width: size,
fabFocusOut: { height: size,
"0": { tintColor: color,
scale: 1.1, translateY: -6 }}
}, />
"1": { );
scale: 1, translateY: 0 return (
}, <Image
} source={UNFOCUSED_ICON}
}); style={{
} width: size,
height: size,
iconRender = ({size, color}) => tintColor: color,
this.props.focused }}
? <Image />
source={this.focusedIcon} );
style={{width: size, height: size, tintColor: color}} };
/>
: <Image
source={this.unFocusedIcon}
style={{width: size, height: size, tintColor: color}}
/>;
shouldComponentUpdate(nextProps: Props): boolean {
return (nextProps.focused !== this.props.focused);
}
render(): React$Node {
const props = this.props;
return (
<View
style={{
flex: 1,
justifyContent: 'center',
}}>
<TouchableRipple
onPress={props.onPress}
onLongPress={props.onLongPress}
borderless={true}
rippleColor={Platform.OS === 'android' ? this.props.theme.colors.primary : 'transparent'}
style={{
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
height: this.props.tabBarHeight + 30,
marginBottom: -15,
}}
>
<AnimatedFAB
duration={200}
easing={"ease-out"}
animation={props.focused ? "fabFocusIn" : "fabFocusOut"}
icon={this.iconRender}
style={{
marginTop: 15,
marginLeft: 'auto',
marginRight: 'auto'
}}/>
</TouchableRipple>
</View>
);
}
render(): React.Node {
const {props} = this;
return (
<View
style={{
flex: 1,
justifyContent: 'center',
}}>
<TouchableRipple
onPress={props.onPress}
onLongPress={props.onLongPress}
borderless
rippleColor={
Platform.OS === 'android'
? props.theme.colors.primary
: 'transparent'
}
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}
style={{
marginTop: 15,
marginLeft: 'auto',
marginRight: 'auto',
}}
/>
</TouchableRipple>
</View>
);
}
} }
export default withTheme(TabHomeIcon); export default withTheme(TabHomeIcon);

View file

@ -1,114 +1,117 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from "react-native"; import {View} from 'react-native';
import {TouchableRipple, withTheme} from 'react-native-paper'; import {TouchableRipple, withTheme} from 'react-native-paper';
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons"; import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import * as Animatable from "react-native-animatable"; import * as Animatable from 'react-native-animatable';
import type {CustomThemeType} from '../../managers/ThemeManager';
type Props = {
focused: boolean,
color: string,
label: string,
icon: MaterialCommunityIconsGlyphs,
onPress: Function,
onLongPress: Function,
theme: Object,
extraData: any,
}
type PropsType = {
focused: boolean,
color: string,
label: string,
icon: MaterialCommunityIconsGlyphs,
onPress: () => void,
onLongPress: () => void,
theme: CustomThemeType,
extraData: null | boolean | number | string,
};
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class TabIcon extends React.Component<Props> { class TabIcon extends React.Component<PropsType> {
firstRender: boolean;
firstRender: boolean; constructor(props: PropsType) {
super(props);
Animatable.initializeRegistryWithDefinitions({
focusIn: {
'0': {
scale: 1,
translateY: 0,
},
'0.9': {
scale: 1.3,
translateY: 7,
},
'1': {
scale: 1.2,
translateY: 6,
},
},
focusOut: {
'0': {
scale: 1.2,
translateY: 6,
},
'1': {
scale: 1,
translateY: 0,
},
},
});
this.firstRender = true;
}
constructor(props) { componentDidMount() {
super(props); this.firstRender = false;
Animatable.initializeRegistryWithDefinitions({ }
focusIn: {
"0": {
scale: 1, translateY: 0
},
"0.9": {
scale: 1.3, translateY: 7
},
"1": {
scale: 1.2, translateY: 6
},
},
focusOut: {
"0": {
scale: 1.2, translateY: 6
},
"1": {
scale: 1, translateY: 0
},
}
});
this.firstRender = true;
}
componentDidMount() { shouldComponentUpdate(nextProps: PropsType): boolean {
this.firstRender = false; const {props} = this;
} return (
nextProps.focused !== props.focused ||
nextProps.theme.dark !== props.theme.dark ||
nextProps.extraData !== props.extraData
);
}
shouldComponentUpdate(nextProps: Props): boolean { render(): React.Node {
return (nextProps.focused !== this.props.focused) const {props} = this;
|| (nextProps.theme.dark !== this.props.theme.dark) return (
|| (nextProps.extraData !== this.props.extraData); <TouchableRipple
} onPress={props.onPress}
onLongPress={props.onLongPress}
render(): React$Node { borderless
const props = this.props; rippleColor={props.theme.colors.primary}
return ( style={{
<TouchableRipple flex: 1,
onPress={props.onPress} justifyContent: 'center',
onLongPress={props.onLongPress} }}>
borderless={true} <View>
rippleColor={this.props.theme.colors.primary} <Animatable.View
style={{ duration={200}
flex: 1, easing="ease-out"
justifyContent: 'center', animation={props.focused ? 'focusIn' : 'focusOut'}
}} useNativeDriver>
> <MaterialCommunityIcons
<View> name={props.icon}
<Animatable.View color={props.color}
duration={200} size={26}
easing={"ease-out"} style={{
animation={props.focused ? "focusIn" : "focusOut"} marginLeft: 'auto',
useNativeDriver marginRight: 'auto',
> }}
<MaterialCommunityIcons />
name={props.icon} </Animatable.View>
color={props.color} <Animatable.Text
size={26} animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
style={{ useNativeDriver
marginLeft: 'auto', style={{
marginRight: 'auto', color: props.color,
}} marginLeft: 'auto',
/> marginRight: 'auto',
</Animatable.View> fontSize: 10,
<Animatable.Text }}>
animation={props.focused ? "fadeOutDown" : "fadeIn"} {props.label}
useNativeDriver </Animatable.Text>
</View>
style={{ </TouchableRipple>
color: props.color, );
marginLeft: 'auto', }
marginRight: 'auto',
fontSize: 10,
}}
>
{props.label}
</Animatable.Text>
</View>
</TouchableRipple>
);
}
} }
export default withTheme(TabIcon); export default withTheme(TabIcon);

View file

@ -1,13 +1,13 @@
export default { export default {
websites: { websites: {
AMICALE: "https://www.amicale-insat.fr/", AMICALE: 'https://www.amicale-insat.fr/',
AVAILABLE_ROOMS: "http://planex.insa-toulouse.fr/salles.php", AVAILABLE_ROOMS: 'http://planex.insa-toulouse.fr/salles.php',
BIB: "https://bibbox.insa-toulouse.fr/", BIB: 'https://bibbox.insa-toulouse.fr/',
BLUEMIND: "https://etud-mel.insa-toulouse.fr/webmail/", BLUEMIND: 'https://etud-mel.insa-toulouse.fr/webmail/',
ELUS_ETUDIANTS: "https://etud.insa-toulouse.fr/~eeinsat/", ELUS_ETUDIANTS: 'https://etud.insa-toulouse.fr/~eeinsat/',
ENT: "https://ent.insa-toulouse.fr/", ENT: 'https://ent.insa-toulouse.fr/',
INSA_ACCOUNT: "https://moncompte.insa-toulouse.fr/", INSA_ACCOUNT: 'https://moncompte.insa-toulouse.fr/',
TUTOR_INSA: "https://www.etud.insa-toulouse.fr/~tutorinsa/", TUTOR_INSA: 'https://www.etud.insa-toulouse.fr/~tutorinsa/',
WIKETUD: "https://wiki.etud.insa-toulouse.fr/", WIKETUD: 'https://wiki.etud.insa-toulouse.fr/',
}, },
} };

View file

@ -1,20 +1,20 @@
export default { export default {
machineStates: { machineStates: {
"AVAILABLE": 0, AVAILABLE: 0,
"RUNNING": 1, RUNNING: 1,
"RUNNING_NOT_STARTED": 2, RUNNING_NOT_STARTED: 2,
"FINISHED": 3, FINISHED: 3,
"UNAVAILABLE": 4, UNAVAILABLE: 4,
"ERROR": 5, ERROR: 5,
"UNKNOWN": 6, UNKNOWN: 6,
}, },
stateIcons: { stateIcons: {
0: 'radiobox-blank', 0: 'radiobox-blank',
1: 'progress-check', 1: 'progress-check',
2: 'alert-circle-outline', 2: 'alert-circle-outline',
3: 'check-circle', 3: 'check-circle',
4: 'alert-octagram-outline', 4: 'alert-octagram-outline',
5: 'alert', 5: 'alert',
6: 'help-circle-outline', 6: 'help-circle-outline',
} },
}; };

View file

@ -1,6 +1,6 @@
// @flow // @flow
import i18n from "i18n-js"; import i18n from 'i18n-js';
/** /**
* Singleton used to manage update slides. * Singleton used to manage update slides.
@ -14,51 +14,47 @@ import i18n from "i18n-js";
* </ul> * </ul>
*/ */
export default class Update { export default class Update {
// Increment the number to show the update slide
static number = 6;
// Increment the number to show the update slide // Change the number of slides to display
static number = 6; static slidesNumber = 4;
// Change the number of slides to display
static slidesNumber = 4;
// Change the icons to be displayed on the update slide
static iconList = [
'star',
'clock',
'qrcode-scan',
'account',
];
static colorsList = [
['#e01928', '#be1522'],
['#7c33ec', '#5e11d1'],
['#337aec', '#114ed1'],
['#e01928', '#be1522'],
]
static instance: Update | null = null; // Change the icons to be displayed on the update slide
static iconList = ['star', 'clock', 'qrcode-scan', 'account'];
titleList: Array<string>; static colorsList = [
descriptionList: Array<string>; ['#e01928', '#be1522'],
['#7c33ec', '#5e11d1'],
['#337aec', '#114ed1'],
['#e01928', '#be1522'],
];
/** static instance: Update | null = null;
* Init translations
*/ titleList: Array<string>;
constructor() {
this.titleList = []; descriptionList: Array<string>;
this.descriptionList = [];
for (let i = 0; i < Update.slidesNumber; i++) { /**
this.titleList.push(i18n.t('intro.updateSlide' + i + '.title')) * Init translations
this.descriptionList.push(i18n.t('intro.updateSlide' + i + '.text')) */
} constructor() {
this.titleList = [];
this.descriptionList = [];
for (let i = 0; i < Update.slidesNumber; i += 1) {
this.titleList.push(i18n.t(`intro.updateSlide${i}.title`));
this.descriptionList.push(i18n.t(`intro.updateSlide${i}.text`));
} }
}
/** /**
* Get this class instance or create one if none is found * Get this class instance or create one if none is found
* *
* @returns {Update} * @returns {Update}
*/ */
static getInstance(): Update { static getInstance(): Update {
return Update.instance === null ? if (Update.instance == null) Update.instance = new Update();
Update.instance = new Update() : return Update.instance;
Update.instance; }
} }
};

View file

@ -1,130 +1,138 @@
// @flow // @flow
import type {Machine} from "../screens/Proxiwash/ProxiwashScreen"; import type {ProxiwashMachineType} from '../screens/Proxiwash/ProxiwashScreen';
import type {CustomThemeType} from './ThemeManager';
import type {RuFoodCategoryType} from '../screens/Services/SelfMenuScreen';
/** /**
* Singleton class used to manage april fools * Singleton class used to manage april fools
*/ */
export default class AprilFoolsManager { export default class AprilFoolsManager {
static instance: AprilFoolsManager | null = null;
static instance: AprilFoolsManager | null = null; static fakeMachineNumber = [
static fakeMachineNumber = [ '',
"", 'cos(ln(1))',
"cos(ln(1))", '0,5⁻¹',
"0,5⁻¹", '567/189',
"567/189", '√2×√8',
"√2×√8", '√50×sin(9π/4)',
"√50×sin(9π/4)", '⌈π+e⌉',
"⌈π+e⌉", 'div(rot(B))+7',
"div(rot(B))+7", '4×cosh(0)+4',
"4×cosh(0)+4", '8-(-i)²',
"8-(-i)²", '|5√2+5√2i|',
"|5√2+5√2i|", '1×10¹+1×10⁰',
"1×10¹+1×10⁰", 'Re(√192e^(iπ/6))',
"Re(√192e^(iπ/6))", ];
];
aprilFoolsEnabled: boolean;
constructor() { aprilFoolsEnabled: boolean;
let today = new Date();
this.aprilFoolsEnabled = (today.getDate() === 1 && today.getMonth() === 3); constructor() {
const today = new Date();
this.aprilFoolsEnabled = today.getDate() === 1 && today.getMonth() === 3;
}
/**
* Get this class instance or create one if none is found
* @returns {ThemeManager}
*/
static getInstance(): AprilFoolsManager {
if (AprilFoolsManager.instance == null)
AprilFoolsManager.instance = new AprilFoolsManager();
return AprilFoolsManager.instance;
}
/**
* Adds fake menu entries
*
* @param menu
* @returns {Object}
*/
static getFakeMenuItem(
menu: Array<RuFoodCategoryType>,
): Array<RuFoodCategoryType> {
menu[1].dishes.splice(4, 0, {name: 'Coq au vin'});
menu[1].dishes.splice(2, 0, {name: "Bat'Soupe"});
menu[1].dishes.splice(1, 0, {name: 'Pave de loup'});
menu[1].dishes.splice(0, 0, {name: 'Béranger à point'});
menu[1].dishes.splice(0, 0, {name: "Pieds d'Arnaud"});
return menu;
}
/**
* Changes proxiwash dryers order
*
* @param dryers
*/
static getNewProxiwashDryerOrderedList(
dryers: Array<ProxiwashMachineType> | null,
) {
if (dryers != null) {
const second = dryers[1];
dryers.splice(1, 1);
dryers.push(second);
} }
}
/** /**
* Get this class instance or create one if none is found * Changes proxiwash washers order
* @returns {ThemeManager} *
*/ * @param washers
static getInstance(): AprilFoolsManager { */
return AprilFoolsManager.instance === null ? static getNewProxiwashWasherOrderedList(
AprilFoolsManager.instance = new AprilFoolsManager() : washers: Array<ProxiwashMachineType> | null,
AprilFoolsManager.instance; ) {
if (washers != null) {
const first = washers[0];
const second = washers[1];
const fifth = washers[4];
const ninth = washers[8];
washers.splice(8, 1, second);
washers.splice(4, 1, ninth);
washers.splice(1, 1, first);
washers.splice(0, 1, fifth);
} }
}
/** /**
* Adds fake menu entries * Gets the new display number for the given machine number
* *
* @param menu * @param number
* @returns {Object} * @returns {string}
*/ */
static getFakeMenuItem(menu: Array<{dishes: Array<{name: string}>}>) { static getProxiwashMachineDisplayNumber(number: number): string {
menu[1]["dishes"].splice(4, 0, {name: "Coq au vin"}); return AprilFoolsManager.fakeMachineNumber[number];
menu[1]["dishes"].splice(2, 0, {name: "Bat'Soupe"}); }
menu[1]["dishes"].splice(1, 0, {name: "Pave de loup"});
menu[1]["dishes"].splice(0, 0, {name: "Béranger à point"});
menu[1]["dishes"].splice(0, 0, {name: "Pieds d'Arnaud"});
return menu;
}
/** /**
* Changes proxiwash dryers order * Gets the new and ugly april fools theme
* *
* @param dryers * @param currentTheme
*/ * @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}}
static getNewProxiwashDryerOrderedList(dryers: Array<Machine> | null) { */
if (dryers != null) { static getAprilFoolsTheme(currentTheme: CustomThemeType): CustomThemeType {
let second = dryers[1]; return {
dryers.splice(1, 1); ...currentTheme,
dryers.push(second); colors: {
} ...currentTheme.colors,
} primary: '#00be45',
accent: '#00be45',
background: '#d02eee',
tabIcon: '#380d43',
card: '#eed639',
surface: '#eed639',
dividerBackground: '#c72ce4',
textDisabled: '#b9b9b9',
/** // Calendar/Agenda
* Changes proxiwash washers order agendaBackgroundColor: '#c72ce4',
* agendaDayTextColor: '#6d6d6d',
* @param washers },
*/ };
static getNewProxiwashWasherOrderedList(washers: Array<Machine> | null) { }
if (washers != null) {
let first = washers[0];
let second = washers[1];
let fifth = washers[4];
let ninth = washers[8];
washers.splice(8, 1, second);
washers.splice(4, 1, ninth);
washers.splice(1, 1, first);
washers.splice(0, 1, fifth);
}
}
/** isAprilFoolsEnabled(): boolean {
* Gets the new display number for the given machine number return this.aprilFoolsEnabled;
* }
* @param number }
* @returns {string}
*/
static getProxiwashMachineDisplayNumber(number: number) {
return AprilFoolsManager.fakeMachineNumber[number];
}
/**
* Gets the new and ugly april fools theme
*
* @param currentTheme
* @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}}
*/
static getAprilFoolsTheme(currentTheme: Object) {
return {
...currentTheme,
colors: {
...currentTheme.colors,
primary: '#00be45',
accent: '#00be45',
background: '#d02eee',
tabIcon: "#380d43",
card: "#eed639",
surface: "#eed639",
dividerBackground: '#c72ce4',
textDisabled: '#b9b9b9',
// Calendar/Agenda
agendaBackgroundColor: '#c72ce4',
agendaDayTextColor: '#6d6d6d',
},
};
}
isAprilFoolsEnabled() {
return this.aprilFoolsEnabled;
}
};

View file

@ -1,7 +1,7 @@
// @flow // @flow
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import {SERVICES_KEY} from "./ServicesManager"; import {SERVICES_KEY} from './ServicesManager';
/** /**
* Singleton used to manage preferences. * Singleton used to manage preferences.
@ -10,227 +10,232 @@ import {SERVICES_KEY} from "./ServicesManager";
*/ */
export default class AsyncStorageManager { export default class AsyncStorageManager {
static instance: AsyncStorageManager | null = null;
static instance: AsyncStorageManager | null = null; static PREFERENCES = {
debugUnlocked: {
key: 'debugUnlocked',
default: '0',
},
showIntro: {
key: 'showIntro',
default: '1',
},
updateNumber: {
key: 'updateNumber',
default: '0',
},
proxiwashNotifications: {
key: 'proxiwashNotifications',
default: '5',
},
nightModeFollowSystem: {
key: 'nightModeFollowSystem',
default: '1',
},
nightMode: {
key: 'nightMode',
default: '1',
},
defaultStartScreen: {
key: 'defaultStartScreen',
default: 'home',
},
servicesShowBanner: {
key: 'servicesShowBanner',
default: '1',
},
proxiwashShowBanner: {
key: 'proxiwashShowBanner',
default: '1',
},
homeShowBanner: {
key: 'homeShowBanner',
default: '1',
},
eventsShowBanner: {
key: 'eventsShowBanner',
default: '1',
},
planexShowBanner: {
key: 'planexShowBanner',
default: '1',
},
loginShowBanner: {
key: 'loginShowBanner',
default: '1',
},
voteShowBanner: {
key: 'voteShowBanner',
default: '1',
},
equipmentShowBanner: {
key: 'equipmentShowBanner',
default: '1',
},
gameStartShowBanner: {
key: 'gameStartShowBanner',
default: '1',
},
proxiwashWatchedMachines: {
key: 'proxiwashWatchedMachines',
default: '[]',
},
showAprilFoolsStart: {
key: 'showAprilFoolsStart',
default: '1',
},
planexCurrentGroup: {
key: 'planexCurrentGroup',
default: '',
},
planexFavoriteGroups: {
key: 'planexFavoriteGroups',
default: '[]',
},
dashboardItems: {
key: 'dashboardItems',
default: JSON.stringify([
SERVICES_KEY.EMAIL,
SERVICES_KEY.WASHERS,
SERVICES_KEY.PROXIMO,
SERVICES_KEY.TUTOR_INSA,
SERVICES_KEY.RU,
]),
},
gameScores: {
key: 'gameScores',
default: '[]',
},
};
static PREFERENCES = { #currentPreferences: {[key: string]: string};
debugUnlocked: {
key: 'debugUnlocked', constructor() {
default: '0', this.#currentPreferences = {};
}, }
showIntro: {
key: 'showIntro', /**
default: '1', * Get this class instance or create one if none is found
}, * @returns {AsyncStorageManager}
updateNumber: { */
key: 'updateNumber', static getInstance(): AsyncStorageManager {
default: '0', if (AsyncStorageManager.instance == null)
}, AsyncStorageManager.instance = new AsyncStorageManager();
proxiwashNotifications: { return AsyncStorageManager.instance;
key: 'proxiwashNotifications', }
default: '5',
}, /**
nightModeFollowSystem: { * Saves the value associated to the given key to preferences.
key: 'nightModeFollowSystem', *
default: '1', * @param key
}, * @param value
nightMode: { */
key: 'nightMode', static set(
default: '1', key: string,
}, // eslint-disable-next-line flowtype/no-weak-types
defaultStartScreen: { value: number | string | boolean | {...} | Array<any>,
key: 'defaultStartScreen', ) {
default: 'home', AsyncStorageManager.getInstance().setPreference(key, value);
}, }
servicesShowBanner: {
key: 'servicesShowBanner', /**
default: '1', * Gets the string value of the given preference
}, *
proxiwashShowBanner: { * @param key
key: 'proxiwashShowBanner', * @returns {string}
default: '1', */
}, static getString(key: string): string {
homeShowBanner: { const value = AsyncStorageManager.getInstance().getPreference(key);
key: 'homeShowBanner', return value != null ? value : '';
default: '1', }
},
eventsShowBanner: { /**
key: 'eventsShowBanner', * Gets the boolean value of the given preference
default: '1', *
}, * @param key
planexShowBanner: { * @returns {boolean}
key: 'planexShowBanner', */
default: '1', static getBool(key: string): boolean {
}, const value = AsyncStorageManager.getString(key);
loginShowBanner: { return value === '1' || value === 'true';
key: 'loginShowBanner', }
default: '1',
}, /**
voteShowBanner: { * Gets the number value of the given preference
key: 'voteShowBanner', *
default: '1', * @param key
}, * @returns {number}
equipmentShowBanner: { */
key: 'equipmentShowBanner', static getNumber(key: string): number {
default: '1', return parseFloat(AsyncStorageManager.getString(key));
}, }
gameStartShowBanner: {
key: 'gameStartShowBanner', /**
default: '1', * Gets the object value of the given preference
}, *
proxiwashWatchedMachines: { * @param key
key: 'proxiwashWatchedMachines', * @returns {{...}}
default: '[]', */
}, // eslint-disable-next-line flowtype/no-weak-types
showAprilFoolsStart: { static getObject(key: string): any {
key: 'showAprilFoolsStart', return JSON.parse(AsyncStorageManager.getString(key));
default: '1', }
},
planexCurrentGroup: { /**
key: 'planexCurrentGroup', * Set preferences object current values from AsyncStorage.
default: '', * This function should be called at the app's start.
}, *
planexFavoriteGroups: { * @return {Promise<void>}
key: 'planexFavoriteGroups', */
default: '[]', async loadPreferences() {
}, const prefKeys = [];
dashboardItems: { // Get all available keys
key: 'dashboardItems', Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => {
default: JSON.stringify([ prefKeys.push(key);
SERVICES_KEY.EMAIL, });
SERVICES_KEY.WASHERS, // Get corresponding values
SERVICES_KEY.PROXIMO, const resultArray = await AsyncStorage.multiGet(prefKeys);
SERVICES_KEY.TUTOR_INSA, // Save those values for later use
SERVICES_KEY.RU, resultArray.forEach((item: [string, string | null]) => {
]), const key = item[0];
}, let val = item[1];
gameScores: { if (val === null) val = AsyncStorageManager.PREFERENCES[key].default;
key: 'gameScores', this.#currentPreferences[key] = val;
default: '[]', });
}, }
}
/**
#currentPreferences: {[key: string]: string}; * Saves the value associated to the given key to preferences.
* This updates the preferences object and saves it to AsyncStorage.
constructor() { *
this.#currentPreferences = {}; * @param key
} * @param value
*/
/** setPreference(
* Get this class instance or create one if none is found key: string,
* @returns {AsyncStorageManager} // eslint-disable-next-line flowtype/no-weak-types
*/ value: number | string | boolean | {...} | Array<any>,
static getInstance(): AsyncStorageManager { ) {
return AsyncStorageManager.instance === null ? if (AsyncStorageManager.PREFERENCES[key] != null) {
AsyncStorageManager.instance = new AsyncStorageManager() : let convertedValue;
AsyncStorageManager.instance; if (typeof value === 'string') convertedValue = value;
} else if (typeof value === 'boolean' || typeof value === 'number')
convertedValue = value.toString();
/** else convertedValue = JSON.stringify(value);
* Set preferences object current values from AsyncStorage. this.#currentPreferences[key] = convertedValue;
* This function should be called at the app's start. AsyncStorage.setItem(key, convertedValue);
*
* @return {Promise<void>}
*/
async loadPreferences() {
let prefKeys = [];
// Get all available keys
for (let key in AsyncStorageManager.PREFERENCES) {
prefKeys.push(key);
}
// Get corresponding values
let resultArray: Array<Array<string>> = await AsyncStorage.multiGet(prefKeys);
// Save those values for later use
for (let i = 0; i < resultArray.length; i++) {
let key: string = resultArray[i][0];
let val: string | null = resultArray[i][1];
if (val === null)
val = AsyncStorageManager.PREFERENCES[key].default;
this.#currentPreferences[key] = val;
}
}
/**
* Saves the value associated to the given key to preferences.
* This updates the preferences object and saves it to AsyncStorage.
*
* @param key
* @param value
*/
setPreference(key: string, value: any) {
if (AsyncStorageManager.PREFERENCES[key] != null) {
let convertedValue = "";
if (typeof value === "string")
convertedValue = value;
else if (typeof value === "boolean" || typeof value === "number")
convertedValue = value.toString();
else
convertedValue = JSON.stringify(value);
this.#currentPreferences[key] = convertedValue;
AsyncStorage.setItem(key, convertedValue);
}
}
/**
* Gets the value at the given key.
* If the key is not available, returns null
*
* @param key
* @returns {string|null}
*/
getPreference(key: string) {
return this.#currentPreferences[key];
}
/**
* aves the value associated to the given key to preferences.
*
* @param key
* @param value
*/
static set(key: string, value: any) {
AsyncStorageManager.getInstance().setPreference(key, value);
}
/**
* Gets the string value of the given preference
*
* @param key
* @returns {boolean}
*/
static getString(key: string) {
return AsyncStorageManager.getInstance().getPreference(key);
}
/**
* Gets the boolean value of the given preference
*
* @param key
* @returns {boolean}
*/
static getBool(key: string) {
const value = AsyncStorageManager.getString(key);
return value === "1" || value === "true";
}
/**
* Gets the number value of the given preference
*
* @param key
* @returns {boolean}
*/
static getNumber(key: string) {
return parseFloat(AsyncStorageManager.getString(key));
}
/**
* Gets the object value of the given preference
*
* @param key
* @returns {boolean}
*/
static getObject(key: string) {
return JSON.parse(AsyncStorageManager.getString(key));
} }
}
/**
* Gets the value at the given key.
* If the key is not available, returns null
*
* @param key
* @returns {string|null}
*/
getPreference(key: string): string | null {
return this.#currentPreferences[key];
}
} }

View file

@ -1,7 +1,8 @@
// @flow // @flow
import * as Keychain from 'react-native-keychain'; import * as Keychain from 'react-native-keychain';
import {apiRequest, ERROR_TYPE} from "../utils/WebData"; import type {ApiDataLoginType, ApiGenericDataType} from '../utils/WebData';
import {apiRequest, ERROR_TYPE} from '../utils/WebData';
/** /**
* champ: error * champ: error
@ -14,161 +15,167 @@ import {apiRequest, ERROR_TYPE} from "../utils/WebData";
* 500 : SERVER_ERROR -> pb coté serveur * 500 : SERVER_ERROR -> pb coté serveur
*/ */
const SERVER_NAME = "amicale-insat.fr"; const SERVER_NAME = 'amicale-insat.fr';
const AUTH_PATH = "password"; const AUTH_PATH = 'password';
export default class ConnectionManager { export default class ConnectionManager {
static instance: ConnectionManager | null = null; static instance: ConnectionManager | null = null;
#email: string; #email: string;
#token: string | null;
constructor() { #token: string | null;
this.#token = null;
}
/** constructor() {
* Gets this class instance or create one if none is found this.#token = null;
* }
* @returns {ConnectionManager}
*/
static getInstance(): ConnectionManager {
return ConnectionManager.instance === null ?
ConnectionManager.instance = new ConnectionManager() :
ConnectionManager.instance;
}
/** /**
* Gets the current token * Gets this class instance or create one if none is found
* *
* @returns {string | null} * @returns {ConnectionManager}
*/ */
getToken(): string | null { static getInstance(): ConnectionManager {
return this.#token; if (ConnectionManager.instance == null)
} ConnectionManager.instance = new ConnectionManager();
return ConnectionManager.instance;
}
/** /**
* Tries to recover login token from the secure keychain * Gets the current token
* *
* @returns {Promise<R>} * @returns {string | null}
*/ */
async recoverLogin() { getToken(): string | null {
return new Promise((resolve, reject) => { return this.#token;
if (this.getToken() !== null) }
resolve(this.getToken());
else {
Keychain.getInternetCredentials(SERVER_NAME)
.then((data) => {
if (data) {
this.#token = data.password;
resolve(this.#token);
} else
reject(false);
})
.catch(() => {
reject(false);
});
}
});
}
/** /**
* Check if the user has a valid token * Tries to recover login token from the secure keychain
* *
* @returns {boolean} * @returns Promise<string>
*/ */
isLoggedIn() { async recoverLogin(): Promise<string> {
return this.getToken() !== null; return new Promise(
} (resolve: (token: string) => void, reject: () => void) => {
const token = this.getToken();
if (token != null) resolve(token);
else {
Keychain.getInternetCredentials(SERVER_NAME)
.then((data: Keychain.UserCredentials | false) => {
if (
data != null &&
data.password != null &&
typeof data.password === 'string'
) {
this.#token = data.password;
resolve(this.#token);
} else reject();
})
.catch((): void => reject());
}
},
);
}
/** /**
* Saves the login token in the secure keychain * Check if the user has a valid token
* *
* @param email * @returns {boolean}
* @param token */
* @returns {Promise<R>} isLoggedIn(): boolean {
*/ return this.getToken() !== null;
async saveLogin(email: string, token: string) { }
return new Promise((resolve, reject) => {
Keychain.setInternetCredentials(SERVER_NAME, 'token', token)
.then(() => {
this.#token = token;
this.#email = email;
resolve(true);
})
.catch(() => {
reject(false);
});
});
}
/** /**
* Deletes the login token from the keychain * Saves the login token in the secure keychain
* *
* @returns {Promise<R>} * @param email
*/ * @param token
async disconnect() { * @returns Promise<void>
return new Promise((resolve, reject) => { */
Keychain.resetInternetCredentials(SERVER_NAME) async saveLogin(email: string, token: string): Promise<void> {
.then(() => { return new Promise((resolve: () => void, reject: () => void) => {
this.#token = null; Keychain.setInternetCredentials(SERVER_NAME, 'token', token)
resolve(true); .then(() => {
}) this.#token = token;
.catch(() => { this.#email = email;
reject(false); resolve();
}); })
}); .catch((): void => reject());
} });
}
/**
* Deletes the login token from the keychain
*
* @returns Promise<void>
*/
async disconnect(): Promise<void> {
return new Promise((resolve: () => void, reject: () => void) => {
Keychain.resetInternetCredentials(SERVER_NAME)
.then(() => {
this.#token = null;
resolve();
})
.catch((): void => reject());
});
}
/** /**
* Sends the given login and password to the api. * Sends the given login and password to the api.
* If the combination is valid, the login token is received and saved in the secure keychain. * If the combination is valid, the login token is received and saved in the secure keychain.
* If not, the promise is rejected with the corresponding error code. * If not, the promise is rejected with the corresponding error code.
* *
* @param email * @param email
* @param password * @param password
* @returns {Promise<R>} * @returns Promise<void>
*/ */
async connect(email: string, password: string) { async connect(email: string, password: string): Promise<void> {
return new Promise((resolve, reject) => { return new Promise(
const data = { (resolve: () => void, reject: (error: number) => void) => {
email: email, const data = {
password: password, email,
}; password,
apiRequest(AUTH_PATH, 'POST', data) };
.then((response) => { apiRequest(AUTH_PATH, 'POST', data)
this.saveLogin(email, response.token) .then((response: ApiDataLoginType) => {
.then(() => { if (response.token != null) {
resolve(true); this.saveLogin(email, response.token)
}) .then((): void => resolve())
.catch(() => { .catch((): void => reject(ERROR_TYPE.TOKEN_SAVE));
reject(ERROR_TYPE.TOKEN_SAVE); } else reject(ERROR_TYPE.SERVER_ERROR);
}); })
}) .catch((error: number): void => reject(error));
.catch((error) => reject(error)); },
}); );
} }
/** /**
* Sends an authenticated request with the login token to the API * Sends an authenticated request with the login token to the API
* *
* @param path * @param path
* @param params * @param params
* @returns {Promise<R>} * @returns Promise<ApiGenericDataType>
*/ */
async authenticatedRequest(path: string, params: Object) { async authenticatedRequest(
return new Promise((resolve, reject) => { path: string,
if (this.getToken() !== null) { params: {...},
let data = { ): Promise<ApiGenericDataType> {
token: this.getToken(), return new Promise(
...params (
}; resolve: (response: ApiGenericDataType) => void,
apiRequest(path, 'POST', data) reject: (error: number) => void,
.then((response) => resolve(response)) ) => {
.catch((error) => reject(error)); if (this.getToken() !== null) {
} else const data = {
reject(ERROR_TYPE.TOKEN_RETRIEVE); ...params,
}); token: this.getToken(),
} };
apiRequest(path, 'POST', data)
.then((response: ApiGenericDataType): void => resolve(response))
.catch((error: number): void => reject(error));
} else reject(ERROR_TYPE.TOKEN_RETRIEVE);
},
);
}
} }

View file

@ -1,27 +1,21 @@
// @flow // @flow
import type {ServiceItem} from "./ServicesManager"; import type {ServiceItemType} from './ServicesManager';
import ServicesManager from "./ServicesManager"; import ServicesManager from './ServicesManager';
import {StackNavigationProp} from "@react-navigation/stack"; import {getSublistWithIds} from '../utils/Utils';
import {getSublistWithIds} from "../utils/Utils"; import AsyncStorageManager from './AsyncStorageManager';
import AsyncStorageManager from "./AsyncStorageManager";
export default class DashboardManager extends ServicesManager { export default class DashboardManager extends ServicesManager {
getCurrentDashboard(): Array<ServiceItemType | null> {
constructor(nav: StackNavigationProp) { const dashboardIdList = AsyncStorageManager.getObject(
super(nav) AsyncStorageManager.PREFERENCES.dashboardItems.key,
} );
const allDatasets = [
getCurrentDashboard(): Array<ServiceItem> { ...this.amicaleDataset,
const dashboardIdList = AsyncStorageManager ...this.studentsDataset,
.getObject(AsyncStorageManager.PREFERENCES.dashboardItems.key); ...this.insaDataset,
const allDatasets = [ ...this.specialDataset,
...this.amicaleDataset, ];
...this.studentsDataset, return getSublistWithIds(dashboardIdList, allDatasets);
...this.insaDataset, }
...this.specialDataset,
];
return getSublistWithIds(dashboardIdList, allDatasets);
}
} }

View file

@ -7,63 +7,68 @@ import i18n from 'i18n-js';
* Translations are hardcoded as toLocaleDateString does not work on current android JS engine * Translations are hardcoded as toLocaleDateString does not work on current android JS engine
*/ */
export default class DateManager { export default class DateManager {
static instance: DateManager | null = null; static instance: DateManager | null = null;
daysOfWeek = []; daysOfWeek = [];
monthsOfYear = [];
constructor() { monthsOfYear = [];
this.daysOfWeek.push(i18n.t("date.daysOfWeek.sunday")); // 0 represents sunday
this.daysOfWeek.push(i18n.t("date.daysOfWeek.monday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.tuesday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.wednesday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.thursday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.friday"));
this.daysOfWeek.push(i18n.t("date.daysOfWeek.saturday"));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.january")); constructor() {
this.monthsOfYear.push(i18n.t("date.monthsOfYear.february")); this.daysOfWeek.push(i18n.t('date.daysOfWeek.sunday')); // 0 represents sunday
this.monthsOfYear.push(i18n.t("date.monthsOfYear.march")); this.daysOfWeek.push(i18n.t('date.daysOfWeek.monday'));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.april")); this.daysOfWeek.push(i18n.t('date.daysOfWeek.tuesday'));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.may")); this.daysOfWeek.push(i18n.t('date.daysOfWeek.wednesday'));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.june")); this.daysOfWeek.push(i18n.t('date.daysOfWeek.thursday'));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.july")); this.daysOfWeek.push(i18n.t('date.daysOfWeek.friday'));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.august")); this.daysOfWeek.push(i18n.t('date.daysOfWeek.saturday'));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.september"));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.october"));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.november"));
this.monthsOfYear.push(i18n.t("date.monthsOfYear.december"));
}
/** this.monthsOfYear.push(i18n.t('date.monthsOfYear.january'));
* Get this class instance or create one if none is found this.monthsOfYear.push(i18n.t('date.monthsOfYear.february'));
* @returns {DateManager} this.monthsOfYear.push(i18n.t('date.monthsOfYear.march'));
*/ this.monthsOfYear.push(i18n.t('date.monthsOfYear.april'));
static getInstance(): DateManager { this.monthsOfYear.push(i18n.t('date.monthsOfYear.may'));
return DateManager.instance === null ? this.monthsOfYear.push(i18n.t('date.monthsOfYear.june'));
DateManager.instance = new DateManager() : this.monthsOfYear.push(i18n.t('date.monthsOfYear.july'));
DateManager.instance; this.monthsOfYear.push(i18n.t('date.monthsOfYear.august'));
} this.monthsOfYear.push(i18n.t('date.monthsOfYear.september'));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.october'));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.november'));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.december'));
}
static isWeekend(date: Date) { /**
return date.getDay() === 6 || date.getDay() === 0; * Get this class instance or create one if none is found
} * @returns {DateManager}
*/
static getInstance(): DateManager {
if (DateManager.instance == null) DateManager.instance = new DateManager();
return DateManager.instance;
}
getMonthsOfYear() { static isWeekend(date: Date): boolean {
return this.monthsOfYear; return date.getDay() === 6 || date.getDay() === 0;
} }
/** getMonthsOfYear(): Array<string> {
* Gets a translated string representing the given date. return this.monthsOfYear;
* }
* @param dateString The date with the format YYYY-MM-DD
* @return {string} The translated string
*/
getTranslatedDate(dateString: string) {
let dateArray = dateString.split('-');
let date = new Date();
date.setFullYear(parseInt(dateArray[0]), parseInt(dateArray[1]) - 1, parseInt(dateArray[2]));
return this.daysOfWeek[date.getDay()] + " " + date.getDate() + " " + this.monthsOfYear[date.getMonth()] + " " + date.getFullYear();
}
/**
* Gets a translated string representing the given date.
*
* @param dateString The date with the format YYYY-MM-DD
* @return {string} The translated string
*/
getTranslatedDate(dateString: string): string {
const dateArray = dateString.split('-');
const date = new Date();
date.setFullYear(
parseInt(dateArray[0], 10),
parseInt(dateArray[1], 10) - 1,
parseInt(dateArray[2], 10),
);
return `${this.daysOfWeek[date.getDay()]} ${date.getDate()} ${
this.monthsOfYear[date.getMonth()]
} ${date.getFullYear()}`;
}
} }

View file

@ -1,22 +1,24 @@
// @flow // @flow
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import * as RNLocalize from "react-native-localize"; import * as RNLocalize from 'react-native-localize';
import en from '../../locales/en'; import en from '../../locales/en.json';
import fr from '../../locales/fr.json'; import fr from '../../locales/fr.json';
/** /**
* Static class used to manage locales * Static class used to manage locales
*/ */
export default class LocaleManager { export default class LocaleManager {
/**
/** * Initialize translations using language files
* Initialize translations using language files */
*/ static initTranslations() {
static initTranslations() { i18n.fallbacks = true;
i18n.fallbacks = true; i18n.translations = {fr, en};
i18n.translations = {fr, en}; i18n.locale = RNLocalize.findBestAvailableLanguage([
i18n.locale = RNLocalize.findBestAvailableLanguage(["en", "fr"]).languageTag; 'en',
} 'fr',
]).languageTag;
}
} }

View file

@ -1,378 +1,384 @@
// @flow // @flow
import i18n from "i18n-js"; import i18n from 'i18n-js';
import AvailableWebsites from "../constants/AvailableWebsites"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import AvailableWebsites from '../constants/AvailableWebsites';
import ConnectionManager from "./ConnectionManager"; import ConnectionManager from './ConnectionManager';
import type {fullDashboard} from "../screens/Home/HomeScreen"; import type {FullDashboardType} from '../screens/Home/HomeScreen';
import getStrippedServicesList from '../utils/Services';
// AMICALE // AMICALE
const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png"; const CLUBS_IMAGE =
const PROFILE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png';
const EQUIPMENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png"; const PROFILE_IMAGE =
const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png';
const AMICALE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png"; const EQUIPMENT_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png';
const VOTE_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png';
const AMICALE_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png';
// STUDENTS // STUDENTS
const PROXIMO_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png" const PROXIMO_IMAGE =
const WIKETUD_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png';
const EE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png"; const WIKETUD_IMAGE =
const TUTORINSA_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png';
const EE_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png';
const TUTORINSA_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.png';
// INSA // INSA
const BIB_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bib.png"; const BIB_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/Bib.png';
const RU_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/RU.png"; const RU_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/RU.png';
const ROOM_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png"; const ROOM_IMAGE =
const EMAIL_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png';
const ENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png"; const EMAIL_IMAGE =
const ACCOUNT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Account.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png';
const ENT_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png';
const ACCOUNT_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/Account.png';
// SPECIAL // SPECIAL
const WASHER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png"; const WASHER_IMAGE =
const DRYER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png';
const DRYER_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png';
const AMICALE_LOGO = require("../../assets/amicale.png"); const AMICALE_LOGO = require('../../assets/amicale.png');
export const SERVICES_KEY = { export const SERVICES_KEY = {
CLUBS: "clubs", CLUBS: 'clubs',
PROFILE: "profile", PROFILE: 'profile',
EQUIPMENT: "equipment", EQUIPMENT: 'equipment',
AMICALE_WEBSITE: "amicale_website", AMICALE_WEBSITE: 'amicale_website',
VOTE: "vote", VOTE: 'vote',
PROXIMO: "proximo", PROXIMO: 'proximo',
WIKETUD: "wiketud", WIKETUD: 'wiketud',
ELUS_ETUDIANTS: "elus_etudiants", ELUS_ETUDIANTS: 'elus_etudiants',
TUTOR_INSA: "tutor_insa", TUTOR_INSA: 'tutor_insa',
RU: "ru", RU: 'ru',
AVAILABLE_ROOMS: "available_rooms", AVAILABLE_ROOMS: 'available_rooms',
BIB: "bib", BIB: 'bib',
EMAIL: "email", EMAIL: 'email',
ENT: "ent", ENT: 'ent',
INSA_ACCOUNT: "insa_account", INSA_ACCOUNT: 'insa_account',
WASHERS: "washers", WASHERS: 'washers',
DRYERS: "dryers", DRYERS: 'dryers',
} };
export const SERVICES_CATEGORIES_KEY = { export const SERVICES_CATEGORIES_KEY = {
AMICALE: "amicale", AMICALE: 'amicale',
STUDENTS: "students", STUDENTS: 'students',
INSA: "insa", INSA: 'insa',
SPECIAL: "special", SPECIAL: 'special',
} };
export type ServiceItemType = {
key: string,
title: string,
subtitle: string,
image: string,
onPress: () => void,
badgeFunction?: (dashboard: FullDashboardType) => number,
};
export type ServiceItem = { export type ServiceCategoryType = {
key: string, key: string,
title: string, title: string,
subtitle: string, subtitle: string,
image: string, image: string | number,
onPress: () => void, content: Array<ServiceItemType>,
badgeFunction?: (dashboard: fullDashboard) => number, };
}
export type ServiceCategory = {
key: string,
title: string,
subtitle: string,
image: string | number,
content: Array<ServiceItem>
}
export default class ServicesManager { export default class ServicesManager {
navigation: StackNavigationProp;
navigation: StackNavigationProp; amicaleDataset: Array<ServiceItemType>;
amicaleDataset: Array<ServiceItem>; studentsDataset: Array<ServiceItemType>;
studentsDataset: Array<ServiceItem>;
insaDataset: Array<ServiceItem>;
specialDataset: Array<ServiceItem>;
categoriesDataset: Array<ServiceCategory>; insaDataset: Array<ServiceItemType>;
constructor(nav: StackNavigationProp) { specialDataset: Array<ServiceItemType>;
this.navigation = nav;
this.amicaleDataset = [
{
key: SERVICES_KEY.CLUBS,
title: i18n.t('screens.clubs.title'),
subtitle: i18n.t('screens.services.descriptions.clubs'),
image: CLUBS_IMAGE,
onPress: () => this.onAmicaleServicePress("club-list"),
},
{
key: SERVICES_KEY.PROFILE,
title: i18n.t('screens.profile.title'),
subtitle: i18n.t('screens.services.descriptions.profile'),
image: PROFILE_IMAGE,
onPress: () => this.onAmicaleServicePress("profile"),
},
{
key: SERVICES_KEY.EQUIPMENT,
title: i18n.t('screens.equipment.title'),
subtitle: i18n.t('screens.services.descriptions.equipment'),
image: EQUIPMENT_IMAGE,
onPress: () => this.onAmicaleServicePress("equipment-list"),
},
{
key: SERVICES_KEY.AMICALE_WEBSITE,
title: i18n.t('screens.websites.amicale'),
subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'),
image: AMICALE_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.AMICALE,
title: i18n.t('screens.websites.amicale')
}),
},
{
key: SERVICES_KEY.VOTE,
title: i18n.t('screens.vote.title'),
subtitle: i18n.t('screens.services.descriptions.vote'),
image: VOTE_IMAGE,
onPress: () => this.onAmicaleServicePress("vote"),
},
];
this.studentsDataset = [
{
key: SERVICES_KEY.PROXIMO,
title: i18n.t('screens.proximo.title'),
subtitle: i18n.t('screens.services.descriptions.proximo'),
image: PROXIMO_IMAGE,
onPress: () => nav.navigate("proximo"),
badgeFunction: (dashboard: fullDashboard) => dashboard.proximo_articles
},
{
key: SERVICES_KEY.WIKETUD,
title: "Wiketud",
subtitle: i18n.t('screens.services.descriptions.wiketud'),
image: WIKETUD_IMAGE,
onPress: () => nav.navigate("website", {host: AvailableWebsites.websites.WIKETUD, title: "Wiketud"}),
},
{
key: SERVICES_KEY.ELUS_ETUDIANTS,
title: "Élus Étudiants",
subtitle: i18n.t('screens.services.descriptions.elusEtudiants'),
image: EE_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.ELUS_ETUDIANTS,
title: "Élus Étudiants"
}),
},
{
key: SERVICES_KEY.TUTOR_INSA,
title: "Tutor'INSA",
subtitle: i18n.t('screens.services.descriptions.tutorInsa'),
image: TUTORINSA_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.TUTOR_INSA,
title: "Tutor'INSA"
}),
badgeFunction: (dashboard: fullDashboard) => dashboard.available_tutorials
},
];
this.insaDataset = [
{
key: SERVICES_KEY.RU,
title: i18n.t('screens.menu.title'),
subtitle: i18n.t('screens.services.descriptions.self'),
image: RU_IMAGE,
onPress: () => nav.navigate("self-menu"),
badgeFunction: (dashboard: fullDashboard) => dashboard.today_menu.length
},
{
key: SERVICES_KEY.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'),
subtitle: i18n.t('screens.services.descriptions.availableRooms'),
image: ROOM_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms')
}),
},
{
key: SERVICES_KEY.BIB,
title: i18n.t('screens.websites.bib'),
subtitle: i18n.t('screens.services.descriptions.bib'),
image: BIB_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.BIB,
title: i18n.t('screens.websites.bib')
}),
},
{
key: SERVICES_KEY.EMAIL,
title: i18n.t('screens.websites.mails'),
subtitle: i18n.t('screens.services.descriptions.mails'),
image: EMAIL_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.BLUEMIND,
title: i18n.t('screens.websites.mails')
}),
},
{
key: SERVICES_KEY.ENT,
title: i18n.t('screens.websites.ent'),
subtitle: i18n.t('screens.services.descriptions.ent'),
image: ENT_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.ENT,
title: i18n.t('screens.websites.ent')
}),
},
{
key: SERVICES_KEY.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title'),
subtitle: i18n.t('screens.services.descriptions.insaAccount'),
image: ACCOUNT_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title')
}),
},
];
this.specialDataset = [
{
key: SERVICES_KEY.WASHERS,
title: i18n.t('screens.proxiwash.washers'),
subtitle: i18n.t('screens.services.descriptions.washers'),
image: WASHER_IMAGE,
onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: fullDashboard) => dashboard.available_washers
},
{
key: SERVICES_KEY.DRYERS,
title: i18n.t('screens.proxiwash.dryers'),
subtitle: i18n.t('screens.services.descriptions.washers'),
image: DRYER_IMAGE,
onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: fullDashboard) => dashboard.available_dryers
}
];
this.categoriesDataset = [
{
key: SERVICES_CATEGORIES_KEY.AMICALE,
title: i18n.t("screens.services.categories.amicale"),
subtitle: i18n.t("screens.services.more"),
image: AMICALE_LOGO,
content: this.amicaleDataset
},
{
key: SERVICES_CATEGORIES_KEY.STUDENTS,
title: i18n.t("screens.services.categories.students"),
subtitle: i18n.t("screens.services.more"),
image: 'account-group',
content: this.studentsDataset
},
{
key: SERVICES_CATEGORIES_KEY.INSA,
title: i18n.t("screens.services.categories.insa"),
subtitle: i18n.t("screens.services.more"),
image: 'school',
content: this.insaDataset
},
{
key: SERVICES_CATEGORIES_KEY.SPECIAL,
title: i18n.t("screens.services.categories.special"),
subtitle: i18n.t("screens.services.categories.special"),
image: 'star',
content: this.specialDataset
},
];
}
/** categoriesDataset: Array<ServiceCategoryType>;
* Redirects the user to the login screen if he is not logged in
*
* @param route
* @returns {null}
*/
onAmicaleServicePress(route: string) {
if (ConnectionManager.getInstance().isLoggedIn())
this.navigation.navigate(route);
else
this.navigation.navigate("login", {nextScreen: route});
}
/** constructor(nav: StackNavigationProp) {
* Gets the given services list without items of the given ids this.navigation = nav;
* this.amicaleDataset = [
* @param idList The ids of items to remove {
* @param sourceList The item list to use as source key: SERVICES_KEY.CLUBS,
* @returns {[]} title: i18n.t('screens.clubs.title'),
*/ subtitle: i18n.t('screens.services.descriptions.clubs'),
getStrippedList(idList: Array<string>, sourceList: Array<{key: string, [key: string]: any}>) { image: CLUBS_IMAGE,
let newArray = []; onPress: (): void => this.onAmicaleServicePress('club-list'),
for (let i = 0; i < sourceList.length; i++) { },
const item = sourceList[i]; {
if (!(idList.includes(item.key))) key: SERVICES_KEY.PROFILE,
newArray.push(item); title: i18n.t('screens.profile.title'),
} subtitle: i18n.t('screens.services.descriptions.profile'),
return newArray; image: PROFILE_IMAGE,
} onPress: (): void => this.onAmicaleServicePress('profile'),
},
{
key: SERVICES_KEY.EQUIPMENT,
title: i18n.t('screens.equipment.title'),
subtitle: i18n.t('screens.services.descriptions.equipment'),
image: EQUIPMENT_IMAGE,
onPress: (): void => this.onAmicaleServicePress('equipment-list'),
},
{
key: SERVICES_KEY.AMICALE_WEBSITE,
title: i18n.t('screens.websites.amicale'),
subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'),
image: AMICALE_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.AMICALE,
title: i18n.t('screens.websites.amicale'),
}),
},
{
key: SERVICES_KEY.VOTE,
title: i18n.t('screens.vote.title'),
subtitle: i18n.t('screens.services.descriptions.vote'),
image: VOTE_IMAGE,
onPress: (): void => this.onAmicaleServicePress('vote'),
},
];
this.studentsDataset = [
{
key: SERVICES_KEY.PROXIMO,
title: i18n.t('screens.proximo.title'),
subtitle: i18n.t('screens.services.descriptions.proximo'),
image: PROXIMO_IMAGE,
onPress: (): void => nav.navigate('proximo'),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.proximo_articles,
},
{
key: SERVICES_KEY.WIKETUD,
title: 'Wiketud',
subtitle: i18n.t('screens.services.descriptions.wiketud'),
image: WIKETUD_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.WIKETUD,
title: 'Wiketud',
}),
},
{
key: SERVICES_KEY.ELUS_ETUDIANTS,
title: 'Élus Étudiants',
subtitle: i18n.t('screens.services.descriptions.elusEtudiants'),
image: EE_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.ELUS_ETUDIANTS,
title: 'Élus Étudiants',
}),
},
{
key: SERVICES_KEY.TUTOR_INSA,
title: "Tutor'INSA",
subtitle: i18n.t('screens.services.descriptions.tutorInsa'),
image: TUTORINSA_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.TUTOR_INSA,
title: "Tutor'INSA",
}),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.available_tutorials,
},
];
this.insaDataset = [
{
key: SERVICES_KEY.RU,
title: i18n.t('screens.menu.title'),
subtitle: i18n.t('screens.services.descriptions.self'),
image: RU_IMAGE,
onPress: (): void => nav.navigate('self-menu'),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.today_menu.length,
},
{
key: SERVICES_KEY.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'),
subtitle: i18n.t('screens.services.descriptions.availableRooms'),
image: ROOM_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'),
}),
},
{
key: SERVICES_KEY.BIB,
title: i18n.t('screens.websites.bib'),
subtitle: i18n.t('screens.services.descriptions.bib'),
image: BIB_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.BIB,
title: i18n.t('screens.websites.bib'),
}),
},
{
key: SERVICES_KEY.EMAIL,
title: i18n.t('screens.websites.mails'),
subtitle: i18n.t('screens.services.descriptions.mails'),
image: EMAIL_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.BLUEMIND,
title: i18n.t('screens.websites.mails'),
}),
},
{
key: SERVICES_KEY.ENT,
title: i18n.t('screens.websites.ent'),
subtitle: i18n.t('screens.services.descriptions.ent'),
image: ENT_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.ENT,
title: i18n.t('screens.websites.ent'),
}),
},
{
key: SERVICES_KEY.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title'),
subtitle: i18n.t('screens.services.descriptions.insaAccount'),
image: ACCOUNT_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title'),
}),
},
];
this.specialDataset = [
{
key: SERVICES_KEY.WASHERS,
title: i18n.t('screens.proxiwash.washers'),
subtitle: i18n.t('screens.services.descriptions.washers'),
image: WASHER_IMAGE,
onPress: (): void => nav.navigate('proxiwash'),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.available_washers,
},
{
key: SERVICES_KEY.DRYERS,
title: i18n.t('screens.proxiwash.dryers'),
subtitle: i18n.t('screens.services.descriptions.washers'),
image: DRYER_IMAGE,
onPress: (): void => nav.navigate('proxiwash'),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.available_dryers,
},
];
this.categoriesDataset = [
{
key: SERVICES_CATEGORIES_KEY.AMICALE,
title: i18n.t('screens.services.categories.amicale'),
subtitle: i18n.t('screens.services.more'),
image: AMICALE_LOGO,
content: this.amicaleDataset,
},
{
key: SERVICES_CATEGORIES_KEY.STUDENTS,
title: i18n.t('screens.services.categories.students'),
subtitle: i18n.t('screens.services.more'),
image: 'account-group',
content: this.studentsDataset,
},
{
key: SERVICES_CATEGORIES_KEY.INSA,
title: i18n.t('screens.services.categories.insa'),
subtitle: i18n.t('screens.services.more'),
image: 'school',
content: this.insaDataset,
},
{
key: SERVICES_CATEGORIES_KEY.SPECIAL,
title: i18n.t('screens.services.categories.special'),
subtitle: i18n.t('screens.services.categories.special'),
image: 'star',
content: this.specialDataset,
},
];
}
/** /**
* Gets the list of amicale's services * Redirects the user to the login screen if he is not logged in
* *
* @param excludedItems Ids of items to exclude from the returned list * @param route
* @returns {Array<ServiceItem>} * @returns {null}
*/ */
getAmicaleServices(excludedItems?: Array<string>) { onAmicaleServicePress(route: string) {
if (excludedItems != null) if (ConnectionManager.getInstance().isLoggedIn())
return this.getStrippedList(excludedItems, this.amicaleDataset) this.navigation.navigate(route);
else else this.navigation.navigate('login', {nextScreen: route});
return this.amicaleDataset; }
}
/** /**
* Gets the list of students' services * Gets the list of amicale's services
* *
* @param excludedItems Ids of items to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceItem>} * @returns {Array<ServiceItemType>}
*/ */
getStudentServices(excludedItems?: Array<string>) { getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null)
return this.getStrippedList(excludedItems, this.studentsDataset) return getStrippedServicesList(excludedItems, this.amicaleDataset);
else return this.amicaleDataset;
return this.studentsDataset; }
}
/** /**
* Gets the list of INSA's services * Gets the list of students' services
* *
* @param excludedItems Ids of items to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceItem>} * @returns {Array<ServiceItemType>}
*/ */
getINSAServices(excludedItems?: Array<string>) { getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null)
return this.getStrippedList(excludedItems, this.insaDataset) return getStrippedServicesList(excludedItems, this.studentsDataset);
else return this.studentsDataset;
return this.insaDataset; }
}
/** /**
* Gets the list of special services * Gets the list of INSA's services
* *
* @param excludedItems Ids of items to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceItem>} * @returns {Array<ServiceItemType>}
*/ */
getSpecialServices(excludedItems?: Array<string>) { getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null)
return this.getStrippedList(excludedItems, this.specialDataset) return getStrippedServicesList(excludedItems, this.insaDataset);
else return this.insaDataset;
return this.specialDataset; }
}
/** /**
* Gets all services sorted by category * Gets the list of special services
* *
* @param excludedItems Ids of categories to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceCategory>} * @returns {Array<ServiceItemType>}
*/ */
getCategories(excludedItems?: Array<string>) { getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null)
return this.getStrippedList(excludedItems, this.categoriesDataset) return getStrippedServicesList(excludedItems, this.specialDataset);
else return this.specialDataset;
return this.categoriesDataset; }
}
/**
* Gets all services sorted by category
*
* @param excludedItems Ids of categories to exclude from the returned list
* @returns {Array<ServiceCategoryType>}
*/
getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> {
if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.categoriesDataset);
return this.categoriesDataset;
}
} }

View file

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

View file

@ -1,204 +1,231 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
import i18n from 'i18n-js';
import {Platform} from 'react-native';
import SettingsScreen from '../screens/Other/Settings/SettingsScreen'; import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
import AboutScreen from '../screens/About/AboutScreen'; import AboutScreen from '../screens/About/AboutScreen';
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen'; import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
import DebugScreen from '../screens/About/DebugScreen'; import DebugScreen from '../screens/About/DebugScreen';
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack"; import TabNavigator from './TabNavigator';
import i18n from "i18n-js"; import GameMainScreen from '../screens/Game/screens/GameMainScreen';
import TabNavigator from "./TabNavigator"; import VoteScreen from '../screens/Amicale/VoteScreen';
import GameMainScreen from "../screens/Game/screens/GameMainScreen"; import LoginScreen from '../screens/Amicale/LoginScreen';
import VoteScreen from "../screens/Amicale/VoteScreen"; import SelfMenuScreen from '../screens/Services/SelfMenuScreen';
import LoginScreen from "../screens/Amicale/LoginScreen"; import ProximoMainScreen from '../screens/Services/Proximo/ProximoMainScreen';
import {Platform} from "react-native"; import ProximoListScreen from '../screens/Services/Proximo/ProximoListScreen';
import SelfMenuScreen from "../screens/Services/SelfMenuScreen"; import ProximoAboutScreen from '../screens/Services/Proximo/ProximoAboutScreen';
import ProximoMainScreen from "../screens/Services/Proximo/ProximoMainScreen"; import ProfileScreen from '../screens/Amicale/ProfileScreen';
import ProximoListScreen from "../screens/Services/Proximo/ProximoListScreen"; import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
import ProximoAboutScreen from "../screens/Services/Proximo/ProximoAboutScreen"; import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
import ProfileScreen from "../screens/Amicale/ProfileScreen"; import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
import ClubListScreen from "../screens/Amicale/Clubs/ClubListScreen"; import {
import ClubAboutScreen from "../screens/Amicale/Clubs/ClubAboutScreen"; createScreenCollapsibleStack,
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen"; getWebsiteStack,
import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils"; } from '../utils/CollapsibleUtils';
import BugReportScreen from "../screens/Other/FeedbackScreen"; import BugReportScreen from '../screens/Other/FeedbackScreen';
import WebsiteScreen from "../screens/Services/WebsiteScreen"; import WebsiteScreen from '../screens/Services/WebsiteScreen';
import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen"; import EquipmentScreen from '../screens/Amicale/Equipment/EquipmentListScreen';
import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen"; import EquipmentLendScreen from '../screens/Amicale/Equipment/EquipmentRentScreen';
import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen"; import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfirmScreen';
import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen"; import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
import GameStartScreen from "../screens/Game/screens/GameStartScreen"; import GameStartScreen from '../screens/Game/screens/GameStartScreen';
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS; const modalTransition =
Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS
: TransitionPresets.ModalTransition;
const defaultScreenOptions = { const defaultScreenOptions = {
gestureEnabled: true, gestureEnabled: true,
cardOverlayEnabled: true, cardOverlayEnabled: true,
...TransitionPresets.SlideFromRightIOS, ...TransitionPresets.SlideFromRightIOS,
}; };
const MainStack = createStackNavigator(); const MainStack = createStackNavigator();
function MainStackComponent(props: { createTabNavigator: () => React.Node }) { function MainStackComponent(props: {
return ( createTabNavigator: () => React.Node,
<MainStack.Navigator }): React.Node {
initialRouteName={'main'} const {createTabNavigator} = props;
headerMode={'screen'} return (
screenOptions={defaultScreenOptions} <MainStack.Navigator
> initialRouteName="main"
<MainStack.Screen headerMode="screen"
name="main" screenOptions={defaultScreenOptions}>
component={props.createTabNavigator} <MainStack.Screen
options={{ name="main"
headerShown: false, component={createTabNavigator}
title: i18n.t('screens.home.title'), options={{
}} headerShown: false,
/> title: i18n.t('screens.home.title'),
{createScreenCollapsibleStack( }}
"settings", />
MainStack, {createScreenCollapsibleStack(
SettingsScreen, 'settings',
i18n.t('screens.settings.title'))} MainStack,
{createScreenCollapsibleStack( SettingsScreen,
"dashboard-edit", i18n.t('screens.settings.title'),
MainStack, )}
DashboardEditScreen, {createScreenCollapsibleStack(
i18n.t('screens.settings.dashboardEdit.title'))} 'dashboard-edit',
{createScreenCollapsibleStack( MainStack,
"about", DashboardEditScreen,
MainStack, i18n.t('screens.settings.dashboardEdit.title'),
AboutScreen, )}
i18n.t('screens.about.title'))} {createScreenCollapsibleStack(
{createScreenCollapsibleStack( 'about',
"dependencies", MainStack,
MainStack, AboutScreen,
AboutDependenciesScreen, i18n.t('screens.about.title'),
i18n.t('screens.about.libs'))} )}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
"debug", 'dependencies',
MainStack, MainStack,
DebugScreen, AboutDependenciesScreen,
i18n.t('screens.about.debug'))} i18n.t('screens.about.libs'),
)}
{createScreenCollapsibleStack(
'debug',
MainStack,
DebugScreen,
i18n.t('screens.about.debug'),
)}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
"game-start", 'game-start',
MainStack, MainStack,
GameStartScreen, GameStartScreen,
i18n.t('screens.game.title'))} i18n.t('screens.game.title'),
<MainStack.Screen )}
name="game-main" <MainStack.Screen
component={GameMainScreen} name="game-main"
options={{ component={GameMainScreen}
title: i18n.t("screens.game.title"), options={{
}} title: i18n.t('screens.game.title'),
/> }}
{createScreenCollapsibleStack( />
"login", {createScreenCollapsibleStack(
MainStack, 'login',
LoginScreen, MainStack,
i18n.t('screens.login.title'), LoginScreen,
true, i18n.t('screens.login.title'),
{headerTintColor: "#fff"}, true,
'transparent')} {headerTintColor: '#fff'},
{getWebsiteStack("website", MainStack, WebsiteScreen, "")} 'transparent',
)}
{getWebsiteStack('website', MainStack, WebsiteScreen, '')}
{createScreenCollapsibleStack(
'self-menu',
MainStack,
SelfMenuScreen,
i18n.t('screens.menu.title'),
)}
{createScreenCollapsibleStack(
'proximo',
MainStack,
ProximoMainScreen,
i18n.t('screens.proximo.title'),
)}
{createScreenCollapsibleStack(
'proximo-list',
MainStack,
ProximoListScreen,
i18n.t('screens.proximo.articleList'),
)}
{createScreenCollapsibleStack(
'proximo-about',
MainStack,
ProximoAboutScreen,
i18n.t('screens.proximo.title'),
true,
{...modalTransition},
)}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
"self-menu", 'profile',
MainStack, MainStack,
SelfMenuScreen, ProfileScreen,
i18n.t('screens.menu.title'))} i18n.t('screens.profile.title'),
{createScreenCollapsibleStack( )}
"proximo", {createScreenCollapsibleStack(
MainStack, 'club-list',
ProximoMainScreen, MainStack,
i18n.t('screens.proximo.title'))} ClubListScreen,
{createScreenCollapsibleStack( i18n.t('screens.clubs.title'),
"proximo-list", )}
MainStack, {createScreenCollapsibleStack(
ProximoListScreen, 'club-information',
i18n.t('screens.proximo.articleList'), MainStack,
)} ClubDisplayScreen,
{createScreenCollapsibleStack( i18n.t('screens.clubs.details'),
"proximo-about", true,
MainStack, {...modalTransition},
ProximoAboutScreen, )}
i18n.t('screens.proximo.title'), {createScreenCollapsibleStack(
true, 'club-about',
{...modalTransition}, MainStack,
)} ClubAboutScreen,
i18n.t('screens.clubs.title'),
true,
{...modalTransition},
)}
{createScreenCollapsibleStack(
'equipment-list',
MainStack,
EquipmentScreen,
i18n.t('screens.equipment.title'),
)}
{createScreenCollapsibleStack(
'equipment-rent',
MainStack,
EquipmentLendScreen,
i18n.t('screens.equipment.book'),
)}
{createScreenCollapsibleStack(
'equipment-confirm',
MainStack,
EquipmentConfirmScreen,
i18n.t('screens.equipment.confirm'),
)}
{createScreenCollapsibleStack(
'vote',
MainStack,
VoteScreen,
i18n.t('screens.vote.title'),
)}
{createScreenCollapsibleStack(
'feedback',
MainStack,
BugReportScreen,
i18n.t('screens.feedback.title'),
)}
</MainStack.Navigator>
);
}
{createScreenCollapsibleStack( type PropsType = {
"profile", defaultHomeRoute: string | null,
MainStack, // eslint-disable-next-line flowtype/no-weak-types
ProfileScreen, defaultHomeData: {[key: string]: string},
i18n.t('screens.profile.title'))} };
{createScreenCollapsibleStack(
"club-list", export default class MainNavigator extends React.Component<PropsType> {
MainStack, createTabNavigator: () => React.Node;
ClubListScreen,
i18n.t('screens.clubs.title'))} constructor(props: PropsType) {
{createScreenCollapsibleStack( super(props);
"club-information", this.createTabNavigator = (): React.Node => (
MainStack, <TabNavigator
ClubDisplayScreen, defaultHomeRoute={props.defaultHomeRoute}
i18n.t('screens.clubs.details'), defaultHomeData={props.defaultHomeData}
true, />
{...modalTransition})}
{createScreenCollapsibleStack(
"club-about",
MainStack,
ClubAboutScreen,
i18n.t('screens.clubs.title'),
true,
{...modalTransition})}
{createScreenCollapsibleStack(
"equipment-list",
MainStack,
EquipmentScreen,
i18n.t('screens.equipment.title'))}
{createScreenCollapsibleStack(
"equipment-rent",
MainStack,
EquipmentLendScreen,
i18n.t('screens.equipment.book'))}
{createScreenCollapsibleStack(
"equipment-confirm",
MainStack,
EquipmentConfirmScreen,
i18n.t('screens.equipment.confirm'))}
{createScreenCollapsibleStack(
"vote",
MainStack,
VoteScreen,
i18n.t('screens.vote.title'))}
{createScreenCollapsibleStack(
"feedback",
MainStack,
BugReportScreen,
i18n.t('screens.feedback.title'))}
</MainStack.Navigator>
); );
} }
type Props = { render(): React.Node {
defaultHomeRoute: string | null, return <MainStackComponent createTabNavigator={this.createTabNavigator} />;
defaultHomeData: { [key: string]: any } }
}
export default class MainNavigator extends React.Component<Props> {
createTabNavigator: () => React.Node;
constructor(props: Props) {
super(props);
this.createTabNavigator = () => <TabNavigator {...props}/>
}
render() {
return (
<MainStackComponent createTabNavigator={this.createTabNavigator}/>
);
}
} }

View file

@ -1,271 +1,292 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack'; import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs"; import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import {Title, useTheme} from 'react-native-paper';
import {Platform} from 'react-native';
import i18n from 'i18n-js';
import {createCollapsibleStack} from 'react-navigation-collapsible';
import {View} from 'react-native-animatable';
import HomeScreen from '../screens/Home/HomeScreen'; import HomeScreen from '../screens/Home/HomeScreen';
import PlanningScreen from '../screens/Planning/PlanningScreen'; import PlanningScreen from '../screens/Planning/PlanningScreen';
import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen'; import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen'; import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen'; import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
import PlanexScreen from '../screens/Planex/PlanexScreen'; import PlanexScreen from '../screens/Planex/PlanexScreen';
import AsyncStorageManager from "../managers/AsyncStorageManager"; import AsyncStorageManager from '../managers/AsyncStorageManager';
import {Title, useTheme} from 'react-native-paper'; import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
import {Platform} from 'react-native'; import ScannerScreen from '../screens/Home/ScannerScreen';
import i18n from "i18n-js"; import FeedItemScreen from '../screens/Home/FeedItemScreen';
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen"; import GroupSelectionScreen from '../screens/Planex/GroupSelectionScreen';
import ScannerScreen from "../screens/Home/ScannerScreen"; import CustomTabBar from '../components/Tabbar/CustomTabBar';
import FeedItemScreen from "../screens/Home/FeedItemScreen"; import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
import {createCollapsibleStack} from "react-navigation-collapsible"; import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
import GroupSelectionScreen from "../screens/Planex/GroupSelectionScreen"; import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
import CustomTabBar from "../components/Tabbar/CustomTabBar"; import {
import WebsitesHomeScreen from "../screens/Services/ServicesScreen"; createScreenCollapsibleStack,
import ServicesSectionScreen from "../screens/Services/ServicesSectionScreen"; getWebsiteStack,
import AmicaleContactScreen from "../screens/Amicale/AmicaleContactScreen"; } from '../utils/CollapsibleUtils';
import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils"; import Mascot, {MASCOT_STYLE} from '../components/Mascot/Mascot';
import {View} from "react-native-animatable";
import Mascot, {MASCOT_STYLE} from "../components/Mascot/Mascot";
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
const modalTransition =
Platform.OS === 'ios'
? TransitionPresets.ModalPresentationIOS
: TransitionPresets.ModalTransition;
const defaultScreenOptions = { const defaultScreenOptions = {
gestureEnabled: true, gestureEnabled: true,
cardOverlayEnabled: true, cardOverlayEnabled: true,
...modalTransition, ...modalTransition,
}; };
const ServicesStack = createStackNavigator(); const ServicesStack = createStackNavigator();
function ServicesStackComponent() { function ServicesStackComponent(): React.Node {
return ( return (
<ServicesStack.Navigator <ServicesStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode={"screen"} headerMode="screen"
screenOptions={defaultScreenOptions} screenOptions={defaultScreenOptions}>
> {createScreenCollapsibleStack(
{createScreenCollapsibleStack( 'index',
"index", ServicesStack,
ServicesStack, WebsitesHomeScreen,
WebsitesHomeScreen, i18n.t('screens.services.title'),
i18n.t('screens.services.title'))} )}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
"services-section", 'services-section',
ServicesStack, ServicesStack,
ServicesSectionScreen, ServicesSectionScreen,
"SECTION")} 'SECTION',
{createScreenCollapsibleStack( )}
"amicale-contact", {createScreenCollapsibleStack(
ServicesStack, 'amicale-contact',
AmicaleContactScreen, ServicesStack,
i18n.t('screens.amicaleAbout.title'))} AmicaleContactScreen,
</ServicesStack.Navigator> i18n.t('screens.amicaleAbout.title'),
); )}
</ServicesStack.Navigator>
);
} }
const ProxiwashStack = createStackNavigator(); const ProxiwashStack = createStackNavigator();
function ProxiwashStackComponent() { function ProxiwashStackComponent(): React.Node {
return ( return (
<ProxiwashStack.Navigator <ProxiwashStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode={"screen"} headerMode="screen"
screenOptions={defaultScreenOptions} screenOptions={defaultScreenOptions}>
> {createScreenCollapsibleStack(
{createScreenCollapsibleStack( 'index',
"index", ProxiwashStack,
ProxiwashStack, ProxiwashScreen,
ProxiwashScreen, i18n.t('screens.proxiwash.title'),
i18n.t('screens.proxiwash.title'))} )}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
"proxiwash-about", 'proxiwash-about',
ProxiwashStack, ProxiwashStack,
ProxiwashAboutScreen, ProxiwashAboutScreen,
i18n.t('screens.proxiwash.title'))} i18n.t('screens.proxiwash.title'),
</ProxiwashStack.Navigator> )}
); </ProxiwashStack.Navigator>
);
} }
const PlanningStack = createStackNavigator(); const PlanningStack = createStackNavigator();
function PlanningStackComponent() { function PlanningStackComponent(): React.Node {
return ( return (
<PlanningStack.Navigator <PlanningStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode={"screen"} headerMode="screen"
screenOptions={defaultScreenOptions} screenOptions={defaultScreenOptions}>
> <PlanningStack.Screen
<PlanningStack.Screen name="index"
name="index" component={PlanningScreen}
component={PlanningScreen} options={{title: i18n.t('screens.planning.title')}}
options={{title: i18n.t('screens.planning.title'),}} />
/> {createScreenCollapsibleStack(
{createScreenCollapsibleStack( 'planning-information',
"planning-information", PlanningStack,
PlanningStack, PlanningDisplayScreen,
PlanningDisplayScreen, i18n.t('screens.planning.eventDetails'),
i18n.t('screens.planning.eventDetails'))} )}
</PlanningStack.Navigator> </PlanningStack.Navigator>
); );
} }
const HomeStack = createStackNavigator(); const HomeStack = createStackNavigator();
function HomeStackComponent(initialRoute: string | null, defaultData: { [key: string]: any }) { function HomeStackComponent(
let params = undefined; initialRoute: string | null,
if (initialRoute != null) defaultData: {[key: string]: string},
params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true}; ): React.Node {
const {colors} = useTheme(); let params;
return ( if (initialRoute != null)
<HomeStack.Navigator params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true};
initialRouteName={"index"} const {colors} = useTheme();
headerMode={"screen"} return (
screenOptions={defaultScreenOptions} <HomeStack.Navigator
> initialRouteName="index"
{createCollapsibleStack( headerMode="screen"
<HomeStack.Screen screenOptions={defaultScreenOptions}>
name="index" {createCollapsibleStack(
component={HomeScreen} <HomeStack.Screen
options={{ name="index"
title: i18n.t('screens.home.title'), component={HomeScreen}
headerStyle: { options={{
backgroundColor: colors.surface, title: i18n.t('screens.home.title'),
}, headerStyle: {
headerTitle: () => backgroundColor: colors.surface,
<View style={{flexDirection: "row"}}> },
<Mascot headerTitle: (): React.Node => (
style={{ <View style={{flexDirection: 'row'}}>
width: 50 <Mascot
}} style={{
emotion={MASCOT_STYLE.RANDOM} width: 50,
animated={true} }}
entryAnimation={{ emotion={MASCOT_STYLE.RANDOM}
animation: "bounceIn", animated
duration: 1000 entryAnimation={{
}} animation: 'bounceIn',
loopAnimation={{ duration: 1000,
animation: "pulse", }}
duration: 2000, loopAnimation={{
iterationCount: "infinite" animation: 'pulse',
}} duration: 2000,
/> iterationCount: 'infinite',
<Title style={{ }}
marginLeft: 10, />
marginTop: "auto", <Title
marginBottom: "auto", style={{
}}>{i18n.t('screens.home.title')}</Title> marginLeft: 10,
</View> marginTop: 'auto',
}} marginBottom: 'auto',
initialParams={params} }}>
/>, {i18n.t('screens.home.title')}
{ </Title>
collapsedColor: colors.surface, </View>
useNativeDriver: true, ),
} }}
)} initialParams={params}
<HomeStack.Screen />,
name="scanner" {
component={ScannerScreen} collapsedColor: colors.surface,
options={{title: i18n.t('screens.scanner.title'),}} useNativeDriver: true,
/> },
)}
<HomeStack.Screen
name="scanner"
component={ScannerScreen}
options={{title: i18n.t('screens.scanner.title')}}
/>
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
"club-information", 'club-information',
HomeStack, HomeStack,
ClubDisplayScreen, ClubDisplayScreen,
i18n.t('screens.clubs.details'))} i18n.t('screens.clubs.details'),
{createScreenCollapsibleStack( )}
"feed-information", {createScreenCollapsibleStack(
HomeStack, 'feed-information',
FeedItemScreen, HomeStack,
i18n.t('screens.home.feed'))} FeedItemScreen,
{createScreenCollapsibleStack( i18n.t('screens.home.feed'),
"planning-information", )}
HomeStack, {createScreenCollapsibleStack(
PlanningDisplayScreen, 'planning-information',
i18n.t('screens.planning.eventDetails'))} HomeStack,
</HomeStack.Navigator> PlanningDisplayScreen,
); i18n.t('screens.planning.eventDetails'),
)}
</HomeStack.Navigator>
);
} }
const PlanexStack = createStackNavigator(); const PlanexStack = createStackNavigator();
function PlanexStackComponent() { function PlanexStackComponent(): React.Node {
return ( return (
<PlanexStack.Navigator <PlanexStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode={"screen"} headerMode="screen"
screenOptions={defaultScreenOptions} screenOptions={defaultScreenOptions}>
> {getWebsiteStack(
{getWebsiteStack( 'index',
"index", PlanexStack,
PlanexStack, PlanexScreen,
PlanexScreen, i18n.t('screens.planex.title'),
i18n.t("screens.planex.title"))} )}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
"group-select", 'group-select',
PlanexStack, PlanexStack,
GroupSelectionScreen, GroupSelectionScreen,
"")} '',
</PlanexStack.Navigator> )}
); </PlanexStack.Navigator>
);
} }
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
type Props = { type PropsType = {
defaultHomeRoute: string | null, defaultHomeRoute: string | null,
defaultHomeData: { [key: string]: any } defaultHomeData: {[key: string]: string},
} };
export default class TabNavigator extends React.Component<Props> { export default class TabNavigator extends React.Component<PropsType> {
createHomeStackComponent: () => React.Node;
createHomeStackComponent: () => HomeStackComponent;
defaultRoute: string; defaultRoute: string;
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
if (props.defaultHomeRoute != null) if (props.defaultHomeRoute != null) this.defaultRoute = 'home';
this.defaultRoute = 'home'; else
else this.defaultRoute = AsyncStorageManager.getString(
this.defaultRoute = AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key).toLowerCase(); AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
this.createHomeStackComponent = () => HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData); ).toLowerCase();
} this.createHomeStackComponent = (): React.Node =>
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
render() { }
return (
<Tab.Navigator render(): React.Node {
initialRouteName={this.defaultRoute} return (
tabBar={props => <CustomTabBar {...props} />} <Tab.Navigator
> initialRouteName={this.defaultRoute}
<Tab.Screen // eslint-disable-next-line react/jsx-props-no-spreading
name="services" tabBar={(props: {...}): React.Node => <CustomTabBar {...props} />}>
option <Tab.Screen
component={ServicesStackComponent} name="services"
options={{title: i18n.t('screens.services.title')}} option
/> component={ServicesStackComponent}
<Tab.Screen options={{title: i18n.t('screens.services.title')}}
name="proxiwash" />
component={ProxiwashStackComponent} <Tab.Screen
options={{title: i18n.t('screens.proxiwash.title')}} name="proxiwash"
/> component={ProxiwashStackComponent}
<Tab.Screen options={{title: i18n.t('screens.proxiwash.title')}}
name="home" />
component={this.createHomeStackComponent} <Tab.Screen
options={{title: i18n.t('screens.home.title')}} name="home"
/> component={this.createHomeStackComponent}
<Tab.Screen options={{title: i18n.t('screens.home.title')}}
name="planning" />
component={PlanningStackComponent} <Tab.Screen
options={{title: i18n.t('screens.planning.title')}} name="planning"
/> component={PlanningStackComponent}
options={{title: i18n.t('screens.planning.title')}}
<Tab.Screen />
name="planex"
component={PlanexStackComponent} <Tab.Screen
options={{title: i18n.t("screens.planex.title")}} name="planex"
/> component={PlanexStackComponent}
</Tab.Navigator> options={{title: i18n.t('screens.planex.title')}}
); />
} </Tab.Navigator>
);
}
} }

View file

@ -1,36 +1,31 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import packageJson from '../../../package';
import {List} from 'react-native-paper'; import {List} from 'react-native-paper';
import {StackNavigationProp} from "@react-navigation/stack"; import {View} from 'react-native-animatable';
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList"; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import {View} from "react-native-animatable"; import packageJson from '../../../package.json';
type listItem = { type ListItemType = {
name: string, name: string,
version: string version: string,
}; };
/** /**
* Generates the dependencies list from the raw json * Generates the dependencies list from the raw json
* *
* @param object The raw json * @param object The raw json
* @return {Array<listItem>} * @return {Array<ListItemType>}
*/ */
function generateListFromObject(object: { [key: string]: string }): Array<listItem> { function generateListFromObject(object: {
let list = []; [key: string]: string,
let keys = Object.keys(object); }): Array<ListItemType> {
let values = Object.values(object); const list = [];
for (let i = 0; i < keys.length; i++) { const keys = Object.keys(object);
list.push({name: keys[i], version: values[i]}); keys.forEach((key: string) => {
} list.push({name: key, version: object[key]});
//$FlowFixMe });
return list; return list;
}
type Props = {
navigation: StackNavigationProp,
} }
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -38,38 +33,45 @@ const LIST_ITEM_HEIGHT = 64;
/** /**
* Class defining a screen showing the list of libraries used by the app, taken from package.json * Class defining a screen showing the list of libraries used by the app, taken from package.json
*/ */
export default class AboutDependenciesScreen extends React.Component<Props> { export default class AboutDependenciesScreen extends React.Component<null> {
data: Array<ListItemType>;
data: Array<listItem>; constructor() {
super();
this.data = generateListFromObject(packageJson.dependencies);
}
constructor() { keyExtractor = (item: ListItemType): string => item.name;
super();
this.data = generateListFromObject(packageJson.dependencies);
}
keyExtractor = (item: listItem) => item.name; getRenderItem = ({item}: {item: ListItemType}): React.Node => (
<List.Item
title={item.name}
description={item.version.replace('^', '').replace('~', '')}
style={{height: LIST_ITEM_HEIGHT}}
/>
);
renderItem = ({item}: { item: listItem }) => getItemLayout = (
<List.Item data: ListItemType,
title={item.name} index: number,
description={item.version.replace('^', '').replace('~', '')} ): {length: number, offset: number, index: number} => ({
style={{height: LIST_ITEM_HEIGHT}} length: LIST_ITEM_HEIGHT,
/>; offset: LIST_ITEM_HEIGHT * index,
index,
});
itemLayout = (data: any, index: number) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); render(): React.Node {
return (
render() { <View>
return ( <CollapsibleFlatList
<View> data={this.data}
<CollapsibleFlatList keyExtractor={this.keyExtractor}
data={this.data} renderItem={this.getRenderItem}
keyExtractor={this.keyExtractor} // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
renderItem={this.renderItem} removeClippedSubviews
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration getItemLayout={this.getItemLayout}
removeClippedSubviews={true} />
getItemLayout={this.itemLayout} </View>
/> );
</View> }
);
}
} }

View file

@ -1,351 +1,388 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {FlatList, Linking, Platform, View} from 'react-native'; import {FlatList, Linking, Platform} from 'react-native';
import i18n from "i18n-js"; import i18n from 'i18n-js';
import {Avatar, Card, List, Title, withTheme} from 'react-native-paper'; import {Avatar, Card, List, Title, withTheme} from 'react-native-paper';
import packageJson from "../../../package.json"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import packageJson from '../../../package.json';
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList"; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import APP_LOGO from '../../../assets/android.icon.png';
type ListItem = { type ListItemType = {
onPressCallback: () => void, onPressCallback: () => void,
icon: string, icon: string,
text: string, text: string,
showChevron: boolean showChevron: boolean,
}; };
const links = { const links = {
appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148', appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148',
playstore: 'https://play.google.com/store/apps/details?id=fr.amicaleinsat.application', playstore:
git: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/README.md', 'https://play.google.com/store/apps/details?id=fr.amicaleinsat.application',
changelog: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/Changelog.md', git:
license: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/LICENSE', 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/README.md',
authorMail: "mailto:vergnet@etud.insa-toulouse.fr?" + changelog:
"subject=" + 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/Changelog.md',
"Application Amicale INSA Toulouse" + license:
"&body=" + 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/LICENSE',
"Coucou !\n\n", authorMail:
authorLinkedin: 'https://www.linkedin.com/in/arnaud-vergnet-434ba5179/', 'mailto:vergnet@etud.insa-toulouse.fr?' +
yohanMail: "mailto:ysimard@etud.insa-toulouse.fr?" + 'subject=' +
"subject=" + 'Application Amicale INSA Toulouse' +
"Application Amicale INSA Toulouse" + '&body=' +
"&body=" + 'Coucou !\n\n',
"Coucou !\n\n", authorLinkedin: 'https://www.linkedin.com/in/arnaud-vergnet-434ba5179/',
yohanLinkedin: 'https://www.linkedin.com/in/yohan-simard', yohanMail:
react: 'https://facebook.github.io/react-native/', 'mailto:ysimard@etud.insa-toulouse.fr?' +
meme: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" 'subject=' +
'Application Amicale INSA Toulouse' +
'&body=' +
'Coucou !\n\n',
yohanLinkedin: 'https://www.linkedin.com/in/yohan-simard',
react: 'https://facebook.github.io/react-native/',
meme: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
}; };
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
}; };
/** /**
* Opens a link in the device's browser * Opens a link in the device's browser
* @param link The link to open * @param link The link to open
*/ */
function openWebLink(link) { function openWebLink(link: string) {
Linking.openURL(link).catch((err) => console.error('Error opening link', err)); Linking.openURL(link);
} }
/** /**
* Class defining an about screen. This screen shows the user information about the app and it's author. * Class defining an about screen. This screen shows the user information about the app and it's author.
*/ */
class AboutScreen extends React.Component<Props> { class AboutScreen extends React.Component<PropsType> {
/**
* Data to be displayed in the app card
*/
appData = [
{
onPressCallback: () => {
openWebLink(Platform.OS === 'ios' ? links.appstore : links.playstore);
},
icon: Platform.OS === 'ios' ? 'apple' : 'google-play',
text:
Platform.OS === 'ios'
? i18n.t('screens.about.appstore')
: i18n.t('screens.about.playstore'),
showChevron: true,
},
{
onPressCallback: () => {
const {navigation} = this.props;
navigation.navigate('feedback');
},
icon: 'bug',
text: i18n.t('screens.feedback.homeButtonTitle'),
showChevron: true,
},
{
onPressCallback: () => {
openWebLink(links.git);
},
icon: 'git',
text: 'Git',
showChevron: true,
},
{
onPressCallback: () => {
openWebLink(links.changelog);
},
icon: 'refresh',
text: i18n.t('screens.about.changelog'),
showChevron: true,
},
{
onPressCallback: () => {
openWebLink(links.license);
},
icon: 'file-document',
text: i18n.t('screens.about.license'),
showChevron: true,
},
];
/** /**
* Data to be displayed in the app card * Data to be displayed in the author card
*/ */
appData = [ authorData = [
{ {
onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore), onPressCallback: () => {
icon: Platform.OS === "ios" ? 'apple' : 'google-play', openWebLink(links.meme);
text: Platform.OS === "ios" ? i18n.t('screens.about.appstore') : i18n.t('screens.about.playstore'), },
showChevron: true icon: 'account-circle',
}, text: 'Arnaud VERGNET',
{ showChevron: false,
onPressCallback: () => this.props.navigation.navigate("feedback"), },
icon: 'bug', {
text: i18n.t("screens.feedback.homeButtonTitle"), onPressCallback: () => {
showChevron: true openWebLink(links.authorMail);
}, },
{ icon: 'email',
onPressCallback: () => openWebLink(links.git), text: i18n.t('screens.about.authorMail'),
icon: 'git', showChevron: true,
text: 'Git', },
showChevron: true {
}, onPressCallback: () => {
{ openWebLink(links.authorLinkedin);
onPressCallback: () => openWebLink(links.changelog), },
icon: 'refresh', icon: 'linkedin',
text: i18n.t('screens.about.changelog'), text: 'Linkedin',
showChevron: true showChevron: true,
}, },
{ ];
onPressCallback: () => openWebLink(links.license),
icon: 'file-document',
text: i18n.t('screens.about.license'),
showChevron: true
},
];
/**
* Data to be displayed in the author card
*/
authorData = [
{
onPressCallback: () => openWebLink(links.meme),
icon: 'account-circle',
text: 'Arnaud VERGNET',
showChevron: false
},
{
onPressCallback: () => openWebLink(links.authorMail),
icon: 'email',
text: i18n.t('screens.about.authorMail'),
showChevron: true
},
{
onPressCallback: () => openWebLink(links.authorLinkedin),
icon: 'linkedin',
text: 'Linkedin',
showChevron: true
},
];
/**
* Data to be displayed in the additional developer card
*/
additionalDevData = [
{
onPressCallback: () => console.log('Meme this'),
icon: 'account',
text: 'Yohan SIMARD',
showChevron: false
},
{
onPressCallback: () => openWebLink(links.yohanMail),
icon: 'email',
text: i18n.t('screens.about.authorMail'),
showChevron: true
},
{
onPressCallback: () => openWebLink(links.yohanLinkedin),
icon: 'linkedin',
text: 'Linkedin',
showChevron: true
},
];
/**
* Data to be displayed in the technologies card
*/
technoData = [
{
onPressCallback: () => openWebLink(links.react),
icon: 'react',
text: i18n.t('screens.about.reactNative'),
showChevron: true
},
{
onPressCallback: () => this.props.navigation.navigate('dependencies'),
icon: 'developer-board',
text: i18n.t('screens.about.libs'),
showChevron: true
},
];
/**
* Order of information cards
*/
dataOrder = [
{
id: 'app',
},
{
id: 'team',
},
{
id: 'techno',
},
];
/** /**
* Gets the app icon * Data to be displayed in the additional developer card
* */
* @param props additionalDevData = [
* @return {*} {
*/ onPressCallback: () => {},
getAppIcon(props) { icon: 'account',
return ( text: 'Yohan SIMARD',
showChevron: false,
},
{
onPressCallback: () => {
openWebLink(links.yohanMail);
},
icon: 'email',
text: i18n.t('screens.about.authorMail'),
showChevron: true,
},
{
onPressCallback: () => {
openWebLink(links.yohanLinkedin);
},
icon: 'linkedin',
text: 'Linkedin',
showChevron: true,
},
];
/**
* Data to be displayed in the technologies card
*/
technoData = [
{
onPressCallback: () => {
openWebLink(links.react);
},
icon: 'react',
text: i18n.t('screens.about.reactNative'),
showChevron: true,
},
{
onPressCallback: () => {
const {navigation} = this.props;
navigation.navigate('dependencies');
},
icon: 'developer-board',
text: i18n.t('screens.about.libs'),
showChevron: true,
},
];
/**
* Order of information cards
*/
dataOrder = [
{
id: 'app',
},
{
id: 'team',
},
{
id: 'techno',
},
];
/**
* Gets the app card showing information and links about the app.
*
* @return {*}
*/
getAppCard(): React.Node {
return (
<Card style={{marginBottom: 10}}>
<Card.Title
title="Campus"
subtitle={packageJson.version}
left={({size}: {size: number}): React.Node => (
<Avatar.Image <Avatar.Image
{...props} size={size}
source={require('../../../assets/android.icon.png')} source={APP_LOGO}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
/> />
); )}
/>
<Card.Content>
<FlatList
data={this.appData}
keyExtractor={this.keyExtractor}
renderItem={this.getCardItem}
/>
</Card.Content>
</Card>
);
}
/**
* Gets the team card showing information and links about the team
*
* @return {*}
*/
getTeamCard(): React.Node {
return (
<Card style={{marginBottom: 10}}>
<Card.Title
title={i18n.t('screens.about.team')}
left={({size, color}: {size: number, color: string}): React.Node => (
<Avatar.Icon size={size} color={color} icon="account-multiple" />
)}
/>
<Card.Content>
<Title>{i18n.t('screens.about.author')}</Title>
<FlatList
data={this.authorData}
keyExtractor={this.keyExtractor}
listKey="1"
renderItem={this.getCardItem}
/>
<Title>{i18n.t('screens.about.additionalDev')}</Title>
<FlatList
data={this.additionalDevData}
keyExtractor={this.keyExtractor}
listKey="2"
renderItem={this.getCardItem}
/>
</Card.Content>
</Card>
);
}
/**
* Gets the techno card showing information and links about the technologies used in the app
*
* @return {*}
*/
getTechnoCard(): React.Node {
return (
<Card style={{marginBottom: 10}}>
<Card.Content>
<Title>{i18n.t('screens.about.technologies')}</Title>
<FlatList
data={this.technoData}
keyExtractor={this.keyExtractor}
renderItem={this.getCardItem}
/>
</Card.Content>
</Card>
);
}
/**
* Gets a chevron icon
*
* @param props
* @return {*}
*/
static getChevronIcon({
size,
color,
}: {
size: number,
color: string,
}): React.Node {
return <List.Icon size={size} color={color} icon="chevron-right" />;
}
/**
* Gets a custom list item icon
*
* @param item The item to show the icon for
* @param props
* @return {*}
*/
static getItemIcon(
item: ListItemType,
{size, color}: {size: number, color: string},
): React.Node {
return <List.Icon size={size} color={color} icon={item.icon} />;
}
/**
* Gets a clickable card item to be rendered inside a card.
*
* @returns {*}
*/
getCardItem = ({item}: {item: ListItemType}): React.Node => {
const getItemIcon = (props: {size: number, color: string}): React.Node =>
AboutScreen.getItemIcon(item, props);
if (item.showChevron) {
return (
<List.Item
title={item.text}
left={getItemIcon}
right={AboutScreen.getChevronIcon}
onPress={item.onPressCallback}
/>
);
} }
return (
<List.Item
title={item.text}
left={getItemIcon}
onPress={item.onPressCallback}
/>
);
};
/** /**
* Extracts a key from the given item * Gets a card, depending on the given item's id
* *
* @param item The item to extract the key from * @param item The item to show
* @return {string} The extracted key * @return {*}
*/ */
keyExtractor(item: ListItem): string { getMainCard = ({item}: {item: {id: string}}): React.Node => {
return item.icon; switch (item.id) {
case 'app':
return this.getAppCard();
case 'team':
return this.getTeamCard();
case 'techno':
return this.getTechnoCard();
default:
return null;
} }
};
/** /**
* Gets the app card showing information and links about the app. * Extracts a key from the given item
* *
* @return {*} * @param item The item to extract the key from
*/ * @return {string} The extracted key
getAppCard() { */
return ( keyExtractor = (item: ListItemType): string => item.icon;
<Card style={{marginBottom: 10}}>
<Card.Title
title={"Campus"}
subtitle={packageJson.version}
left={this.getAppIcon}/>
<Card.Content>
<FlatList
data={this.appData}
keyExtractor={this.keyExtractor}
renderItem={this.getCardItem}
/>
</Card.Content>
</Card>
);
}
/** render(): React.Node {
* Gets the team card showing information and links about the team return (
* <CollapsibleFlatList
* @return {*} style={{padding: 5}}
*/ data={this.dataOrder}
getTeamCard() { renderItem={this.getMainCard}
return ( />
<Card style={{marginBottom: 10}}> );
<Card.Title }
title={i18n.t('screens.about.team')}
left={(props) => <Avatar.Icon {...props} icon={'account-multiple'}/>}/>
<Card.Content>
<Title>{i18n.t('screens.about.author')}</Title>
<FlatList
data={this.authorData}
keyExtractor={this.keyExtractor}
listKey={"1"}
renderItem={this.getCardItem}
/>
<Title>{i18n.t('screens.about.additionalDev')}</Title>
<FlatList
data={this.additionalDevData}
keyExtractor={this.keyExtractor}
listKey={"2"}
renderItem={this.getCardItem}
/>
</Card.Content>
</Card>
);
}
/**
* Gets the techno card showing information and links about the technologies used in the app
*
* @return {*}
*/
getTechnoCard() {
return (
<Card style={{marginBottom: 10}}>
<Card.Content>
<Title>{i18n.t('screens.about.technologies')}</Title>
<FlatList
data={this.technoData}
keyExtractor={this.keyExtractor}
renderItem={this.getCardItem}
/>
</Card.Content>
</Card>
);
}
/**
* Gets a chevron icon
*
* @param props
* @return {*}
*/
getChevronIcon(props) {
return (
<List.Icon {...props} icon={'chevron-right'}/>
);
}
/**
* Gets a custom list item icon
*
* @param item The item to show the icon for
* @param props
* @return {*}
*/
getItemIcon(item: ListItem, props) {
return (
<List.Icon {...props} icon={item.icon}/>
);
}
/**
* Gets a clickable card item to be rendered inside a card.
*
* @returns {*}
*/
getCardItem = ({item}: { item: ListItem }) => {
const getItemIcon = this.getItemIcon.bind(this, item);
if (item.showChevron) {
return (
<List.Item
title={item.text}
left={getItemIcon}
right={this.getChevronIcon}
onPress={item.onPressCallback}
/>
);
} else {
return (
<List.Item
title={item.text}
left={getItemIcon}
onPress={item.onPressCallback}
/>
);
}
};
/**
* Gets a card, depending on the given item's id
*
* @param item The item to show
* @return {*}
*/
getMainCard = ({item}: { item: { id: string } }) => {
switch (item.id) {
case 'app':
return this.getAppCard();
case 'team':
return this.getTeamCard();
case 'techno':
return this.getTechnoCard();
}
return <View/>;
};
render() {
return (
<CollapsibleFlatList
style={{padding: 5}}
data={this.dataOrder}
renderItem={this.getMainCard}
/>
);
}
} }
export default withTheme(AboutScreen); export default withTheme(AboutScreen);

View file

@ -1,183 +1,211 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from "react-native"; import {View} from 'react-native';
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import {
import CustomModal from "../../components/Overrides/CustomModal"; Button,
import {Button, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper'; List,
import {StackNavigationProp} from "@react-navigation/stack"; Subheading,
import {Modalize} from "react-native-modalize"; TextInput,
import type {CustomTheme} from "../../managers/ThemeManager"; Title,
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList"; withTheme,
} from 'react-native-paper';
import {Modalize} from 'react-native-modalize';
import CustomModal from '../../components/Overrides/CustomModal';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import type {CustomThemeType} from '../../managers/ThemeManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
type PreferenceItem = { type PreferenceItemType = {
key: string, key: string,
default: string, default: string,
current: string, current: string,
}
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme
}; };
type State = { type PropsType = {
modalCurrentDisplayItem: PreferenceItem, theme: CustomThemeType,
currentPreferences: Array<PreferenceItem>, };
}
type StateType = {
modalCurrentDisplayItem: PreferenceItemType,
currentPreferences: Array<PreferenceItemType>,
};
/** /**
* Class defining the Debug screen. * Class defining the Debug screen.
* This screen allows the user to get and modify information on the app/device. * This screen allows the user to get and modify information on the app/device.
*/ */
class DebugScreen extends React.Component<Props, State> { class DebugScreen extends React.Component<PropsType, StateType> {
modalRef: Modalize;
modalRef: Modalize; modalInputValue: string;
modalInputValue: string;
/** /**
* Copies user preferences to state for easier manipulation * Copies user preferences to state for easier manipulation
* *
* @param props * @param props
*/ */
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
this.modalInputValue = ""; this.modalInputValue = '';
let currentPreferences : Array<PreferenceItem> = []; const currentPreferences: Array<PreferenceItemType> = [];
Object.values(AsyncStorageManager.PREFERENCES).map((object: any) => { // eslint-disable-next-line flowtype/no-weak-types
let newObject: PreferenceItem = {...object}; Object.values(AsyncStorageManager.PREFERENCES).forEach((object: any) => {
newObject.current = AsyncStorageManager.getString(newObject.key); const newObject: PreferenceItemType = {...object};
currentPreferences.push(newObject); newObject.current = AsyncStorageManager.getString(newObject.key);
}); currentPreferences.push(newObject);
this.state = { });
modalCurrentDisplayItem: {}, this.state = {
currentPreferences: currentPreferences modalCurrentDisplayItem: {},
}; currentPreferences,
}
/**
* Shows the edit modal
*
* @param item
*/
showEditModal(item: PreferenceItem) {
this.setState({
modalCurrentDisplayItem: item
});
if (this.modalRef) {
this.modalRef.open();
}
}
/**
* Gets the edit modal content
*
* @return {*}
*/
getModalContent() {
return (
<View style={{
flex: 1,
padding: 20
}}>
<Title>{this.state.modalCurrentDisplayItem.key}</Title>
<Subheading>Default: {this.state.modalCurrentDisplayItem.default}</Subheading>
<Subheading>Current: {this.state.modalCurrentDisplayItem.current}</Subheading>
<TextInput
label='New Value'
onChangeText={(text) => this.modalInputValue = text}
/>
<View style={{
flexDirection: 'row',
marginTop: 10,
}}>
<Button
mode="contained"
dark={true}
color={this.props.theme.colors.success}
onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.modalInputValue)}>
Save new value
</Button>
<Button
mode="contained"
dark={true}
color={this.props.theme.colors.danger}
onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.state.modalCurrentDisplayItem.default)}>
Reset to default
</Button>
</View>
</View>
);
}
/**
* Finds the index of the given key in the preferences array
*
* @param key THe key to find the index of
* @returns {number}
*/
findIndexOfKey(key: string) {
let index = -1;
for (let i = 0; i < this.state.currentPreferences.length; i++) {
if (this.state.currentPreferences[i].key === key) {
index = i;
break;
}
}
return index;
}
/**
* Saves the new value of the given preference
*
* @param key The pref key
* @param value The pref value
*/
saveNewPrefs(key: string, value: string) {
this.setState((prevState) => {
let currentPreferences = [...prevState.currentPreferences];
currentPreferences[this.findIndexOfKey(key)].current = value;
return {currentPreferences};
});
AsyncStorageManager.set(key, value);
this.modalRef.close();
}
/**
* Callback used when receiving the modal ref
*
* @param ref
*/
onModalRef = (ref: Modalize) => {
this.modalRef = ref;
}
renderItem = ({item}: {item: PreferenceItem}) => {
return (
<List.Item
title={item.key}
description={'Click to edit'}
onPress={() => this.showEditModal(item)}
/>
);
}; };
}
render() { /**
return ( * Gets the edit modal content
<View> *
<CustomModal onRef={this.onModalRef}> * @return {*}
{this.getModalContent()} */
</CustomModal> getModalContent(): React.Node {
{/*$FlowFixMe*/} const {props, state} = this;
<CollapsibleFlatList return (
data={this.state.currentPreferences} <View
extraData={this.state.currentPreferences} style={{
renderItem={this.renderItem} flex: 1,
/> padding: 20,
</View> }}>
); <Title>{state.modalCurrentDisplayItem.key}</Title>
<Subheading>
Default: {state.modalCurrentDisplayItem.default}
</Subheading>
<Subheading>
Current: {state.modalCurrentDisplayItem.current}
</Subheading>
<TextInput
label="New Value"
onChangeText={(text: string) => {
this.modalInputValue = text;
}}
/>
<View
style={{
flexDirection: 'row',
marginTop: 10,
}}>
<Button
mode="contained"
dark
color={props.theme.colors.success}
onPress={() => {
this.saveNewPrefs(
state.modalCurrentDisplayItem.key,
this.modalInputValue,
);
}}>
Save new value
</Button>
<Button
mode="contained"
dark
color={props.theme.colors.danger}
onPress={() => {
this.saveNewPrefs(
state.modalCurrentDisplayItem.key,
state.modalCurrentDisplayItem.default,
);
}}>
Reset to default
</Button>
</View>
</View>
);
}
getRenderItem = ({item}: {item: PreferenceItemType}): React.Node => {
return (
<List.Item
title={item.key}
description="Click to edit"
onPress={() => {
this.showEditModal(item);
}}
/>
);
};
/**
* Callback used when receiving the modal ref
*
* @param ref
*/
onModalRef = (ref: Modalize) => {
this.modalRef = ref;
};
/**
* Shows the edit modal
*
* @param item
*/
showEditModal(item: PreferenceItemType) {
this.setState({
modalCurrentDisplayItem: item,
});
if (this.modalRef) this.modalRef.open();
}
/**
* Finds the index of the given key in the preferences array
*
* @param key THe key to find the index of
* @returns {number}
*/
findIndexOfKey(key: string): number {
const {currentPreferences} = this.state;
let index = -1;
for (let i = 0; i < currentPreferences.length; i += 1) {
if (currentPreferences[i].key === key) {
index = i;
break;
}
} }
return index;
}
/**
* Saves the new value of the given preference
*
* @param key The pref key
* @param value The pref value
*/
saveNewPrefs(key: string, value: string) {
this.setState((prevState: StateType): {
currentPreferences: Array<PreferenceItemType>,
} => {
const currentPreferences = [...prevState.currentPreferences];
currentPreferences[this.findIndexOfKey(key)].current = value;
return {currentPreferences};
});
AsyncStorageManager.set(key, value);
this.modalRef.close();
}
render(): React.Node {
const {state} = this;
return (
<View>
<CustomModal onRef={this.onModalRef}>
{this.getModalContent()}
</CustomModal>
{/* $FlowFixMe */}
<CollapsibleFlatList
data={state.currentPreferences}
extraData={state.currentPreferences}
renderItem={this.getRenderItem}
/>
</View>
);
}
} }
export default withTheme(DebugScreen); export default withTheme(DebugScreen);

View file

@ -4,137 +4,157 @@ import * as React from 'react';
import {FlatList, Image, Linking, View} from 'react-native'; import {FlatList, Image, Linking, View} from 'react-native';
import {Card, List, Text, withTheme} from 'react-native-paper'; import {Card, List, Text, withTheme} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons"; import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList"; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import AMICALE_LOGO from '../../../assets/amicale.png';
type Props = { type DatasetItemType = {
name: string,
email: string,
icon: MaterialCommunityIconsGlyphs,
}; };
type DatasetItem = {
name: string,
email: string,
icon: MaterialCommunityIconsGlyphs,
}
/** /**
* Class defining a planning event information page. * Class defining a planning event information page.
*/ */
class AmicaleContactScreen extends React.Component<Props> { class AmicaleContactScreen extends React.Component<null> {
// Dataset containing information about contacts
CONTACT_DATASET: Array<DatasetItemType>;
// Dataset containing information about contacts constructor() {
CONTACT_DATASET: Array<DatasetItem>; super();
this.CONTACT_DATASET = [
{
name: i18n.t('screens.amicaleAbout.roles.interSchools'),
email: 'inter.ecoles@amicale-insat.fr',
icon: 'share-variant',
},
{
name: i18n.t('screens.amicaleAbout.roles.culture'),
email: 'culture@amicale-insat.fr',
icon: 'book',
},
{
name: i18n.t('screens.amicaleAbout.roles.animation'),
email: 'animation@amicale-insat.fr',
icon: 'emoticon',
},
{
name: i18n.t('screens.amicaleAbout.roles.clubs'),
email: 'clubs@amicale-insat.fr',
icon: 'account-group',
},
{
name: i18n.t('screens.amicaleAbout.roles.event'),
email: 'evenements@amicale-insat.fr',
icon: 'calendar-range',
},
{
name: i18n.t('screens.amicaleAbout.roles.tech'),
email: 'technique@amicale-insat.fr',
icon: 'cog',
},
{
name: i18n.t('screens.amicaleAbout.roles.communication'),
email: 'amicale@amicale-insat.fr',
icon: 'comment-account',
},
{
name: i18n.t('screens.amicaleAbout.roles.intraSchools'),
email: 'intra.ecoles@amicale-insat.fr',
icon: 'school',
},
{
name: i18n.t('screens.amicaleAbout.roles.publicRelations'),
email: 'rp@amicale-insat.fr',
icon: 'account-tie',
},
];
}
constructor(props: Props) { keyExtractor = (item: DatasetItemType): string => item.email;
super(props);
this.CONTACT_DATASET = [
{
name: i18n.t("screens.amicaleAbout.roles.interSchools"),
email: "inter.ecoles@amicale-insat.fr",
icon: "share-variant"
},
{
name: i18n.t("screens.amicaleAbout.roles.culture"),
email: "culture@amicale-insat.fr",
icon: "book"
},
{
name: i18n.t("screens.amicaleAbout.roles.animation"),
email: "animation@amicale-insat.fr",
icon: "emoticon"
},
{
name: i18n.t("screens.amicaleAbout.roles.clubs"),
email: "clubs@amicale-insat.fr",
icon: "account-group"
},
{
name: i18n.t("screens.amicaleAbout.roles.event"),
email: "evenements@amicale-insat.fr",
icon: "calendar-range"
},
{
name: i18n.t("screens.amicaleAbout.roles.tech"),
email: "technique@amicale-insat.fr",
icon: "cog"
},
{
name: i18n.t("screens.amicaleAbout.roles.communication"),
email: "amicale@amicale-insat.fr",
icon: "comment-account"
},
{
name: i18n.t("screens.amicaleAbout.roles.intraSchools"),
email: "intra.ecoles@amicale-insat.fr",
icon: "school"
},
{
name: i18n.t("screens.amicaleAbout.roles.publicRelations"),
email: "rp@amicale-insat.fr",
icon: "account-tie"
},
];
}
keyExtractor = (item: DatasetItem) => item.email; getChevronIcon = ({
size,
color,
}: {
size: number,
color: string,
}): React.Node => (
<List.Icon size={size} color={color} icon="chevron-right" />
);
getChevronIcon = (props) => <List.Icon {...props} icon={'chevron-right'}/>; getRenderItem = ({item}: {item: DatasetItemType}): React.Node => {
const onPress = () => {
renderItem = ({item}: { item: DatasetItem }) => { Linking.openURL(`mailto:${item.email}`);
const onPress = () => Linking.openURL('mailto:' + item.email);
return <List.Item
title={item.name}
description={item.email}
left={(props) => <List.Icon {...props} icon={item.icon}/>}
right={this.getChevronIcon}
onPress={onPress}
/>
}; };
return (
<List.Item
title={item.name}
description={item.email}
left={({size, color}: {size: number, color: string}): React.Node => (
<List.Icon size={size} color={color} icon={item.icon} />
)}
right={this.getChevronIcon}
onPress={onPress}
/>
);
};
getScreen = () => { getScreen = (): React.Node => {
return ( return (
<View> <View>
<View style={{ <View
width: '100%', style={{
height: 100, width: '100%',
marginTop: 20, height: 100,
marginBottom: 20, marginTop: 20,
justifyContent: 'center', marginBottom: 20,
alignItems: 'center' justifyContent: 'center',
}}> alignItems: 'center',
<Image }}>
source={require('../../../assets/amicale.png')} <Image
style={{flex: 1, resizeMode: "contain"}} source={AMICALE_LOGO}
resizeMode="contain"/> style={{flex: 1, resizeMode: 'contain'}}
</View> resizeMode="contain"
<Card style={{margin: 5}}> />
<Card.Title </View>
title={i18n.t("screens.amicaleAbout.title")} <Card style={{margin: 5}}>
subtitle={i18n.t("screens.amicaleAbout.subtitle")} <Card.Title
left={props => <List.Icon {...props} icon={'information'}/>} title={i18n.t('screens.amicaleAbout.title')}
/> subtitle={i18n.t('screens.amicaleAbout.subtitle')}
<Card.Content> left={({
<Text>{i18n.t("screens.amicaleAbout.message")}</Text> size,
{/*$FlowFixMe*/} color,
<FlatList }: {
data={this.CONTACT_DATASET} size: number,
keyExtractor={this.keyExtractor} color: string,
renderItem={this.renderItem} }): React.Node => (
/> <List.Icon size={size} color={color} icon="information" />
</Card.Content> )}
</Card> />
</View> <Card.Content>
); <Text>{i18n.t('screens.amicaleAbout.message')}</Text>
}; <FlatList
data={this.CONTACT_DATASET}
render() { keyExtractor={this.keyExtractor}
return ( renderItem={this.getRenderItem}
<CollapsibleFlatList
data={[{key: "1"}]}
renderItem={this.getScreen}
hasTab={true}
/> />
); </Card.Content>
} </Card>
</View>
);
};
render(): React.Node {
return (
<CollapsibleFlatList
data={[{key: '1'}]}
renderItem={this.getScreen}
hasTab
/>
);
}
} }
export default withTheme(AmicaleContactScreen); export default withTheme(AmicaleContactScreen);

View file

@ -4,49 +4,49 @@ import * as React from 'react';
import {Image, View} from 'react-native'; import {Image, View} from 'react-native';
import {Card, List, Text, withTheme} from 'react-native-paper'; import {Card, List, Text, withTheme} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import Autolink from "react-native-autolink"; import Autolink from 'react-native-autolink';
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import AMICALE_ICON from '../../../../assets/amicale.png';
type Props = {};
const CONTACT_LINK = 'clubs@amicale-insat.fr'; const CONTACT_LINK = 'clubs@amicale-insat.fr';
class ClubAboutScreen extends React.Component<Props> { // eslint-disable-next-line react/prefer-stateless-function
class ClubAboutScreen extends React.Component<null> {
render() { render(): React.Node {
return ( return (
<CollapsibleScrollView style={{padding: 5}}> <CollapsibleScrollView style={{padding: 5}}>
<View style={{ <View
width: '100%', style={{
height: 100, width: '100%',
marginTop: 20, height: 100,
marginBottom: 20, marginTop: 20,
justifyContent: 'center', marginBottom: 20,
alignItems: 'center' justifyContent: 'center',
}}> alignItems: 'center',
<Image }}>
source={require('../../../../assets/amicale.png')} <Image
style={{flex: 1, resizeMode: "contain"}} source={AMICALE_ICON}
resizeMode="contain"/> style={{flex: 1, resizeMode: 'contain'}}
</View> resizeMode="contain"
<Text>{i18n.t("screens.clubs.about.text")}</Text> />
<Card style={{margin: 5}}> </View>
<Card.Title <Text>{i18n.t('screens.clubs.about.text')}</Text>
title={i18n.t("screens.clubs.about.title")} <Card style={{margin: 5}}>
subtitle={i18n.t("screens.clubs.about.subtitle")} <Card.Title
left={props => <List.Icon {...props} icon={'information'}/>} title={i18n.t('screens.clubs.about.title')}
/> subtitle={i18n.t('screens.clubs.about.subtitle')}
<Card.Content> left={({size}: {size: number}): React.Node => (
<Text>{i18n.t("screens.clubs.about.message")}</Text> <List.Icon size={size} icon="information" />
<Autolink )}
text={CONTACT_LINK} />
component={Text} <Card.Content>
/> <Text>{i18n.t('screens.clubs.about.message')}</Text>
</Card.Content> <Autolink text={CONTACT_LINK} component={Text} />
</Card> </Card.Content>
</CollapsibleScrollView> </Card>
); </CollapsibleScrollView>
} );
}
} }
export default withTheme(ClubAboutScreen); export default withTheme(ClubAboutScreen);

View file

@ -2,252 +2,276 @@
import * as React from 'react'; import * as React from 'react';
import {Linking, View} from 'react-native'; import {Linking, View} from 'react-native';
import {Avatar, Button, Card, Chip, Paragraph, withTheme} from 'react-native-paper'; import {
Avatar,
Button,
Card,
Chip,
Paragraph,
withTheme,
} from 'react-native-paper';
import ImageModal from 'react-native-image-modal'; import ImageModal from 'react-native-image-modal';
import i18n from "i18n-js"; import i18n from 'i18n-js';
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; import {StackNavigationProp} from '@react-navigation/stack';
import CustomHTML from "../../../components/Overrides/CustomHTML"; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import CustomTabBar from "../../../components/Tabbar/CustomTabBar"; import CustomHTML from '../../../components/Overrides/CustomHTML';
import type {category, club} from "./ClubListScreen"; import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {ClubCategoryType, ClubType} from './ClubListScreen';
import {StackNavigationProp} from "@react-navigation/stack"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import {ERROR_TYPE} from "../../../utils/WebData"; import {ERROR_TYPE} from '../../../utils/WebData';
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import type {ApiGenericDataType} from '../../../utils/WebData';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
data?: club, data?: ClubType,
categories?: Array<category>, categories?: Array<ClubCategoryType>,
clubId?: number, clubId?: number,
}, ...
}, },
theme: CustomTheme ...
},
theme: CustomThemeType,
}; };
type State = { const AMICALE_MAIL = 'clubs@amicale-insat.fr';
imageModalVisible: boolean,
};
const AMICALE_MAIL = "clubs@amicale-insat.fr";
/** /**
* Class defining a club event information page. * Class defining a club event information page.
* If called with data and categories navigation parameters, will use those to display the data. * If called with data and categories navigation parameters, will use those to display the data.
* If called with clubId parameter, will fetch the information on the server * If called with clubId parameter, will fetch the information on the server
*/ */
class ClubDisplayScreen extends React.Component<Props, State> { class ClubDisplayScreen extends React.Component<PropsType> {
displayData: ClubType | null;
displayData: club | null; categories: Array<ClubCategoryType> | null;
categories: Array<category> | null;
clubId: number;
shouldFetchData: boolean; clubId: number;
state = { shouldFetchData: boolean;
imageModalVisible: false,
};
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
if (this.props.route.params != null) { if (props.route.params != null) {
if (this.props.route.params.data != null && this.props.route.params.categories != null) { if (
this.displayData = this.props.route.params.data; props.route.params.data != null &&
this.categories = this.props.route.params.categories; props.route.params.categories != null
this.clubId = this.props.route.params.data.id; ) {
this.shouldFetchData = false; this.displayData = props.route.params.data;
} else if (this.props.route.params.clubId != null) { this.categories = props.route.params.categories;
this.displayData = null; this.clubId = props.route.params.data.id;
this.categories = null; this.shouldFetchData = false;
this.clubId = this.props.route.params.clubId; } else if (props.route.params.clubId != null) {
this.shouldFetchData = true; this.displayData = null;
} this.categories = null;
} this.clubId = props.route.params.clubId;
this.shouldFetchData = true;
}
} }
}
/** /**
* Gets the name of the category with the given ID * Gets the name of the category with the given ID
* *
* @param id The category's ID * @param id The category's ID
* @returns {string|*} * @returns {string|*}
*/ */
getCategoryName(id: number) { getCategoryName(id: number): string {
if (this.categories !== null) { let categoryName = '';
for (let i = 0; i < this.categories.length; i++) { if (this.categories !== null) {
if (id === this.categories[i].id) this.categories.forEach((item: ClubCategoryType) => {
return this.categories[i].name; if (id === item.id) categoryName = item.name;
} });
}
return "";
} }
return categoryName;
}
/** /**
* Gets the view for rendering categories * Gets the view for rendering categories
* *
* @param categories The categories to display (max 2) * @param categories The categories to display (max 2)
* @returns {null|*} * @returns {null|*}
*/ */
getCategoriesRender(categories: [number, number]) { getCategoriesRender(categories: Array<number | null>): React.Node {
if (this.categories === null) if (this.categories == null) return null;
return null;
let final = []; const final = [];
for (let i = 0; i < categories.length; i++) { categories.forEach((cat: number | null) => {
let cat = categories[i]; if (cat != null) {
if (cat !== null) { final.push(
final.push( <Chip style={{marginRight: 5}} key={cat}>
<Chip {this.getCategoryName(cat)}
style={{marginRight: 5}} </Chip>,
key={i.toString()}>
{this.getCategoryName(cat)}
</Chip>
);
}
}
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
}
/**
* Gets the view for rendering club managers if any
*
* @param managers The list of manager names
* @param email The club contact email
* @returns {*}
*/
getManagersRender(managers: Array<string>, email: string | null) {
let managersListView = [];
for (let i = 0; i < managers.length; i++) {
managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
}
const hasManagers = managers.length > 0;
return (
<Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title
title={i18n.t('screens.clubs.managers')}
subtitle={hasManagers ? i18n.t('screens.clubs.managersSubtitle') : i18n.t('screens.clubs.managersUnavailable')}
left={(props) => <Avatar.Icon
{...props}
style={{backgroundColor: 'transparent'}}
color={hasManagers ? this.props.theme.colors.success : this.props.theme.colors.primary}
icon="account-tie"/>}
/>
<Card.Content>
{managersListView}
{this.getEmailButton(email, hasManagers)}
</Card.Content>
</Card>
); );
}
});
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
}
/**
* Gets the view for rendering club managers if any
*
* @param managers The list of manager names
* @param email The club contact email
* @returns {*}
*/
getManagersRender(managers: Array<string>, email: string | null): React.Node {
const {props} = this;
const managersListView = [];
managers.forEach((item: string) => {
managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
});
const hasManagers = managers.length > 0;
return (
<Card
style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title
title={i18n.t('screens.clubs.managers')}
subtitle={
hasManagers
? i18n.t('screens.clubs.managersSubtitle')
: i18n.t('screens.clubs.managersUnavailable')
}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
style={{backgroundColor: 'transparent'}}
color={
hasManagers
? props.theme.colors.success
: props.theme.colors.primary
}
icon="account-tie"
/>
)}
/>
<Card.Content>
{managersListView}
{ClubDisplayScreen.getEmailButton(email, hasManagers)}
</Card.Content>
</Card>
);
}
/**
* Gets the email button to contact the club, or the amicale if the club does not have any managers
*
* @param email The club contact email
* @param hasManagers True if the club has managers
* @returns {*}
*/
static getEmailButton(
email: string | null,
hasManagers: boolean,
): React.Node {
const destinationEmail =
email != null && hasManagers ? email : AMICALE_MAIL;
const text =
email != null && hasManagers
? i18n.t('screens.clubs.clubContact')
: i18n.t('screens.clubs.amicaleContact');
return (
<Card.Actions>
<Button
icon="email"
mode="contained"
onPress={() => {
Linking.openURL(`mailto:${destinationEmail}`);
}}
style={{marginLeft: 'auto'}}>
{text}
</Button>
</Card.Actions>
);
}
getScreen = (response: Array<ApiGenericDataType | null>): React.Node => {
const {props} = this;
let data: ClubType | null = null;
if (response[0] != null) {
[data] = response;
this.updateHeaderTitle(data);
} }
if (data != null) {
return (
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
{this.getCategoriesRender(data.category)}
{data.logo !== null ? (
<View
style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
marginBottom: 10,
}}>
<ImageModal
resizeMode="contain"
imageBackgroundColor={props.theme.colors.background}
style={{
width: 300,
height: 300,
}}
source={{
uri: data.logo,
}}
/>
</View>
) : (
<View />
)}
/** {data.description !== null ? (
* Gets the email button to contact the club, or the amicale if the club does not have any managers // Surround description with div to allow text styling if the description is not html
* <Card.Content>
* @param email The club contact email <CustomHTML html={data.description} />
* @param hasManagers True if the club has managers </Card.Content>
* @returns {*} ) : (
*/ <View />
getEmailButton(email: string | null, hasManagers: boolean) { )}
const destinationEmail = email != null && hasManagers {this.getManagersRender(data.responsibles, data.email)}
? email </CollapsibleScrollView>
: AMICALE_MAIL; );
const text = email != null && hasManagers
? i18n.t("screens.clubs.clubContact")
: i18n.t("screens.clubs.amicaleContact");
return (
<Card.Actions>
<Button
icon="email"
mode="contained"
onPress={() => Linking.openURL('mailto:' + destinationEmail)}
style={{marginLeft: 'auto'}}
>
{text}
</Button>
</Card.Actions>
);
} }
return null;
};
/** /**
* Updates the header title to match the given club * Updates the header title to match the given club
* *
* @param data The club data * @param data The club data
*/ */
updateHeaderTitle(data: club) { updateHeaderTitle(data: ClubType) {
this.props.navigation.setOptions({title: data.name}) const {props} = this;
} props.navigation.setOptions({title: data.name});
}
getScreen = (response: Array<{ [key: string]: any } | null>) => { render(): React.Node {
let data: club | null = null; const {props} = this;
if (response[0] != null) { if (this.shouldFetchData)
data = response[0]; return (
this.updateHeaderTitle(data); <AuthenticatedScreen
} navigation={props.navigation}
if (data != null) { requests={[
return ( {
<CollapsibleScrollView link: 'clubs/info',
style={{paddingLeft: 5, paddingRight: 5}} params: {id: this.clubId},
hasTab={true} mandatory: true,
> },
{this.getCategoriesRender(data.category)} ]}
{data.logo !== null ? renderFunction={this.getScreen}
<View style={{ errorViewOverride={[
marginLeft: 'auto', {
marginRight: 'auto', errorCode: ERROR_TYPE.BAD_INPUT,
marginTop: 10, message: i18n.t('screens.clubs.invalidClub'),
marginBottom: 10, icon: 'account-question',
}}> showRetryButton: false,
<ImageModal },
resizeMode="contain" ]}
imageBackgroundColor={this.props.theme.colors.background} />
style={{ );
width: 300, return this.getScreen([this.displayData]);
height: 300, }
}}
source={{
uri: data.logo,
}}
/>
</View>
: <View/>}
{data.description !== null ?
// Surround description with div to allow text styling if the description is not html
<Card.Content>
<CustomHTML html={data.description}/>
</Card.Content>
: <View/>}
{this.getManagersRender(data.responsibles, data.email)}
</CollapsibleScrollView>
);
} else
return null;
};
render() {
if (this.shouldFetchData)
return <AuthenticatedScreen
{...this.props}
requests={[
{
link: 'clubs/info',
params: {'id': this.clubId},
mandatory: true
}
]}
renderFunction={this.getScreen}
errorViewOverride={[
{
errorCode: ERROR_TYPE.BAD_INPUT,
message: i18n.t("screens.clubs.invalidClub"),
icon: "account-question",
showRetryButton: false
}
]}
/>;
else
return this.getScreen([this.displayData]);
}
} }
export default withTheme(ClubDisplayScreen); export default withTheme(ClubDisplayScreen);

View file

@ -1,237 +1,271 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Platform} from "react-native"; import {Platform} from 'react-native';
import {Searchbar} from 'react-native-paper'; import {Searchbar} from 'react-native-paper';
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; import i18n from 'i18n-js';
import i18n from "i18n-js"; import {StackNavigationProp} from '@react-navigation/stack';
import ClubListItem from "../../../components/Lists/Clubs/ClubListItem"; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import {isItemInCategoryFilter, stringMatchQuery} from "../../../utils/Search"; import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
import ClubListHeader from "../../../components/Lists/Clubs/ClubListHeader"; import {isItemInCategoryFilter, stringMatchQuery} from '../../../utils/Search';
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton"; import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
import {StackNavigationProp} from "@react-navigation/stack"; import MaterialHeaderButtons, {
import type {CustomTheme} from "../../../managers/ThemeManager"; Item,
import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList"; } from '../../../components/Overrides/CustomHeaderButton';
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
export type category = { export type ClubCategoryType = {
id: number, id: number,
name: string, name: string,
}; };
export type club = { export type ClubType = {
id: number, id: number,
name: string, name: string,
description: string, description: string,
logo: string, logo: string,
email: string | null, email: string | null,
category: [number, number], category: Array<number | null>,
responsibles: Array<string>, responsibles: Array<string>,
}; };
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, };
}
type State = { type StateType = {
currentlySelectedCategories: Array<number>, currentlySelectedCategories: Array<number>,
currentSearchString: string, currentSearchString: string,
} };
const LIST_ITEM_HEIGHT = 96; const LIST_ITEM_HEIGHT = 96;
class ClubListScreen extends React.Component<Props, State> { class ClubListScreen extends React.Component<PropsType, StateType> {
categories: Array<ClubCategoryType>;
state = { constructor() {
currentlySelectedCategories: [], super();
currentSearchString: '', this.state = {
currentlySelectedCategories: [],
currentSearchString: '',
}; };
}
categories: Array<category>; /**
* Creates the header content
*/
componentDidMount() {
const {props} = this;
props.navigation.setOptions({
headerTitle: this.getSearchBar,
headerRight: this.getHeaderButtons,
headerBackTitleVisible: false,
headerTitleContainerStyle:
Platform.OS === 'ios'
? {marginHorizontal: 0, width: '70%'}
: {marginHorizontal: 0, right: 50, left: 50},
});
}
/** /**
* Creates the header content * Callback used when clicking an article in the list.
*/ * It opens the modal to show detailed information about the article
componentDidMount() { *
this.props.navigation.setOptions({ * @param item The article pressed
headerTitle: this.getSearchBar, */
headerRight: this.getHeaderButtons, onListItemPress(item: ClubType) {
headerBackTitleVisible: false, const {props} = this;
headerTitleContainerStyle: Platform.OS === 'ios' ? props.navigation.navigate('club-information', {
{marginHorizontal: 0, width: '70%'} : data: item,
{marginHorizontal: 0, right: 50, left: 50}, categories: this.categories,
}); });
}
/**
* Callback used when the search changes
*
* @param str The new search string
*/
onSearchStringChange = (str: string) => {
this.updateFilteredData(str, null);
};
/**
* Gets the header search bar
*
* @return {*}
*/
getSearchBar = (): React.Node => {
return (
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange}
/>
);
};
onChipSelect = (id: number) => {
this.updateFilteredData(null, id);
};
/**
* Gets the header button
* @return {*}
*/
getHeaderButtons = (): React.Node => {
const onPress = () => {
const {props} = this;
props.navigation.navigate('club-about');
};
return (
<MaterialHeaderButtons>
<Item title="main" iconName="information" onPress={onPress} />
</MaterialHeaderButtons>
);
};
getScreen = (
data: Array<{
categories: Array<ClubCategoryType>,
clubs: Array<ClubType>,
} | null>,
): React.Node => {
let categoryList = [];
let clubList = [];
if (data[0] != null) {
categoryList = data[0].categories;
clubList = data[0].clubs;
} }
this.categories = categoryList;
return (
<CollapsibleFlatList
data={clubList}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews
getItemLayout={this.itemLayout}
/>
);
};
/** /**
* Gets the header search bar * Gets the list header, with controls to change the categories filter
* *
* @return {*} * @returns {*}
*/ */
getSearchBar = () => { getListHeader(): React.Node {
return ( const {state} = this;
<Searchbar return (
placeholder={i18n.t('screens.proximo.search')} <ClubListHeader
onChangeText={this.onSearchStringChange} categories={this.categories}
/> selectedCategories={state.currentlySelectedCategories}
); onChipSelect={this.onChipSelect}
/>
);
}
/**
* Gets the category object of the given ID
*
* @param id The ID of the category to find
* @returns {*}
*/
getCategoryOfId = (id: number): ClubCategoryType | null => {
let cat = null;
this.categories.forEach((item: ClubCategoryType) => {
if (id === item.id) cat = item;
});
return cat;
};
getRenderItem = ({item}: {item: ClubType}): React.Node => {
const onPress = () => {
this.onListItemPress(item);
}; };
if (this.shouldRenderItem(item)) {
/** return (
* Gets the header button <ClubListItem
* @return {*} categoryTranslator={this.getCategoryOfId}
*/ item={item}
getHeaderButtons = () => { onPress={onPress}
const onPress = () => this.props.navigation.navigate("club-about"); height={LIST_ITEM_HEIGHT}
return <MaterialHeaderButtons> />
<Item title="main" iconName="information" onPress={onPress}/> );
</MaterialHeaderButtons>;
};
/**
* Callback used when the search changes
*
* @param str The new search string
*/
onSearchStringChange = (str: string) => {
this.updateFilteredData(str, null);
};
keyExtractor = (item: club) => item.id.toString();
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
getScreen = (data: Array<{ categories: Array<category>, clubs: Array<club> } | null>) => {
let categoryList = [];
let clubList = [];
if (data[0] != null) {
categoryList = data[0].categories;
clubList = data[0].clubs;
}
this.categories = categoryList;
return (
<CollapsibleFlatList
data={clubList}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews={true}
getItemLayout={this.itemLayout}
/>
)
};
onChipSelect = (id: number) => this.updateFilteredData(null, id);
/**
* Updates the search string and category filter, saving them to the State.
*
* If the given category is already in the filter, it removes it.
* Otherwise it adds it to the filter.
*
* @param filterStr The new filter string to use
* @param categoryId The category to add/remove from the filter
*/
updateFilteredData(filterStr: string | null, categoryId: number | null) {
let newCategoriesState = [...this.state.currentlySelectedCategories];
let newStrState = this.state.currentSearchString;
if (filterStr !== null)
newStrState = filterStr;
if (categoryId !== null) {
let index = newCategoriesState.indexOf(categoryId);
if (index === -1)
newCategoriesState.push(categoryId);
else
newCategoriesState.splice(index, 1);
}
if (filterStr !== null || categoryId !== null)
this.setState({
currentSearchString: newStrState,
currentlySelectedCategories: newCategoriesState,
})
} }
return null;
};
/** keyExtractor = (item: ClubType): string => item.id.toString();
* Gets the list header, with controls to change the categories filter
* itemLayout = (
* @returns {*} data: {...},
*/ index: number,
getListHeader() { ): {length: number, offset: number, index: number} => ({
return <ClubListHeader length: LIST_ITEM_HEIGHT,
categories={this.categories} offset: LIST_ITEM_HEIGHT * index,
selectedCategories={this.state.currentlySelectedCategories} index,
onChipSelect={this.onChipSelect} });
/>;
/**
* Updates the search string and category filter, saving them to the State.
*
* If the given category is already in the filter, it removes it.
* Otherwise it adds it to the filter.
*
* @param filterStr The new filter string to use
* @param categoryId The category to add/remove from the filter
*/
updateFilteredData(filterStr: string | null, categoryId: number | null) {
const {state} = this;
const newCategoriesState = [...state.currentlySelectedCategories];
let newStrState = state.currentSearchString;
if (filterStr !== null) newStrState = filterStr;
if (categoryId !== null) {
const index = newCategoriesState.indexOf(categoryId);
if (index === -1) newCategoriesState.push(categoryId);
else newCategoriesState.splice(index, 1);
} }
if (filterStr !== null || categoryId !== null)
this.setState({
currentSearchString: newStrState,
currentlySelectedCategories: newCategoriesState,
});
}
/** /**
* Gets the category object of the given ID * Checks if the given item should be rendered according to current name and category filters
* *
* @param id The ID of the category to find * @param item The club to check
* @returns {*} * @returns {boolean}
*/ */
getCategoryOfId = (id: number) => { shouldRenderItem(item: ClubType): boolean {
for (let i = 0; i < this.categories.length; i++) { const {state} = this;
if (id === this.categories[i].id) let shouldRender =
return this.categories[i]; state.currentlySelectedCategories.length === 0 ||
} isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
}; if (shouldRender)
shouldRender = stringMatchQuery(item.name, state.currentSearchString);
return shouldRender;
}
/** render(): React.Node {
* Checks if the given item should be rendered according to current name and category filters const {props} = this;
* return (
* @param item The club to check <AuthenticatedScreen
* @returns {boolean} navigation={props.navigation}
*/ requests={[
shouldRenderItem(item: club) { {
let shouldRender = this.state.currentlySelectedCategories.length === 0 link: 'clubs/list',
|| isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category); params: {},
if (shouldRender) mandatory: true,
shouldRender = stringMatchQuery(item.name, this.state.currentSearchString); },
return shouldRender; ]}
} renderFunction={this.getScreen}
/>
getRenderItem = ({item}: { item: club }) => { );
const onPress = this.onListItemPress.bind(this, item); }
if (this.shouldRenderItem(item)) {
return (
<ClubListItem
categoryTranslator={this.getCategoryOfId}
item={item}
onPress={onPress}
height={LIST_ITEM_HEIGHT}
/>
);
} else
return null;
};
/**
* Callback used when clicking an article in the list.
* It opens the modal to show detailed information about the article
*
* @param item The article pressed
*/
onListItemPress(item: club) {
this.props.navigation.navigate("club-information", {data: item, categories: this.categories});
}
render() {
return (
<AuthenticatedScreen
{...this.props}
requests={[
{
link: 'clubs/list',
params: {},
mandatory: true,
}
]}
renderFunction={this.getScreen}
/>
);
}
} }
export default ClubListScreen; export default ClubListScreen;

View file

@ -1,105 +1,102 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Caption, Card, Headline, Paragraph, withTheme} from 'react-native-paper'; import {
import {StackNavigationProp} from "@react-navigation/stack"; Button,
import type {CustomTheme} from "../../../managers/ThemeManager"; Caption,
import type {Device} from "./EquipmentListScreen"; Card,
import {View} from "react-native"; Headline,
import i18n from "i18n-js"; Paragraph,
import {getRelativeDateString} from "../../../utils/EquipmentBooking"; withTheme,
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; } from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {DeviceType} from './EquipmentListScreen';
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
type Props = { type PropsType = {
navigation: StackNavigationProp, route: {
route: { params?: {
params?: { item?: DeviceType,
item?: Device, dates: [string, string],
dates: [string, string]
},
}, },
theme: CustomTheme, },
} theme: CustomThemeType,
};
class EquipmentConfirmScreen extends React.Component<PropsType> {
item: DeviceType | null;
class EquipmentConfirmScreen extends React.Component<Props> { dates: [string, string] | null;
item: Device | null; constructor(props: PropsType) {
dates: [string, string] | null; super(props);
if (props.route.params != null) {
constructor(props: Props) { if (props.route.params.item != null) this.item = props.route.params.item;
super(props); else this.item = null;
if (this.props.route.params != null) { if (props.route.params.dates != null)
if (this.props.route.params.item != null) this.dates = props.route.params.dates;
this.item = this.props.route.params.item; else this.dates = null;
else
this.item = null;
if (this.props.route.params.dates != null)
this.dates = this.props.route.params.dates;
else
this.dates = null;
}
} }
}
render() { render(): React.Node {
const item = this.item; const {item, dates, props} = this;
const dates = this.dates; if (item != null && dates != null) {
if (item != null && dates != null) { const start = new Date(dates[0]);
const start = new Date(dates[0]); const end = new Date(dates[1]);
const end = new Date(dates[1]); let buttonText;
return ( if (start == null) buttonText = i18n.t('screens.equipment.booking');
<CollapsibleScrollView> else if (end != null && start.getTime() !== end.getTime())
<Card style={{margin: 5}}> buttonText = i18n.t('screens.equipment.bookingPeriod', {
<Card.Content> begin: getRelativeDateString(start),
<View style={{flex: 1}}> end: getRelativeDateString(end),
<View style={{ });
marginLeft: "auto", else
marginRight: "auto", buttonText = i18n.t('screens.equipment.bookingDay', {
flexDirection: "row", date: getRelativeDateString(start),
flexWrap: "wrap", });
}}> return (
<Headline style={{textAlign: "center"}}> <CollapsibleScrollView>
{item.name} <Card style={{margin: 5}}>
</Headline> <Card.Content>
<Caption style={{ <View style={{flex: 1}}>
textAlign: "center", <View
lineHeight: 35, style={{
marginLeft: 10, marginLeft: 'auto',
}}> marginRight: 'auto',
({i18n.t('screens.equipment.bail', {cost: item.caution})}) flexDirection: 'row',
</Caption> flexWrap: 'wrap',
</View> }}>
</View> <Headline style={{textAlign: 'center'}}>{item.name}</Headline>
<Button <Caption
icon={"check-circle-outline"} style={{
color={this.props.theme.colors.success} textAlign: 'center',
mode="text" lineHeight: 35,
> marginLeft: 10,
{ }}>
start == null ({i18n.t('screens.equipment.bail', {cost: item.caution})})
? i18n.t('screens.equipment.booking') </Caption>
: end != null && start.getTime() !== end.getTime() </View>
? i18n.t('screens.equipment.bookingPeriod', { </View>
begin: getRelativeDateString(start), <Button
end: getRelativeDateString(end) icon="check-circle-outline"
}) color={props.theme.colors.success}
: i18n.t('screens.equipment.bookingDay', { mode="text">
date: getRelativeDateString(start) {buttonText}
}) </Button>
} <Paragraph style={{textAlign: 'center'}}>
</Button> {i18n.t('screens.equipment.bookingConfirmedMessage')}
<Paragraph style={{textAlign: "center"}}> </Paragraph>
{i18n.t("screens.equipment.bookingConfirmedMessage")} </Card.Content>
</Paragraph> </Card>
</Card.Content> </CollapsibleScrollView>
</Card> );
</CollapsibleScrollView>
);
} else
return null;
} }
return null;
}
} }
export default withTheme(EquipmentConfirmScreen); export default withTheme(EquipmentConfirmScreen);

View file

@ -1,193 +1,197 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from "react-native"; import {View} from 'react-native';
import {Button, withTheme} from 'react-native-paper'; import {Button, withTheme} from 'react-native-paper';
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import i18n from 'i18n-js';
import type {CustomTheme} from "../../../managers/ThemeManager"; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import i18n from "i18n-js"; import type {ClubType} from '../Clubs/ClubListScreen';
import type {club} from "../Clubs/ClubListScreen"; import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
import EquipmentListItem from "../../../components/Lists/Equipment/EquipmentListItem"; import MascotPopup from '../../../components/Mascot/MascotPopup';
import MascotPopup from "../../../components/Mascot/MascotPopup"; import {MASCOT_STYLE} from '../../../components/Mascot/Mascot';
import {MASCOT_STYLE} from "../../../components/Mascot/Mascot"; import AsyncStorageManager from '../../../managers/AsyncStorageManager';
import AsyncStorageManager from "../../../managers/AsyncStorageManager"; import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList"; import type {ApiGenericDataType} from '../../../utils/WebData';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme,
}
type State = {
mascotDialogVisible: boolean,
}
export type Device = {
id: number,
name: string,
caution: number,
booked_at: Array<{ begin: string, end: string }>,
}; };
export type RentedDevice = { type StateType = {
device_id: number, mascotDialogVisible: boolean,
device_name: string, };
begin: string,
end: string, export type DeviceType = {
} id: number,
name: string,
caution: number,
booked_at: Array<{begin: string, end: string}>,
};
export type RentedDeviceType = {
device_id: number,
device_name: string,
begin: string,
end: string,
};
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
class EquipmentListScreen extends React.Component<Props, State> { class EquipmentListScreen extends React.Component<PropsType, StateType> {
data: Array<DeviceType>;
state = { userRents: Array<RentedDeviceType>;
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key),
}
data: Array<Device>; authRef: {current: null | AuthenticatedScreen};
userRents: Array<RentedDevice>;
authRef: { current: null | AuthenticatedScreen }; canRefresh: boolean;
canRefresh: boolean;
constructor(props: Props) { constructor(props: PropsType) {
super(props); super(props);
this.canRefresh = false; this.state = {
this.authRef = React.createRef(); mascotDialogVisible: AsyncStorageManager.getBool(
this.props.navigation.addListener('focus', this.onScreenFocus); AsyncStorageManager.PREFERENCES.equipmentShowBanner.key,
} ),
onScreenFocus = () => {
if (this.canRefresh && this.authRef.current != null)
this.authRef.current.reload();
this.canRefresh = true;
}; };
this.canRefresh = false;
this.authRef = React.createRef();
props.navigation.addListener('focus', this.onScreenFocus);
}
getRenderItem = ({item}: { item: Device }) => { onScreenFocus = () => {
return ( if (this.canRefresh && this.authRef.current != null)
<EquipmentListItem this.authRef.current.reload();
navigation={this.props.navigation} this.canRefresh = true;
item={item} };
userDeviceRentDates={this.getUserDeviceRentDates(item)}
height={LIST_ITEM_HEIGHT}/>
);
};
getUserDeviceRentDates(item: Device) { getRenderItem = ({item}: {item: DeviceType}): React.Node => {
let dates = null; const {navigation} = this.props;
for (let i = 0; i < this.userRents.length; i++) { return (
let device = this.userRents[i]; <EquipmentListItem
if (item.id === device.device_id) { navigation={navigation}
dates = [device.begin, device.end]; item={item}
break; userDeviceRentDates={this.getUserDeviceRentDates(item)}
} height={LIST_ITEM_HEIGHT}
} />
return dates; );
};
getUserDeviceRentDates(item: DeviceType): [number, number] | null {
let dates = null;
this.userRents.forEach((device: RentedDeviceType) => {
if (item.id === device.device_id) {
dates = [device.begin, device.end];
}
});
return dates;
}
/**
* Gets the list header, with explains this screen's purpose
*
* @returns {*}
*/
getListHeader(): React.Node {
return (
<View
style={{
width: '100%',
marginTop: 10,
marginBottom: 10,
}}>
<Button
mode="contained"
icon="help-circle"
onPress={this.showMascotDialog}
style={{
marginRight: 'auto',
marginLeft: 'auto',
}}>
{i18n.t('screens.equipment.mascotDialog.title')}
</Button>
</View>
);
}
keyExtractor = (item: ClubType): string => item.id.toString();
/**
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
if (data[0] != null) {
const fetchedData = data[0];
if (fetchedData != null) this.data = fetchedData.devices;
} }
if (data[1] != null) {
/** const fetchedData = data[1];
* Gets the list header, with explains this screen's purpose if (fetchedData != null) this.userRents = fetchedData.locations;
*
* @returns {*}
*/
getListHeader() {
return (
<View style={{
width: "100%",
marginTop: 10,
marginBottom: 10,
}}>
<Button
mode={"contained"}
icon={"help-circle"}
onPress={this.showMascotDialog}
style={{
marginRight: "auto",
marginLeft: "auto",
}}>
{i18n.t("screens.equipment.mascotDialog.title")}
</Button>
</View>
);
} }
return (
<CollapsibleFlatList
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
data={this.data}
/>
);
};
keyExtractor = (item: club) => item.id.toString(); showMascotDialog = () => {
this.setState({mascotDialogVisible: true});
};
/** hideMascotDialog = () => {
* Gets the main screen component with the fetched data AsyncStorageManager.set(
* AsyncStorageManager.PREFERENCES.equipmentShowBanner.key,
* @param data The data fetched from the server false,
* @returns {*} );
*/ this.setState({mascotDialogVisible: false});
getScreen = (data: Array<{ [key: string]: any } | null>) => { };
if (data[0] != null) {
const fetchedData = data[0];
if (fetchedData != null)
this.data = fetchedData["devices"];
}
if (data[1] != null) {
const fetchedData = data[1];
if (fetchedData != null)
this.userRents = fetchedData["locations"];
}
return (
<CollapsibleFlatList
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()}
data={this.data}
/>
)
};
showMascotDialog = () => { render(): React.Node {
this.setState({mascotDialogVisible: true}) const {props, state} = this;
}; return (
<View style={{flex: 1}}>
hideMascotDialog = () => { <AuthenticatedScreen
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key, false); navigation={props.navigation}
this.setState({mascotDialogVisible: false}) ref={this.authRef}
}; requests={[
{
render() { link: 'location/all',
return ( params: {},
<View style={{flex: 1}}> mandatory: true,
<AuthenticatedScreen },
{...this.props} {
ref={this.authRef} link: 'location/my',
requests={[ params: {},
{ mandatory: false,
link: 'location/all', },
params: {}, ]}
mandatory: true, renderFunction={this.getScreen}
}, />
{ <MascotPopup
link: 'location/my', visible={state.mascotDialogVisible}
params: {}, title={i18n.t('screens.equipment.mascotDialog.title')}
mandatory: false, message={i18n.t('screens.equipment.mascotDialog.message')}
} icon="vote"
]} buttons={{
renderFunction={this.getScreen} action: null,
/> cancel: {
<MascotPopup message: i18n.t('screens.equipment.mascotDialog.button'),
visible={this.state.mascotDialogVisible} icon: 'check',
title={i18n.t("screens.equipment.mascotDialog.title")} onPress: this.hideMascotDialog,
message={i18n.t("screens.equipment.mascotDialog.message")} },
icon={"vote"} }}
buttons={{ emotion={MASCOT_STYLE.WINK}
action: null, />
cancel: { </View>
message: i18n.t("screens.equipment.mascotDialog.button"), );
icon: "check", }
onPress: this.hideMascotDialog,
}
}}
emotion={MASCOT_STYLE.WINK}
/>
</View>
);
}
} }
export default withTheme(EquipmentListScreen); export default withTheme(EquipmentListScreen);

View file

@ -1,441 +1,441 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper';
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../../managers/ThemeManager";
import type {Device} from "./EquipmentListScreen";
import {BackHandler, View} from "react-native";
import * as Animatable from "react-native-animatable";
import i18n from "i18n-js";
import {CalendarList} from "react-native-calendars";
import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
import { import {
generateMarkedDates, Button,
getFirstEquipmentAvailability, Caption,
getISODate, Card,
getRelativeDateString, Headline,
getValidRange, Subheading,
isEquipmentAvailable withTheme,
} from "../../../utils/EquipmentBooking"; } from 'react-native-paper';
import ConnectionManager from "../../../managers/ConnectionManager"; import {StackNavigationProp} from '@react-navigation/stack';
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; import {BackHandler, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
import i18n from 'i18n-js';
import {CalendarList} from 'react-native-calendars';
import type {DeviceType} from './EquipmentListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
import {
generateMarkedDates,
getFirstEquipmentAvailability,
getISODate,
getRelativeDateString,
getValidRange,
isEquipmentAvailable,
} from '../../../utils/EquipmentBooking';
import ConnectionManager from '../../../managers/ConnectionManager';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
item?: Device, item?: DeviceType,
},
}, },
theme: CustomTheme, },
} theme: CustomThemeType,
};
type State = { export type MarkedDatesObjectType = {
dialogVisible: boolean, [key: string]: {startingDay: boolean, endingDay: boolean, color: string},
errorDialogVisible: boolean, };
markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
currentError: number,
}
class EquipmentRentScreen extends React.Component<Props, State> { type StateType = {
dialogVisible: boolean,
errorDialogVisible: boolean,
markedDates: MarkedDatesObjectType,
currentError: number,
};
state = { class EquipmentRentScreen extends React.Component<PropsType, StateType> {
dialogVisible: false, item: DeviceType | null;
errorDialogVisible: false,
markedDates: {},
currentError: 0,
}
item: Device | null; bookedDates: Array<string>;
bookedDates: Array<string>;
bookRef: { current: null | Animatable.View } bookRef: {current: null | Animatable.View};
canBookEquipment: boolean;
lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } } canBookEquipment: boolean;
constructor(props: Props) { lockedDates: {
super(props); [key: string]: {startingDay: boolean, endingDay: boolean, color: string},
this.resetSelection(); };
this.bookRef = React.createRef();
this.canBookEquipment = false;
this.bookedDates = [];
if (this.props.route.params != null) {
if (this.props.route.params.item != null)
this.item = this.props.route.params.item;
else
this.item = null;
}
const item = this.item;
if (item != null) {
this.lockedDates = {};
for (let i = 0; i < item.booked_at.length; i++) {
const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null);
this.lockedDates = {
...this.lockedDates,
...generateMarkedDates(
false,
this.props.theme,
range
)
};
}
}
} constructor(props: PropsType) {
super(props);
/** this.state = {
* Captures focus and blur events to hook on android back button dialogVisible: false,
*/ errorDialogVisible: false,
componentDidMount() { markedDates: {},
this.props.navigation.addListener( currentError: 0,
'focus',
() =>
BackHandler.addEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
this.props.navigation.addListener(
'blur',
() =>
BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid
)
);
}
/**
* Overrides default android back button behaviour to deselect date if any is selected.
*
* @return {boolean}
*/
onBackButtonPressAndroid = () => {
if (this.bookedDates.length > 0) {
this.resetSelection();
this.updateMarkedSelection();
return true;
} else
return false;
}; };
this.resetSelection();
/** this.bookRef = React.createRef();
* Selects a new date on the calendar. this.canBookEquipment = false;
* If both start and end dates are already selected, unselect all. this.bookedDates = [];
* if (props.route.params != null) {
* @param day The day selected if (props.route.params.item != null) this.item = props.route.params.item;
*/ else this.item = null;
selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => {
const selected = new Date(day.dateString);
const start = this.getBookStartDate();
if (!(this.lockedDates.hasOwnProperty(day.dateString))) {
if (start === null) {
this.updateSelectionRange(selected, selected);
this.enableBooking();
} else if (start.getTime() === selected.getTime()) {
this.resetSelection();
} else if (this.bookedDates.length === 1) {
this.updateSelectionRange(start, selected);
this.enableBooking();
} else
this.resetSelection();
this.updateMarkedSelection();
}
} }
const {item} = this;
updateSelectionRange(start: Date, end: Date) { if (item != null) {
this.bookedDates = getValidRange(start, end, this.item); this.lockedDates = {};
item.booked_at.forEach((date: {begin: string, end: string}) => {
const range = getValidRange(
new Date(date.begin),
new Date(date.end),
null,
);
this.lockedDates = {
...this.lockedDates,
...generateMarkedDates(false, props.theme, range),
};
});
} }
}
updateMarkedSelection() { /**
this.setState({ * Captures focus and blur events to hook on android back button
markedDates: generateMarkedDates( */
true, componentDidMount() {
this.props.theme, const {navigation} = this.props;
this.bookedDates navigation.addListener('focus', () => {
), BackHandler.addEventListener(
}); 'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
navigation.addListener('blur', () => {
BackHandler.removeEventListener(
'hardwareBackPress',
this.onBackButtonPressAndroid,
);
});
}
/**
* Overrides default android back button behaviour to deselect date if any is selected.
*
* @return {boolean}
*/
onBackButtonPressAndroid = (): boolean => {
if (this.bookedDates.length > 0) {
this.resetSelection();
this.updateMarkedSelection();
return true;
} }
return false;
};
enableBooking() { onDialogDismiss = () => {
if (!this.canBookEquipment) { this.setState({dialogVisible: false});
this.showBookButton(); };
this.canBookEquipment = true;
} onErrorDialogDismiss = () => {
this.setState({errorDialogVisible: false});
};
/**
* Sends the selected data to the server and waits for a response.
* If the request is a success, navigate to the recap screen.
* If it is an error, display the error to the user.
*
* @returns {Promise<void>}
*/
onDialogAccept = (): Promise<void> => {
return new Promise((resolve: () => void) => {
const {item, props} = this;
const start = this.getBookStartDate();
const end = this.getBookEndDate();
if (item != null && start != null && end != null) {
ConnectionManager.getInstance()
.authenticatedRequest('location/booking', {
device: item.id,
begin: getISODate(start),
end: getISODate(end),
})
.then(() => {
this.onDialogDismiss();
props.navigation.replace('equipment-confirm', {
item: this.item,
dates: [getISODate(start), getISODate(end)],
});
resolve();
})
.catch((error: number) => {
this.onDialogDismiss();
this.showErrorDialog(error);
resolve();
});
} else {
this.onDialogDismiss();
resolve();
}
});
};
getBookStartDate(): Date | null {
return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
}
getBookEndDate(): Date | null {
const {length} = this.bookedDates;
return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
}
/**
* Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all.
*
* @param day The day selected
*/
selectNewDate = (day: {
dateString: string,
day: number,
month: number,
timestamp: number,
year: number,
}) => {
const selected = new Date(day.dateString);
const start = this.getBookStartDate();
if (!this.lockedDates[day.dateString] != null) {
if (start === null) {
this.updateSelectionRange(selected, selected);
this.enableBooking();
} else if (start.getTime() === selected.getTime()) {
this.resetSelection();
} else if (this.bookedDates.length === 1) {
this.updateSelectionRange(start, selected);
this.enableBooking();
} else this.resetSelection();
this.updateMarkedSelection();
} }
};
resetSelection() { showErrorDialog = (error: number) => {
if (this.canBookEquipment) this.setState({
this.hideBookButton(); errorDialogVisible: true,
this.canBookEquipment = false; currentError: error,
this.bookedDates = []; });
};
showDialog = () => {
this.setState({dialogVisible: true});
};
/**
* Shows the book button by plying a fade animation
*/
showBookButton() {
if (this.bookRef.current != null) {
this.bookRef.current.fadeInUp(500);
} }
}
/** /**
* Shows the book button by plying a fade animation * Hides the book button by plying a fade animation
*/ */
showBookButton() { hideBookButton() {
if (this.bookRef.current != null) { if (this.bookRef.current != null) {
this.bookRef.current.fadeInUp(500); this.bookRef.current.fadeOutDown(500);
}
} }
}
/** enableBooking() {
* Hides the book button by plying a fade animation if (!this.canBookEquipment) {
*/ this.showBookButton();
hideBookButton() { this.canBookEquipment = true;
if (this.bookRef.current != null) {
this.bookRef.current.fadeOutDown(500);
}
} }
}
showDialog = () => { resetSelection() {
this.setState({dialogVisible: true}); if (this.canBookEquipment) this.hideBookButton();
} this.canBookEquipment = false;
this.bookedDates = [];
}
showErrorDialog = (error: number) => { updateSelectionRange(start: Date, end: Date) {
this.setState({ this.bookedDates = getValidRange(start, end, this.item);
errorDialogVisible: true, }
currentError: error,
});
}
onDialogDismiss = () => { updateMarkedSelection() {
this.setState({dialogVisible: false}); const {theme} = this.props;
} this.setState({
markedDates: generateMarkedDates(true, theme, this.bookedDates),
});
}
onErrorDialogDismiss = () => { render(): React.Node {
this.setState({errorDialogVisible: false}); const {item, props, state} = this;
} const start = this.getBookStartDate();
const end = this.getBookEndDate();
/** let subHeadingText;
* Sends the selected data to the server and waits for a response. if (start == null) subHeadingText = i18n.t('screens.equipment.booking');
* If the request is a success, navigate to the recap screen. else if (end != null && start.getTime() !== end.getTime())
* If it is an error, display the error to the user. subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
* begin: getRelativeDateString(start),
* @returns {Promise<R>} end: getRelativeDateString(end),
*/ });
onDialogAccept = () => { else
return new Promise((resolve) => { i18n.t('screens.equipment.bookingDay', {
const item = this.item; date: getRelativeDateString(start),
const start = this.getBookStartDate(); });
const end = this.getBookEndDate(); if (item != null) {
if (item != null && start != null && end != null) { const isAvailable = isEquipmentAvailable(item);
console.log({ const firstAvailability = getFirstEquipmentAvailability(item);
"device": item.id, return (
"begin": getISODate(start), <View style={{flex: 1}}>
"end": getISODate(end), <CollapsibleScrollView>
}) <Card style={{margin: 5}}>
ConnectionManager.getInstance().authenticatedRequest( <Card.Content>
"location/booking",
{
"device": item.id,
"begin": getISODate(start),
"end": getISODate(end),
})
.then(() => {
this.onDialogDismiss();
this.props.navigation.replace("equipment-confirm", {
item: this.item,
dates: [getISODate(start), getISODate(end)]
});
resolve();
})
.catch((error: number) => {
this.onDialogDismiss();
this.showErrorDialog(error);
resolve();
});
} else {
this.onDialogDismiss();
resolve();
}
});
}
getBookStartDate() {
return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
}
getBookEndDate() {
const length = this.bookedDates.length;
return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
}
render() {
const item = this.item;
const start = this.getBookStartDate();
const end = this.getBookEndDate();
if (item != null) {
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<CollapsibleScrollView> <View
<Card style={{margin: 5}}> style={{
<Card.Content> marginLeft: 'auto',
<View style={{flex: 1}}> marginRight: 'auto',
<View style={{ flexDirection: 'row',
marginLeft: "auto", flexWrap: 'wrap',
marginRight: "auto", }}>
flexDirection: "row", <Headline style={{textAlign: 'center'}}>
flexWrap: "wrap", {item.name}
}}> </Headline>
<Headline style={{textAlign: "center"}}> <Caption
{item.name} style={{
</Headline> textAlign: 'center',
<Caption style={{ lineHeight: 35,
textAlign: "center", marginLeft: 10,
lineHeight: 35, }}>
marginLeft: 10, ({i18n.t('screens.equipment.bail', {cost: item.caution})})
}}> </Caption>
({i18n.t('screens.equipment.bail', {cost: item.caution})}) </View>
</Caption>
</View>
</View>
<Button
icon={isAvailable ? "check-circle-outline" : "update"}
color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
mode="text"
>
{i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)})}
</Button>
<Subheading style={{
textAlign: "center",
marginBottom: 10,
minHeight: 50
}}>
{
start == null
? i18n.t('screens.equipment.booking')
: end != null && start.getTime() !== end.getTime()
? i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start),
end: getRelativeDateString(end)
})
: i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start)
})
}
</Subheading>
</Card.Content>
</Card>
<CalendarList
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
minDate={new Date()}
// Max amount of months allowed to scroll to the past. Default = 50
pastScrollRange={0}
// Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={3}
// Enable horizontal scrolling, default = false
horizontal={true}
// Enable paging on horizontal, default = false
pagingEnabled={true}
// Handler which gets executed on day press. Default = undefined
onDayPress={this.selectNewDate}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1}
// Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
disableAllTouchEventsForDisabledDays={true}
// Hide month navigation arrows.
hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType={'period'}
markedDates={{...this.lockedDates, ...this.state.markedDates}}
theme={{
backgroundColor: this.props.theme.colors.agendaBackgroundColor,
calendarBackground: this.props.theme.colors.background,
textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: this.props.theme.colors.primary,
selectedDayTextColor: '#ffffff',
todayTextColor: this.props.theme.colors.text,
dayTextColor: this.props.theme.colors.text,
textDisabledColor: this.props.theme.colors.agendaDayTextColor,
dotColor: this.props.theme.colors.primary,
selectedDotColor: '#ffffff',
arrowColor: this.props.theme.colors.primary,
monthTextColor: this.props.theme.colors.text,
indicatorColor: this.props.theme.colors.primary,
textDayFontFamily: 'monospace',
textMonthFontFamily: 'monospace',
textDayHeaderFontFamily: 'monospace',
textDayFontWeight: '300',
textMonthFontWeight: 'bold',
textDayHeaderFontWeight: '300',
textDayFontSize: 16,
textMonthFontSize: 16,
textDayHeaderFontSize: 16,
'stylesheet.day.period': {
base: {
overflow: 'hidden',
height: 34,
width: 34,
alignItems: 'center',
}
}
}}
style={{marginBottom: 50}}
/>
</CollapsibleScrollView>
<LoadingConfirmDialog
visible={this.state.dialogVisible}
onDismiss={this.onDialogDismiss}
onAccept={this.onDialogAccept}
title={i18n.t('screens.equipment.dialogTitle')}
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
message={i18n.t('screens.equipment.dialogMessage')}
/>
<ErrorDialog
visible={this.state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={this.state.currentError}
/>
<Animatable.View
ref={this.bookRef}
style={{
position: "absolute",
bottom: 0,
left: 0,
width: "100%",
flex: 1,
transform: [
{translateY: 100},
]
}}>
<Button
icon="bookmark-check"
mode="contained"
onPress={this.showDialog}
style={{
width: "80%",
flex: 1,
marginLeft: "auto",
marginRight: "auto",
marginBottom: 20,
borderRadius: 10
}}
>
{i18n.t('screens.equipment.bookButton')}
</Button>
</Animatable.View>
</View> </View>
) <Button
} else icon={isAvailable ? 'check-circle-outline' : 'update'}
return <View/>; color={
} isAvailable
? props.theme.colors.success
: props.theme.colors.primary
}
mode="text">
{i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
})}
</Button>
<Subheading
style={{
textAlign: 'center',
marginBottom: 10,
minHeight: 50,
}}>
{subHeadingText}
</Subheading>
</Card.Content>
</Card>
<CalendarList
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
minDate={new Date()}
// Max amount of months allowed to scroll to the past. Default = 50
pastScrollRange={0}
// Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={3}
// Enable horizontal scrolling, default = false
horizontal
// Enable paging on horizontal, default = false
pagingEnabled
// Handler which gets executed on day press. Default = undefined
onDayPress={this.selectNewDate}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1}
// Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
disableAllTouchEventsForDisabledDays
// Hide month navigation arrows.
hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType="period"
markedDates={{...this.lockedDates, ...state.markedDates}}
theme={{
backgroundColor: props.theme.colors.agendaBackgroundColor,
calendarBackground: props.theme.colors.background,
textSectionTitleColor: props.theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: props.theme.colors.primary,
selectedDayTextColor: '#ffffff',
todayTextColor: props.theme.colors.text,
dayTextColor: props.theme.colors.text,
textDisabledColor: props.theme.colors.agendaDayTextColor,
dotColor: props.theme.colors.primary,
selectedDotColor: '#ffffff',
arrowColor: props.theme.colors.primary,
monthTextColor: props.theme.colors.text,
indicatorColor: props.theme.colors.primary,
textDayFontFamily: 'monospace',
textMonthFontFamily: 'monospace',
textDayHeaderFontFamily: 'monospace',
textDayFontWeight: '300',
textMonthFontWeight: 'bold',
textDayHeaderFontWeight: '300',
textDayFontSize: 16,
textMonthFontSize: 16,
textDayHeaderFontSize: 16,
'stylesheet.day.period': {
base: {
overflow: 'hidden',
height: 34,
width: 34,
alignItems: 'center',
},
},
}}
style={{marginBottom: 50}}
/>
</CollapsibleScrollView>
<LoadingConfirmDialog
visible={state.dialogVisible}
onDismiss={this.onDialogDismiss}
onAccept={this.onDialogAccept}
title={i18n.t('screens.equipment.dialogTitle')}
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
message={i18n.t('screens.equipment.dialogMessage')}
/>
<ErrorDialog
visible={state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss}
errorCode={state.currentError}
/>
<Animatable.View
ref={this.bookRef}
style={{
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
flex: 1,
transform: [{translateY: 100}],
}}>
<Button
icon="bookmark-check"
mode="contained"
onPress={this.showDialog}
style={{
width: '80%',
flex: 1,
marginLeft: 'auto',
marginRight: 'auto',
marginBottom: 20,
borderRadius: 10,
}}>
{i18n.t('screens.equipment.bookButton')}
</Button>
</Animatable.View>
</View>
);
}
return null;
}
} }
export default withTheme(EquipmentRentScreen); export default withTheme(EquipmentRentScreen);

View file

@ -1,417 +1,446 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Image, KeyboardAvoidingView, StyleSheet, View} from "react-native"; import {Image, KeyboardAvoidingView, StyleSheet, View} from 'react-native';
import {Button, Card, HelperText, TextInput, withTheme} from 'react-native-paper'; import {
import ConnectionManager from "../../managers/ConnectionManager"; Button,
Card,
HelperText,
TextInput,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import ErrorDialog from "../../components/Dialogs/ErrorDialog"; import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomTheme} from "../../managers/ThemeManager"; import LinearGradient from 'react-native-linear-gradient';
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import ConnectionManager from '../../managers/ConnectionManager';
import {StackNavigationProp} from "@react-navigation/stack"; import ErrorDialog from '../../components/Dialogs/ErrorDialog';
import AvailableWebsites from "../../constants/AvailableWebsites"; import type {CustomThemeType} from '../../managers/ThemeManager';
import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import AsyncStorageManager from '../../managers/AsyncStorageManager';
import MascotPopup from "../../components/Mascot/MascotPopup"; import AvailableWebsites from '../../constants/AvailableWebsites';
import LinearGradient from "react-native-linear-gradient"; import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView"; import MascotPopup from '../../components/Mascot/MascotPopup';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { params: { nextScreen: string } }, route: {params: {nextScreen: string}},
theme: CustomTheme theme: CustomThemeType,
} };
type State = { type StateType = {
email: string, email: string,
password: string, password: string,
isEmailValidated: boolean, isEmailValidated: boolean,
isPasswordValidated: boolean, isPasswordValidated: boolean,
loading: boolean, loading: boolean,
dialogVisible: boolean, dialogVisible: boolean,
dialogError: number, dialogError: number,
mascotDialogVisible: boolean, mascotDialogVisible: boolean,
} };
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
const RESET_PASSWORD_PATH = "https://www.amicale-insat.fr/password/reset"; const RESET_PASSWORD_PATH = 'https://www.amicale-insat.fr/password/reset';
const emailRegex = /^.+@.+\..+$/; const emailRegex = /^.+@.+\..+$/;
class LoginScreen extends React.Component<Props, State> {
state = {
email: '',
password: '',
isEmailValidated: false,
isPasswordValidated: false,
loading: false,
dialogVisible: false,
dialogError: 0,
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.loginShowBanner.key),
};
onEmailChange: (value: string) => null;
onPasswordChange: (value: string) => null;
passwordInputRef: { current: null | TextInput };
nextScreen: string | null;
constructor(props) {
super(props);
this.passwordInputRef = React.createRef();
this.onEmailChange = this.onInputChange.bind(this, true);
this.onPasswordChange = this.onInputChange.bind(this, false);
this.props.navigation.addListener('focus', this.onScreenFocus);
}
onScreenFocus = () => {
this.handleNavigationParams();
};
/**
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
*/
handleNavigationParams() {
if (this.props.route.params != null) {
if (this.props.route.params.nextScreen != null)
this.nextScreen = this.props.route.params.nextScreen;
else
this.nextScreen = null;
}
}
hideMascotDialog = () => {
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.loginShowBanner.key, false);
this.setState({mascotDialogVisible: false})
};
showMascotDialog = () => {
this.setState({mascotDialogVisible: true})
};
/**
* Shows an error dialog with the corresponding login error
*
* @param error The error given by the login request
*/
showErrorDialog = (error: number) =>
this.setState({
dialogVisible: true,
dialogError: error,
});
hideErrorDialog = () => this.setState({dialogVisible: false});
/**
* Navigates to the screen specified in navigation parameters or simply go back tha stack.
* Saves in user preferences to not show the login banner again.
*/
handleSuccess = () => {
// Do not show the home login banner again
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.homeShowBanner.key, false);
if (this.nextScreen == null)
this.props.navigation.goBack();
else
this.props.navigation.replace(this.nextScreen);
};
/**
* Navigates to the Amicale website screen with the reset password link as navigation parameters
*/
onResetPasswordClick = () => this.props.navigation.navigate("website", {
host: AvailableWebsites.websites.AMICALE,
path: RESET_PASSWORD_PATH,
title: i18n.t('screens.websites.amicale')
});
/**
* The user has unfocused the input, his email is ready to be validated
*/
validateEmail = () => this.setState({isEmailValidated: true});
/**
* Checks if the entered email is valid (matches the regex)
*
* @returns {boolean}
*/
isEmailValid() {
return emailRegex.test(this.state.email);
}
/**
* 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}
*/
shouldShowEmailError() {
return this.state.isEmailValidated && !this.isEmailValid();
}
/**
* The user has unfocused the input, his password is ready to be validated
*/
validatePassword = () => this.setState({isPasswordValidated: true});
/**
* Checks if the user has entered a password
*
* @returns {boolean}
*/
isPasswordValid() {
return this.state.password !== '';
}
/**
* 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}
*/
shouldShowPasswordError() {
return this.state.isPasswordValidated && !this.isPasswordValid();
}
/**
* If the email and password are valid, and we are not loading a request, then the login button can be enabled
*
* @returns {boolean}
*/
shouldEnableLogin() {
return this.isEmailValid() && this.isPasswordValid() && !this.state.loading;
}
/**
* Called when the user input changes in the email or password field.
* This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
*
* @param isEmail True if the field is the email field
* @param value The new field value
*/
onInputChange(isEmail: boolean, value: string) {
if (isEmail) {
this.setState({
email: value,
isEmailValidated: false,
});
} else {
this.setState({
password: value,
isPasswordValidated: false,
});
}
}
/**
* Focuses the password field when the email field is done
*
* @returns {*}
*/
onEmailSubmit = () => {
if (this.passwordInputRef.current != null)
this.passwordInputRef.current.focus();
}
/**
* Called when the user clicks on login or finishes to type his password.
*
* Checks if we should allow the user to login,
* then makes the login request and enters a loading state until the request finishes
*
*/
onSubmit = () => {
if (this.shouldEnableLogin()) {
this.setState({loading: true});
ConnectionManager.getInstance().connect(this.state.email, this.state.password)
.then(this.handleSuccess)
.catch(this.showErrorDialog)
.finally(() => {
this.setState({loading: false});
});
}
};
/**
* Gets the form input
*
* @returns {*}
*/
getFormInput() {
return (
<View>
<TextInput
label={i18n.t("screens.login.email")}
mode='outlined'
value={this.state.email}
onChangeText={this.onEmailChange}
onBlur={this.validateEmail}
onSubmitEditing={this.onEmailSubmit}
error={this.shouldShowEmailError()}
textContentType={'emailAddress'}
autoCapitalize={'none'}
autoCompleteType={'email'}
autoCorrect={false}
keyboardType={'email-address'}
returnKeyType={'next'}
secureTextEntry={false}
/>
<HelperText
type="error"
visible={this.shouldShowEmailError()}
>
{i18n.t("screens.login.emailError")}
</HelperText>
<TextInput
ref={this.passwordInputRef}
label={i18n.t("screens.login.password")}
mode='outlined'
value={this.state.password}
onChangeText={this.onPasswordChange}
onBlur={this.validatePassword}
onSubmitEditing={this.onSubmit}
error={this.shouldShowPasswordError()}
textContentType={'password'}
autoCapitalize={'none'}
autoCompleteType={'password'}
autoCorrect={false}
keyboardType={'default'}
returnKeyType={'done'}
secureTextEntry={true}
/>
<HelperText
type="error"
visible={this.shouldShowPasswordError()}
>
{i18n.t("screens.login.passwordError")}
</HelperText>
</View>
);
}
/**
* Gets the card containing the input form
* @returns {*}
*/
getMainCard() {
return (
<View style={styles.card}>
<Card.Title
title={i18n.t("screens.login.title")}
titleStyle={{color: "#fff"}}
subtitle={i18n.t("screens.login.subtitle")}
subtitleStyle={{color: "#fff"}}
left={(props) => <Image
{...props}
source={ICON_AMICALE}
style={{
width: props.size,
height: props.size,
}}/>}
/>
<Card.Content>
{this.getFormInput()}
<Card.Actions style={{flexWrap: "wrap"}}>
<Button
icon="lock-question"
mode="contained"
onPress={this.onResetPasswordClick}
color={this.props.theme.colors.warning}
style={{marginRight: 'auto', marginBottom: 20}}>
{i18n.t("screens.login.resetPassword")}
</Button>
<Button
icon="send"
mode="contained"
disabled={!this.shouldEnableLogin()}
loading={this.state.loading}
onPress={this.onSubmit}
style={{marginLeft: 'auto'}}>
{i18n.t("screens.login.title")}
</Button>
</Card.Actions>
<Card.Actions>
<Button
icon="help-circle"
mode="contained"
onPress={this.showMascotDialog}
style={{
marginLeft: 'auto',
marginRight: 'auto',
}}>
{i18n.t("screens.login.mascotDialog.title")}
</Button>
</Card.Actions>
</Card.Content>
</View>
);
}
render() {
return (
<LinearGradient
style={{
height: "100%"
}}
colors={['#9e0d18', '#530209']}
start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}>
<KeyboardAvoidingView
behavior={"height"}
contentContainerStyle={styles.container}
style={styles.container}
enabled
keyboardVerticalOffset={100}
>
<CollapsibleScrollView>
<View style={{height: "100%"}}>
{this.getMainCard()}
</View>
<MascotPopup
visible={this.state.mascotDialogVisible}
title={i18n.t("screens.login.mascotDialog.title")}
message={i18n.t("screens.login.mascotDialog.message")}
icon={"help"}
buttons={{
action: null,
cancel: {
message: i18n.t("screens.login.mascotDialog.button"),
icon: "check",
onPress: this.hideMascotDialog,
}
}}
emotion={MASCOT_STYLE.NORMAL}
/>
<ErrorDialog
visible={this.state.dialogVisible}
onDismiss={this.hideErrorDialog}
errorCode={this.state.dialogError}
/>
</CollapsibleScrollView>
</KeyboardAvoidingView>
</LinearGradient>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
}, },
card: { card: {
marginTop: 'auto', marginTop: 'auto',
marginBottom: 'auto', marginBottom: 'auto',
}, },
header: { header: {
fontSize: 36, fontSize: 36,
marginBottom: 48 marginBottom: 48,
}, },
textInput: {}, textInput: {},
btnContainer: { btnContainer: {
marginTop: 5, marginTop: 5,
marginBottom: 10, marginBottom: 10,
} },
}); });
class LoginScreen extends React.Component<PropsType, StateType> {
onEmailChange: (value: string) => void;
onPasswordChange: (value: string) => void;
passwordInputRef: {current: null | TextInput};
nextScreen: string | null;
constructor(props: PropsType) {
super(props);
this.passwordInputRef = React.createRef();
this.onEmailChange = (value: string) => {
this.onInputChange(true, value);
};
this.onPasswordChange = (value: string) => {
this.onInputChange(false, value);
};
props.navigation.addListener('focus', this.onScreenFocus);
this.state = {
email: '',
password: '',
isEmailValidated: false,
isPasswordValidated: false,
loading: false,
dialogVisible: false,
dialogError: 0,
mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.loginShowBanner.key,
),
};
}
onScreenFocus = () => {
this.handleNavigationParams();
};
/**
* Navigates to the Amicale website screen with the reset password link as navigation parameters
*/
onResetPasswordClick = () => {
const {navigation} = this.props;
navigation.navigate('website', {
host: AvailableWebsites.websites.AMICALE,
path: RESET_PASSWORD_PATH,
title: i18n.t('screens.websites.amicale'),
});
};
/**
* Called when the user input changes in the email or password field.
* This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
*
* @param isEmail True if the field is the email field
* @param value The new field value
*/
onInputChange(isEmail: boolean, value: string) {
if (isEmail) {
this.setState({
email: value,
isEmailValidated: false,
});
} else {
this.setState({
password: value,
isPasswordValidated: false,
});
}
}
/**
* Focuses the password field when the email field is done
*
* @returns {*}
*/
onEmailSubmit = () => {
if (this.passwordInputRef.current != null)
this.passwordInputRef.current.focus();
};
/**
* Called when the user clicks on login or finishes to type his password.
*
* Checks if we should allow the user to login,
* then makes the login request and enters a loading state until the request finishes
*
*/
onSubmit = () => {
const {email, password} = this.state;
if (this.shouldEnableLogin()) {
this.setState({loading: true});
ConnectionManager.getInstance()
.connect(email, password)
.then(this.handleSuccess)
.catch(this.showErrorDialog)
.finally(() => {
this.setState({loading: false});
});
}
};
/**
* Gets the form input
*
* @returns {*}
*/
getFormInput(): React.Node {
const {email, password} = this.state;
return (
<View>
<TextInput
label={i18n.t('screens.login.email')}
mode="outlined"
value={email}
onChangeText={this.onEmailChange}
onBlur={this.validateEmail}
onSubmitEditing={this.onEmailSubmit}
error={this.shouldShowEmailError()}
textContentType="emailAddress"
autoCapitalize="none"
autoCompleteType="email"
autoCorrect={false}
keyboardType="email-address"
returnKeyType="next"
secureTextEntry={false}
/>
<HelperText type="error" visible={this.shouldShowEmailError()}>
{i18n.t('screens.login.emailError')}
</HelperText>
<TextInput
ref={this.passwordInputRef}
label={i18n.t('screens.login.password')}
mode="outlined"
value={password}
onChangeText={this.onPasswordChange}
onBlur={this.validatePassword}
onSubmitEditing={this.onSubmit}
error={this.shouldShowPasswordError()}
textContentType="password"
autoCapitalize="none"
autoCompleteType="password"
autoCorrect={false}
keyboardType="default"
returnKeyType="done"
secureTextEntry
/>
<HelperText type="error" visible={this.shouldShowPasswordError()}>
{i18n.t('screens.login.passwordError')}
</HelperText>
</View>
);
}
/**
* Gets the card containing the input form
* @returns {*}
*/
getMainCard(): React.Node {
const {props, state} = this;
return (
<View style={styles.card}>
<Card.Title
title={i18n.t('screens.login.title')}
titleStyle={{color: '#fff'}}
subtitle={i18n.t('screens.login.subtitle')}
subtitleStyle={{color: '#fff'}}
left={({size}: {size: number}): React.Node => (
<Image
source={ICON_AMICALE}
style={{
width: size,
height: size,
}}
/>
)}
/>
<Card.Content>
{this.getFormInput()}
<Card.Actions style={{flexWrap: 'wrap'}}>
<Button
icon="lock-question"
mode="contained"
onPress={this.onResetPasswordClick}
color={props.theme.colors.warning}
style={{marginRight: 'auto', marginBottom: 20}}>
{i18n.t('screens.login.resetPassword')}
</Button>
<Button
icon="send"
mode="contained"
disabled={!this.shouldEnableLogin()}
loading={state.loading}
onPress={this.onSubmit}
style={{marginLeft: 'auto'}}>
{i18n.t('screens.login.title')}
</Button>
</Card.Actions>
<Card.Actions>
<Button
icon="help-circle"
mode="contained"
onPress={this.showMascotDialog}
style={{
marginLeft: 'auto',
marginRight: 'auto',
}}>
{i18n.t('screens.login.mascotDialog.title')}
</Button>
</Card.Actions>
</Card.Content>
</View>
);
}
/**
* The user has unfocused the input, his email is ready to be validated
*/
validateEmail = () => {
this.setState({isEmailValidated: true});
};
/**
* The user has unfocused the input, his password is ready to be validated
*/
validatePassword = () => {
this.setState({isPasswordValidated: true});
};
hideMascotDialog = () => {
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.loginShowBanner.key,
false,
);
this.setState({mascotDialogVisible: false});
};
showMascotDialog = () => {
this.setState({mascotDialogVisible: true});
};
/**
* Shows an error dialog with the corresponding login error
*
* @param error The error given by the login request
*/
showErrorDialog = (error: number) => {
this.setState({
dialogVisible: true,
dialogError: error,
});
};
hideErrorDialog = () => {
this.setState({dialogVisible: false});
};
/**
* Navigates to the screen specified in navigation parameters or simply go back tha stack.
* Saves in user preferences to not show the login banner again.
*/
handleSuccess = () => {
const {navigation} = this.props;
// Do not show the home login banner again
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.homeShowBanner.key,
false,
);
if (this.nextScreen == null) navigation.goBack();
else navigation.replace(this.nextScreen);
};
/**
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
*/
handleNavigationParams() {
const {route} = this.props;
if (route.params != null) {
if (route.params.nextScreen != null)
this.nextScreen = route.params.nextScreen;
else this.nextScreen = null;
}
}
/**
* Checks if the entered email is valid (matches the regex)
*
* @returns {boolean}
*/
isEmailValid(): boolean {
const {email} = this.state;
return emailRegex.test(email);
}
/**
* 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}
*/
shouldShowEmailError(): boolean {
const {isEmailValidated} = this.state;
return isEmailValidated && !this.isEmailValid();
}
/**
* Checks if the user has entered a password
*
* @returns {boolean}
*/
isPasswordValid(): boolean {
const {password} = this.state;
return password !== '';
}
/**
* 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}
*/
shouldShowPasswordError(): boolean {
const {isPasswordValidated} = this.state;
return isPasswordValidated && !this.isPasswordValid();
}
/**
* If the email and password are valid, and we are not loading a request, then the login button can be enabled
*
* @returns {boolean}
*/
shouldEnableLogin(): boolean {
const {loading} = this.state;
return this.isEmailValid() && this.isPasswordValid() && !loading;
}
render(): React.Node {
const {mascotDialogVisible, dialogVisible, dialogError} = this.state;
return (
<LinearGradient
style={{
height: '100%',
}}
colors={['#9e0d18', '#530209']}
start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}>
<KeyboardAvoidingView
behavior="height"
contentContainerStyle={styles.container}
style={styles.container}
enabled
keyboardVerticalOffset={100}>
<CollapsibleScrollView>
<View style={{height: '100%'}}>{this.getMainCard()}</View>
<MascotPopup
visible={mascotDialogVisible}
title={i18n.t('screens.login.mascotDialog.title')}
message={i18n.t('screens.login.mascotDialog.message')}
icon="help"
buttons={{
action: null,
cancel: {
message: i18n.t('screens.login.mascotDialog.button'),
icon: 'check',
onPress: this.hideMascotDialog,
},
}}
emotion={MASCOT_STYLE.NORMAL}
/>
<ErrorDialog
visible={dialogVisible}
onDismiss={this.hideErrorDialog}
errorCode={dialogError}
/>
</CollapsibleScrollView>
</KeyboardAvoidingView>
</LinearGradient>
);
}
}
export default withTheme(LoginScreen); export default withTheme(LoginScreen);

View file

@ -1,432 +1,467 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {FlatList, StyleSheet, View} from "react-native"; import {FlatList, StyleSheet, View} from 'react-native';
import {Avatar, Button, Card, Divider, List, Paragraph, withTheme} from 'react-native-paper'; import {
import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen"; Avatar,
Button,
Card,
Divider,
List,
Paragraph,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import LogoutDialog from "../../components/Amicale/LogoutDialog"; import {StackNavigationProp} from '@react-navigation/stack';
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton"; import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
import type {cardList} from "../../components/Lists/CardList/CardList"; import LogoutDialog from '../../components/Amicale/LogoutDialog';
import CardList from "../../components/Lists/CardList/CardList"; import MaterialHeaderButtons, {
import {StackNavigationProp} from "@react-navigation/stack"; Item,
import type {CustomTheme} from "../../managers/ThemeManager"; } from '../../components/Overrides/CustomHeaderButton';
import AvailableWebsites from "../../constants/AvailableWebsites"; import CardList from '../../components/Lists/CardList/CardList';
import Mascot, {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import type {CustomThemeType} from '../../managers/ThemeManager';
import ServicesManager, {SERVICES_KEY} from "../../managers/ServicesManager"; import AvailableWebsites from '../../constants/AvailableWebsites';
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList"; import Mascot, {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import ServicesManager, {SERVICES_KEY} from '../../managers/ServicesManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type {ServiceItemType} from '../../managers/ServicesManager';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomThemeType,
} };
type State = { type StateType = {
dialogVisible: boolean, dialogVisible: boolean,
} };
type ProfileData = { type ClubType = {
first_name: string, id: number,
last_name: string, name: string,
email: string, is_manager: boolean,
birthday: string, };
phone: string,
branch: string,
link: string,
validity: boolean,
clubs: Array<Club>,
}
type Club = {
id: number,
name: string,
is_manager: boolean,
}
class ProfileScreen extends React.Component<Props, State> { type ProfileDataType = {
first_name: string,
state = { last_name: string,
dialogVisible: false, email: string,
}; birthday: string,
phone: string,
data: ProfileData; branch: string,
link: string,
flatListData: Array<{ id: string }>; validity: boolean,
amicaleDataset: cardList; clubs: Array<ClubType>,
};
constructor(props: Props) {
super(props);
this.flatListData = [
{id: '0'},
{id: '1'},
{id: '2'},
{id: '3'},
]
const services = new ServicesManager(props.navigation);
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
}
componentDidMount() {
this.props.navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
showDisconnectDialog = () => this.setState({dialogVisible: true});
hideDisconnectDialog = () => this.setState({dialogVisible: false});
/**
* Gets the logout header button
*
* @returns {*}
*/
getHeaderButton = () => <MaterialHeaderButtons>
<Item title="logout" iconName="logout" onPress={this.showDisconnectDialog}/>
</MaterialHeaderButtons>;
/**
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<{ [key: string]: any } | null>) => {
if (data[0] != null) {
this.data = data[0];
}
return (
<View style={{flex: 1}}>
<CollapsibleFlatList
renderItem={this.getRenderItem}
data={this.flatListData}
/>
<LogoutDialog
{...this.props}
visible={this.state.dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
)
};
getRenderItem = ({item}: { item: { id: string } }) => {
switch (item.id) {
case '0':
return this.getWelcomeCard();
case '1':
return this.getPersonalCard();
case '2':
return this.getClubCard();
default:
return this.getMembershipCar();
}
};
/**
* Gets the list of services available with the Amicale account
*
* @returns {*}
*/
getServicesList() {
return (
<CardList
dataset={this.amicaleDataset}
isHorizontal={true}
/>
);
}
/**
* Gets a card welcoming the user to his account
*
* @returns {*}
*/
getWelcomeCard() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("screens.profile.welcomeTitle", {name: this.data.first_name})}
left={() =>
<Mascot
style={{
width: 60
}}
emotion={MASCOT_STYLE.COOL}
animated={true}
entryAnimation={{
animation: "bounceIn",
duration: 1000
}}
/>}
titleStyle={{marginLeft: 10}}
/>
<Card.Content>
<Divider/>
<Paragraph>
{i18n.t("screens.profile.welcomeDescription")}
</Paragraph>
{this.getServicesList()}
<Paragraph>
{i18n.t("screens.profile.welcomeFeedback")}
</Paragraph>
<Divider/>
<Card.Actions>
<Button
icon="bug"
mode="contained"
onPress={() => this.props.navigation.navigate('feedback')}
style={styles.editButton}>
{i18n.t("screens.feedback.homeButtonTitle")}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Checks if the given field is available
*
* @param field The field to check
* @return {boolean}
*/
isFieldAvailable(field: ?string) {
return field !== null;
}
/**
* Gets the given field value.
* If the field does not have a value, returns a placeholder text
*
* @param field The field to get the value from
* @return {*}
*/
getFieldValue(field: ?string) {
return this.isFieldAvailable(field)
? field
: i18n.t("screens.profile.noData");
}
/**
* Gets a list item showing personal information
*
* @param field The field to display
* @param icon The icon to use
* @return {*}
*/
getPersonalListItem(field: ?string, icon: string) {
let title = this.isFieldAvailable(field) ? this.getFieldValue(field) : ':(';
let subtitle = this.isFieldAvailable(field) ? '' : this.getFieldValue(field);
return (
<List.Item
title={title}
description={subtitle}
left={props => <List.Icon
{...props}
icon={icon}
color={this.isFieldAvailable(field) ? undefined : this.props.theme.colors.textDisabled}
/>}
/>
);
}
/**
* Gets a card containing user personal information
*
* @return {*}
*/
getPersonalCard() {
return (
<Card style={styles.card}>
<Card.Title
title={this.data.first_name + ' ' + this.data.last_name}
subtitle={this.data.email}
left={(props) => <Avatar.Icon
{...props}
icon="account"
color={this.props.theme.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<Divider/>
<List.Section>
<List.Subheader>{i18n.t("screens.profile.personalInformation")}</List.Subheader>
{this.getPersonalListItem(this.data.birthday, "cake-variant")}
{this.getPersonalListItem(this.data.phone, "phone")}
{this.getPersonalListItem(this.data.email, "email")}
{this.getPersonalListItem(this.data.branch, "school")}
</List.Section>
<Divider/>
<Card.Actions>
<Button
icon="account-edit"
mode="contained"
onPress={() => this.props.navigation.navigate("website", {
host: AvailableWebsites.websites.AMICALE,
path: this.data.link,
title: i18n.t('screens.websites.amicale')
})}
style={styles.editButton}>
{i18n.t("screens.profile.editInformation")}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Gets a cars containing clubs the user is part of
*
* @return {*}
*/
getClubCard() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("screens.profile.clubs")}
subtitle={i18n.t("screens.profile.clubsSubtitle")}
left={(props) => <Avatar.Icon
{...props}
icon="account-group"
color={this.props.theme.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<Divider/>
{this.getClubList(this.data.clubs)}
</Card.Content>
</Card>
);
}
/**
* Gets a card showing if the user has payed his membership
*
* @return {*}
*/
getMembershipCar() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("screens.profile.membership")}
subtitle={i18n.t("screens.profile.membershipSubtitle")}
left={(props) => <Avatar.Icon
{...props}
icon="credit-card"
color={this.props.theme.colors.primary}
style={styles.icon}
/>}
/>
<Card.Content>
<List.Section>
{this.getMembershipItem(this.data.validity)}
</List.Section>
</Card.Content>
</Card>
);
}
/**
* Gets the item showing if the user has payed his membership
*
* @return {*}
*/
getMembershipItem(state: boolean) {
return (
<List.Item
title={state ? i18n.t("screens.profile.membershipPayed") : i18n.t("screens.profile.membershipNotPayed")}
left={props => <List.Icon
{...props}
color={state ? this.props.theme.colors.success : this.props.theme.colors.danger}
icon={state ? 'check' : 'close'}
/>}
/>
);
}
/**
* Opens the club details screen for the club of given ID
* @param id The club's id to open
*/
openClubDetailsScreen(id: number) {
this.props.navigation.navigate("club-information", {clubId: id});
}
/**
* Gets a list item for the club list
*
* @param item The club to render
* @return {*}
*/
clubListItem = ({item}: { item: Club }) => {
const onPress = () => this.openClubDetailsScreen(item.id);
let description = i18n.t("screens.profile.isMember");
let icon = (props) => <List.Icon {...props} icon="chevron-right"/>;
if (item.is_manager) {
description = i18n.t("screens.profile.isManager");
icon = (props) => <List.Icon {...props} icon="star" color={this.props.theme.colors.primary}/>;
}
return <List.Item
title={item.name}
description={description}
left={icon}
onPress={onPress}
/>;
};
clubKeyExtractor = (item: Club) => item.name;
sortClubList = (a: Club, b: Club) => a.is_manager ? -1 : 1;
/**
* Renders the list of clubs the user is part of
*
* @param list The club list
* @return {*}
*/
getClubList(list: Array<Club>) {
list.sort(this.sortClubList);
return (
//$FlowFixMe
<FlatList
renderItem={this.clubListItem}
keyExtractor={this.clubKeyExtractor}
data={list}
/>
);
}
render() {
return (
<AuthenticatedScreen
{...this.props}
requests={[
{
link: 'user/profile',
params: {},
mandatory: true,
}
]}
renderFunction={this.getScreen}
/>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
margin: 10, margin: 10,
}, },
icon: { icon: {
backgroundColor: 'transparent' backgroundColor: 'transparent',
}, },
editButton: { editButton: {
marginLeft: 'auto' marginLeft: 'auto',
} },
}); });
class ProfileScreen extends React.Component<PropsType, StateType> {
data: ProfileDataType;
flatListData: Array<{id: string}>;
amicaleDataset: Array<ServiceItemType>;
constructor(props: PropsType) {
super(props);
this.flatListData = [{id: '0'}, {id: '1'}, {id: '2'}, {id: '3'}];
const services = new ServicesManager(props.navigation);
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
this.state = {
dialogVisible: false,
};
}
componentDidMount() {
const {navigation} = this.props;
navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
/**
* Gets the logout header button
*
* @returns {*}
*/
getHeaderButton = (): React.Node => (
<MaterialHeaderButtons>
<Item
title="logout"
iconName="logout"
onPress={this.showDisconnectDialog}
/>
</MaterialHeaderButtons>
);
/**
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<ProfileDataType | null>): React.Node => {
const {dialogVisible} = this.state;
const {navigation} = this.props;
// eslint-disable-next-line prefer-destructuring
if (data[0] != null) this.data = data[0];
return (
<View style={{flex: 1}}>
<CollapsibleFlatList
renderItem={this.getRenderItem}
data={this.flatListData}
/>
<LogoutDialog
navigation={navigation}
visible={dialogVisible}
onDismiss={this.hideDisconnectDialog}
/>
</View>
);
};
getRenderItem = ({item}: {item: {id: string}}): React.Node => {
switch (item.id) {
case '0':
return this.getWelcomeCard();
case '1':
return this.getPersonalCard();
case '2':
return this.getClubCard();
default:
return this.getMembershipCar();
}
};
/**
* Gets the list of services available with the Amicale account
*
* @returns {*}
*/
getServicesList(): React.Node {
return <CardList dataset={this.amicaleDataset} isHorizontal />;
}
/**
* Gets a card welcoming the user to his account
*
* @returns {*}
*/
getWelcomeCard(): React.Node {
const {navigation} = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.welcomeTitle', {
name: this.data.first_name,
})}
left={(): React.Node => (
<Mascot
style={{
width: 60,
}}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 1000,
}}
/>
)}
titleStyle={{marginLeft: 10}}
/>
<Card.Content>
<Divider />
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
{this.getServicesList()}
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
<Divider />
<Card.Actions>
<Button
icon="bug"
mode="contained"
onPress={() => {
navigation.navigate('feedback');
}}
style={styles.editButton}>
{i18n.t('screens.feedback.homeButtonTitle')}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Gets the given field value.
* If the field does not have a value, returns a placeholder text
*
* @param field The field to get the value from
* @return {*}
*/
static getFieldValue(field: ?string): string {
return field != null ? field : i18n.t('screens.profile.noData');
}
/**
* Gets a list item showing personal information
*
* @param field The field to display
* @param icon The icon to use
* @return {*}
*/
getPersonalListItem(field: ?string, icon: string): React.Node {
const {theme} = this.props;
const title = field != null ? ProfileScreen.getFieldValue(field) : ':(';
const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
return (
<List.Item
title={title}
description={subtitle}
left={({size}: {size: number}): React.Node => (
<List.Icon
size={size}
icon={icon}
color={field != null ? null : theme.colors.textDisabled}
/>
)}
/>
);
}
/**
* Gets a card containing user personal information
*
* @return {*}
*/
getPersonalCard(): React.Node {
const {theme, navigation} = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={`${this.data.first_name} ${this.data.last_name}`}
subtitle={this.data.email}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
icon="account"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<Divider />
<List.Section>
<List.Subheader>
{i18n.t('screens.profile.personalInformation')}
</List.Subheader>
{this.getPersonalListItem(this.data.birthday, 'cake-variant')}
{this.getPersonalListItem(this.data.phone, 'phone')}
{this.getPersonalListItem(this.data.email, 'email')}
{this.getPersonalListItem(this.data.branch, 'school')}
</List.Section>
<Divider />
<Card.Actions>
<Button
icon="account-edit"
mode="contained"
onPress={() => {
navigation.navigate('website', {
host: AvailableWebsites.websites.AMICALE,
path: this.data.link,
title: i18n.t('screens.websites.amicale'),
});
}}
style={styles.editButton}>
{i18n.t('screens.profile.editInformation')}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Gets a cars containing clubs the user is part of
*
* @return {*}
*/
getClubCard(): React.Node {
const {theme} = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.clubs')}
subtitle={i18n.t('screens.profile.clubsSubtitle')}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
icon="account-group"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<Divider />
{this.getClubList(this.data.clubs)}
</Card.Content>
</Card>
);
}
/**
* Gets a card showing if the user has payed his membership
*
* @return {*}
*/
getMembershipCar(): React.Node {
const {theme} = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.membership')}
subtitle={i18n.t('screens.profile.membershipSubtitle')}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
icon="credit-card"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<List.Section>
{this.getMembershipItem(this.data.validity)}
</List.Section>
</Card.Content>
</Card>
);
}
/**
* Gets the item showing if the user has payed his membership
*
* @return {*}
*/
getMembershipItem(state: boolean): React.Node {
const {theme} = this.props;
return (
<List.Item
title={
state
? i18n.t('screens.profile.membershipPayed')
: i18n.t('screens.profile.membershipNotPayed')
}
left={({size}: {size: number}): React.Node => (
<List.Icon
size={size}
color={state ? theme.colors.success : theme.colors.danger}
icon={state ? 'check' : 'close'}
/>
)}
/>
);
}
/**
* Gets a list item for the club list
*
* @param item The club to render
* @return {*}
*/
getClubListItem = ({item}: {item: ClubType}): React.Node => {
const {theme} = this.props;
const onPress = () => {
this.openClubDetailsScreen(item.id);
};
let description = i18n.t('screens.profile.isMember');
let icon = ({size, color}: {size: number, color: string}): React.Node => (
<List.Icon size={size} color={color} icon="chevron-right" />
);
if (item.is_manager) {
description = i18n.t('screens.profile.isManager');
icon = ({size}: {size: number}): React.Node => (
<List.Icon size={size} icon="star" color={theme.colors.primary} />
);
}
return (
<List.Item
title={item.name}
description={description}
left={icon}
onPress={onPress}
/>
);
};
/**
* Renders the list of clubs the user is part of
*
* @param list The club list
* @return {*}
*/
getClubList(list: Array<ClubType>): React.Node {
list.sort(this.sortClubList);
return (
<FlatList
renderItem={this.getClubListItem}
keyExtractor={this.clubKeyExtractor}
data={list}
/>
);
}
clubKeyExtractor = (item: ClubType): string => item.name;
sortClubList = (a: ClubType): number => (a.is_manager ? -1 : 1);
showDisconnectDialog = () => {
this.setState({dialogVisible: true});
};
hideDisconnectDialog = () => {
this.setState({dialogVisible: false});
};
/**
* Opens the club details screen for the club of given ID
* @param id The club's id to open
*/
openClubDetailsScreen(id: number) {
const {navigation} = this.props;
navigation.navigate('club-information', {clubId: id});
}
render(): React.Node {
const {navigation} = this.props;
return (
<AuthenticatedScreen
navigation={navigation}
requests={[
{
link: 'user/profile',
params: {},
mandatory: true,
},
]}
renderFunction={this.getScreen}
/>
);
}
}
export default withTheme(ProfileScreen); export default withTheme(ProfileScreen);

View file

@ -1,46 +1,47 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {RefreshControl, View} from "react-native"; import {RefreshControl, View} from 'react-native';
import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen"; import {StackNavigationProp} from '@react-navigation/stack';
import {getTimeOnlyString, stringToDate} from "../../utils/Planning"; import i18n from 'i18n-js';
import VoteTease from "../../components/Amicale/Vote/VoteTease"; import {Button} from 'react-native-paper';
import VoteSelect from "../../components/Amicale/Vote/VoteSelect"; import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
import VoteResults from "../../components/Amicale/Vote/VoteResults"; import {getTimeOnlyString, stringToDate} from '../../utils/Planning';
import VoteWait from "../../components/Amicale/Vote/VoteWait"; import VoteTease from '../../components/Amicale/Vote/VoteTease';
import {StackNavigationProp} from "@react-navigation/stack"; import VoteSelect from '../../components/Amicale/Vote/VoteSelect';
import i18n from "i18n-js"; import VoteResults from '../../components/Amicale/Vote/VoteResults';
import {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import VoteWait from '../../components/Amicale/Vote/VoteWait';
import MascotPopup from "../../components/Mascot/MascotPopup"; import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import MascotPopup from '../../components/Mascot/MascotPopup';
import {Button} from "react-native-paper"; import AsyncStorageManager from '../../managers/AsyncStorageManager';
import VoteNotAvailable from "../../components/Amicale/Vote/VoteNotAvailable"; import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList"; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type {ApiGenericDataType} from '../../utils/WebData';
export type team = { export type VoteTeamType = {
id: number, id: number,
name: string, name: string,
votes: number, votes: number,
}
type teamResponse = {
has_voted: boolean,
teams: Array<team>,
}; };
type stringVoteDates = { type TeamResponseType = {
date_begin: string, has_voted: boolean,
date_end: string, teams: Array<VoteTeamType>,
date_result_begin: string, };
date_result_end: string,
}
type objectVoteDates = { type VoteDatesStringType = {
date_begin: Date, date_begin: string,
date_end: Date, date_end: string,
date_result_begin: Date, date_result_begin: string,
date_result_end: Date, date_result_end: string,
} };
type VoteDatesObjectType = {
date_begin: Date,
date_end: Date,
date_result_begin: Date,
date_result_end: Date,
};
// const FAKE_DATE = { // const FAKE_DATE = {
// "date_begin": "2020-08-19 15:50", // "date_begin": "2020-08-19 15:50",
@ -92,287 +93,322 @@ type objectVoteDates = {
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
type Props = { type PropsType = {
navigation: StackNavigationProp navigation: StackNavigationProp,
} };
type State = { type StateType = {
hasVoted: boolean, hasVoted: boolean,
mascotDialogVisible: boolean, mascotDialogVisible: boolean,
} };
/** /**
* Screen displaying vote information and controls * Screen displaying vote information and controls
*/ */
export default class VoteScreen extends React.Component<Props, State> { export default class VoteScreen extends React.Component<PropsType, StateType> {
teams: Array<VoteTeamType>;
state = { hasVoted: boolean;
hasVoted: false,
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.voteShowBanner.key), datesString: null | VoteDatesStringType;
dates: null | VoteDatesObjectType;
today: Date;
mainFlatListData: Array<{key: string}>;
lastRefresh: Date | null;
authRef: {current: null | AuthenticatedScreen};
constructor() {
super();
this.state = {
hasVoted: false,
mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.voteShowBanner.key,
),
}; };
this.hasVoted = false;
this.today = new Date();
this.authRef = React.createRef();
this.lastRefresh = null;
this.mainFlatListData = [{key: 'main'}, {key: 'info'}];
}
teams: Array<team>; /**
hasVoted: boolean; * Gets the string representation of the given date.
datesString: null | stringVoteDates; *
dates: null | objectVoteDates; * If the given date is the same day as today, only return the tile.
* Otherwise, return the full date.
*
* @param date The Date object representation of the wanted date
* @param dateString The string representation of the wanted date
* @returns {string}
*/
getDateString(date: Date, dateString: string): string {
if (this.today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString);
return str != null ? str : '';
}
return dateString;
}
today: Date; getMainRenderItem = ({item}: {item: {key: string}}): React.Node => {
if (item.key === 'info')
return (
<View>
<Button
mode="contained"
icon="help-circle"
onPress={this.showMascotDialog}
style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 20,
}}>
{i18n.t('screens.vote.mascotDialog.title')}
</Button>
</View>
);
return this.getContent();
};
mainFlatListData: Array<{ key: string }>; getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
lastRefresh: Date | null; const {state} = this;
// data[0] = FAKE_TEAMS2;
// data[1] = FAKE_DATE;
this.lastRefresh = new Date();
authRef: { current: null | AuthenticatedScreen }; const teams: TeamResponseType | null = data[0];
const dateStrings: VoteDatesStringType | null = data[1];
constructor() { if (dateStrings != null && dateStrings.date_begin == null)
super(); this.datesString = null;
this.hasVoted = false; else this.datesString = dateStrings;
this.today = new Date();
this.authRef = React.createRef(); if (teams != null) {
this.lastRefresh = null; this.teams = teams.teams;
this.mainFlatListData = [ this.hasVoted = teams.has_voted;
{key: 'main'},
{key: 'info'},
]
} }
/** this.generateDateObject();
* Reloads vote data if last refresh delta is smaller than the minimum refresh time return (
*/ <CollapsibleFlatList
reloadData = () => { data={this.mainFlatListData}
let canRefresh; refreshControl={
const lastRefresh = this.lastRefresh; <RefreshControl refreshing={false} onRefresh={this.reloadData} />
if (lastRefresh != null)
canRefresh = (new Date().getTime() - lastRefresh.getTime()) > MIN_REFRESH_TIME;
else
canRefresh = true;
if (canRefresh && this.authRef.current != null)
this.authRef.current.reload()
};
/**
* Generates the objects containing string and Date representations of key vote dates
*/
generateDateObject() {
const strings = this.datesString;
if (strings != null) {
const dateBegin = stringToDate(strings.date_begin);
const dateEnd = stringToDate(strings.date_end);
const dateResultBegin = stringToDate(strings.date_result_begin);
const dateResultEnd = stringToDate(strings.date_result_end);
if (dateBegin != null && dateEnd != null && dateResultBegin != null && dateResultEnd != null) {
this.dates = {
date_begin: dateBegin,
date_end: dateEnd,
date_result_begin: dateResultBegin,
date_result_end: dateResultEnd,
};
} else
this.dates = null;
} else
this.dates = null;
}
/**
* Gets the string representation of the given date.
*
* If the given date is the same day as today, only return the tile.
* Otherwise, return the full date.
*
* @param date The Date object representation of the wanted date
* @param dateString The string representation of the wanted date
* @returns {string}
*/
getDateString(date: Date, dateString: string): string {
if (this.today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString);
return str != null ? str : "";
} else
return dateString;
}
isVoteRunning() {
return this.dates != null && this.today > this.dates.date_begin && this.today < this.dates.date_end;
}
isVoteStarted() {
return this.dates != null && this.today > this.dates.date_begin;
}
isResultRunning() {
return this.dates != null && this.today > this.dates.date_result_begin && this.today < this.dates.date_result_end;
}
isResultStarted() {
return this.dates != null && this.today > this.dates.date_result_begin;
}
mainRenderItem = ({item}: { item: { key: string } }) => {
if (item.key === 'info')
return (
<View>
<Button
mode={"contained"}
icon={"help-circle"}
onPress={this.showMascotDialog}
style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: 20
}}>
{i18n.t("screens.vote.mascotDialog.title")}
</Button>
</View>
);
else
return this.getContent();
};
getScreen = (data: Array<{ [key: string]: any } | null>) => {
// data[0] = FAKE_TEAMS2;
// data[1] = FAKE_DATE;
this.lastRefresh = new Date();
const teams: teamResponse | null = data[0];
const dateStrings: stringVoteDates | null = data[1];
if (dateStrings != null && dateStrings.date_begin == null)
this.datesString = null;
else
this.datesString = dateStrings;
if (teams != null) {
this.teams = teams.teams;
this.hasVoted = teams.has_voted;
} }
extraData={state.hasVoted.toString()}
renderItem={this.getMainRenderItem}
/>
);
};
this.generateDateObject(); getContent(): React.Node {
return ( const {state} = this;
<CollapsibleFlatList if (!this.isVoteStarted()) return this.getTeaseVoteCard();
data={this.mainFlatListData} if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted)
refreshControl={ return this.getVoteCard();
<RefreshControl if (!this.isResultStarted()) return this.getWaitVoteCard();
refreshing={false} if (this.isResultRunning()) return this.getVoteResultCard();
onRefresh={this.reloadData} return <VoteNotAvailable />;
/> }
}
extraData={this.state.hasVoted.toString()}
renderItem={this.mainRenderItem}
/>
);
};
getContent() { onVoteSuccess = (): void => this.setState({hasVoted: true});
if (!this.isVoteStarted())
return this.getTeaseVoteCard();
else if (this.isVoteRunning() && (!this.hasVoted && !this.state.hasVoted))
return this.getVoteCard();
else if (!this.isResultStarted())
return this.getWaitVoteCard();
else if (this.isResultRunning())
return this.getVoteResultCard();
else
return <VoteNotAvailable/>;
}
onVoteSuccess = () => this.setState({hasVoted: true}); /**
* The user has not voted yet, and the votes are open
*/
getVoteCard(): React.Node {
return (
<VoteSelect
teams={this.teams}
onVoteSuccess={this.onVoteSuccess}
onVoteError={this.reloadData}
/>
);
}
/** /**
* The user has not voted yet, and the votes are open * Votes have ended, results can be displayed
*/ */
getVoteCard() { getVoteResultCard(): React.Node {
return <VoteSelect teams={this.teams} onVoteSuccess={this.onVoteSuccess} onVoteError={this.reloadData}/>; if (this.dates != null && this.datesString != null)
} return (
<VoteResults
teams={this.teams}
dateEnd={this.getDateString(
this.dates.date_result_end,
this.datesString.date_result_end,
)}
/>
);
return <VoteNotAvailable />;
}
/** /**
* Votes have ended, results can be displayed * Vote will open shortly
*/ */
getVoteResultCard() { getTeaseVoteCard(): React.Node {
if (this.dates != null && this.datesString != null) if (this.dates != null && this.datesString != null)
return <VoteResults return (
teams={this.teams} <VoteTease
dateEnd={this.getDateString( startDate={this.getDateString(
this.dates.date_result_end, this.dates.date_begin,
this.datesString.date_result_end)} this.datesString.date_begin,
/>; )}
else />
return <VoteNotAvailable/>; );
} return <VoteNotAvailable />;
}
/** /**
* Vote will open shortly * Votes have ended, or user has voted waiting for results
*/ */
getTeaseVoteCard() { getWaitVoteCard(): React.Node {
if (this.dates != null && this.datesString != null) const {state} = this;
return <VoteTease let startDate = null;
startDate={this.getDateString(this.dates.date_begin, this.datesString.date_begin)}/>; if (
else this.dates != null &&
return <VoteNotAvailable/>; this.datesString != null &&
} this.dates.date_result_begin != null
)
startDate = this.getDateString(
this.dates.date_result_begin,
this.datesString.date_result_begin,
);
return (
<VoteWait
startDate={startDate}
hasVoted={this.hasVoted || state.hasVoted}
justVoted={state.hasVoted}
isVoteRunning={this.isVoteRunning()}
/>
);
}
/** /**
* Votes have ended, or user has voted waiting for results * Reloads vote data if last refresh delta is smaller than the minimum refresh time
*/ */
getWaitVoteCard() { reloadData = () => {
let startDate = null; let canRefresh;
if (this.dates != null && this.datesString != null && this.dates.date_result_begin != null) const {lastRefresh} = this;
startDate = this.getDateString(this.dates.date_result_begin, this.datesString.date_result_begin); if (lastRefresh != null)
return <VoteWait startDate={startDate} hasVoted={this.hasVoted || this.state.hasVoted} canRefresh =
justVoted={this.state.hasVoted} new Date().getTime() - lastRefresh.getTime() > MIN_REFRESH_TIME;
isVoteRunning={this.isVoteRunning()}/>; else canRefresh = true;
} if (canRefresh && this.authRef.current != null)
this.authRef.current.reload();
};
showMascotDialog = () => { showMascotDialog = () => {
this.setState({mascotDialogVisible: true}) this.setState({mascotDialogVisible: true});
}; };
hideMascotDialog = () => { hideMascotDialog = () => {
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.voteShowBanner.key, false); AsyncStorageManager.set(
this.setState({mascotDialogVisible: false}) AsyncStorageManager.PREFERENCES.voteShowBanner.key,
}; false,
);
this.setState({mascotDialogVisible: false});
};
/** isVoteStarted(): boolean {
* Renders the authenticated screen. return this.dates != null && this.today > this.dates.date_begin;
* }
* Teams and dates are not mandatory to allow showing the information box even if api requests fail
* isResultRunning(): boolean {
* @returns {*} return (
*/ this.dates != null &&
render() { this.today > this.dates.date_result_begin &&
return ( this.today < this.dates.date_result_end
<View style={{flex: 1}}> );
<AuthenticatedScreen }
{...this.props}
ref={this.authRef} isResultStarted(): boolean {
requests={[ return this.dates != null && this.today > this.dates.date_result_begin;
{ }
link: 'elections/teams',
params: {}, isVoteRunning(): boolean {
mandatory: false, return (
}, this.dates != null &&
{ this.today > this.dates.date_begin &&
link: 'elections/dates', this.today < this.dates.date_end
params: {}, );
mandatory: false, }
},
]} /**
renderFunction={this.getScreen} * Generates the objects containing string and Date representations of key vote dates
/> */
<MascotPopup generateDateObject() {
visible={this.state.mascotDialogVisible} const strings = this.datesString;
title={i18n.t("screens.vote.mascotDialog.title")} if (strings != null) {
message={i18n.t("screens.vote.mascotDialog.message")} const dateBegin = stringToDate(strings.date_begin);
icon={"vote"} const dateEnd = stringToDate(strings.date_end);
buttons={{ const dateResultBegin = stringToDate(strings.date_result_begin);
action: null, const dateResultEnd = stringToDate(strings.date_result_end);
cancel: { if (
message: i18n.t("screens.vote.mascotDialog.button"), dateBegin != null &&
icon: "check", dateEnd != null &&
onPress: this.hideMascotDialog, dateResultBegin != null &&
} dateResultEnd != null
}} ) {
emotion={MASCOT_STYLE.CUTE} this.dates = {
/> date_begin: dateBegin,
</View> date_end: dateEnd,
); date_result_begin: dateResultBegin,
} date_result_end: dateResultEnd,
};
} else this.dates = null;
} else this.dates = null;
}
/**
* Renders the authenticated screen.
*
* Teams and dates are not mandatory to allow showing the information box even if api requests fail
*
* @returns {*}
*/
render(): React.Node {
const {props, state} = this;
return (
<View style={{flex: 1}}>
<AuthenticatedScreen
navigation={props.navigation}
ref={this.authRef}
requests={[
{
link: 'elections/teams',
params: {},
mandatory: false,
},
{
link: 'elections/dates',
params: {},
mandatory: false,
},
]}
renderFunction={this.getScreen}
/>
<MascotPopup
visible={state.mascotDialogVisible}
title={i18n.t('screens.vote.mascotDialog.title')}
message={i18n.t('screens.vote.mascotDialog.message')}
icon="vote"
buttons={{
action: null,
cancel: {
message: i18n.t('screens.vote.mascotDialog.button'),
icon: 'check',
onPress: this.hideMascotDialog,
},
}}
emotion={MASCOT_STYLE.CUTE}
/>
</View>
);
}
} }

View file

@ -1,13 +1,13 @@
// @flow // @flow
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
export type Coordinates = { export type CoordinatesType = {
x: number, x: number,
y: number, y: number,
} };
type Shape = Array<Array<number>>; export type ShapeType = Array<Array<number>>;
/** /**
* Abstract class used to represent a BaseShape. * Abstract class used to represent a BaseShape.
@ -15,96 +15,98 @@ type Shape = Array<Array<number>>;
* and in methods to implement * and in methods to implement
*/ */
export default class BaseShape { export default class BaseShape {
#currentShape: ShapeType;
#currentShape: Shape; #rotation: number;
#rotation: number;
position: Coordinates;
theme: CustomTheme;
/** position: CoordinatesType;
* Prevent instantiation if classname is BaseShape to force class to be abstract
*/
constructor(theme: CustomTheme) {
if (this.constructor === BaseShape)
throw new Error("Abstract class can't be instantiated");
this.theme = theme;
this.#rotation = 0;
this.position = {x: 0, y: 0};
this.#currentShape = this.getShapes()[this.#rotation];
}
/** theme: CustomThemeType;
* Gets this shape's color.
* Must be implemented by child class
*/
getColor(): string {
throw new Error("Method 'getColor()' must be implemented");
}
/** /**
* Gets this object's all possible shapes as an array. * Prevent instantiation if classname is BaseShape to force class to be abstract
* Must be implemented by child class. */
* constructor(theme: CustomThemeType) {
* Used by tests to read private fields if (this.constructor === BaseShape)
*/ throw new Error("Abstract class can't be instantiated");
getShapes(): Array<Shape> { this.theme = theme;
throw new Error("Method 'getShapes()' must be implemented"); this.#rotation = 0;
} this.position = {x: 0, y: 0};
this.#currentShape = this.getShapes()[this.#rotation];
}
/** /**
* Gets this object's current shape. * Gets this shape's color.
*/ * Must be implemented by child class
getCurrentShape(): Shape { */
return this.#currentShape; // eslint-disable-next-line class-methods-use-this
} getColor(): string {
throw new Error("Method 'getColor()' must be implemented");
}
/** /**
* Gets this object's coordinates. * Gets this object's all possible shapes as an array.
* This will return an array of coordinates representing the positions of the cells used by this object. * Must be implemented by child class.
* *
* @param isAbsolute Should we take into account the current position of the object? * Used by tests to read private fields
* @return {Array<Coordinates>} This object cells coordinates */
*/ // eslint-disable-next-line class-methods-use-this
getCellsCoordinates(isAbsolute: boolean): Array<Coordinates> { getShapes(): Array<ShapeType> {
let coordinates = []; throw new Error("Method 'getShapes()' must be implemented");
for (let row = 0; row < this.#currentShape.length; row++) { }
for (let col = 0; col < this.#currentShape[row].length; col++) {
if (this.#currentShape[row][col] === 1) /**
if (isAbsolute) * Gets this object's current shape.
coordinates.push({x: this.position.x + col, y: this.position.y + row}); */
else getCurrentShape(): ShapeType {
coordinates.push({x: col, y: row}); return this.#currentShape;
} }
/**
* Gets this object's coordinates.
* This will return an array of coordinates representing the positions of the cells used by this object.
*
* @param isAbsolute Should we take into account the current position of the object?
* @return {Array<CoordinatesType>} This object cells coordinates
*/
getCellsCoordinates(isAbsolute: boolean): Array<CoordinatesType> {
const coordinates = [];
for (let row = 0; row < this.#currentShape.length; row += 1) {
for (let col = 0; col < this.#currentShape[row].length; col += 1) {
if (this.#currentShape[row][col] === 1) {
if (isAbsolute) {
coordinates.push({
x: this.position.x + col,
y: this.position.y + row,
});
} else coordinates.push({x: col, y: row});
} }
return coordinates; }
} }
return coordinates;
}
/** /**
* Rotate this object * Rotate this object
* *
* @param isForward Should we rotate clockwise? * @param isForward Should we rotate clockwise?
*/ */
rotate(isForward: boolean) { rotate(isForward: boolean) {
if (isForward) if (isForward) this.#rotation += 1;
this.#rotation++; else this.#rotation -= 1;
else if (this.#rotation > 3) this.#rotation = 0;
this.#rotation--; else if (this.#rotation < 0) this.#rotation = 3;
if (this.#rotation > 3) this.#currentShape = this.getShapes()[this.#rotation];
this.#rotation = 0; }
else if (this.#rotation < 0)
this.#rotation = 3;
this.#currentShape = this.getShapes()[this.#rotation];
}
/**
* Move this object
*
* @param x Position X offset to add
* @param y Position Y offset to add
*/
move(x: number, y: number) {
this.position.x += x;
this.position.y += y;
}
/**
* Move this object
*
* @param x Position X offset to add
* @param y Position Y offset to add
*/
move(x: number, y: number) {
this.position.x += x;
this.position.y += y;
}
} }

View file

@ -1,45 +1,46 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeI extends BaseShape { export default class ShapeI extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisI;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisI; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [0, 0, 0, 0],
return [ [1, 1, 1, 1],
[ [0, 0, 0, 0],
[0, 0, 0, 0], [0, 0, 0, 0],
[1, 1, 1, 1], ],
[0, 0, 0, 0], [
[0, 0, 0, 0], [0, 0, 1, 0],
], [0, 0, 1, 0],
[ [0, 0, 1, 0],
[0, 0, 1, 0], [0, 0, 1, 0],
[0, 0, 1, 0], ],
[0, 0, 1, 0], [
[0, 0, 1, 0], [0, 0, 0, 0],
], [0, 0, 0, 0],
[ [1, 1, 1, 1],
[0, 0, 0, 0], [0, 0, 0, 0],
[0, 0, 0, 0], ],
[1, 1, 1, 1], [
[0, 0, 0, 0], [0, 1, 0, 0],
], [0, 1, 0, 0],
[ [0, 1, 0, 0],
[0, 1, 0, 0], [0, 1, 0, 0],
[0, 1, 0, 0], ],
[0, 1, 0, 0], ];
[0, 1, 0, 0], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeJ extends BaseShape { export default class ShapeJ extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisJ;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisJ; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [1, 0, 0],
return [ [1, 1, 1],
[ [0, 0, 0],
[1, 0, 0], ],
[1, 1, 1], [
[0, 0, 0], [0, 1, 1],
], [0, 1, 0],
[ [0, 1, 0],
[0, 1, 1], ],
[0, 1, 0], [
[0, 1, 0], [0, 0, 0],
], [1, 1, 1],
[ [0, 0, 1],
[0, 0, 0], ],
[1, 1, 1], [
[0, 0, 1], [0, 1, 0],
], [0, 1, 0],
[ [1, 1, 0],
[0, 1, 0], ],
[0, 1, 0], ];
[1, 1, 0], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeL extends BaseShape { export default class ShapeL extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisL;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisL; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [0, 0, 1],
return [ [1, 1, 1],
[ [0, 0, 0],
[0, 0, 1], ],
[1, 1, 1], [
[0, 0, 0], [0, 1, 0],
], [0, 1, 0],
[ [0, 1, 1],
[0, 1, 0], ],
[0, 1, 0], [
[0, 1, 1], [0, 0, 0],
], [1, 1, 1],
[ [1, 0, 0],
[0, 0, 0], ],
[1, 1, 1], [
[1, 0, 0], [1, 1, 0],
], [0, 1, 0],
[ [0, 1, 0],
[1, 1, 0], ],
[0, 1, 0], ];
[0, 1, 0], }
],
];
}
} }

View file

@ -1,37 +1,38 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeO extends BaseShape { export default class ShapeO extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 4;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisO;
this.position.x = 4; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisO; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [1, 1],
return [ [1, 1],
[ ],
[1, 1], [
[1, 1], [1, 1],
], [1, 1],
[ ],
[1, 1], [
[1, 1], [1, 1],
], [1, 1],
[ ],
[1, 1], [
[1, 1], [1, 1],
], [1, 1],
[ ],
[1, 1], ];
[1, 1], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeS extends BaseShape { export default class ShapeS extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisS;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisS; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [0, 1, 1],
return [ [1, 1, 0],
[ [0, 0, 0],
[0, 1, 1], ],
[1, 1, 0], [
[0, 0, 0], [0, 1, 0],
], [0, 1, 1],
[ [0, 0, 1],
[0, 1, 0], ],
[0, 1, 1], [
[0, 0, 1], [0, 0, 0],
], [0, 1, 1],
[ [1, 1, 0],
[0, 0, 0], ],
[0, 1, 1], [
[1, 1, 0], [1, 0, 0],
], [1, 1, 0],
[ [0, 1, 0],
[1, 0, 0], ],
[1, 1, 0], ];
[0, 1, 0], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeT extends BaseShape { export default class ShapeT extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisT;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisT; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [0, 1, 0],
return [ [1, 1, 1],
[ [0, 0, 0],
[0, 1, 0], ],
[1, 1, 1], [
[0, 0, 0], [0, 1, 0],
], [0, 1, 1],
[ [0, 1, 0],
[0, 1, 0], ],
[0, 1, 1], [
[0, 1, 0], [0, 0, 0],
], [1, 1, 1],
[ [0, 1, 0],
[0, 0, 0], ],
[1, 1, 1], [
[0, 1, 0], [0, 1, 0],
], [1, 1, 0],
[ [0, 1, 0],
[0, 1, 0], ],
[1, 1, 0], ];
[0, 1, 0], }
],
];
}
} }

View file

@ -1,41 +1,42 @@
// @flow // @flow
import BaseShape from "./BaseShape"; import BaseShape from './BaseShape';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {ShapeType} from './BaseShape';
export default class ShapeZ extends BaseShape { export default class ShapeZ extends BaseShape {
constructor(theme: CustomThemeType) {
super(theme);
this.position.x = 3;
}
constructor(theme: CustomTheme) { getColor(): string {
super(theme); return this.theme.colors.tetrisZ;
this.position.x = 3; }
}
getColor(): string { // eslint-disable-next-line class-methods-use-this
return this.theme.colors.tetrisZ; getShapes(): Array<ShapeType> {
} return [
[
getShapes() { [1, 1, 0],
return [ [0, 1, 1],
[ [0, 0, 0],
[1, 1, 0], ],
[0, 1, 1], [
[0, 0, 0], [0, 0, 1],
], [0, 1, 1],
[ [0, 1, 0],
[0, 0, 1], ],
[0, 1, 1], [
[0, 1, 0], [0, 0, 0],
], [1, 1, 0],
[ [0, 1, 1],
[0, 0, 0], ],
[1, 1, 0], [
[0, 1, 1], [0, 1, 0],
], [1, 1, 0],
[ [1, 0, 0],
[0, 1, 0], ],
[1, 1, 0], ];
[1, 0, 0], }
],
];
}
} }

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