Compare commits

..

No commits in common. "b3784735917a299b25d2bdfe4cb1afe68de6d280" and "05ef28f3b5cd7793e8d12a3e2d6606ebc6172c15" have entirely different histories.

145 changed files with 16510 additions and 18120 deletions

View file

@ -13,8 +13,6 @@ 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,
@ -39,8 +37,4 @@ module.exports = {
onlyFilesWithFlowAnnotation: false, onlyFilesWithFlowAnnotation: false,
}, },
}, },
globals: {
fetch: false,
Headers: false,
},
}; };

View file

@ -0,0 +1,16 @@
<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>

203
App.js
View file

@ -1,61 +1,64 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {LogBox, Platform, SafeAreaView, View} from 'react-native'; import {LogBox, Platform, SafeAreaView, StatusBar, View} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {Provider as PaperProvider} from 'react-native-paper';
import {setSafeBounceHeight} from 'react-navigation-collapsible';
import SplashScreen from 'react-native-splash-screen';
import {OverflowMenuProvider} from 'react-navigation-header-buttons';
import LocaleManager from './src/managers/LocaleManager'; import LocaleManager from './src/managers/LocaleManager';
import AsyncStorageManager from './src/managers/AsyncStorageManager'; import AsyncStorageManager from "./src/managers/AsyncStorageManager";
import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider'; import CustomIntroSlider from "./src/components/Overrides/CustomIntroSlider";
import type {CustomThemeType} from './src/managers/ThemeManager'; import type {CustomTheme} from "./src/managers/ThemeManager";
import ThemeManager from './src/managers/ThemeManager'; import ThemeManager from './src/managers/ThemeManager';
import {NavigationContainer} from '@react-navigation/native';
import MainNavigator from './src/navigation/MainNavigator'; import MainNavigator from './src/navigation/MainNavigator';
import AprilFoolsManager from './src/managers/AprilFoolsManager'; import {Provider as PaperProvider} from 'react-native-paper';
import Update from './src/constants/Update'; import AprilFoolsManager from "./src/managers/AprilFoolsManager";
import ConnectionManager from './src/managers/ConnectionManager'; import Update from "./src/constants/Update";
import type {ParsedUrlDataType} from './src/utils/URLHandler'; import ConnectionManager from "./src/managers/ConnectionManager";
import URLHandler from './src/utils/URLHandler'; import URLHandler from "./src/utils/URLHandler";
import {setupStatusBar} from './src/utils/Utils'; import {setSafeBounceHeight} from "react-navigation-collapsible";
import SplashScreen from 'react-native-splash-screen'
import {OverflowMenuProvider} from "react-navigation-header-buttons";
// 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([
// collapsible headers cause this warning, just ignore as it is not an issue LogBox.ignoreLogs([ // 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 StateType = { type Props = {};
type State = {
isLoading: boolean, isLoading: boolean,
showIntro: boolean, showIntro: boolean,
showUpdate: boolean, showUpdate: boolean,
showAprilFools: boolean, showAprilFools: boolean,
currentTheme: CustomThemeType | null, currentTheme: CustomTheme | null,
}; };
export default class App extends React.Component<null, StateType> { export default class App extends React.Component<Props, State> {
navigatorRef: {current: null | NavigationContainer};
defaultHomeRoute: string | null; state = {
defaultHomeData: {[key: string]: string};
urlHandler: URLHandler;
constructor() {
super();
this.state = {
isLoading: true, isLoading: true,
showIntro: true, showIntro: true,
showUpdate: true, showUpdate: true,
showAprilFools: false, showAprilFools: false,
currentTheme: null, currentTheme: null,
}; };
navigatorRef: { current: null | NavigationContainer };
defaultHomeRoute: string | null;
defaultHomeData: { [key: string]: any }
createDrawerNavigator: () => React.Node;
urlHandler: URLHandler;
constructor() {
super();
LocaleManager.initTranslations(); LocaleManager.initTranslations();
this.navigatorRef = React.createRef(); this.navigatorRef = React.createRef();
this.defaultHomeRoute = null; this.defaultHomeRoute = null;
@ -63,7 +66,7 @@ export default class App extends React.Component<null, StateType> {
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL); this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
this.urlHandler.listen(); this.urlHandler.listen();
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20); setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
this.loadAssetsAsync().finally(() => { this.loadAssetsAsync().then(() => {
this.onLoadFinished(); this.onLoadFinished();
}); });
} }
@ -74,7 +77,7 @@ export default class App extends React.Component<null, StateType> {
* *
* @param parsedData The data parsed from the url * @param parsedData The data parsed from the url
*/ */
onInitialURLParsed = (parsedData: ParsedUrlDataType) => { onInitialURLParsed = (parsedData: { route: string, data: { [key: string]: any } }) => {
this.defaultHomeRoute = parsedData.route; this.defaultHomeRoute = parsedData.route;
this.defaultHomeData = parsedData.data; this.defaultHomeData = parsedData.data;
}; };
@ -85,13 +88,12 @@ export default class App extends React.Component<null, StateType> {
* *
* @param parsedData The data parsed from the url * @param parsedData The data parsed from the url
*/ */
onDetectURL = (parsedData: ParsedUrlDataType) => { onDetectURL = (parsedData: { route: string, data: { [key: string]: any } }) => {
// Navigate to nested navigator and pass data to the index screen // Navigate to nested navigator and pass data to the index screen
const nav = this.navigatorRef.current; if (this.navigatorRef.current != null) {
if (nav != null) { this.navigatorRef.current.navigate('home', {
nav.navigate('home', {
screen: 'index', screen: 'index',
params: {nextScreen: parsedData.route, data: parsedData.data}, params: {nextScreen: parsedData.route, data: parsedData.data}
}); });
} }
}; };
@ -101,11 +103,25 @@ export default class App extends React.Component<null, StateType> {
*/ */
onUpdateTheme = () => { onUpdateTheme = () => {
this.setState({ this.setState({
currentTheme: ThemeManager.getCurrentTheme(), currentTheme: ThemeManager.getCurrentTheme()
}); });
setupStatusBar(); this.setupStatusBar();
}; };
/**
* Updates status bar content color if on iOS only,
* as the android status bar is always set to black.
*/
setupStatusBar() {
if (ThemeManager.getNightMode()) {
StatusBar.setBarStyle('light-content', true);
} else {
StatusBar.setBarStyle('dark-content', true);
}
if (Platform.OS === "android")
StatusBar.setBackgroundColor(ThemeManager.getCurrentTheme().colors.surface, true);
}
/** /**
* Callback when user ends the intro. Save in preferences to avoid showing back the introSlides * Callback when user ends the intro. Save in preferences to avoid showing back the introSlides
*/ */
@ -115,49 +131,11 @@ export default class App extends React.Component<null, StateType> {
showUpdate: false, showUpdate: false,
showAprilFools: false, showAprilFools: false,
}); });
AsyncStorageManager.set( AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.showIntro.key, false);
AsyncStorageManager.PREFERENCES.showIntro.key, AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.updateNumber.key, Update.number);
false, AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key, false);
);
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.updateNumber.key,
Update.number,
);
AsyncStorageManager.set(
AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key,
false,
);
}; };
/**
* Async loading is done, finish processing startup data
*/
onLoadFinished() {
// Only show intro if this is the first time starting the app
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
// Status bar goes dark if set too fast on ios
if (Platform.OS === 'ios') setTimeout(setupStatusBar, 1000);
else setupStatusBar();
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();
}
/** /**
* Loads every async data * Loads every async data
* *
@ -165,38 +143,58 @@ export default class App extends React.Component<null, StateType> {
*/ */
loadAssetsAsync = async () => { loadAssetsAsync = async () => {
await AsyncStorageManager.getInstance().loadPreferences(); await AsyncStorageManager.getInstance().loadPreferences();
try {
await ConnectionManager.getInstance().recoverLogin(); 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 * Renders the app based on loading state
*/ */
render(): React.Node { render() {
const {state} = this; if (this.state.isLoading) {
if (state.isLoading) {
return null; return null;
} } else if (this.state.showIntro || this.state.showUpdate || this.state.showAprilFools) {
if (state.showIntro || state.showUpdate || state.showAprilFools) { return <CustomIntroSlider
return (
<CustomIntroSlider
onDone={this.onIntroDone} onDone={this.onIntroDone}
isUpdate={state.showUpdate && !state.showIntro} isUpdate={this.state.showUpdate && !this.state.showIntro}
isAprilFools={state.showAprilFools && !state.showIntro} isAprilFools={this.state.showAprilFools && !this.state.showIntro}
/> />;
); } else {
}
return ( return (
<PaperProvider theme={state.currentTheme}> <PaperProvider theme={this.state.currentTheme}>
<OverflowMenuProvider> <OverflowMenuProvider>
<View <View style={{backgroundColor: ThemeManager.getCurrentTheme().colors.background, flex: 1}}>
style={{
backgroundColor: ThemeManager.getCurrentTheme().colors.background,
flex: 1,
}}>
<SafeAreaView style={{flex: 1}}> <SafeAreaView style={{flex: 1}}>
<NavigationContainer <NavigationContainer theme={this.state.currentTheme} ref={this.navigatorRef}>
theme={state.currentTheme}
ref={this.navigatorRef}>
<MainNavigator <MainNavigator
defaultHomeRoute={this.defaultHomeRoute} defaultHomeRoute={this.defaultHomeRoute}
defaultHomeData={this.defaultHomeData} defaultHomeData={this.defaultHomeData}
@ -208,4 +206,5 @@ export default class App extends React.Component<null, StateType> {
</PaperProvider> </PaperProvider>
); );
} }
}
} }

View file

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

View file

@ -1,345 +1,319 @@
/* 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 jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-01-14 14:50:35').getTime()); new Date('2020-01-14 14:50:35').getTime()
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(
new Date('2020-01-14').getTime(),
); );
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(new Date("2020-01-14").getTime());
}); });
test('isEquipmentAvailable', () => { test('isEquipmentAvailable', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-07-09').getTime()); 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 jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-07-09').getTime()); 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'}];
expect(
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
).toBe(new Date('2020-07-10').getTime());
testDevice.booked_at = [ testDevice.booked_at = [
{begin: '2020-07-07', end: '2020-07-09'}, {begin: "2020-07-07", end: "2020-07-09"},
{begin: '2020-07-10', end: '2020-07-16'}, {begin: "2020-07-10", end: "2020-07-16"},
]; ];
expect( expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-17").getTime());
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
).toBe(new Date('2020-07-17').getTime());
testDevice.booked_at = [ testDevice.booked_at = [
{begin: '2020-07-07', end: '2020-07-09'}, {begin: "2020-07-07", end: "2020-07-09"},
{begin: '2020-07-10', end: '2020-07-12'}, {begin: "2020-07-10", end: "2020-07-12"},
{begin: '2020-07-14', end: '2020-07-16'}, {begin: "2020-07-14", end: "2020-07-16"},
]; ];
expect( expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-13").getTime());
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
).toBe(new Date('2020-07-13').getTime());
}); });
test('getRelativeDateString', () => { test('getRelativeDateString', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-07-09').getTime()); new Date('2020-07-09').getTime()
jest.spyOn(i18n, 't').mockImplementation((translationString: string) => {
const prefix = 'screens.equipment.';
if (translationString === prefix + 'otherYear') return '0';
else if (translationString === prefix + 'otherMonth') return '1';
else if (translationString === prefix + 'thisMonth') return '2';
else if (translationString === prefix + 'tomorrow') return '3';
else if (translationString === prefix + 'today') return '4';
else return null;
});
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-09'))).toBe(
'4',
); );
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-10'))).toBe( jest.spyOn(i18n, 't')
'3', .mockImplementation((translationString: string) => {
); const prefix = "screens.equipment.";
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-11'))).toBe( if (translationString === prefix + "otherYear")
'2', return "0";
); else if (translationString === prefix + "otherMonth")
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-30'))).toBe( return "1";
'2', else if (translationString === prefix + "thisMonth")
); return "2";
expect(EquipmentBooking.getRelativeDateString(new Date('2020-08-30'))).toBe( else if (translationString === prefix + "tomorrow")
'1', return "3";
); else if (translationString === prefix + "today")
expect(EquipmentBooking.getRelativeDateString(new Date('2020-11-10'))).toBe( return "4";
'1', else
); return null;
expect(EquipmentBooking.getRelativeDateString(new Date('2021-11-10'))).toBe( }
'0',
); );
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-09"))).toBe("4");
expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-10"))).toBe("3");
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("1");
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( expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
result,
);
testDevice.booked_at = [ testDevice.booked_at = [
{begin: '2020-07-07', end: '2020-07-10'}, {begin: "2020-07-07", end: "2020-07-10"},
{begin: '2020-07-13', end: '2020-07-15'}, {begin: "2020-07-13", end: "2020-07-15"},
]; ];
result = ['2020-07-11', '2020-07-12']; result = [
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual( "2020-07-11",
result, "2020-07-12",
); ];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}]; testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"}];
result = ['2020-07-11']; result = ["2020-07-11"];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual( expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
result, testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-12"},];
); result = [
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-12'}]; "2020-07-13",
result = ['2020-07-13', '2020-07-14', '2020-07-15']; "2020-07-14",
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual( "2020-07-15",
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 = ['2020-07-14']; result = [
expect( "2020-07-14",
EquipmentBooking.getValidRange(start, start, testDevice), ];
).toStrictEqual(result); expect(EquipmentBooking.getValidRange(start, start, testDevice)).toStrictEqual(result);
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual( expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
result, expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(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 = ['2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17']; result = [
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual( "2020-07-14",
result, "2020-07-15",
); "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 = ['2020-07-14', '2020-07-15', '2020-07-16']; result = [
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual( "2020-07-14",
result, "2020-07-15",
); "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 = ['2020-07-21', '2020-07-22', '2020-07-23']; result = [
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual( "2020-07-21",
result, "2020-07-22",
); "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( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
result = { result = {
'2020-07-11': { "2020-07-11": {
startingDay: true, startingDay: true,
endingDay: false, endingDay: false,
color: theme.colors.textDisabled, color: theme.colors.textDisabled
}, },
'2020-07-12': { "2020-07-12": {
startingDay: false, startingDay: false,
endingDay: false, endingDay: false,
color: theme.colors.textDisabled, color: theme.colors.textDisabled
}, },
'2020-07-13': { "2020-07-13": {
startingDay: false, startingDay: false,
endingDay: true, endingDay: true,
color: theme.colors.textDisabled, color: theme.colors.textDisabled
}, },
}; };
expect( expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
EquipmentBooking.generateMarkedDates(false, theme, range),
).toStrictEqual(result);
result = { result = {
'2020-07-11': { "2020-07-11": {
startingDay: true, startingDay: true,
endingDay: false, endingDay: false,
color: theme.colors.textDisabled, color: theme.colors.textDisabled
}, },
'2020-07-12': { "2020-07-12": {
startingDay: false, startingDay: false,
endingDay: false, endingDay: false,
color: theme.colors.textDisabled, color: theme.colors.textDisabled
}, },
'2020-07-13': { "2020-07-13": {
startingDay: false, startingDay: false,
endingDay: true, endingDay: true,
color: theme.colors.textDisabled, color: theme.colors.textDisabled
}, },
}; };
range = EquipmentBooking.getValidRange(end, start, testDevice); range = EquipmentBooking.getValidRange(end, start, testDevice);
expect( expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
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( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
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( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
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( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
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( expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
EquipmentBooking.generateMarkedDates(true, theme, range),
).toStrictEqual(result);
}); });

View file

@ -1,41 +1,35 @@
/* 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( expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16:00")).toBeFalse();
Planning.isEventDateStringFormatValid('3214-64-12 01:16:00'), expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16")).toBeFalse();
).toBeFalse(); expect(Planning.isEventDateStringFormatValid("3214-f4-12 01:16")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('3214-64-12 1:16')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("sqdd 09:00")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('3214-f4-12 01:16')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("2020-03-21")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('sqdd 09:00')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("2020-03-21 truc")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('2020-03-21')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16:65")).toBeFalse();
expect(Planning.isEventDateStringFormatValid('2020-03-21 truc')).toBeFalse(); expect(Planning.isEventDateStringFormatValid("garbage")).toBeFalse();
expect( expect(Planning.isEventDateStringFormatValid("")).toBeFalse();
Planning.isEventDateStringFormatValid('3214-64-12 1:16:65'),
).toBeFalse();
expect(Planning.isEventDateStringFormatValid('garbage')).toBeFalse();
expect(Planning.isEventDateStringFormatValid('')).toBeFalse();
expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse(); expect(Planning.isEventDateStringFormatValid(undefined)).toBeFalse();
expect(Planning.isEventDateStringFormatValid(null)).toBeFalse(); expect(Planning.isEventDateStringFormatValid(null)).toBeFalse();
}); });
@ -43,144 +37,136 @@ test('isEventDateStringFormatValid', () => {
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)).toBe('/ - /'); expect(Planning.getFormattedEventTime(null, null))
expect(Planning.getFormattedEventTime(undefined, undefined)).toBe('/ - /'); .toBe('/ - /');
expect(Planning.getFormattedEventTime('20:30', '23:00')).toBe('/ - /'); expect(Planning.getFormattedEventTime(undefined, undefined))
expect(Planning.getFormattedEventTime('2020-03-30', '2020-03-31')).toBe( .toBe('/ - /');
'/ - /', expect(Planning.getFormattedEventTime("20:30", "23:00"))
); .toBe('/ - /');
expect(Planning.getFormattedEventTime("2020-03-30", "2020-03-31"))
.toBe('/ - /');
expect(
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-21 09:00'), expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-21 09:00"))
).toBe('09:00'); .toBe('09:00');
expect( expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-22 17:00"))
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-22 17:00'), .toBe('09:00 - 23:59');
).toBe('09:00 - 23:59'); expect(Planning.getFormattedEventTime("2020-03-30 20:30", "2020-03-30 23:00"))
expect( .toBe('20:30 - 23:00');
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( expect(Planning.isEventBefore(
Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00'), "2020-03-21 09:00", "2020-03-21 10:00")).toBeTrue();
).toBeTrue(); expect(Planning.isEventBefore(
expect( "2020-03-21 10:00", "2020-03-21 10:15")).toBeTrue();
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:15'), expect(Planning.isEventBefore(
).toBeTrue(); "2020-03-21 10:15", "2021-03-21 10:15")).toBeTrue();
expect( expect(Planning.isEventBefore(
Planning.isEventBefore('2020-03-21 10:15', '2021-03-21 10:15'), "2020-03-21 10:15", "2020-05-21 10:15")).toBeTrue();
).toBeTrue(); expect(Planning.isEventBefore(
expect( "2020-03-21 10:15", "2020-03-30 10:15")).toBeTrue();
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( expect(Planning.isEventBefore(
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:00'), "2020-03-21 10:00", "2020-03-21 10:00")).toBeFalse();
).toBeFalse(); expect(Planning.isEventBefore(
expect( "2020-03-21 10:00", "2020-03-21 09:00")).toBeFalse();
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 09:00'), expect(Planning.isEventBefore(
).toBeFalse(); "2020-03-21 10:15", "2020-03-21 10:00")).toBeFalse();
expect( expect(Planning.isEventBefore(
Planning.isEventBefore('2020-03-21 10:15', '2020-03-21 10:00'), "2021-03-21 10:15", "2020-03-21 10:15")).toBeFalse();
).toBeFalse(); expect(Planning.isEventBefore(
expect( "2020-05-21 10:15", "2020-03-21 10:15")).toBeFalse();
Planning.isEventBefore('2021-03-21 10:15', '2020-03-21 10:15'), expect(Planning.isEventBefore(
).toBeFalse(); "2020-03-30 10:15", "2020-03-21 10:15")).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('garbage', '2020-03-21 10:15')).toBeFalse(); expect(Planning.isEventBefore(
expect(Planning.isEventBefore(undefined, undefined)).toBeFalse(); "garbage", "2020-03-21 10:15")).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 jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime()); 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);
@ -190,29 +176,31 @@ test('pushEventInOrder', () => {
}); });
test('generateEventAgenda', () => { test('generateEventAgenda', () => {
jest jest.spyOn(Date, 'now')
.spyOn(Date, 'now') .mockImplementation(() =>
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime()); 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').mockImplementation(() => { jest.spyOn(Date, 'now')
.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);

View file

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

View file

@ -0,0 +1,45 @@
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

@ -1,47 +0,0 @@
/* 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 34 versionCode 32
versionName "4.0.1" versionName "3.1.4"
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', '@babel/preset-flow'], presets: ['module:metro-react-native-babel-preset'],
}; };

View file

@ -6,5 +6,4 @@ 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

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

View file

@ -1,55 +1,49 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {StackNavigationProp} from '@react-navigation/stack'; import ConnectionManager from "../../managers/ConnectionManager";
import ConnectionManager from '../../managers/ConnectionManager'; import {ERROR_TYPE} from "../../utils/WebData";
import type {ApiGenericDataType} from '../../utils/WebData'; import ErrorView from "../Screens/ErrorView";
import {ERROR_TYPE} from '../../utils/WebData'; import BasicLoadingScreen from "../Screens/BasicLoadingScreen";
import ErrorView from '../Screens/ErrorView'; import {StackNavigationProp} from "@react-navigation/stack";
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
requests: Array<{ requests: Array<{
link: string, link: string,
params: {...}, params: Object,
mandatory: boolean, mandatory: boolean
}>, }>,
renderFunction: (Array<ApiGenericDataType | null>) => React.Node, renderFunction: (Array<{ [key: string]: any } | 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 StateType = { type State = {
loading: boolean, loading: boolean,
}; }
class AuthenticatedScreen extends React.Component<PropsType, StateType> { class AuthenticatedScreen extends React.Component<Props, State> {
static defaultProps = {
errorViewOverride: null, state = {
loading: true,
}; };
currentUserToken: string | null; currentUserToken: string | null;
connectionManager: ConnectionManager; connectionManager: ConnectionManager;
errors: Array<number>; errors: Array<number>;
fetchedData: Array<{ [key: string]: any } | null>;
fetchedData: Array<ApiGenericDataType | null>; constructor(props: Object) {
constructor(props: PropsType) {
super(props); super(props);
this.state = {
loading: true,
};
this.connectionManager = ConnectionManager.getInstance(); this.connectionManager = ConnectionManager.getInstance();
props.navigation.addListener('focus', this.onScreenFocus); this.props.navigation.addListener('focus', this.onScreenFocus);
this.fetchedData = new Array(props.requests.length); this.fetchedData = new Array(this.props.requests.length);
this.errors = new Array(props.requests.length); this.errors = new Array(this.props.requests.length);
} }
/** /**
@ -62,6 +56,35 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
} }
}; };
/**
* 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. * Callback used when a request finishes, successfully or not.
* Saves data and error code. * Saves data and error code.
@ -72,20 +95,51 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
* @param index The index for the data * @param index The index for the data
* @param error The error code received * @param error The error code received
*/ */
onRequestFinished( onRequestFinished(data: { [key: string]: any } | null, index: number, error: number) {
data: ApiGenericDataType | null, if (index >= 0 && index < this.props.requests.length) {
index: number,
error?: number,
) {
const {props} = this;
if (index >= 0 && index < props.requests.length) {
this.fetchedData[index] = data; this.fetchedData[index] = data;
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS; this.errors[index] = error;
} }
// Token expired, logout user
if (error === ERROR_TYPE.BAD_TOKEN) this.connectionManager.disconnect();
if (this.allRequestsFinished()) this.setState({loading: false}); if (error === ERROR_TYPE.BAD_TOKEN) // Token expired, logout user
this.connectionManager.disconnect();
if (this.allRequestsFinished())
this.setState({loading: false});
}
/**
* Checks if all requests finished processing
*
* @return {boolean} True if all finished
*/
allRequestsFinished() {
let finished = true;
for (let i = 0; i < this.fetchedData.length; i++) {
if (this.fetchedData[i] === undefined) {
finished = false;
break;
}
}
return finished;
}
/**
* Checks if all requests have finished successfully.
* 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 successfully
*/
allRequestsValid() {
let valid = true;
for (let i = 0; i < this.fetchedData.length; i++) {
if (this.fetchedData[i] === null && this.props.requests[i].mandatory) {
valid = false;
break;
}
}
return valid;
} }
/** /**
@ -95,13 +149,9 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
* *
* @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found * @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
*/ */
getError(): number { getError() {
const {props} = this; for (let i = 0; i < this.errors.length; i++) {
for (let i = 0; i < this.errors.length; i += 1) { if (this.errors[i] !== 0 && this.props.requests[i].mandatory) {
if (
this.errors[i] !== ERROR_TYPE.SUCCESS &&
props.requests[i].mandatory
) {
return this.errors[i]; return this.errors[i];
} }
} }
@ -113,14 +163,13 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getErrorRender(): React.Node { getErrorRender() {
const {props} = this;
const errorCode = this.getError(); const errorCode = this.getError();
let shouldOverride = false; let shouldOverride = false;
let override = null; let override = null;
const overrideList = props.errorViewOverride; const overrideList = this.props.errorViewOverride;
if (overrideList != null) { if (overrideList != null) {
for (let i = 0; i < overrideList.length; i += 1) { for (let i = 0; i < overrideList.length; i++) {
if (overrideList[i].errorCode === errorCode) { if (overrideList[i].errorCode === errorCode) {
shouldOverride = true; shouldOverride = true;
override = overrideList[i]; override = overrideList[i];
@ -128,62 +177,25 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
} }
} }
} }
if (shouldOverride && override != null) { if (shouldOverride && override != null) {
return ( return (
<ErrorView <ErrorView
{...this.props}
icon={override.icon} icon={override.icon}
message={override.message} message={override.message}
showRetryButton={override.showRetryButton} showRetryButton={override.showRetryButton}
/> />
); );
} } else {
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />; return (
} <ErrorView
{...this.props}
/** errorCode={errorCode}
* Fetches the data from the server. onRefresh={this.fetchData}
* />
* If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
*
* If the user is logged in, send all requests.
*/
fetchData = () => {
const {state, props} = this;
if (!state.loading) this.setState({loading: true});
if (this.connectionManager.isLoggedIn()) {
for (let i = 0; i < props.requests.length; i += 1) {
this.connectionManager
.authenticatedRequest(
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 finished processing
*
* @return {boolean} True if all finished
*/
allRequestsFinished(): boolean {
let finished = true;
this.errors.forEach((error: number | null) => {
if (error == null) finished = false;
});
return finished;
} }
/** /**
@ -193,12 +205,14 @@ class AuthenticatedScreen extends React.Component<PropsType, StateType> {
this.fetchData(); this.fetchData();
} }
render(): React.Node { render() {
const {state, props} = this; return (
if (state.loading) return <BasicLoadingScreen />; this.state.loading
if (this.getError() === ERROR_TYPE.SUCCESS) ? <BasicLoadingScreen/>
return props.renderFunction(this.fetchedData); : (this.allRequestsValid()
return this.getErrorRender(); ? this.props.renderFunction(this.fetchedData)
: this.getErrorRender())
);
} }
} }

View file

@ -2,43 +2,42 @@
import * as React from 'react'; import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import LoadingConfirmDialog from "../Dialogs/LoadingConfirmDialog";
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog'; import ConnectionManager from "../../managers/ConnectionManager";
import ConnectionManager from '../../managers/ConnectionManager'; import {StackNavigationProp} from "@react-navigation/stack";
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
visible: boolean, visible: boolean,
onDismiss: () => void, onDismiss: () => void,
}; }
class LogoutDialog extends React.PureComponent<PropsType> { class LogoutDialog extends React.PureComponent<Props> {
onClickAccept = async (): Promise<void> => {
const {props} = this; onClickAccept = async () => {
return new Promise((resolve: () => void) => { return new Promise((resolve) => {
ConnectionManager.getInstance() ConnectionManager.getInstance().disconnect()
.disconnect()
.then(() => { .then(() => {
props.navigation.reset({ this.props.navigation.reset({
index: 0, index: 0,
routes: [{name: 'main'}], routes: [{name: 'main'}],
}); });
props.onDismiss(); this.props.onDismiss();
resolve(); resolve();
}); });
}); });
}; };
render(): React.Node { render() {
const {props} = this;
return ( return (
<LoadingConfirmDialog <LoadingConfirmDialog
visible={props.visible} {...this.props}
onDismiss={props.onDismiss} visible={this.props.visible}
onDismiss={this.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")}
/> />
); );
} }

View file

@ -2,35 +2,33 @@
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 {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
type PropsType = { type Props = {
theme: CustomThemeType, theme: CustomTheme
}; }
class VoteNotAvailable extends React.Component<PropsType> { class VoteNotAvailable extends React.Component<Props> {
shouldComponentUpdate(): boolean {
shouldComponentUpdate() {
return false; return false;
} }
render(): React.Node { render() {
const {props} = this;
return ( return (
<View <View style={{
style={{ width: "100%",
width: '100%',
marginTop: 10, marginTop: 10,
marginBottom: 10, marginBottom: 10,
}}> }}>
<Headline <Headline
style={{ style={{
color: props.theme.colors.textDisabled, color: this.props.theme.colors.textDisabled,
textAlign: 'center', textAlign: "center",
}}> }}
{i18n.t('screens.vote.noVote')} >{i18n.t("screens.vote.noVote")}</Headline>
</Headline>
</View> </View>
); );
} }

View file

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

View file

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

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 PropsType = { type Props = {
startDate: string, startDate: string,
}; }
const styles = StyleSheet.create({ export default class VoteTease extends React.Component<Props> {
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
});
export default class VoteTease extends React.Component<PropsType> { shouldComponentUpdate() {
shouldComponentUpdate(): boolean {
return false; return false;
} }
render(): React.Node { render() {
const {props} = this;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
title={i18n.t('screens.vote.tease.title')} title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')} subtitle={i18n.t('screens.vote.tease.subtitle')}
left={({size}: {size: number}): React.Node => ( left={props => <Avatar.Icon
<Avatar.Icon size={size} icon="vote" /> {...props}
)} icon="vote"/>}
/> />
<Card.Content> <Card.Content>
<Paragraph> <Paragraph>
{`${i18n.t('screens.vote.tease.message')} ${props.startDate}`} {i18n.t('screens.vote.tease.message') + ' ' + this.props.startDate}
</Paragraph> </Paragraph>
</Card.Content> </Card.Content>
</Card> </Card>
); );
} }
} }
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
});

View file

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

View file

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

View file

@ -1,33 +1,142 @@
// @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 {StackNavigationProp} from '@react-navigation/stack'; import CustomTabBar from "../Tabbar/CustomTabBar";
import AutoHideHandler from '../../utils/AutoHideHandler'; import {StackNavigationProp} from "@react-navigation/stack";
import CustomTabBar from '../Tabbar/CustomTabBar'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {OnScrollType} from '../../utils/AutoHideHandler';
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
onPress: (action: string, data?: string) => void, onPress: (action: string, data: any) => void,
seekAttention: boolean, seekAttention: boolean,
}; }
type StateType = { type State = {
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: {
@ -44,136 +153,18 @@ const styles = StyleSheet.create({
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,30 +1,24 @@
// @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 AutoHideHandler from '../../utils/AutoHideHandler'; import CustomTabBar from "../Tabbar/CustomTabBar";
import CustomTabBar from '../Tabbar/CustomTabBar'; import {StackNavigationProp} from "@react-navigation/stack";
type PropsType = { type Props = {
navigation: StackNavigationProp,
icon: string, icon: string,
onPress: () => void, onPress: () => void,
}; }
const AnimatedFab = Animatable.createAnimatableComponent(FAB); const AnimatedFab = Animatable.createAnimatableComponent(FAB);
const styles = StyleSheet.create({ export default class AnimatedFAB extends React.Component<Props> {
fab: {
position: 'absolute',
margin: 16,
right: 0,
},
});
export default class AnimatedFAB extends React.Component<PropsType> {
ref: {current: null | Animatable.View};
ref: { current: null | Animatable.View };
hideHandler: AutoHideHandler; hideHandler: AutoHideHandler;
constructor() { constructor() {
@ -40,24 +34,33 @@ export default class AnimatedFAB extends React.Component<PropsType> {
onHideChange = (shouldHide: boolean) => { onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) { if (this.ref.current != null) {
if (shouldHide) this.ref.current.bounceOutDown(1000); if (shouldHide)
else this.ref.current.bounceInUp(1000); this.ref.current.bounceOutDown(1000);
else
this.ref.current.bounceInUp(1000);
}
} }
};
render(): React.Node { render() {
const {props} = this;
return ( return (
<AnimatedFab <AnimatedFab
ref={this.ref} ref={this.ref}
useNativeDriver useNativeDriver
icon={props.icon} icon={this.props.icon}
onPress={props.onPress} onPress={this.props.onPress}
style={{ style={{
...styles.fab, ...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT, bottom: CustomTabBar.TAB_BAR_HEIGHT
}} }}
/> />
); );
} }
} }
const styles = StyleSheet.create({
fab: {
position: 'absolute',
margin: 16,
right: 0,
},
});

View file

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

View file

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

View file

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

View file

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

View file

@ -2,27 +2,29 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper'; import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from "i18n-js";
type PropsType = { type Props = {
visible: boolean, visible: boolean,
onDismiss: () => void, onDismiss: () => void,
title: string, title: string,
message: string, message: string,
}; }
class AlertDialog extends React.PureComponent<PropsType> { class AlertDialog extends React.PureComponent<Props> {
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}
onDismiss={this.props.onDismiss}>
<Dialog.Title>{this.props.title}</Dialog.Title>
<Dialog.Content> <Dialog.Content>
<Paragraph>{props.message}</Paragraph> <Paragraph>{this.props.message}</Paragraph>
</Dialog.Content> </Dialog.Content>
<Dialog.Actions> <Dialog.Actions>
<Button onPress={props.onDismiss}>{i18n.t('dialog.ok')}</Button> <Button onPress={this.props.onDismiss}>{i18n.t("dialog.ok")}</Button>
</Dialog.Actions> </Dialog.Actions>
</Dialog> </Dialog>
</Portal> </Portal>

View file

@ -1,69 +1,62 @@
// @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 PropsType = { type Props = {
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() {
const {props} = this; this.title = i18n.t("errors.title");
this.title = i18n.t('errors.title'); switch (this.props.errorCode) {
switch (props.errorCode) {
case ERROR_TYPE.BAD_CREDENTIALS: case ERROR_TYPE.BAD_CREDENTIALS:
this.message = i18n.t('errors.badCredentials'); this.message = i18n.t("errors.badCredentials");
break; break;
case ERROR_TYPE.BAD_TOKEN: case ERROR_TYPE.BAD_TOKEN:
this.message = i18n.t('errors.badToken'); this.message = i18n.t("errors.badToken");
break; break;
case ERROR_TYPE.NO_CONSENT: case ERROR_TYPE.NO_CONSENT:
this.message = i18n.t('errors.noConsent'); this.message = i18n.t("errors.noConsent");
break; break;
case ERROR_TYPE.TOKEN_SAVE: case ERROR_TYPE.TOKEN_SAVE:
this.message = i18n.t('errors.tokenSave'); this.message = i18n.t("errors.tokenSave");
break; break;
case ERROR_TYPE.TOKEN_RETRIEVE: case ERROR_TYPE.TOKEN_RETRIEVE:
this.message = i18n.t('errors.unknown'); this.message = i18n.t("errors.unknown");
break; break;
case ERROR_TYPE.BAD_INPUT: case ERROR_TYPE.BAD_INPUT:
this.message = i18n.t('errors.badInput'); this.message = i18n.t("errors.badInput");
break; break;
case ERROR_TYPE.FORBIDDEN: case ERROR_TYPE.FORBIDDEN:
this.message = i18n.t('errors.forbidden'); this.message = i18n.t("errors.forbidden");
break; break;
case ERROR_TYPE.CONNECTION_ERROR: case ERROR_TYPE.CONNECTION_ERROR:
this.message = i18n.t('errors.connectionError'); this.message = i18n.t("errors.connectionError");
break; break;
case ERROR_TYPE.SERVER_ERROR: case ERROR_TYPE.SERVER_ERROR:
this.message = i18n.t('errors.serverError'); this.message = i18n.t("errors.serverError");
break; break;
default: default:
this.message = i18n.t('errors.unknown'); this.message = i18n.t("errors.unknown");
break; break;
} }
this.message += `\n\nCode ${props.errorCode}`; this.message += "\n\nCode " + this.props.errorCode;
} }
render(): React.Node { render() {
this.generateMessage(); this.generateMessage();
const {props} = this;
return ( return (
<AlertDialog <AlertDialog {...this.props} title={this.title} message={this.message}/>
visible={props.visible}
onDismiss={props.onDismiss}
title={this.title}
message={this.message}
/>
); );
} }
} }

View file

@ -1,102 +1,88 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { import {ActivityIndicator, Button, Dialog, Paragraph, Portal} from 'react-native-paper';
ActivityIndicator, import i18n from "i18n-js";
Button,
Dialog,
Paragraph,
Portal,
} from 'react-native-paper';
import i18n from 'i18n-js';
type PropsType = { type Props = {
visible: boolean, visible: boolean,
onDismiss?: () => void, onDismiss: () => void,
onAccept?: () => Promise<void>, // async function to be executed onAccept: () => Promise<void>, // async function to be executed
title?: string, title: string,
titleLoading?: string, titleLoading: string,
message?: string, message: string,
startLoading?: boolean, startLoading: boolean,
}; }
type StateType = { type State = {
loading: boolean, loading: boolean,
}; }
class LoadingConfirmDialog extends React.PureComponent<Props, State> {
class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
static defaultProps = { static defaultProps = {
onDismiss: () => {},
onAccept: (): Promise<void> => {
return Promise.resolve();
},
title: '', title: '',
titleLoading: '',
message: '', message: '',
onDismiss: () => {},
onAccept: () => {return Promise.resolve()},
startLoading: false, startLoading: false,
};
constructor(props: PropsType) {
super(props);
this.state = {
loading:
props.startLoading != null
? 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 = () => {
const {props} = this;
this.setState({loading: true}); this.setState({loading: true});
if (props.onAccept != null) props.onAccept().then(this.hideLoading); this.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 = (): TimeoutID => hideLoading = () => setTimeout(() => {
setTimeout(() => { this.setState({loading: false})
this.setState({loading: false});
}, 200); }, 200);
/** /**
* Hide the dialog if it is not loading * Hide the dialog if it is not loading
*/ */
onDismiss = () => { onDismiss = () => {
const {state, props} = this; if (!this.state.loading)
if (!state.loading && props.onDismiss != null) props.onDismiss(); this.props.onDismiss();
}; };
render(): React.Node { render() {
const {state, props} = this;
return ( return (
<Portal> <Portal>
<Dialog visible={props.visible} onDismiss={this.onDismiss}> <Dialog
visible={this.props.visible}
onDismiss={this.onDismiss}>
<Dialog.Title> <Dialog.Title>
{state.loading ? props.titleLoading : props.title} {this.state.loading
? this.props.titleLoading
: this.props.title}
</Dialog.Title> </Dialog.Title>
<Dialog.Content> <Dialog.Content>
{state.loading ? ( {this.state.loading
<ActivityIndicator animating size="large" /> ? <ActivityIndicator
) : ( animating={true}
<Paragraph>{props.message}</Paragraph> size={'large'}/>
)} : <Paragraph>{this.props.message}</Paragraph>
}
</Dialog.Content> </Dialog.Content>
{state.loading ? null : ( {this.state.loading
<Dialog.Actions> ? null
<Button onPress={this.onDismiss} style={{marginRight: 10}}> : <Dialog.Actions>
{i18n.t('dialog.cancel')} <Button onPress={this.onDismiss}
</Button> style={{marginRight: 10}}>{i18n.t("dialog.cancel")}</Button>
<Button onPress={this.onClickAccept}> <Button onPress={this.onClickAccept}>{i18n.t("dialog.yes")}</Button>
{i18n.t('dialog.yes')}
</Button>
</Dialog.Actions> </Dialog.Actions>
)} }
</Dialog> </Dialog>
</Portal> </Portal>
); );

View file

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

View file

@ -2,44 +2,35 @@
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 PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
}; }
class ActionsDashBoardItem extends React.Component<PropsType> { class ActionsDashBoardItem extends React.Component<Props> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this; shouldComponentUpdate(nextProps: Props): boolean {
return nextProps.theme.dark !== props.theme.dark; return (nextProps.theme.dark !== this.props.theme.dark);
} }
render(): React.Node { render() {
const {props} = this;
return ( return (
<View> <View>
<List.Item <List.Item
title={i18n.t('screens.feedback.homeButtonTitle')} title={i18n.t("screens.feedback.homeButtonTitle")}
description={i18n.t('screens.feedback.homeButtonSubtitle')} description={i18n.t("screens.feedback.homeButtonSubtitle")}
left={({size}: {size: number}): React.Node => ( left={props => <List.Icon {...props} icon={"comment-quote"}/>}
<List.Icon size={size} icon="comment-quote" /> right={props => <List.Icon {...props} icon={"chevron-right"}/>}
)} onPress={() => this.props.navigation.navigate("feedback")}
right={({size}: {size: number}): React.Node => ( style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}}
<List.Icon size={size} icon="chevron-right" />
)}
onPress={(): void => props.navigation.navigate('feedback')}
style={{
paddingTop: 0,
paddingBottom: 0,
marginLeft: 10,
marginRight: 10,
}}
/> />
</View> </View>
); );
} }
} }

View file

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

View file

@ -2,47 +2,67 @@
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 {FeedItemType} from '../../screens/Home/HomeScreen'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {feedItem} from "../../screens/Home/HomeScreen";
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
item: FeedItemType, theme: CustomTheme,
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<PropsType> { class FeedItem extends React.Component<Props> {
shouldComponentUpdate(): boolean {
shouldComponentUpdate() {
return false; return false;
} }
/**
* Gets the amicale INSAT logo
*
* @return {*}
*/
getAvatar() {
return (
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48,
height: 48,
}}/>
);
}
onPress = () => { onPress = () => {
const {props} = this; this.props.navigation.navigate(
props.navigation.navigate('feed-information', { 'feed-information',
data: props.item, {
date: props.subtitle, data: this.props.item,
date: this.props.subtitle
}); });
}; };
render(): React.Node { render() {
const {props} = this; const item = this.props.item;
const {item} = props; const hasImage = item.full_picture !== '' && item.full_picture !== undefined;
const hasImage =
item.full_picture !== '' && item.full_picture !== undefined;
const cardMargin = 10; const cardMargin = 10;
const cardHeight = props.height - 2 * cardMargin; const cardHeight = this.props.height - 2 * cardMargin;
const imageSize = 250; const imageSize = 250;
const titleHeight = 80; const titleHeight = 80;
const actionsHeight = 60; const actionsHeight = 60;
@ -54,29 +74,23 @@ class FeedItem extends React.Component<PropsType> {
style={{ style={{
margin: cardMargin, margin: cardMargin,
height: cardHeight, height: cardHeight,
}}> }}
<TouchableRipple style={{flex: 1}} onPress={this.onPress}> >
<TouchableRipple
style={{flex: 1}}
onPress={this.onPress}>
<View> <View>
<Card.Title <Card.Title
title={props.title} title={this.props.title}
subtitle={props.subtitle} subtitle={this.props.subtitle}
left={(): React.Node => ( left={this.getAvatar}
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48,
height: 48,
}}
/>
)}
style={{height: titleHeight}} style={{height: titleHeight}}
/> />
{hasImage ? ( {hasImage ?
<View style={{marginLeft: 'auto', marginRight: 'auto'}}> <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal <ImageModal
resizeMode="contain" resizeMode="contain"
imageBackgroundColor="#000" imageBackgroundColor={"#000"}
style={{ style={{
width: imageSize, width: imageSize,
height: imageSize, height: imageSize,
@ -84,23 +98,21 @@ class FeedItem extends React.Component<PropsType> {
source={{ source={{
uri: item.full_picture, uri: item.full_picture,
}} }}
/> /></View> : null}
</View>
) : null}
<Card.Content> <Card.Content>
{item.message !== undefined ? ( {item.message !== undefined ?
<Autolink <Autolink
text={item.message} text={item.message}
hashtag="facebook" hashtag="facebook"
component={Text} component={Text}
style={{height: textHeight}} style={{height: textHeight}}
/> /> : null
) : null} }
</Card.Content> </Card.Content>
<Card.Actions style={{height: actionsHeight}}> <Card.Actions style={{height: actionsHeight}}>
<Button <Button
onPress={this.onPress} onPress={this.onPress}
icon="plus" icon={'plus'}
style={{marginLeft: 'auto'}}> style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')} {i18n.t('screens.home.dashboard.seeMore')}
</Button> </Button>

View file

@ -1,21 +1,81 @@
// @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 {EventType} from '../../screens/Home/HomeScreen'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {event} from "../../screens/Home/HomeScreen";
type PropsType = { type Props = {
event?: EventType | null, event?: event,
clickAction: () => void, clickAction: () => void,
}; theme?: CustomTheme,
}
/**
* Component used to display an event preview if an event is available
*/
class PreviewEventDashboardItem extends React.Component<Props> {
render() {
const props = this.props;
const isEmpty = props.event == null
? true
: isDescriptionEmpty(props.event.description);
if (props.event != null) {
const event = props.event;
const hasImage = event.logo !== '' && event.logo != null;
const getImage = () => <Avatar.Image
source={{uri: event.logo}}
size={50}
style={styles.avatar}/>;
return (
<Card
style={styles.card}
elevation={3}
>
<TouchableRipple
style={{flex: 1}}
onPress={props.clickAction}>
<View>
{hasImage ?
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
left={getImage}
/> :
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
/>}
{!isEmpty ?
<Card.Content style={styles.content}>
<CustomHTML html={event.description}/>
</Card.Content> : null}
<Card.Actions style={styles.actions}>
<Button
icon={'chevron-right'}
>
{i18n.t("screens.home.dashboard.seeMore")}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
} else
return null;
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
marginBottom: 10, marginBottom: 10
}, },
content: { content: {
maxHeight: 150, maxHeight: 150,
@ -24,77 +84,11 @@ const styles = StyleSheet.create({
actions: { actions: {
marginLeft: 'auto', marginLeft: 'auto',
marginTop: 'auto', marginTop: 'auto',
flexDirection: 'row', flexDirection: 'row'
}, },
avatar: { avatar: {
backgroundColor: 'transparent', backgroundColor: 'transparent'
}, }
}); });
/**
* Component used to display an event preview if an event is available
*/
// eslint-disable-next-line react/prefer-stateless-function
class PreviewEventDashboardItem extends React.Component<PropsType> {
static defaultProps = {
event: null,
};
render(): React.Node {
const {props} = this;
const {event} = props;
const isEmpty =
event == null ? true : isDescriptionEmpty(event.description);
if (event != null) {
const hasImage = event.logo !== '' && event.logo != null;
const getImage = (): React.Node => (
<Avatar.Image
source={{uri: event.logo}}
size={50}
style={styles.avatar}
/>
);
return (
<Card style={styles.card} elevation={3}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
{hasImage ? (
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(
event.date_begin,
event.date_end,
)}
left={getImage}
/>
) : (
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(
event.date_begin,
event.date_end,
)}
/>
)}
{!isEmpty ? (
<Card.Content style={styles.content}>
<CustomHTML html={event.description} />
</Card.Content>
) : null}
<Card.Actions style={styles.actions}>
<Button icon="chevron-right">
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
return null;
}
}
export default PreviewEventDashboardItem; 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 * as Animatable from 'react-native-animatable'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {CustomThemeType} from '../../managers/ThemeManager'; import * as Animatable from "react-native-animatable";
type PropsType = { type Props = {
image: string | null, image: string,
onPress: () => void | null, onPress: () => void,
badgeCount: number | null, badgeCount: number | null,
theme: CustomThemeType, theme: CustomTheme,
}; };
const AnimatableBadge = Animatable.createAnimatableComponent(Badge); const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
@ -18,51 +18,50 @@ 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<PropsType> { class SmallDashboardItem extends React.Component<Props> {
itemSize: number; itemSize: number;
constructor(props: PropsType) { constructor(props: Props) {
super(props); super(props);
this.itemSize = Dimensions.get('window').width / 8; this.itemSize = Dimensions.get('window').width / 8;
} }
shouldComponentUpdate(nextProps: PropsType): boolean { shouldComponentUpdate(nextProps: Props) {
const {props} = this; return (nextProps.theme.dark !== this.props.theme.dark)
return ( || (nextProps.badgeCount !== this.props.badgeCount);
nextProps.theme.dark !== props.theme.dark ||
nextProps.badgeCount !== props.badgeCount
);
} }
render(): React.Node { render() {
const {props} = this; const props = this.props;
return ( return (
<TouchableRipple <TouchableRipple
onPress={props.onPress} onPress={this.props.onPress}
borderless borderless={true}
style={{ style={{
marginLeft: this.itemSize / 6, marginLeft: this.itemSize / 6,
marginRight: this.itemSize / 6, marginRight: this.itemSize / 6,
}}> }}
<View >
style={{ <View style={{
width: this.itemSize, width: this.itemSize,
height: this.itemSize, height: this.itemSize,
}}> }}>
<Image <Image
source={{uri: props.image}} source={{uri: props.image}}
style={{ style={{
width: '80%', width: "80%",
height: '80%', height: "80%",
marginLeft: 'auto', marginLeft: "auto",
marginRight: 'auto', marginRight: "auto",
marginTop: 'auto', marginTop: "auto",
marginBottom: 'auto', marginBottom: "auto",
}} }}
/> />
{props.badgeCount != null && props.badgeCount > 0 ? ( {
props.badgeCount != null && props.badgeCount > 0 ?
<AnimatableBadge <AnimatableBadge
animation="zoomIn" animation={"zoomIn"}
duration={300} duration={300}
useNativeDriver useNativeDriver
style={{ style={{
@ -74,12 +73,14 @@ class SmallDashboardItem extends React.Component<PropsType> {
borderWidth: 2, borderWidth: 2,
}}> }}>
{props.badgeCount} {props.badgeCount}
</AnimatableBadge> </AnimatableBadge> : null
) : null} }
</View> </View>
</TouchableRipple> </TouchableRipple>
); );
} }
} }
export default withTheme(SmallDashboardItem); export default withTheme(SmallDashboardItem);

View file

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

View file

@ -2,23 +2,25 @@
import * as React from 'react'; import * as React from 'react';
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper'; import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
import {View} from 'react-native'; import {View} from "react-native";
import type {ServiceItemType} from '../../../managers/ServicesManager'; import type {cardItem} from "./CardList";
type PropsType = { type Props = {
item: ServiceItemType, item: cardItem,
}; }
export default class CardListItem extends React.Component<PropsType> { export default class CardListItem extends React.Component<Props> {
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
: {uri: item.image};
return ( return (
<Card <Card
style={{ style={{
@ -26,16 +28,23 @@ export default class CardListItem extends React.Component<PropsType> {
margin: 5, margin: 5,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}}> }}
<TouchableRipple style={{flex: 1}} onPress={item.onPress}> >
<TouchableRipple
style={{flex: 1}}
onPress={item.onPress}>
<View> <View>
<Card.Cover style={{height: 80}} source={source} /> <Card.Cover
style={{height: 80}}
source={source}
/>
<Card.Content> <Card.Content>
<Paragraph>{item.title}</Paragraph> <Paragraph>{item.title}</Paragraph>
<Caption>{item.subtitle}</Caption> <Caption>{item.subtitle}</Caption>
</Card.Content> </Card.Content>
</View> </View>
</TouchableRipple> </TouchableRipple>
</Card> </Card>
); );
} }

View file

@ -3,47 +3,48 @@
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 {ServiceItemType} from '../../../managers/ServicesManager'; import type {cardItem} from "./CardList";
type PropsType = { type Props = {
item: ServiceItemType, item: cardItem,
width: number, width: number,
}; }
export default class ImageListItem extends React.Component<PropsType> { export default class ImageListItem extends React.Component<Props> {
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> <View>
<Image <Image
style={{ style={{
width: props.width - 20, width: this.props.width - 20,
height: props.width - 20, height: this.props.width - 20,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}} }}
source={source} source={source}
/> />
<Text <Text style={{
style={{
marginTop: 5, marginTop: 5,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
textAlign: 'center', textAlign: 'center'
}}> }}>
{item.title} {item.title}
</Text> </Text>

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

@ -2,43 +2,43 @@
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 PropsType = { type Props = {
onPress: () => void, onPress: Function,
color: string, color: string,
item: ProximoArticleType, item: Object,
height: number, height: number,
}; }
class ProximoListItem extends React.Component<PropsType> { class ProximoListItem extends React.Component<Props> {
shouldComponentUpdate(): boolean {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
shouldComponentUpdate() {
return false; return false;
} }
render(): React.Node { render() {
const {props} = this;
return ( return (
<List.Item <List.Item
title={props.item.name} title={this.props.item.name}
description={`${props.item.quantity} ${i18n.t( description={this.props.item.quantity + ' ' + i18n.t('screens.proximo.inStock')}
'screens.proximo.inStock', descriptionStyle={{color: this.props.color}}
)}`} onPress={this.props.onPress}
descriptionStyle={{color: props.color}} left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64}
onPress={props.onPress} source={{uri: this.props.item.image}}/>}
left={(): React.Node => ( right={() =>
<Avatar.Image <Text style={{fontWeight: "bold"}}>
style={{backgroundColor: 'transparent'}} {this.props.item.price}
size={64} </Text>}
source={{uri: props.item.image}}
/>
)}
right={(): React.Node => (
<Text style={{fontWeight: 'bold'}}>{props.item.price}</Text>
)}
style={{ style={{
height: props.height, height: this.props.height,
justifyContent: 'center', justifyContent: 'center',
}} }}
/> />

View file

@ -1,65 +1,36 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import { import {Avatar, Caption, List, ProgressBar, Surface, Text, withTheme} from 'react-native-paper';
Avatar, import {StyleSheet, View} from "react-native";
Caption, import ProxiwashConstants from "../../../constants/ProxiwashConstants";
List, import i18n from "i18n-js";
ProgressBar, import AprilFoolsManager from "../../../managers/AprilFoolsManager";
Surface, import * as Animatable from "react-native-animatable";
Text, import type {CustomTheme} from "../../../managers/ThemeManager";
withTheme, import type {Machine} from "../../../screens/Proxiwash/ProxiwashScreen";
} 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 = { type Props = {
item: ProxiwashMachineType, item: Machine,
theme: CustomThemeType, theme: CustomTheme,
onPress: ( onPress: Function,
title: string,
item: ProxiwashMachineType,
isDryer: boolean,
) => void,
isWatched: boolean, isWatched: boolean,
isDryer: boolean, isDryer: boolean,
height: number, height: number,
}; }
const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon); const AnimatedIcon = Animatable.createAnimatableComponent(Avatar.Icon);
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<PropsType> { class ProxiwashListItem extends React.Component<Props> {
stateColors: {[key: string]: string};
stateStrings: {[key: string]: string}; stateColors: Object;
stateStrings: Object;
title: string; title: string;
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
this.stateColors = {}; this.stateColors = {};
this.stateStrings = {}; this.stateStrings = {};
@ -68,113 +39,80 @@ class ProxiwashListItem extends React.Component<PropsType> {
let displayNumber = props.item.number; let displayNumber = props.item.number;
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber( displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(parseInt(props.item.number));
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 += `${displayNumber}`; this.title += ' n°' + displayNumber;
} }
shouldComponentUpdate(nextProps: PropsType): boolean { shouldComponentUpdate(nextProps: Props): boolean {
const {props} = this; const props = this.props;
return ( return (nextProps.theme.dark !== props.theme.dark)
nextProps.theme.dark !== props.theme.dark || || (nextProps.item.state !== props.item.state)
nextProps.item.state !== props.item.state || || (nextProps.item.donePercent !== props.item.donePercent)
nextProps.item.donePercent !== props.item.donePercent || || (nextProps.isWatched !== props.isWatched);
nextProps.isWatched !== props.isWatched
);
} }
onListItemPress = () => {
const {props} = this;
props.onPress(this.title, props.item, props.isDryer);
};
updateStateStrings() { updateStateStrings() {
this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t( this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t('screens.proxiwash.states.ready');
'screens.proxiwash.states.ready', this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t('screens.proxiwash.states.running');
); this.stateStrings[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = i18n.t('screens.proxiwash.states.runningNotStarted');
this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t( this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t('screens.proxiwash.states.finished');
'screens.proxiwash.states.running', this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t('screens.proxiwash.states.broken');
); this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t('screens.proxiwash.states.error');
this.stateStrings[ this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t('screens.proxiwash.states.unknown');
ProxiwashConstants.machineStates.RUNNING_NOT_STARTED
] = i18n.t('screens.proxiwash.states.runningNotStarted');
this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t(
'screens.proxiwash.states.finished',
);
this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t(
'screens.proxiwash.states.broken',
);
this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t(
'screens.proxiwash.states.error',
);
this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t(
'screens.proxiwash.states.unknown',
);
} }
updateStateColors() { updateStateColors() {
const {props} = this; const colors = this.props.theme.colors;
const {colors} = props.theme; this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] = colors.proxiwashReadyColor;
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] = this.stateColors[ProxiwashConstants.machineStates.RUNNING] = colors.proxiwashRunningColor;
colors.proxiwashReadyColor; this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = colors.proxiwashRunningNotStartedColor;
this.stateColors[ProxiwashConstants.machineStates.RUNNING] = this.stateColors[ProxiwashConstants.machineStates.FINISHED] = colors.proxiwashFinishedColor;
colors.proxiwashRunningColor; this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] = colors.proxiwashBrokenColor;
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = this.stateColors[ProxiwashConstants.machineStates.ERROR] = colors.proxiwashErrorColor;
colors.proxiwashRunningNotStartedColor; this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] = colors.proxiwashUnknownColor;
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(): React.Node { onListItemPress = () => this.props.onPress(this.title, this.props.item, this.props.isDryer);
const {props} = this;
const {colors} = props.theme; render() {
const props = this.props;
const colors = props.theme.colors;
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 const description = isRunning ? props.item.startTime + '/' + props.item.endTime : '';
? `${props.item.startTime}/${props.item.endTime}`
: '';
const stateIcon = ProxiwashConstants.stateIcons[machineState]; const stateIcon = ProxiwashConstants.stateIcons[machineState];
const stateString = this.stateStrings[machineState]; const stateString = this.stateStrings[machineState];
let progress; const progress = isRunning
if (isRunning && props.item.donePercent !== '') ? props.item.donePercent !== ''
progress = parseFloat(props.item.donePercent) / 100; ? parseFloat(props.item.donePercent) / 100
else if (isRunning) progress = 0; : 0
else progress = 1; : 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
<AnimatedIcon
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'} icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
animation={isRunning ? 'pulse' : undefined} animation={isRunning ? "pulse" : undefined}
iterationCount="infinite" iterationCount={"infinite"}
easing="linear" easing={"linear"}
duration={1000} duration={1000}
useNativeDriver useNativeDriver
size={40} size={40}
color={colors.text} color={colors.text}
style={styles.icon} style={styles.icon}
/> />;
);
this.updateStateColors(); this.updateStateColors();
return ( return (
<Surface <Surface
@ -182,17 +120,20 @@ class ProxiwashListItem extends React.Component<PropsType> {
...styles.container, ...styles.container,
height: props.height, height: props.height,
borderRadius: 4, borderRadius: 4,
}}> }}
{!isReady ? ( >
<ProgressBar {
!isReady
? <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}
@ -201,23 +142,25 @@ class ProxiwashListItem extends React.Component<PropsType> {
justifyContent: 'center', justifyContent: 'center',
}} }}
onPress={this.onListItemPress} onPress={this.onListItemPress}
left={(): React.Node => icon} left={() => icon}
right={(): React.Node => ( right={() => (
<View style={{flexDirection: 'row'}}> <View style={{flexDirection: 'row',}}>
<View style={{justifyContent: 'center'}}> <View style={{justifyContent: 'center',}}>
<Text <Text style={
style={ machineState === ProxiwashConstants.machineStates.FINISHED ?
machineState === ProxiwashConstants.machineStates.FINISHED {fontWeight: 'bold',} : {}
? {fontWeight: 'bold'} }
: {} >
}>
{stateString} {stateString}
</Text> </Text>
{machineState === ProxiwashConstants.machineStates.RUNNING ? ( {
<Caption>{props.item.remainingTime} min</Caption> machineState === ProxiwashConstants.machineStates.RUNNING
) : null} ? <Caption>{props.item.remainingTime} min</Caption>
: null
}
</View> </View>
<View style={{justifyContent: 'center'}}> <View style={{justifyContent: 'center',}}>
<Avatar.Icon <Avatar.Icon
icon={stateIcon} icon={stateIcon}
color={colors.text} color={colors.text}
@ -225,12 +168,27 @@ class ProxiwashListItem extends React.Component<PropsType> {
style={styles.icon} style={styles.icon}
/> />
</View> </View>
</View> </View>)}
)}
/> />
</Surface> </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,17 +1,56 @@
// @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Text, withTheme} from 'react-native-paper'; import {Avatar, Text, withTheme} from 'react-native-paper';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from "react-native";
import i18n from 'i18n-js'; import i18n from "i18n-js";
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = { type Props = {
theme: CustomThemeType,
title: string, title: string,
isDryer: boolean, isDryer: boolean,
nbAvailable: number, nbAvailable: number,
}; }
/**
* Component used to display a proxiwash item, showing machine progression and state
*/
class ProxiwashListItem extends React.Component<Props> {
constructor(props) {
super(props);
}
shouldComponentUpdate(nextProps: Props) {
return (nextProps.theme.dark !== this.props.theme.dark)
|| (nextProps.nbAvailable !== this.props.nbAvailable)
}
render() {
const props = this.props;
const subtitle = props.nbAvailable + ' ' + (
(props.nbAvailable <= 1)
? i18n.t('screens.proxiwash.numAvailable')
: i18n.t('screens.proxiwash.numAvailablePlural'));
const iconColor = props.nbAvailable > 0
? this.props.theme.colors.success
: this.props.theme.colors.primary;
return (
<View style={styles.container}>
<Avatar.Icon
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
color={iconColor}
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({ const styles = StyleSheet.create({
container: { container: {
@ -22,51 +61,12 @@ const styles = StyleSheet.create({
marginTop: 20, marginTop: 20,
}, },
icon: { icon: {
backgroundColor: 'transparent', backgroundColor: 'transparent'
}, },
text: { text: {
fontSize: 20, fontSize: 20,
fontWeight: 'bold', fontWeight: 'bold',
}, }
}); });
/**
* Component used to display a proxiwash item, showing machine progression and state
*/
class ProxiwashListItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.nbAvailable !== props.nbAvailable
);
}
render(): React.Node {
const {props} = this;
const subtitle = `${props.nbAvailable} ${
props.nbAvailable <= 1
? i18n.t('screens.proxiwash.numAvailable')
: i18n.t('screens.proxiwash.numAvailablePlural')
}`;
const iconColor =
props.nbAvailable > 0
? props.theme.colors.success
: props.theme.colors.primary;
return (
<View style={styles.container}>
<Avatar.Icon
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
color={iconColor}
style={styles.icon}
/>
<View style={{justifyContent: 'center'}}>
<Text style={styles.text}>{props.title}</Text>
<Text style={{color: props.theme.colors.subtitle}}>{subtitle}</Text>
</View>
</View>
);
}
}
export default withTheme(ProxiwashListItem); export default withTheme(ProxiwashListItem);

View file

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

View file

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

View file

@ -1,42 +1,32 @@
// @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 PropsType = { type Props = {
style?: ViewStyle | null, style?: ViewStyle,
size: number, size: number,
color: string, color: string,
}; }
export default class SpeechArrow extends React.Component<PropsType> { export default class SpeechArrow extends React.Component<Props> {
static defaultProps = {
style: null,
};
shouldComponentUpdate(): boolean { render() {
return false;
}
render(): React.Node {
const {props} = this;
return ( return (
<View style={props.style}> <View style={this.props.style}>
<View <View style={{
style={{
width: 0, width: 0,
height: 0, height: 0,
borderLeftWidth: 0, borderLeftWidth: 0,
borderRightWidth: props.size, borderRightWidth: this.props.size,
borderBottomWidth: props.size, borderBottomWidth: this.props.size,
borderStyle: 'solid', borderStyle: 'solid',
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderLeftColor: 'transparent', borderLeftColor: 'transparent',
borderRightColor: 'transparent', borderRightColor: 'transparent',
borderBottomColor: props.color, borderBottomColor: this.props.color,
}} }}/>
/>
</View> </View>
); );
} }

View file

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

View file

@ -1,57 +1,46 @@
/* 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 PropsType = { type Props = {
theme: CustomThemeType, theme: Object,
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<PropsType> { class CustomHTML extends React.Component<Props> {
openWebLink = (event: {...}, link: string) => {
Linking.openURL(link); openWebLink = (event, link) => {
Linking.openURL(link).catch((err) => console.error('Error opening link', err));
}; };
getBasicText = ( getBasicText = (htmlAttribs, children, convertedCSSStyles, passProps) => {
htmlAttribs,
children,
convertedCSSStyles,
passProps,
): React.Node => {
// eslint-disable-next-line react/jsx-props-no-spreading
return <Text {...passProps}>{children}</Text>; return <Text {...passProps}>{children}</Text>;
}; };
getListBullet = (): React.Node => { getListBullet = (htmlAttribs, children, convertedCSSStyles, passProps) => {
return <Text>- </Text>; return (
<Text>- </Text>
);
}; };
render(): React.Node { render() {
const {props} = this;
// Surround description with p to allow text styling if the description is not html // Surround description with p to allow text styling if the description is not html
return ( return <HTML
<HTML html={"<p>" + this.props.html + "</p>"}
html={`<p>${props.html}</p>`}
renderers={{ renderers={{
p: this.getBasicText, p: this.getBasicText,
li: this.getBasicText, li: this.getBasicText,
}} }}
listsPrefixesRenderers={{ listsPrefixesRenderers={{
ul: this.getListBullet, ul: this.getListBullet
}} }}
ignoredTags={['img']} ignoredTags={['img']}
ignoredStyles={['color', 'background-color']} ignoredStyles={['color', 'background-color']}
onLinkPress={this.openWebLink} onLinkPress={this.openWebLink}/>;
/>
);
} }
} }

View file

@ -1,39 +1,27 @@
// @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: { const MaterialHeaderButton = (props: Object) =>
theme: CustomThemeType,
color: string,
}): React.Node => {
const {color, theme} = props;
return (
// $FlowFixMe
<HeaderButton <HeaderButton
// eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}
IconComponent={MaterialCommunityIcons} IconComponent={MaterialCommunityIcons}
iconSize={26} iconSize={26}
color={color != null ? color : theme.colors.text} color={props.color != null ? props.color : props.theme.colors.text}
/> />;
);
};
const MaterialHeaderButtons = (props: {...}): React.Node => { const MaterialHeaderButtons = (props: Object) => {
return ( return (
// $FlowFixMe
<HeaderButtons <HeaderButtons
// eslint-disable-next-line react/jsx-props-no-spreading
{...props} {...props}
HeaderButtonComponent={withTheme(MaterialHeaderButton)} HeaderButtonComponent={withTheme(MaterialHeaderButton)}
/> />
); );
}; };
export default MaterialHeaderButtons; export default withTheme(MaterialHeaderButtons);
export {Item} from 'react-navigation-header-buttons'; export {Item} from 'react-navigation-header-buttons';

View file

@ -1,37 +1,390 @@
// @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 * as Animatable from 'react-native-animatable'; import Mascot, {MASCOT_STYLE} from "../Mascot/Mascot";
import {Card} from 'react-native-paper'; import * as Animatable from "react-native-animatable";
import Update from '../../constants/Update'; import {Card} from "react-native-paper";
import ThemeManager from '../../managers/ThemeManager';
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
type PropsType = { type Props = {
onDone: () => void, onDone: Function,
isUpdate: boolean, isUpdate: boolean,
isAprilFools: boolean, isAprilFools: boolean,
}; };
type StateType = { type State = {
currentSlide: number, currentSlide: number,
}; }
type IntroSlideType = { type Slide = {
key: string, key: string,
title: string, title: string,
text: string, text: string,
view: () => React.Node, view: () => React.Node,
mascotStyle: number, mascotStyle: number,
colors: [string, string], colors: [string, string]
}; };
/**
* Class used to create intro slides
*/
export default class CustomIntroSlider extends React.Component<Props, State> {
state = {
currentSlide: 0,
}
sliderRef: { current: null | AppIntroSlider };
introSlides: Array<Slide>;
updateSlides: Array<Slide>;
aprilFoolsSlides: Array<Slide>;
currentSlides: Array<Slide>;
/**
* 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 = [
{
key: '1',
title: i18n.t('intro.aprilFoolsSlide.title'),
text: i18n.t('intro.aprilFoolsSlide.text'),
view: () => <View/>,
mascotStyle: MASCOT_STYLE.NORMAL,
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});
};
onSkip = () => {
this.setStatusBarColor(this.currentSlides[this.currentSlides.length - 1].colors[0]);
if (this.sliderRef.current != null)
this.sliderRef.current.goToSlide(this.currentSlides.length - 1);
}
onDone = () => {
this.setStatusBarColor(ThemeManager.getCurrentTheme().colors.surface);
this.props.onDone();
}
renderNextButton = () => {
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>
)
}
renderDoneButton = () => {
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() {
this.currentSlides = this.introSlides;
if (this.props.isUpdate)
this.currentSlides = this.updateSlides;
else if (this.props.isAprilFools)
this.currentSlides = this.aprilFoolsSlides;
this.setStatusBarColor(this.currentSlides[0].colors[0]);
return (
<AppIntroSlider
ref={this.sliderRef}
data={this.currentSlides}
extraData={this.state.currentSlide}
renderItem={this.getIntroRenderItem}
renderNextButton={this.renderNextButton}
renderDoneButton={this.renderDoneButton}
onDone={this.onDone}
onSlideChange={this.onSlideChange}
onSkip={this.onSkip}
/>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
mainContent: { mainContent: {
paddingBottom: 100, paddingBottom: 100,
@ -56,348 +409,3 @@ const styles = StyleSheet.create({
marginLeft: 'auto', marginLeft: 'auto',
}, },
}); });
/**
* Class used to create intro slides
*/
export default class CustomIntroSlider extends React.Component<
PropsType,
StateType,
> {
sliderRef: {current: null | AppIntroSlider};
introSlides: Array<IntroSlideType>;
updateSlides: Array<IntroSlideType>;
aprilFoolsSlides: Array<IntroSlideType>;
currentSlides: Array<IntroSlideType>;
/**
* Generates intro slides
*/
constructor() {
super();
this.state = {
currentSlide: 0,
};
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: (): 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],
});
}
this.aprilFoolsSlides = [
{
key: '1',
title: i18n.t('intro.aprilFoolsSlide.title'),
text: i18n.t('intro.aprilFoolsSlide.text'),
view: (): React.Node => <View />,
mascotStyle: MASCOT_STYLE.NORMAL,
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: 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={{
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 = (): React.Node => {
return (
<View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
height: '80%',
}}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'slideInDown',
duration: 2000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
</View>
);
};
getWelcomeView = (): React.Node => {
return (
<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>
);
};
static getIconView(icon: MaterialCommunityIconsGlyphs): React.Node {
return (
<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}
/>
);
}
}

View file

@ -2,10 +2,9 @@
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
@ -13,29 +12,25 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
* @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) {
theme: CustomThemeType, const {colors} = props.theme;
onRef: (re: Modalize) => void,
children?: React.Node,
}): React.Node {
const {theme, onRef, children} = props;
return ( return (
<Modalize <Modalize
ref={onRef} ref={props.onRef}
adjustToContentHeight adjustToContentHeight
handlePosition="inside" handlePosition={'inside'}
modalStyle={{backgroundColor: theme.colors.card}} modalStyle={{backgroundColor: colors.card}}
handleStyle={{backgroundColor: theme.colors.primary}}> handleStyle={{backgroundColor: colors.primary}}
<View >
style={{ <View style={{
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT, paddingBottom: CustomTabBar.TAB_BAR_HEIGHT
}}> }}>
{children} {props.children}
</View> </View>
</Modalize> </Modalize>
); );
} }
CustomModal.defaultProps = {children: null};
export default withTheme(CustomModal); 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 Slider, {SliderProps} from '@react-native-community/slider'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {CustomThemeType} from '../../managers/ThemeManager'; import Slider, {SliderProps} from "@react-native-community/slider";
type PropsType = { type Props = {
theme: CustomThemeType, theme: CustomTheme,
valueSuffix?: string, valueSuffix: string,
...SliderProps, ...SliderProps
}; }
type StateType = { type State = {
currentValue: number, currentValue: number,
}; }
/** /**
* Abstraction layer for Modalize component, using custom configuration * Abstraction layer for Modalize component, using custom configuration
@ -22,44 +22,37 @@ type StateType = {
* @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<PropsType, StateType> { class CustomSlider extends React.Component<Props, State> {
static defaultProps = {
valueSuffix: '',
};
constructor(props: PropsType) { static defaultProps = {
super(props); valueSuffix: "",
this.state = { }
currentValue: props.value,
}; state = {
currentValue: this.props.value,
} }
onValueChange = (value: number) => { onValueChange = (value: number) => {
const {props} = this;
this.setState({currentValue: value}); this.setState({currentValue: value});
if (props.onValueChange != null) props.onValueChange(value); if (this.props.onValueChange != null)
}; this.props.onValueChange(value);
}
render(): React.Node { render() {
const {props, state} = this;
return ( return (
<View style={{flex: 1, flexDirection: 'row'}}> <View style={{flex: 1, flexDirection: 'row'}}>
<Text <Text style={{marginHorizontal: 10, marginTop: 'auto', marginBottom: 'auto'}}>
style={{ {this.state.currentValue}min
marginHorizontal: 10,
marginTop: 'auto',
marginBottom: 'auto',
}}>
{state.currentValue}min
</Text> </Text>
<Slider <Slider
// eslint-disable-next-line react/jsx-props-no-spreading {...this.props}
{...props}
onValueChange={this.onValueChange} onValueChange={this.onValueChange}
/> />
</View> </View>
); );
} }
} }
export default withTheme(CustomSlider); export default withTheme(CustomSlider);

View file

@ -3,7 +3,6 @@
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
@ -11,27 +10,26 @@ import type {CustomThemeType} from '../../managers/ThemeManager';
* @param props Props to pass to the component * @param props Props to pass to the component
* @return {*} * @return {*}
*/ */
function BasicLoadingScreen(props: { function BasicLoadingScreen(props) {
theme: CustomThemeType, const {colors} = props.theme;
isAbsolute: boolean, let position = undefined;
}): React.Node { if (props.isAbsolute !== undefined && props.isAbsolute)
const {theme, isAbsolute} = props; position = 'absolute';
const {colors} = theme;
let position;
if (isAbsolute != null && isAbsolute) position = 'absolute';
return ( return (
<View <View style={{
style={{
backgroundColor: colors.background, backgroundColor: colors.background,
position, position: position,
top: 0, top: 0,
right: 0, right: 0,
width: '100%', width: '100%',
height: '100%', height: '100%',
justifyContent: 'center', justifyContent: 'center',
}}> }}>
<ActivityIndicator animating size="large" color={colors.primary} /> <ActivityIndicator
animating={true}
size={'large'}
color={colors.primary}/>
</View> </View>
); );
} }

View file

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

View file

@ -1,50 +1,47 @@
// @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 { import BasicLoadingScreen from "./BasicLoadingScreen";
Divider, import ErrorView from "./ErrorView";
HiddenItem, import {ERROR_TYPE} from "../../utils/WebData";
OverflowMenu,
} from 'react-navigation-header-buttons';
import i18n from 'i18n-js';
import {Animated, BackHandler, Linking} from 'react-native';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import {withTheme} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack';
import {Collapsible} from 'react-navigation-collapsible';
import type {CustomThemeType} from '../../managers/ThemeManager';
import withCollapsible from '../../utils/withCollapsible';
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton'; import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
import {ERROR_TYPE} from '../../utils/WebData'; import {Divider, HiddenItem, OverflowMenu} from "react-navigation-header-buttons";
import ErrorView from './ErrorView'; import i18n from 'i18n-js';
import BasicLoadingScreen from './BasicLoadingScreen'; import {Animated, BackHandler, Linking} from "react-native";
import {withCollapsible} from "../../utils/withCollapsible";
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import {withTheme} from "react-native-paper";
import type {CustomTheme} from "../../managers/ThemeManager";
import {StackNavigationProp} from "@react-navigation/stack";
import {Collapsible} from "react-navigation-collapsible";
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
url: string, url: string,
customJS: string,
customPaddingFunction: null | (padding: number) => string,
collapsibleStack: Collapsible, collapsibleStack: Collapsible,
onMessage: (event: {nativeEvent: {data: string}}) => void, onMessage: Function,
onScroll: (event: SyntheticEvent<EventTarget>) => void, onScroll: Function,
customJS?: string, showAdvancedControls: boolean,
customPaddingFunction?: null | ((padding: number) => string), }
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<PropsType> { class WebViewScreen extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = {
customJS: '', customJS: '',
showAdvancedControls: true, showAdvancedControls: true,
customPaddingFunction: null, customPaddingFunction: null,
}; };
webviewRef: {current: null | WebView}; webviewRef: { current: null | WebView };
canGoBack: boolean; canGoBack: boolean;
@ -58,24 +55,27 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* Creates header buttons and listens to events after mounting * Creates header buttons and listens to events after mounting
*/ */
componentDidMount() { componentDidMount() {
const {props} = this; this.props.navigation.setOptions({
props.navigation.setOptions({ headerRight: this.props.showAdvancedControls
headerRight: props.showAdvancedControls
? this.getAdvancedButtons ? this.getAdvancedButtons
: this.getBasicButton, : this.getBasicButton,
}); });
props.navigation.addListener('focus', () => { this.props.navigation.addListener(
'focus',
() =>
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid, this.onBackButtonPressAndroid
)
); );
}); this.props.navigation.addListener(
props.navigation.addListener('blur', () => { 'blur',
() =>
BackHandler.removeEventListener( BackHandler.removeEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid, this.onBackButtonPressAndroid
)
); );
});
} }
/** /**
@ -83,7 +83,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @returns {boolean} * @returns {boolean}
*/ */
onBackButtonPressAndroid = (): boolean => { onBackButtonPressAndroid = () => {
if (this.canGoBack) { if (this.canGoBack) {
this.onGoBackClicked(); this.onGoBackClicked();
return true; return true;
@ -96,19 +96,17 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @return {*} * @return {*}
*/ */
getBasicButton = (): React.Node => { getBasicButton = () => {
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
title="refresh" title="refresh"
iconName="refresh" iconName="refresh"
onPress={this.onRefreshClicked} onPress={this.onRefreshClicked}/>
/>
<Item <Item
title={i18n.t('general.openInBrowser')} title={i18n.t("general.openInBrowser")}
iconName="open-in-new" iconName="open-in-new"
onPress={this.onOpenClicked} onPress={this.onOpenClicked}/>
/>
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
}; };
@ -119,8 +117,7 @@ class WebViewScreen extends React.PureComponent<PropsType> {
* *
* @returns {*} * @returns {*}
*/ */
getAdvancedButtons = (): React.Node => { getAdvancedButtons = () => {
const {props} = this;
return ( return (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
@ -134,74 +131,40 @@ class WebViewScreen extends React.PureComponent<PropsType> {
<MaterialCommunityIcons <MaterialCommunityIcons
name="dots-vertical" name="dots-vertical"
size={26} size={26}
color={props.theme.colors.text} color={this.props.theme.colors.text}
/> />}
}> >
<HiddenItem <HiddenItem
title={i18n.t('general.goBack')} title={i18n.t("general.goBack")}
onPress={this.onGoBackClicked} onPress={this.onGoBackClicked}/>
/>
<HiddenItem <HiddenItem
title={i18n.t('general.goForward')} title={i18n.t("general.goForward")}
onPress={this.onGoForwardClicked} onPress={this.onGoForwardClicked}/>
/> <Divider/>
<Divider />
<HiddenItem <HiddenItem
title={i18n.t('general.openInBrowser')} title={i18n.t("general.openInBrowser")}
onPress={this.onOpenClicked} onPress={this.onOpenClicked}/>
/>
</OverflowMenu> </OverflowMenu>
</MaterialHeaderButtons> </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. * Callback to use when refresh button is clicked. Reloads the webview.
*/ */
onRefreshClicked = () => { onRefreshClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.reload(); if (this.webviewRef.current != null)
}; this.webviewRef.current.reload();
}
onGoBackClicked = () => { onGoBackClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goBack(); if (this.webviewRef.current != null)
}; this.webviewRef.current.goBack();
}
onGoForwardClicked = () => { onGoForwardClicked = () => {
if (this.webviewRef.current != null) this.webviewRef.current.goForward(); if (this.webviewRef.current != null)
}; this.webviewRef.current.goForward();
}
onOpenClicked = () => { onOpenClicked = () => Linking.openURL(this.props.url);
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 * Injects the given javascript string into the web page
@ -211,32 +174,55 @@ class WebViewScreen extends React.PureComponent<PropsType> {
injectJavaScript = (script: string) => { injectJavaScript = (script: string) => {
if (this.webviewRef.current != null) if (this.webviewRef.current != null)
this.webviewRef.current.injectJavaScript(script); this.webviewRef.current.injectJavaScript(script);
}; }
render(): React.Node { /**
const {props} = this; * Gets the loading indicator
const {containerPaddingTop, onScrollWithListener} = props.collapsibleStack; *
* @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 ( return (
<AnimatedWebView <AnimatedWebView
ref={this.webviewRef} ref={this.webviewRef}
source={{uri: props.url}} source={{uri: this.props.url}}
startInLoadingState startInLoadingState={true}
injectedJavaScript={props.customJS} injectedJavaScript={this.props.customJS}
javaScriptEnabled javaScriptEnabled={true}
renderLoading={this.getRenderLoading} renderLoading={this.getRenderLoading}
renderError={(): React.Node => ( renderError={() => <ErrorView
<ErrorView
errorCode={ERROR_TYPE.CONNECTION_ERROR} errorCode={ERROR_TYPE.CONNECTION_ERROR}
onRefresh={this.onRefreshClicked} onRefresh={this.onRefreshClicked}
/> />}
)} onNavigationStateChange={navState => {
onNavigationStateChange={(navState: {canGoBack: boolean}) => {
this.canGoBack = navState.canGoBack; this.canGoBack = navState.canGoBack;
}} }}
onMessage={props.onMessage} onMessage={this.props.onMessage}
onLoad={() => { onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))}
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
}}
// Animations // Animations
onScroll={onScrollWithListener(this.onScroll)} onScroll={onScrollWithListener(this.onScroll)}
/> />

View file

@ -1,45 +1,24 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import {Collapsible} from 'react-navigation-collapsible'; import TabIcon from "./TabIcon";
import {StackNavigationProp} from '@react-navigation/stack'; import TabHomeIcon from "./TabHomeIcon";
import TabIcon from './TabIcon'; import {Animated} from 'react-native';
import TabHomeIcon from './TabHomeIcon'; import {Collapsible} from "react-navigation-collapsible";
import type {CustomThemeType} from '../../managers/ThemeManager';
type RouteType = { type Props = {
name: string, state: Object,
key: string, descriptors: Object,
params: {collapsible: Collapsible}, navigation: Object,
state: { theme: Object,
index: number, collapsibleStack: Object,
routes: Array<RouteType>, }
},
};
type PropsType = { type State = {
state: { translateY: AnimatedValue,
index: number, barSynced: boolean,
routes: Array<RouteType>, }
},
descriptors: {
[key: string]: {
options: {
tabBarLabel: string,
title: string,
},
},
},
navigation: StackNavigationProp,
theme: CustomThemeType,
};
type StateType = {
// eslint-disable-next-line flowtype/no-weak-types
translateY: any,
};
const TAB_ICONS = { const TAB_ICONS = {
proxiwash: 'tshirt-crew', proxiwash: 'tshirt-crew',
@ -48,16 +27,30 @@ const TAB_ICONS = {
planex: 'clock', planex: 'clock',
}; };
class CustomTabBar extends React.Component<PropsType, StateType> { class CustomTabBar extends React.Component<Props, State> {
static TAB_BAR_HEIGHT = 48; static TAB_BAR_HEIGHT = 48;
constructor() { state = {
super();
this.state = {
translateY: new Animated.Value(0), translateY: new Animated.Value(0),
};
} }
syncTabBar = (route, index) => {
const state = this.props.state;
const isFocused = state.index === index;
if (isFocused) {
const stackState = route.state;
const stackRoute = stackState ? stackState.routes[stackState.index] : undefined;
const params: { collapsible: Collapsible } = stackRoute ? stackRoute.params : undefined;
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
* *
@ -65,15 +58,14 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
* @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: RouteType, currentIndex: number, destIndex: number) { onItemPress(route: Object, currentIndex: number, destIndex: number) {
const {navigation} = this.props; const event = this.props.navigation.emit({
const event = navigation.emit({
type: 'tabPress', type: 'tabPress',
target: route.key, target: route.key,
canPreventDefault: true, canPreventDefault: true,
}); });
if (currentIndex !== destIndex && !event.defaultPrevented) if (currentIndex !== destIndex && !event.defaultPrevented)
navigation.navigate(route.name); this.props.navigation.navigate(route.name);
} }
/** /**
@ -81,25 +73,16 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
* *
* @param route * @param route
*/ */
onItemLongPress(route: RouteType) { onItemLongPress(route: Object) {
const {navigation} = this.props; const event = this.props.navigation.emit({
const event = navigation.emit({
type: 'tabLongPress', type: 'tabLongPress',
target: route.key, target: route.key,
canPreventDefault: true, canPreventDefault: true,
}); });
if (route.name === 'home' && !event.defaultPrevented) if (route.name === "home" && !event.defaultPrevented)
navigation.navigate('game-start'); this.props.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 * Gets an icon for the given route if it is not the home one as it uses a custom button
* *
@ -107,13 +90,22 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
* @param focused * @param focused
* @returns {null} * @returns {null}
*/ */
getTabBarIcon = (route: RouteType, focused: boolean): React.Node => { tabBarIcon = (route, focused) => {
let icon = TAB_ICONS[route.name]; let icon = TAB_ICONS[route.name];
icon = focused ? icon : `${icon}-outline`; icon = focused ? icon : icon + ('-outline');
if (route.name !== 'home') return icon; if (route.name !== "home")
return icon;
else
return null; 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. * Gets a tab icon render.
* If the given route is focused, it syncs the tab bar and header bar animations together * If the given route is focused, it syncs the tab bar and header bar animations together
@ -122,80 +114,50 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
* @param index The index of the current route * @param index The index of the current route
* @returns {*} * @returns {*}
*/ */
getRenderIcon = (route: RouteType, index: number): React.Node => { renderIcon = (route, index) => {
const {props} = this; const state = this.props.state;
const {state} = props; const {options} = this.props.descriptors[route.key];
const {options} = props.descriptors[route.key]; const label =
let label; options.tabBarLabel != null
if (options.tabBarLabel != null) label = options.tabBarLabel; ? options.tabBarLabel
else if (options.title != null) label = options.title; : options.title != null
else label = route.name; ? options.title
: route.name;
const onPress = () => { const onPress = () => this.onItemPress(route, state.index, index);
this.onItemPress(route, state.index, index); const onLongPress = () => this.onItemLongPress(route);
};
const onLongPress = () => {
this.onItemLongPress(route);
};
const isFocused = state.index === index; const isFocused = state.index === index;
const color = isFocused const color = isFocused ? this.props.theme.colors.primary : this.props.theme.colors.tabIcon;
? props.theme.colors.primary if (route.name !== "home") {
: props.theme.colors.tabIcon; return <TabIcon
if (route.name !== 'home') {
return (
<TabIcon
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
icon={this.getTabBarIcon(route, isFocused)} icon={this.tabBarIcon(route, isFocused)}
color={color} color={color}
label={label} label={label}
focused={isFocused} focused={isFocused}
extraData={state.index > index} extraData={state.index > index}
key={route.key} key={route.key}
/> />
); } else
} return <TabHomeIcon
return (
<TabHomeIcon
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
focused={isFocused} focused={isFocused}
key={route.key} key={route.key}
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT} tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
/> />
);
}; };
getIcons(): React.Node { getIcons() {
const {props} = this; return this.props.state.routes.map(this.renderIcon);
return props.state.routes.map(this.getRenderIcon);
} }
syncTabBar = (route: RouteType, index: number) => { render() {
const {state} = this.props; this.props.navigation.addListener('state', this.onRouteChange);
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
});
}
}
};
render(): React.Node {
const {props, state} = this;
props.navigation.addListener('state', this.onRouteChange);
const icons = this.getIcons(); const icons = this.getIcons();
return ( return (
// $FlowFixMe
<Animated.View <Animated.View
useNativeDriver useNativeDriver
style={{ style={{
@ -205,9 +167,10 @@ class CustomTabBar extends React.Component<PropsType, StateType> {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
backgroundColor: props.theme.colors.surface, backgroundColor: this.props.theme.colors.surface,
transform: [{translateY: state.translateY}], transform: [{translateY: this.state.translateY}],
}}> }}
>
{icons} {icons}
</Animated.View> </Animated.View>
); );

View file

@ -1,95 +1,70 @@
// @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 PropsType = { type Props = {
focused: boolean, focused: boolean,
onPress: () => void, onPress: Function,
onLongPress: () => void, onLongPress: Function,
theme: CustomThemeType, theme: Object,
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<PropsType> { class TabHomeIcon extends React.Component<Props> {
constructor(props: PropsType) {
focusedIcon = require('../../../assets/tab-icon.png');
unFocusedIcon = require('../../../assets/tab-icon-outline.png');
constructor(props) {
super(props); super(props);
Animatable.initializeRegistryWithDefinitions({ Animatable.initializeRegistryWithDefinitions({
fabFocusIn: { fabFocusIn: {
'0': { "0": {
scale: 1, scale: 1, translateY: 0
translateY: 0,
}, },
'0.9': { "0.9": {
scale: 1.2, scale: 1.2, translateY: -9
translateY: -9,
}, },
'1': { "1": {
scale: 1.1, scale: 1.1, translateY: -7
translateY: -7,
}, },
}, },
fabFocusOut: { fabFocusOut: {
'0': { "0": {
scale: 1.1, scale: 1.1, translateY: -6
translateY: -6,
},
'1': {
scale: 1,
translateY: 0,
}, },
"1": {
scale: 1, translateY: 0
}, },
}
}); });
} }
shouldComponentUpdate(nextProps: PropsType): boolean { iconRender = ({size, color}) =>
const {focused} = this.props; this.props.focused
return nextProps.focused !== 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);
} }
getIconRender = ({ render(): React$Node {
size, const props = this.props;
color,
}: {
size: number,
color: string,
}): React.Node => {
const {focused} = this.props;
if (focused)
return (
<Image
source={FOCUSED_ICON}
style={{
width: size,
height: size,
tintColor: color,
}}
/>
);
return (
<Image
source={UNFOCUSED_ICON}
style={{
width: size,
height: size,
tintColor: color,
}}
/>
);
};
render(): React.Node {
const {props} = this;
return ( return (
<View <View
style={{ style={{
@ -99,35 +74,33 @@ class TabHomeIcon extends React.Component<PropsType> {
<TouchableRipple <TouchableRipple
onPress={props.onPress} onPress={props.onPress}
onLongPress={props.onLongPress} onLongPress={props.onLongPress}
borderless borderless={true}
rippleColor={ rippleColor={Platform.OS === 'android' ? this.props.theme.colors.primary : 'transparent'}
Platform.OS === 'android'
? props.theme.colors.primary
: 'transparent'
}
style={{ style={{
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
width: '100%', width: '100%',
height: props.tabBarHeight + 30, height: this.props.tabBarHeight + 30,
marginBottom: -15, marginBottom: -15,
}}> }}
>
<AnimatedFAB <AnimatedFAB
duration={200} duration={200}
easing="ease-out" easing={"ease-out"}
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'} animation={props.focused ? "fabFocusIn" : "fabFocusOut"}
icon={this.getIconRender} icon={this.iconRender}
style={{ style={{
marginTop: 15, marginTop: 15,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto'
}} }}/>
/>
</TouchableRipple> </TouchableRipple>
</View> </View>
); );
} }
} }
export default withTheme(TabHomeIcon); export default withTheme(TabHomeIcon);

View file

@ -1,57 +1,53 @@
// @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 PropsType = { type Props = {
focused: boolean, focused: boolean,
color: string, color: string,
label: string, label: string,
icon: MaterialCommunityIconsGlyphs, icon: MaterialCommunityIconsGlyphs,
onPress: () => void, onPress: Function,
onLongPress: () => void, onLongPress: Function,
theme: CustomThemeType, theme: Object,
extraData: null | boolean | number | string, extraData: any,
}; }
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class TabIcon extends React.Component<PropsType> { class TabIcon extends React.Component<Props> {
firstRender: boolean; firstRender: boolean;
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
Animatable.initializeRegistryWithDefinitions({ Animatable.initializeRegistryWithDefinitions({
focusIn: { focusIn: {
'0': { "0": {
scale: 1, scale: 1, translateY: 0
translateY: 0,
}, },
'0.9': { "0.9": {
scale: 1.3, scale: 1.3, translateY: 7
translateY: 7,
}, },
'1': { "1": {
scale: 1.2, scale: 1.2, translateY: 6
translateY: 6,
}, },
}, },
focusOut: { focusOut: {
'0': { "0": {
scale: 1.2, scale: 1.2, translateY: 6
translateY: 6,
},
'1': {
scale: 1,
translateY: 0,
}, },
"1": {
scale: 1, translateY: 0
}, },
}
}); });
this.firstRender = true; this.firstRender = true;
} }
@ -60,33 +56,32 @@ class TabIcon extends React.Component<PropsType> {
this.firstRender = false; this.firstRender = false;
} }
shouldComponentUpdate(nextProps: PropsType): boolean { shouldComponentUpdate(nextProps: Props): boolean {
const {props} = this; return (nextProps.focused !== this.props.focused)
return ( || (nextProps.theme.dark !== this.props.theme.dark)
nextProps.focused !== props.focused || || (nextProps.extraData !== this.props.extraData);
nextProps.theme.dark !== props.theme.dark ||
nextProps.extraData !== props.extraData
);
} }
render(): React.Node { render(): React$Node {
const {props} = this; const props = this.props;
return ( return (
<TouchableRipple <TouchableRipple
onPress={props.onPress} onPress={props.onPress}
onLongPress={props.onLongPress} onLongPress={props.onLongPress}
borderless borderless={true}
rippleColor={props.theme.colors.primary} rippleColor={this.props.theme.colors.primary}
style={{ style={{
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}}> }}
>
<View> <View>
<Animatable.View <Animatable.View
duration={200} duration={200}
easing="ease-out" easing={"ease-out"}
animation={props.focused ? 'focusIn' : 'focusOut'} animation={props.focused ? "focusIn" : "focusOut"}
useNativeDriver> useNativeDriver
>
<MaterialCommunityIcons <MaterialCommunityIcons
name={props.icon} name={props.icon}
color={props.color} color={props.color}
@ -98,14 +93,16 @@ class TabIcon extends React.Component<PropsType> {
/> />
</Animatable.View> </Animatable.View>
<Animatable.Text <Animatable.Text
animation={props.focused ? 'fadeOutDown' : 'fadeIn'} animation={props.focused ? "fadeOutDown" : "fadeIn"}
useNativeDriver useNativeDriver
style={{ style={{
color: props.color, color: props.color,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
fontSize: 10, fontSize: 10,
}}> }}
>
{props.label} {props.label}
</Animatable.Text> </Animatable.Text>
</View> </View>

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,12 +1,12 @@
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',
@ -16,5 +16,5 @@ export default {
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,26 +14,28 @@ import i18n from 'i18n-js';
* </ul> * </ul>
*/ */
export default class Update { export default class Update {
// Increment the number to show the update slide // Increment the number to show the update slide
static number = 6; static number = 6;
// Change the number of slides to display // Change the number of slides to display
static slidesNumber = 4; static slidesNumber = 4;
// Change the icons to be displayed on the update slide // Change the icons to be displayed on the update slide
static iconList = ['star', 'clock', 'qrcode-scan', 'account']; static iconList = [
'star',
'clock',
'qrcode-scan',
'account',
];
static colorsList = [ static colorsList = [
['#e01928', '#be1522'], ['#e01928', '#be1522'],
['#7c33ec', '#5e11d1'], ['#7c33ec', '#5e11d1'],
['#337aec', '#114ed1'], ['#337aec', '#114ed1'],
['#e01928', '#be1522'], ['#e01928', '#be1522'],
]; ]
static instance: Update | null = null; static instance: Update | null = null;
titleList: Array<string>; titleList: Array<string>;
descriptionList: Array<string>; descriptionList: Array<string>;
/** /**
@ -42,9 +44,9 @@ export default class Update {
constructor() { constructor() {
this.titleList = []; this.titleList = [];
this.descriptionList = []; this.descriptionList = [];
for (let i = 0; i < Update.slidesNumber; i += 1) { for (let i = 0; i < Update.slidesNumber; i++) {
this.titleList.push(i18n.t(`intro.updateSlide${i}.title`)); this.titleList.push(i18n.t('intro.updateSlide' + i + '.title'))
this.descriptionList.push(i18n.t(`intro.updateSlide${i}.text`)); this.descriptionList.push(i18n.t('intro.updateSlide' + i + '.text'))
} }
} }
@ -54,7 +56,9 @@ export default class Update {
* @returns {Update} * @returns {Update}
*/ */
static getInstance(): Update { static getInstance(): Update {
if (Update.instance == null) Update.instance = new Update(); return Update.instance === null ?
return Update.instance; Update.instance = new Update() :
Update.instance;
} }
}
};

View file

@ -1,36 +1,33 @@
// @flow // @flow
import type {ProxiwashMachineType} from '../screens/Proxiwash/ProxiwashScreen'; import type {Machine} 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; aprilFoolsEnabled: boolean;
constructor() { constructor() {
const today = new Date(); let today = new Date();
this.aprilFoolsEnabled = today.getDate() === 1 && today.getMonth() === 3; this.aprilFoolsEnabled = (today.getDate() === 1 && today.getMonth() === 3);
} }
/** /**
@ -38,9 +35,9 @@ export default class AprilFoolsManager {
* @returns {ThemeManager} * @returns {ThemeManager}
*/ */
static getInstance(): AprilFoolsManager { static getInstance(): AprilFoolsManager {
if (AprilFoolsManager.instance == null) return AprilFoolsManager.instance === null ?
AprilFoolsManager.instance = new AprilFoolsManager(); AprilFoolsManager.instance = new AprilFoolsManager() :
return AprilFoolsManager.instance; AprilFoolsManager.instance;
} }
/** /**
@ -49,14 +46,12 @@ export default class AprilFoolsManager {
* @param menu * @param menu
* @returns {Object} * @returns {Object}
*/ */
static getFakeMenuItem( static getFakeMenuItem(menu: Array<{dishes: Array<{name: string}>}>) {
menu: Array<RuFoodCategoryType>, menu[1]["dishes"].splice(4, 0, {name: "Coq au vin"});
): Array<RuFoodCategoryType> { menu[1]["dishes"].splice(2, 0, {name: "Bat'Soupe"});
menu[1].dishes.splice(4, 0, {name: 'Coq au vin'}); menu[1]["dishes"].splice(1, 0, {name: "Pave de loup"});
menu[1].dishes.splice(2, 0, {name: "Bat'Soupe"}); menu[1]["dishes"].splice(0, 0, {name: "Béranger à point"});
menu[1].dishes.splice(1, 0, {name: 'Pave de loup'}); menu[1]["dishes"].splice(0, 0, {name: "Pieds d'Arnaud"});
menu[1].dishes.splice(0, 0, {name: 'Béranger à point'});
menu[1].dishes.splice(0, 0, {name: "Pieds d'Arnaud"});
return menu; return menu;
} }
@ -65,11 +60,9 @@ export default class AprilFoolsManager {
* *
* @param dryers * @param dryers
*/ */
static getNewProxiwashDryerOrderedList( static getNewProxiwashDryerOrderedList(dryers: Array<Machine> | null) {
dryers: Array<ProxiwashMachineType> | null,
) {
if (dryers != null) { if (dryers != null) {
const second = dryers[1]; let second = dryers[1];
dryers.splice(1, 1); dryers.splice(1, 1);
dryers.push(second); dryers.push(second);
} }
@ -80,14 +73,12 @@ export default class AprilFoolsManager {
* *
* @param washers * @param washers
*/ */
static getNewProxiwashWasherOrderedList( static getNewProxiwashWasherOrderedList(washers: Array<Machine> | null) {
washers: Array<ProxiwashMachineType> | null,
) {
if (washers != null) { if (washers != null) {
const first = washers[0]; let first = washers[0];
const second = washers[1]; let second = washers[1];
const fifth = washers[4]; let fifth = washers[4];
const ninth = washers[8]; let ninth = washers[8];
washers.splice(8, 1, second); washers.splice(8, 1, second);
washers.splice(4, 1, ninth); washers.splice(4, 1, ninth);
washers.splice(1, 1, first); washers.splice(1, 1, first);
@ -101,7 +92,7 @@ export default class AprilFoolsManager {
* @param number * @param number
* @returns {string} * @returns {string}
*/ */
static getProxiwashMachineDisplayNumber(number: number): string { static getProxiwashMachineDisplayNumber(number: number) {
return AprilFoolsManager.fakeMachineNumber[number]; return AprilFoolsManager.fakeMachineNumber[number];
} }
@ -111,7 +102,7 @@ export default class AprilFoolsManager {
* @param currentTheme * @param currentTheme
* @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}} * @returns {{colors: {textDisabled: string, agendaDayTextColor: string, surface: string, background: string, dividerBackground: string, accent: string, agendaBackgroundColor: string, tabIcon: string, card: string, primary: string}}}
*/ */
static getAprilFoolsTheme(currentTheme: CustomThemeType): CustomThemeType { static getAprilFoolsTheme(currentTheme: Object) {
return { return {
...currentTheme, ...currentTheme,
colors: { colors: {
@ -119,9 +110,9 @@ export default class AprilFoolsManager {
primary: '#00be45', primary: '#00be45',
accent: '#00be45', accent: '#00be45',
background: '#d02eee', background: '#d02eee',
tabIcon: '#380d43', tabIcon: "#380d43",
card: '#eed639', card: "#eed639",
surface: '#eed639', surface: "#eed639",
dividerBackground: '#c72ce4', dividerBackground: '#c72ce4',
textDisabled: '#b9b9b9', textDisabled: '#b9b9b9',
@ -132,7 +123,8 @@ export default class AprilFoolsManager {
}; };
} }
isAprilFoolsEnabled(): boolean { isAprilFoolsEnabled() {
return this.aprilFoolsEnabled; 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,6 +10,7 @@ import {SERVICES_KEY} from './ServicesManager';
*/ */
export default class AsyncStorageManager { export default class AsyncStorageManager {
static instance: AsyncStorageManager | null = null; static instance: AsyncStorageManager | null = null;
static PREFERENCES = { static PREFERENCES = {
@ -107,7 +108,7 @@ export default class AsyncStorageManager {
key: 'gameScores', key: 'gameScores',
default: '[]', default: '[]',
}, },
}; }
#currentPreferences: {[key: string]: string}; #currentPreferences: {[key: string]: string};
@ -120,66 +121,9 @@ export default class AsyncStorageManager {
* @returns {AsyncStorageManager} * @returns {AsyncStorageManager}
*/ */
static getInstance(): AsyncStorageManager { static getInstance(): AsyncStorageManager {
if (AsyncStorageManager.instance == null) return AsyncStorageManager.instance === null ?
AsyncStorageManager.instance = new AsyncStorageManager(); AsyncStorageManager.instance = new AsyncStorageManager() :
return AsyncStorageManager.instance; AsyncStorageManager.instance;
}
/**
* Saves the value associated to the given key to preferences.
*
* @param key
* @param value
*/
static set(
key: string,
// eslint-disable-next-line flowtype/no-weak-types
value: number | string | boolean | {...} | Array<any>,
) {
AsyncStorageManager.getInstance().setPreference(key, value);
}
/**
* Gets the string value of the given preference
*
* @param key
* @returns {string}
*/
static getString(key: string): string {
const value = AsyncStorageManager.getInstance().getPreference(key);
return value != null ? value : '';
}
/**
* Gets the boolean value of the given preference
*
* @param key
* @returns {boolean}
*/
static getBool(key: string): boolean {
const value = AsyncStorageManager.getString(key);
return value === '1' || value === 'true';
}
/**
* Gets the number value of the given preference
*
* @param key
* @returns {number}
*/
static getNumber(key: string): number {
return parseFloat(AsyncStorageManager.getString(key));
}
/**
* Gets the object value of the given preference
*
* @param key
* @returns {{...}}
*/
// eslint-disable-next-line flowtype/no-weak-types
static getObject(key: string): any {
return JSON.parse(AsyncStorageManager.getString(key));
} }
/** /**
@ -189,20 +133,21 @@ export default class AsyncStorageManager {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async loadPreferences() { async loadPreferences() {
const prefKeys = []; let prefKeys = [];
// Get all available keys // Get all available keys
Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => { for (let key in AsyncStorageManager.PREFERENCES) {
prefKeys.push(key); prefKeys.push(key);
}); }
// Get corresponding values // Get corresponding values
const resultArray = await AsyncStorage.multiGet(prefKeys); let resultArray: Array<Array<string>> = await AsyncStorage.multiGet(prefKeys);
// Save those values for later use // Save those values for later use
resultArray.forEach((item: [string, string | null]) => { for (let i = 0; i < resultArray.length; i++) {
const key = item[0]; let key: string = resultArray[i][0];
let val = item[1]; let val: string | null = resultArray[i][1];
if (val === null) val = AsyncStorageManager.PREFERENCES[key].default; if (val === null)
val = AsyncStorageManager.PREFERENCES[key].default;
this.#currentPreferences[key] = val; this.#currentPreferences[key] = val;
}); }
} }
/** /**
@ -212,17 +157,15 @@ export default class AsyncStorageManager {
* @param key * @param key
* @param value * @param value
*/ */
setPreference( setPreference(key: string, value: any) {
key: string,
// eslint-disable-next-line flowtype/no-weak-types
value: number | string | boolean | {...} | Array<any>,
) {
if (AsyncStorageManager.PREFERENCES[key] != null) { if (AsyncStorageManager.PREFERENCES[key] != null) {
let convertedValue; let convertedValue = "";
if (typeof value === 'string') convertedValue = value; if (typeof value === "string")
else if (typeof value === 'boolean' || typeof value === 'number') convertedValue = value;
else if (typeof value === "boolean" || typeof value === "number")
convertedValue = value.toString(); convertedValue = value.toString();
else convertedValue = JSON.stringify(value); else
convertedValue = JSON.stringify(value);
this.#currentPreferences[key] = convertedValue; this.#currentPreferences[key] = convertedValue;
AsyncStorage.setItem(key, convertedValue); AsyncStorage.setItem(key, convertedValue);
} }
@ -235,7 +178,59 @@ export default class AsyncStorageManager {
* @param key * @param key
* @returns {string|null} * @returns {string|null}
*/ */
getPreference(key: string): string | null { getPreference(key: string) {
return this.#currentPreferences[key]; 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));
}
} }

View file

@ -1,8 +1,7 @@
// @flow // @flow
import * as Keychain from 'react-native-keychain'; import * as Keychain from 'react-native-keychain';
import type {ApiDataLoginType, ApiGenericDataType} from '../utils/WebData'; import {apiRequest, ERROR_TYPE} from "../utils/WebData";
import {apiRequest, ERROR_TYPE} from '../utils/WebData';
/** /**
* champ: error * champ: error
@ -15,14 +14,13 @@ 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; #token: string | null;
constructor() { constructor() {
@ -35,9 +33,9 @@ export default class ConnectionManager {
* @returns {ConnectionManager} * @returns {ConnectionManager}
*/ */
static getInstance(): ConnectionManager { static getInstance(): ConnectionManager {
if (ConnectionManager.instance == null) return ConnectionManager.instance === null ?
ConnectionManager.instance = new ConnectionManager(); ConnectionManager.instance = new ConnectionManager() :
return ConnectionManager.instance; ConnectionManager.instance;
} }
/** /**
@ -52,29 +50,26 @@ export default class ConnectionManager {
/** /**
* Tries to recover login token from the secure keychain * Tries to recover login token from the secure keychain
* *
* @returns Promise<string> * @returns {Promise<R>}
*/ */
async recoverLogin(): Promise<string> { async recoverLogin() {
return new Promise( return new Promise((resolve, reject) => {
(resolve: (token: string) => void, reject: () => void) => { if (this.getToken() !== null)
const token = this.getToken(); resolve(this.getToken());
if (token != null) resolve(token);
else { else {
Keychain.getInternetCredentials(SERVER_NAME) Keychain.getInternetCredentials(SERVER_NAME)
.then((data: Keychain.UserCredentials | false) => { .then((data) => {
if ( if (data) {
data != null &&
data.password != null &&
typeof data.password === 'string'
) {
this.#token = data.password; this.#token = data.password;
resolve(this.#token); resolve(this.#token);
} else reject(); } else
reject(false);
}) })
.catch((): void => reject()); .catch(() => {
reject(false);
});
} }
}, });
);
} }
/** /**
@ -82,7 +77,7 @@ export default class ConnectionManager {
* *
* @returns {boolean} * @returns {boolean}
*/ */
isLoggedIn(): boolean { isLoggedIn() {
return this.getToken() !== null; return this.getToken() !== null;
} }
@ -91,36 +86,41 @@ export default class ConnectionManager {
* *
* @param email * @param email
* @param token * @param token
* @returns Promise<void> * @returns {Promise<R>}
*/ */
async saveLogin(email: string, token: string): Promise<void> { async saveLogin(email: string, token: string) {
return new Promise((resolve: () => void, reject: () => void) => { return new Promise((resolve, reject) => {
Keychain.setInternetCredentials(SERVER_NAME, 'token', token) Keychain.setInternetCredentials(SERVER_NAME, 'token', token)
.then(() => { .then(() => {
this.#token = token; this.#token = token;
this.#email = email; this.#email = email;
resolve(); resolve(true);
}) })
.catch((): void => reject()); .catch(() => {
reject(false);
});
}); });
} }
/** /**
* Deletes the login token from the keychain * Deletes the login token from the keychain
* *
* @returns Promise<void> * @returns {Promise<R>}
*/ */
async disconnect(): Promise<void> { async disconnect() {
return new Promise((resolve: () => void, reject: () => void) => { return new Promise((resolve, reject) => {
Keychain.resetInternetCredentials(SERVER_NAME) Keychain.resetInternetCredentials(SERVER_NAME)
.then(() => { .then(() => {
this.#token = null; this.#token = null;
resolve(); resolve(true);
}) })
.catch((): void => reject()); .catch(() => {
reject(false);
});
}); });
} }
/** /**
* 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.
@ -128,26 +128,26 @@ export default class ConnectionManager {
* *
* @param email * @param email
* @param password * @param password
* @returns Promise<void> * @returns {Promise<R>}
*/ */
async connect(email: string, password: string): Promise<void> { async connect(email: string, password: string) {
return new Promise( return new Promise((resolve, reject) => {
(resolve: () => void, reject: (error: number) => void) => {
const data = { const data = {
email, email: email,
password, password: password,
}; };
apiRequest(AUTH_PATH, 'POST', data) apiRequest(AUTH_PATH, 'POST', data)
.then((response: ApiDataLoginType) => { .then((response) => {
if (response.token != null) {
this.saveLogin(email, response.token) this.saveLogin(email, response.token)
.then((): void => resolve()) .then(() => {
.catch((): void => reject(ERROR_TYPE.TOKEN_SAVE)); resolve(true);
} else reject(ERROR_TYPE.SERVER_ERROR);
}) })
.catch((error: number): void => reject(error)); .catch(() => {
}, reject(ERROR_TYPE.TOKEN_SAVE);
); });
})
.catch((error) => reject(error));
});
} }
/** /**
@ -155,27 +155,20 @@ export default class ConnectionManager {
* *
* @param path * @param path
* @param params * @param params
* @returns Promise<ApiGenericDataType> * @returns {Promise<R>}
*/ */
async authenticatedRequest( async authenticatedRequest(path: string, params: Object) {
path: string, return new Promise((resolve, reject) => {
params: {...},
): Promise<ApiGenericDataType> {
return new Promise(
(
resolve: (response: ApiGenericDataType) => void,
reject: (error: number) => void,
) => {
if (this.getToken() !== null) { if (this.getToken() !== null) {
const data = { let data = {
...params,
token: this.getToken(), token: this.getToken(),
...params
}; };
apiRequest(path, 'POST', data) apiRequest(path, 'POST', data)
.then((response: ApiGenericDataType): void => resolve(response)) .then((response) => resolve(response))
.catch((error: number): void => reject(error)); .catch((error) => reject(error));
} else reject(ERROR_TYPE.TOKEN_RETRIEVE); } else
}, reject(ERROR_TYPE.TOKEN_RETRIEVE);
); });
} }
} }

View file

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

View file

@ -10,30 +10,29 @@ export default class DateManager {
static instance: DateManager | null = null; static instance: DateManager | null = null;
daysOfWeek = []; daysOfWeek = [];
monthsOfYear = []; monthsOfYear = [];
constructor() { constructor() {
this.daysOfWeek.push(i18n.t('date.daysOfWeek.sunday')); // 0 represents sunday 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.monday"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.tuesday')); this.daysOfWeek.push(i18n.t("date.daysOfWeek.tuesday"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.wednesday')); this.daysOfWeek.push(i18n.t("date.daysOfWeek.wednesday"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.thursday')); this.daysOfWeek.push(i18n.t("date.daysOfWeek.thursday"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.friday')); this.daysOfWeek.push(i18n.t("date.daysOfWeek.friday"));
this.daysOfWeek.push(i18n.t('date.daysOfWeek.saturday')); this.daysOfWeek.push(i18n.t("date.daysOfWeek.saturday"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.january')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.january"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.february')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.february"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.march')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.march"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.april')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.april"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.may')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.may"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.june')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.june"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.july')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.july"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.august')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.august"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.september')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.september"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.october')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.october"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.november')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.november"));
this.monthsOfYear.push(i18n.t('date.monthsOfYear.december')); this.monthsOfYear.push(i18n.t("date.monthsOfYear.december"));
} }
/** /**
@ -41,15 +40,16 @@ export default class DateManager {
* @returns {DateManager} * @returns {DateManager}
*/ */
static getInstance(): DateManager { static getInstance(): DateManager {
if (DateManager.instance == null) DateManager.instance = new DateManager(); return DateManager.instance === null ?
return DateManager.instance; DateManager.instance = new DateManager() :
DateManager.instance;
} }
static isWeekend(date: Date): boolean { static isWeekend(date: Date) {
return date.getDay() === 6 || date.getDay() === 0; return date.getDay() === 6 || date.getDay() === 0;
} }
getMonthsOfYear(): Array<string> { getMonthsOfYear() {
return this.monthsOfYear; return this.monthsOfYear;
} }
@ -59,16 +59,11 @@ export default class DateManager {
* @param dateString The date with the format YYYY-MM-DD * @param dateString The date with the format YYYY-MM-DD
* @return {string} The translated string * @return {string} The translated string
*/ */
getTranslatedDate(dateString: string): string { getTranslatedDate(dateString: string) {
const dateArray = dateString.split('-'); let dateArray = dateString.split('-');
const date = new Date(); let date = new Date();
date.setFullYear( date.setFullYear(parseInt(dateArray[0]), parseInt(dateArray[1]) - 1, parseInt(dateArray[2]));
parseInt(dateArray[0], 10), return this.daysOfWeek[date.getDay()] + " " + date.getDate() + " " + this.monthsOfYear[date.getMonth()] + " " + date.getFullYear();
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,24 +1,22 @@
// @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.json'; import en from '../../locales/en';
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,107 +1,94 @@
// @flow // @flow
import i18n from 'i18n-js'; import i18n from "i18n-js";
import {StackNavigationProp} from '@react-navigation/stack'; import AvailableWebsites from "../constants/AvailableWebsites";
import AvailableWebsites from '../constants/AvailableWebsites'; import {StackNavigationProp} from "@react-navigation/stack";
import ConnectionManager from './ConnectionManager'; import ConnectionManager from "./ConnectionManager";
import type {FullDashboardType} from '../screens/Home/HomeScreen'; import type {fullDashboard} from "../screens/Home/HomeScreen";
import getStrippedServicesList from '../utils/Services';
// AMICALE // AMICALE
const CLUBS_IMAGE = const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png'; const PROFILE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png";
const PROFILE_IMAGE = const EQUIPMENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png'; const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png";
const EQUIPMENT_IMAGE = const AMICALE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png";
'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 = const PROXIMO_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png"
'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png'; const WIKETUD_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png";
const WIKETUD_IMAGE = const EE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png'; const TUTORINSA_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.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 = const ROOM_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png'; const EMAIL_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png";
const EMAIL_IMAGE = const ENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png'; const ACCOUNT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Account.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 = const WASHER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.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 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 = {
export type ServiceItem = {
key: string, key: string,
title: string, title: string,
subtitle: string, subtitle: string,
image: string, image: string,
onPress: () => void, onPress: () => void,
badgeFunction?: (dashboard: FullDashboardType) => number, badgeFunction?: (dashboard: fullDashboard) => number,
}; }
export type ServiceCategoryType = { export type ServiceCategory = {
key: string, key: string,
title: string, title: string,
subtitle: string, subtitle: string,
image: string | number, image: string | number,
content: Array<ServiceItemType>, content: Array<ServiceItem>
}; }
export default class ServicesManager { export default class ServicesManager {
navigation: StackNavigationProp; navigation: StackNavigationProp;
amicaleDataset: Array<ServiceItemType>; amicaleDataset: Array<ServiceItem>;
studentsDataset: Array<ServiceItem>;
insaDataset: Array<ServiceItem>;
specialDataset: Array<ServiceItem>;
studentsDataset: Array<ServiceItemType>; categoriesDataset: Array<ServiceCategory>;
insaDataset: Array<ServiceItemType>;
specialDataset: Array<ServiceItemType>;
categoriesDataset: Array<ServiceCategoryType>;
constructor(nav: StackNavigationProp) { constructor(nav: StackNavigationProp) {
this.navigation = nav; this.navigation = nav;
@ -111,31 +98,30 @@ export default class ServicesManager {
title: i18n.t('screens.clubs.title'), title: i18n.t('screens.clubs.title'),
subtitle: i18n.t('screens.services.descriptions.clubs'), subtitle: i18n.t('screens.services.descriptions.clubs'),
image: CLUBS_IMAGE, image: CLUBS_IMAGE,
onPress: (): void => this.onAmicaleServicePress('club-list'), onPress: () => this.onAmicaleServicePress("club-list"),
}, },
{ {
key: SERVICES_KEY.PROFILE, key: SERVICES_KEY.PROFILE,
title: i18n.t('screens.profile.title'), title: i18n.t('screens.profile.title'),
subtitle: i18n.t('screens.services.descriptions.profile'), subtitle: i18n.t('screens.services.descriptions.profile'),
image: PROFILE_IMAGE, image: PROFILE_IMAGE,
onPress: (): void => this.onAmicaleServicePress('profile'), onPress: () => this.onAmicaleServicePress("profile"),
}, },
{ {
key: SERVICES_KEY.EQUIPMENT, key: SERVICES_KEY.EQUIPMENT,
title: i18n.t('screens.equipment.title'), title: i18n.t('screens.equipment.title'),
subtitle: i18n.t('screens.services.descriptions.equipment'), subtitle: i18n.t('screens.services.descriptions.equipment'),
image: EQUIPMENT_IMAGE, image: EQUIPMENT_IMAGE,
onPress: (): void => this.onAmicaleServicePress('equipment-list'), onPress: () => this.onAmicaleServicePress("equipment-list"),
}, },
{ {
key: SERVICES_KEY.AMICALE_WEBSITE, key: SERVICES_KEY.AMICALE_WEBSITE,
title: i18n.t('screens.websites.amicale'), title: i18n.t('screens.websites.amicale'),
subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'), subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'),
image: AMICALE_IMAGE, image: AMICALE_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.AMICALE, host: AvailableWebsites.websites.AMICALE,
title: i18n.t('screens.websites.amicale'), title: i18n.t('screens.websites.amicale')
}), }),
}, },
{ {
@ -143,7 +129,7 @@ export default class ServicesManager {
title: i18n.t('screens.vote.title'), title: i18n.t('screens.vote.title'),
subtitle: i18n.t('screens.services.descriptions.vote'), subtitle: i18n.t('screens.services.descriptions.vote'),
image: VOTE_IMAGE, image: VOTE_IMAGE,
onPress: (): void => this.onAmicaleServicePress('vote'), onPress: () => this.onAmicaleServicePress("vote"),
}, },
]; ];
this.studentsDataset = [ this.studentsDataset = [
@ -152,30 +138,24 @@ export default class ServicesManager {
title: i18n.t('screens.proximo.title'), title: i18n.t('screens.proximo.title'),
subtitle: i18n.t('screens.services.descriptions.proximo'), subtitle: i18n.t('screens.services.descriptions.proximo'),
image: PROXIMO_IMAGE, image: PROXIMO_IMAGE,
onPress: (): void => nav.navigate('proximo'), onPress: () => nav.navigate("proximo"),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.proximo_articles
dashboard.proximo_articles,
}, },
{ {
key: SERVICES_KEY.WIKETUD, key: SERVICES_KEY.WIKETUD,
title: 'Wiketud', title: "Wiketud",
subtitle: i18n.t('screens.services.descriptions.wiketud'), subtitle: i18n.t('screens.services.descriptions.wiketud'),
image: WIKETUD_IMAGE, image: WIKETUD_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {host: AvailableWebsites.websites.WIKETUD, title: "Wiketud"}),
nav.navigate('website', {
host: AvailableWebsites.websites.WIKETUD,
title: 'Wiketud',
}),
}, },
{ {
key: SERVICES_KEY.ELUS_ETUDIANTS, key: SERVICES_KEY.ELUS_ETUDIANTS,
title: 'Élus Étudiants', title: "Élus Étudiants",
subtitle: i18n.t('screens.services.descriptions.elusEtudiants'), subtitle: i18n.t('screens.services.descriptions.elusEtudiants'),
image: EE_IMAGE, image: EE_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.ELUS_ETUDIANTS, host: AvailableWebsites.websites.ELUS_ETUDIANTS,
title: 'Élus Étudiants', title: "Élus Étudiants"
}), }),
}, },
{ {
@ -183,13 +163,11 @@ export default class ServicesManager {
title: "Tutor'INSA", title: "Tutor'INSA",
subtitle: i18n.t('screens.services.descriptions.tutorInsa'), subtitle: i18n.t('screens.services.descriptions.tutorInsa'),
image: TUTORINSA_IMAGE, image: TUTORINSA_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.TUTOR_INSA, host: AvailableWebsites.websites.TUTOR_INSA,
title: "Tutor'INSA", title: "Tutor'INSA"
}), }),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.available_tutorials
dashboard.available_tutorials,
}, },
]; ];
this.insaDataset = [ this.insaDataset = [
@ -198,19 +176,17 @@ export default class ServicesManager {
title: i18n.t('screens.menu.title'), title: i18n.t('screens.menu.title'),
subtitle: i18n.t('screens.services.descriptions.self'), subtitle: i18n.t('screens.services.descriptions.self'),
image: RU_IMAGE, image: RU_IMAGE,
onPress: (): void => nav.navigate('self-menu'), onPress: () => nav.navigate("self-menu"),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.today_menu.length
dashboard.today_menu.length,
}, },
{ {
key: SERVICES_KEY.AVAILABLE_ROOMS, key: SERVICES_KEY.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'), title: i18n.t('screens.websites.rooms'),
subtitle: i18n.t('screens.services.descriptions.availableRooms'), subtitle: i18n.t('screens.services.descriptions.availableRooms'),
image: ROOM_IMAGE, image: ROOM_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.AVAILABLE_ROOMS, host: AvailableWebsites.websites.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'), title: i18n.t('screens.websites.rooms')
}), }),
}, },
{ {
@ -218,10 +194,9 @@ export default class ServicesManager {
title: i18n.t('screens.websites.bib'), title: i18n.t('screens.websites.bib'),
subtitle: i18n.t('screens.services.descriptions.bib'), subtitle: i18n.t('screens.services.descriptions.bib'),
image: BIB_IMAGE, image: BIB_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.BIB, host: AvailableWebsites.websites.BIB,
title: i18n.t('screens.websites.bib'), title: i18n.t('screens.websites.bib')
}), }),
}, },
{ {
@ -229,10 +204,9 @@ export default class ServicesManager {
title: i18n.t('screens.websites.mails'), title: i18n.t('screens.websites.mails'),
subtitle: i18n.t('screens.services.descriptions.mails'), subtitle: i18n.t('screens.services.descriptions.mails'),
image: EMAIL_IMAGE, image: EMAIL_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.BLUEMIND, host: AvailableWebsites.websites.BLUEMIND,
title: i18n.t('screens.websites.mails'), title: i18n.t('screens.websites.mails')
}), }),
}, },
{ {
@ -240,10 +214,9 @@ export default class ServicesManager {
title: i18n.t('screens.websites.ent'), title: i18n.t('screens.websites.ent'),
subtitle: i18n.t('screens.services.descriptions.ent'), subtitle: i18n.t('screens.services.descriptions.ent'),
image: ENT_IMAGE, image: ENT_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.ENT, host: AvailableWebsites.websites.ENT,
title: i18n.t('screens.websites.ent'), title: i18n.t('screens.websites.ent')
}), }),
}, },
{ {
@ -251,10 +224,9 @@ export default class ServicesManager {
title: i18n.t('screens.insaAccount.title'), title: i18n.t('screens.insaAccount.title'),
subtitle: i18n.t('screens.services.descriptions.insaAccount'), subtitle: i18n.t('screens.services.descriptions.insaAccount'),
image: ACCOUNT_IMAGE, image: ACCOUNT_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.INSA_ACCOUNT, host: AvailableWebsites.websites.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title'), title: i18n.t('screens.insaAccount.title')
}), }),
}, },
]; ];
@ -264,48 +236,46 @@ export default class ServicesManager {
title: i18n.t('screens.proxiwash.washers'), title: i18n.t('screens.proxiwash.washers'),
subtitle: i18n.t('screens.services.descriptions.washers'), subtitle: i18n.t('screens.services.descriptions.washers'),
image: WASHER_IMAGE, image: WASHER_IMAGE,
onPress: (): void => nav.navigate('proxiwash'), onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.available_washers
dashboard.available_washers,
}, },
{ {
key: SERVICES_KEY.DRYERS, key: SERVICES_KEY.DRYERS,
title: i18n.t('screens.proxiwash.dryers'), title: i18n.t('screens.proxiwash.dryers'),
subtitle: i18n.t('screens.services.descriptions.washers'), subtitle: i18n.t('screens.services.descriptions.washers'),
image: DRYER_IMAGE, image: DRYER_IMAGE,
onPress: (): void => nav.navigate('proxiwash'), onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.available_dryers
dashboard.available_dryers, }
},
]; ];
this.categoriesDataset = [ this.categoriesDataset = [
{ {
key: SERVICES_CATEGORIES_KEY.AMICALE, key: SERVICES_CATEGORIES_KEY.AMICALE,
title: i18n.t('screens.services.categories.amicale'), title: i18n.t("screens.services.categories.amicale"),
subtitle: i18n.t('screens.services.more'), subtitle: i18n.t("screens.services.more"),
image: AMICALE_LOGO, image: AMICALE_LOGO,
content: this.amicaleDataset, content: this.amicaleDataset
}, },
{ {
key: SERVICES_CATEGORIES_KEY.STUDENTS, key: SERVICES_CATEGORIES_KEY.STUDENTS,
title: i18n.t('screens.services.categories.students'), title: i18n.t("screens.services.categories.students"),
subtitle: i18n.t('screens.services.more'), subtitle: i18n.t("screens.services.more"),
image: 'account-group', image: 'account-group',
content: this.studentsDataset, content: this.studentsDataset
}, },
{ {
key: SERVICES_CATEGORIES_KEY.INSA, key: SERVICES_CATEGORIES_KEY.INSA,
title: i18n.t('screens.services.categories.insa'), title: i18n.t("screens.services.categories.insa"),
subtitle: i18n.t('screens.services.more'), subtitle: i18n.t("screens.services.more"),
image: 'school', image: 'school',
content: this.insaDataset, content: this.insaDataset
}, },
{ {
key: SERVICES_CATEGORIES_KEY.SPECIAL, key: SERVICES_CATEGORIES_KEY.SPECIAL,
title: i18n.t('screens.services.categories.special'), title: i18n.t("screens.services.categories.special"),
subtitle: i18n.t('screens.services.categories.special'), subtitle: i18n.t("screens.services.categories.special"),
image: 'star', image: 'star',
content: this.specialDataset, content: this.specialDataset
}, },
]; ];
} }
@ -319,18 +289,37 @@ export default class ServicesManager {
onAmicaleServicePress(route: string) { onAmicaleServicePress(route: string) {
if (ConnectionManager.getInstance().isLoggedIn()) if (ConnectionManager.getInstance().isLoggedIn())
this.navigation.navigate(route); this.navigation.navigate(route);
else this.navigation.navigate('login', {nextScreen: route}); else
this.navigation.navigate("login", {nextScreen: route});
}
/**
* Gets the given services list without items of the given ids
*
* @param idList The ids of items to remove
* @param sourceList The item list to use as source
* @returns {[]}
*/
getStrippedList(idList: Array<string>, sourceList: Array<{key: string, [key: string]: any}>) {
let newArray = [];
for (let i = 0; i < sourceList.length; i++) {
const item = sourceList[i];
if (!(idList.includes(item.key)))
newArray.push(item);
}
return newArray;
} }
/** /**
* Gets the list of amicale's 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<ServiceItemType>} * @returns {Array<ServiceItem>}
*/ */
getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> { getAmicaleServices(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.amicaleDataset); return this.getStrippedList(excludedItems, this.amicaleDataset)
else
return this.amicaleDataset; return this.amicaleDataset;
} }
@ -338,11 +327,12 @@ export default class ServicesManager {
* Gets the list of students' 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<ServiceItemType>} * @returns {Array<ServiceItem>}
*/ */
getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> { getStudentServices(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.studentsDataset); return this.getStrippedList(excludedItems, this.studentsDataset)
else
return this.studentsDataset; return this.studentsDataset;
} }
@ -350,11 +340,12 @@ export default class ServicesManager {
* Gets the list of INSA's 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<ServiceItemType>} * @returns {Array<ServiceItem>}
*/ */
getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> { getINSAServices(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.insaDataset); return this.getStrippedList(excludedItems, this.insaDataset)
else
return this.insaDataset; return this.insaDataset;
} }
@ -362,11 +353,12 @@ export default class ServicesManager {
* Gets the list of special services * Gets the list of special 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<ServiceItemType>} * @returns {Array<ServiceItem>}
*/ */
getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> { getSpecialServices(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.specialDataset); return this.getStrippedList(excludedItems, this.specialDataset)
else
return this.specialDataset; return this.specialDataset;
} }
@ -374,11 +366,13 @@ export default class ServicesManager {
* Gets all services sorted by category * Gets all services sorted by category
* *
* @param excludedItems Ids of categories to exclude from the returned list * @param excludedItems Ids of categories to exclude from the returned list
* @returns {Array<ServiceCategoryType>} * @returns {Array<ServiceCategory>}
*/ */
getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> { getCategories(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.categoriesDataset); return this.getStrippedList(excludedItems, this.categoriesDataset)
else
return this.categoriesDataset; return this.categoriesDataset;
} }
} }

View file

@ -1,13 +1,13 @@
// @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 CustomThemeType = { export type CustomTheme = {
...DefaultTheme, ...DefaultTheme,
colors: { colors: {
primary: string, primary: string,
@ -63,15 +63,15 @@ export type CustomThemeType = {
// 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;
updateThemeCallback: null | (() => void); static instance: ThemeManager | null = null;
updateThemeCallback: Function;
constructor() { constructor() {
this.updateThemeCallback = null; this.updateThemeCallback = null;
@ -80,25 +80,25 @@ export default class ThemeManager {
/** /**
* Gets the light theme * Gets the light theme
* *
* @return {CustomThemeType} Object containing theme variables * @return {CustomTheme} Object containing theme variables
* */ * */
static getWhiteTheme(): CustomThemeType { static getWhiteTheme(): CustomTheme {
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
@ -106,14 +106,14 @@ export default class ThemeManager {
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',
@ -133,12 +133,12 @@ export default class ThemeManager {
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",
}, },
}; };
} }
@ -146,40 +146,40 @@ export default class ThemeManager {
/** /**
* Gets the dark theme * Gets the dark theme
* *
* @return {CustomThemeType} Object containing theme variables * @return {CustomTheme} Object containing theme variables
* */ * */
static getDarkTheme(): CustomThemeType { static getDarkTheme(): CustomTheme {
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',
@ -199,12 +199,12 @@ export default class ThemeManager {
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",
}, },
}; };
} }
@ -215,9 +215,9 @@ export default class ThemeManager {
* @returns {ThemeManager} * @returns {ThemeManager}
*/ */
static getInstance(): ThemeManager { static getInstance(): ThemeManager {
if (ThemeManager.instance == null) return ThemeManager.instance === null ?
ThemeManager.instance = new ThemeManager(); ThemeManager.instance = new ThemeManager() :
return ThemeManager.instance; ThemeManager.instance;
} }
/** /**
@ -228,39 +228,34 @@ export default class ThemeManager {
* @returns {boolean} Night mode state * @returns {boolean} Night mode state
*/ */
static getNightMode(): boolean { static getNightMode(): boolean {
return ( return (AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightMode.key) &&
(AsyncStorageManager.getBool( (!AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
AsyncStorageManager.PREFERENCES.nightMode.key, || colorScheme === 'no-preference')) ||
) && (AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
(!AsyncStorageManager.getBool( && colorScheme === 'dark');
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 {CustomThemeType} The current theme * @returns {CustomTheme} The current theme
*/ */
static getCurrentTheme(): CustomThemeType { static getCurrentTheme(): CustomTheme {
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled()) if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme()); return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme());
return ThemeManager.getBaseTheme(); else
return ThemeManager.getBaseTheme()
} }
/** /**
* Get the theme based on night mode * Get the theme based on night mode
* *
* @return {CustomThemeType} The theme * @return {CustomTheme} The theme
*/ */
static getBaseTheme(): CustomThemeType { static getBaseTheme(): CustomTheme {
if (ThemeManager.getNightMode()) return ThemeManager.getDarkTheme(); if (ThemeManager.getNightMode())
return ThemeManager.getDarkTheme();
else
return ThemeManager.getWhiteTheme(); return ThemeManager.getWhiteTheme();
} }
@ -279,10 +274,9 @@ export default class ThemeManager {
* @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.set(AsyncStorageManager.PREFERENCES.nightMode.key, isNightMode);
AsyncStorageManager.PREFERENCES.nightMode.key, if (this.updateThemeCallback != null)
isNightMode, this.updateThemeCallback();
);
if (this.updateThemeCallback != null) this.updateThemeCallback();
} }
}
};

View file

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

View file

@ -1,39 +1,32 @@
// @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 ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; import {Title, useTheme} from 'react-native-paper';
import ScannerScreen from '../screens/Home/ScannerScreen'; import {Platform} from 'react-native';
import FeedItemScreen from '../screens/Home/FeedItemScreen'; import i18n from "i18n-js";
import GroupSelectionScreen from '../screens/Planex/GroupSelectionScreen'; import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
import CustomTabBar from '../components/Tabbar/CustomTabBar'; import ScannerScreen from "../screens/Home/ScannerScreen";
import WebsitesHomeScreen from '../screens/Services/ServicesScreen'; import FeedItemScreen from "../screens/Home/FeedItemScreen";
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen'; import {createCollapsibleStack} from "react-navigation-collapsible";
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen'; import GroupSelectionScreen from "../screens/Planex/GroupSelectionScreen";
import { import CustomTabBar from "../components/Tabbar/CustomTabBar";
createScreenCollapsibleStack, import WebsitesHomeScreen from "../screens/Services/ServicesScreen";
getWebsiteStack, import ServicesSectionScreen from "../screens/Services/ServicesSectionScreen";
} from '../utils/CollapsibleUtils'; import AmicaleContactScreen from "../screens/Amicale/AmicaleContactScreen";
import Mascot, {MASCOT_STYLE} from '../components/Mascot/Mascot'; import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils";
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,
@ -41,98 +34,94 @@ const defaultScreenOptions = {
...modalTransition, ...modalTransition,
}; };
const ServicesStack = createStackNavigator(); const ServicesStack = createStackNavigator();
function ServicesStackComponent(): React.Node { function ServicesStackComponent() {
return ( return (
<ServicesStack.Navigator <ServicesStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode="screen" headerMode={"screen"}
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}
>
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'index', "index",
ServicesStack, ServicesStack,
WebsitesHomeScreen, WebsitesHomeScreen,
i18n.t('screens.services.title'), i18n.t('screens.services.title'))}
)}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'services-section', "services-section",
ServicesStack, ServicesStack,
ServicesSectionScreen, ServicesSectionScreen,
'SECTION', "SECTION")}
)}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'amicale-contact', "amicale-contact",
ServicesStack, ServicesStack,
AmicaleContactScreen, AmicaleContactScreen,
i18n.t('screens.amicaleAbout.title'), i18n.t('screens.amicaleAbout.title'))}
)}
</ServicesStack.Navigator> </ServicesStack.Navigator>
); );
} }
const ProxiwashStack = createStackNavigator(); const ProxiwashStack = createStackNavigator();
function ProxiwashStackComponent(): React.Node { function ProxiwashStackComponent() {
return ( return (
<ProxiwashStack.Navigator <ProxiwashStack.Navigator
initialRouteName="index" initialRouteName="index"
headerMode="screen" headerMode={"screen"}
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}
>
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'index', "index",
ProxiwashStack, ProxiwashStack,
ProxiwashScreen, ProxiwashScreen,
i18n.t('screens.proxiwash.title'), i18n.t('screens.proxiwash.title'))}
)}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'proxiwash-about', "proxiwash-about",
ProxiwashStack, ProxiwashStack,
ProxiwashAboutScreen, ProxiwashAboutScreen,
i18n.t('screens.proxiwash.title'), i18n.t('screens.proxiwash.title'))}
)}
</ProxiwashStack.Navigator> </ProxiwashStack.Navigator>
); );
} }
const PlanningStack = createStackNavigator(); const PlanningStack = createStackNavigator();
function PlanningStackComponent(): React.Node { function PlanningStackComponent() {
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( function HomeStackComponent(initialRoute: string | null, defaultData: { [key: string]: any }) {
initialRoute: string | null, let params = undefined;
defaultData: {[key: string]: string},
): React.Node {
let params;
if (initialRoute != null) if (initialRoute != null)
params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true}; params = {data: defaultData, nextScreen: initialRoute, shouldOpen: true};
const {colors} = useTheme(); const {colors} = useTheme();
return ( return (
<HomeStack.Navigator <HomeStack.Navigator
initialRouteName="index" initialRouteName={"index"}
headerMode="screen" headerMode={"screen"}
screenOptions={defaultScreenOptions}> screenOptions={defaultScreenOptions}
>
{createCollapsibleStack( {createCollapsibleStack(
<HomeStack.Screen <HomeStack.Screen
name="index" name="index"
@ -142,123 +131,113 @@ function HomeStackComponent(
headerStyle: { headerStyle: {
backgroundColor: colors.surface, backgroundColor: colors.surface,
}, },
headerTitle: (): React.Node => ( headerTitle: () =>
<View style={{flexDirection: 'row'}}> <View style={{flexDirection: "row"}}>
<Mascot <Mascot
style={{ style={{
width: 50, width: 50
}} }}
emotion={MASCOT_STYLE.RANDOM} emotion={MASCOT_STYLE.RANDOM}
animated animated={true}
entryAnimation={{ entryAnimation={{
animation: 'bounceIn', animation: "bounceIn",
duration: 1000, duration: 1000
}} }}
loopAnimation={{ loopAnimation={{
animation: 'pulse', animation: "pulse",
duration: 2000, duration: 2000,
iterationCount: 'infinite', iterationCount: "infinite"
}} }}
/> />
<Title <Title style={{
style={{
marginLeft: 10, marginLeft: 10,
marginTop: 'auto', marginTop: "auto",
marginBottom: 'auto', marginBottom: "auto",
}}> }}>{i18n.t('screens.home.title')}</Title>
{i18n.t('screens.home.title')}
</Title>
</View> </View>
),
}} }}
initialParams={params} initialParams={params}
/>, />,
{ {
collapsedColor: colors.surface, collapsedColor: colors.surface,
useNativeDriver: true, useNativeDriver: true,
}, }
)} )}
<HomeStack.Screen <HomeStack.Screen
name="scanner" name="scanner"
component={ScannerScreen} component={ScannerScreen}
options={{title: i18n.t('screens.scanner.title')}} options={{title: i18n.t('screens.scanner.title'),}}
/> />
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'club-information', "club-information",
HomeStack, HomeStack,
ClubDisplayScreen, ClubDisplayScreen,
i18n.t('screens.clubs.details'), i18n.t('screens.clubs.details'))}
)}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'feed-information', "feed-information",
HomeStack, HomeStack,
FeedItemScreen, FeedItemScreen,
i18n.t('screens.home.feed'), i18n.t('screens.home.feed'))}
)}
{createScreenCollapsibleStack( {createScreenCollapsibleStack(
'planning-information', "planning-information",
HomeStack, HomeStack,
PlanningDisplayScreen, PlanningDisplayScreen,
i18n.t('screens.planning.eventDetails'), i18n.t('screens.planning.eventDetails'))}
)}
</HomeStack.Navigator> </HomeStack.Navigator>
); );
} }
const PlanexStack = createStackNavigator(); const PlanexStack = createStackNavigator();
function PlanexStackComponent(): React.Node { function PlanexStackComponent() {
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 PropsType = { type Props = {
defaultHomeRoute: string | null, defaultHomeRoute: string | null,
defaultHomeData: {[key: string]: string}, defaultHomeData: { [key: string]: any }
}; }
export default class TabNavigator extends React.Component<PropsType> { export default class TabNavigator extends React.Component<Props> {
createHomeStackComponent: () => React.Node;
createHomeStackComponent: () => HomeStackComponent;
defaultRoute: string; defaultRoute: string;
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
if (props.defaultHomeRoute != null) this.defaultRoute = 'home'; if (props.defaultHomeRoute != null)
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(): React.Node { render() {
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={this.defaultRoute} initialRouteName={this.defaultRoute}
// eslint-disable-next-line react/jsx-props-no-spreading tabBar={props => <CustomTabBar {...props} />}
tabBar={(props: {...}): React.Node => <CustomTabBar {...props} />}> >
<Tab.Screen <Tab.Screen
name="services" name="services"
option option
@ -284,7 +263,7 @@ export default class TabNavigator extends React.Component<PropsType> {
<Tab.Screen <Tab.Screen
name="planex" name="planex"
component={PlanexStackComponent} component={PlanexStackComponent}
options={{title: i18n.t('screens.planex.title')}} options={{title: i18n.t("screens.planex.title")}}
/> />
</Tab.Navigator> </Tab.Navigator>
); );

View file

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

View file

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

View file

@ -1,43 +1,38 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from "react-native";
import { import AsyncStorageManager from "../../managers/AsyncStorageManager";
Button, import CustomModal from "../../components/Overrides/CustomModal";
List, import {Button, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper';
Subheading, import {StackNavigationProp} from "@react-navigation/stack";
TextInput, import {Modalize} from "react-native-modalize";
Title, import type {CustomTheme} from "../../managers/ThemeManager";
withTheme, import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
} 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 PreferenceItemType = { type PreferenceItem = {
key: string, key: string,
default: string, default: string,
current: string, current: string,
}
type Props = {
navigation: StackNavigationProp,
theme: CustomTheme
}; };
type PropsType = { type State = {
theme: CustomThemeType, modalCurrentDisplayItem: PreferenceItem,
}; 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<PropsType, StateType> { class DebugScreen extends React.Component<Props, State> {
modalRef: Modalize;
modalRef: Modalize;
modalInputValue: string; modalInputValue: string;
/** /**
@ -45,126 +40,87 @@ class DebugScreen extends React.Component<PropsType, StateType> {
* *
* @param props * @param props
*/ */
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
this.modalInputValue = ''; this.modalInputValue = "";
const currentPreferences: Array<PreferenceItemType> = []; let currentPreferences : Array<PreferenceItem> = [];
// eslint-disable-next-line flowtype/no-weak-types Object.values(AsyncStorageManager.PREFERENCES).map((object: any) => {
Object.values(AsyncStorageManager.PREFERENCES).forEach((object: any) => { let newObject: PreferenceItem = {...object};
const newObject: PreferenceItemType = {...object};
newObject.current = AsyncStorageManager.getString(newObject.key); newObject.current = AsyncStorageManager.getString(newObject.key);
currentPreferences.push(newObject); currentPreferences.push(newObject);
}); });
this.state = { this.state = {
modalCurrentDisplayItem: {}, modalCurrentDisplayItem: {},
currentPreferences, currentPreferences: 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 * Gets the edit modal content
* *
* @return {*} * @return {*}
*/ */
getModalContent(): React.Node { getModalContent() {
const {props, state} = this;
return ( return (
<View <View style={{
style={{
flex: 1, flex: 1,
padding: 20, padding: 20
}}> }}>
<Title>{state.modalCurrentDisplayItem.key}</Title> <Title>{this.state.modalCurrentDisplayItem.key}</Title>
<Subheading> <Subheading>Default: {this.state.modalCurrentDisplayItem.default}</Subheading>
Default: {state.modalCurrentDisplayItem.default} <Subheading>Current: {this.state.modalCurrentDisplayItem.current}</Subheading>
</Subheading>
<Subheading>
Current: {state.modalCurrentDisplayItem.current}
</Subheading>
<TextInput <TextInput
label="New Value" label='New Value'
onChangeText={(text: string) => { onChangeText={(text) => this.modalInputValue = text}
this.modalInputValue = text;
}}
/> />
<View <View style={{
style={{
flexDirection: 'row', flexDirection: 'row',
marginTop: 10, marginTop: 10,
}}> }}>
<Button <Button
mode="contained" mode="contained"
dark dark={true}
color={props.theme.colors.success} color={this.props.theme.colors.success}
onPress={() => { onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.modalInputValue)}>
this.saveNewPrefs(
state.modalCurrentDisplayItem.key,
this.modalInputValue,
);
}}>
Save new value Save new value
</Button> </Button>
<Button <Button
mode="contained" mode="contained"
dark dark={true}
color={props.theme.colors.danger} color={this.props.theme.colors.danger}
onPress={() => { onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.state.modalCurrentDisplayItem.default)}>
this.saveNewPrefs(
state.modalCurrentDisplayItem.key,
state.modalCurrentDisplayItem.default,
);
}}>
Reset to default Reset to default
</Button> </Button>
</View> </View>
</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 * Finds the index of the given key in the preferences array
* *
* @param key THe key to find the index of * @param key THe key to find the index of
* @returns {number} * @returns {number}
*/ */
findIndexOfKey(key: string): number { findIndexOfKey(key: string) {
const {currentPreferences} = this.state;
let index = -1; let index = -1;
for (let i = 0; i < currentPreferences.length; i += 1) { for (let i = 0; i < this.state.currentPreferences.length; i++) {
if (currentPreferences[i].key === key) { if (this.state.currentPreferences[i].key === key) {
index = i; index = i;
break; break;
} }
@ -179,10 +135,8 @@ class DebugScreen extends React.Component<PropsType, StateType> {
* @param value The pref value * @param value The pref value
*/ */
saveNewPrefs(key: string, value: string) { saveNewPrefs(key: string, value: string) {
this.setState((prevState: StateType): { this.setState((prevState) => {
currentPreferences: Array<PreferenceItemType>, let currentPreferences = [...prevState.currentPreferences];
} => {
const currentPreferences = [...prevState.currentPreferences];
currentPreferences[this.findIndexOfKey(key)].current = value; currentPreferences[this.findIndexOfKey(key)].current = value;
return {currentPreferences}; return {currentPreferences};
}); });
@ -190,18 +144,36 @@ class DebugScreen extends React.Component<PropsType, StateType> {
this.modalRef.close(); this.modalRef.close();
} }
render(): React.Node { /**
const {state} = this; * 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 ( return (
<View> <View>
<CustomModal onRef={this.onModalRef}> <CustomModal onRef={this.onModalRef}>
{this.getModalContent()} {this.getModalContent()}
</CustomModal> </CustomModal>
{/* $FlowFixMe */} {/*$FlowFixMe*/}
<CollapsibleFlatList <CollapsibleFlatList
data={state.currentPreferences} data={this.state.currentPreferences}
extraData={state.currentPreferences} extraData={this.state.currentPreferences}
renderItem={this.getRenderItem} renderItem={this.renderItem}
/> />
</View> </View>
); );

View file

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

View file

@ -4,44 +4,44 @@ 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';
// eslint-disable-next-line react/prefer-stateless-function class ClubAboutScreen extends React.Component<Props> {
class ClubAboutScreen extends React.Component<null> {
render(): React.Node { render() {
return ( return (
<CollapsibleScrollView style={{padding: 5}}> <CollapsibleScrollView style={{padding: 5}}>
<View <View style={{
style={{
width: '100%', width: '100%',
height: 100, height: 100,
marginTop: 20, marginTop: 20,
marginBottom: 20, marginBottom: 20,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center'
}}> }}>
<Image <Image
source={AMICALE_ICON} source={require('../../../../assets/amicale.png')}
style={{flex: 1, resizeMode: 'contain'}} style={{flex: 1, resizeMode: "contain"}}
resizeMode="contain" resizeMode="contain"/>
/>
</View> </View>
<Text>{i18n.t('screens.clubs.about.text')}</Text> <Text>{i18n.t("screens.clubs.about.text")}</Text>
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Title <Card.Title
title={i18n.t('screens.clubs.about.title')} title={i18n.t("screens.clubs.about.title")}
subtitle={i18n.t('screens.clubs.about.subtitle')} subtitle={i18n.t("screens.clubs.about.subtitle")}
left={({size}: {size: number}): React.Node => ( left={props => <List.Icon {...props} icon={'information'}/>}
<List.Icon size={size} icon="information" />
)}
/> />
<Card.Content> <Card.Content>
<Text>{i18n.t('screens.clubs.about.message')}</Text> <Text>{i18n.t("screens.clubs.about.message")}</Text>
<Autolink text={CONTACT_LINK} component={Text} /> <Autolink
text={CONTACT_LINK}
component={Text}
/>
</Card.Content> </Card.Content>
</Card> </Card>
</CollapsibleScrollView> </CollapsibleScrollView>

View file

@ -2,70 +2,65 @@
import * as React from 'react'; import * as React from 'react';
import {Linking, View} from 'react-native'; import {Linking, View} from 'react-native';
import { import {Avatar, Button, Card, Chip, Paragraph, withTheme} from 'react-native-paper';
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 {StackNavigationProp} from '@react-navigation/stack'; import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; import CustomHTML from "../../../components/Overrides/CustomHTML";
import CustomHTML from '../../../components/Overrides/CustomHTML'; import CustomTabBar from "../../../components/Tabbar/CustomTabBar";
import CustomTabBar from '../../../components/Tabbar/CustomTabBar'; import type {category, club} from "./ClubListScreen";
import type {ClubCategoryType, ClubType} from './ClubListScreen'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {CustomThemeType} from '../../../managers/ThemeManager'; import {StackNavigationProp} from "@react-navigation/stack";
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 PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
data?: ClubType, data?: club,
categories?: Array<ClubCategoryType>, categories?: Array<category>,
clubId?: number, clubId?: number,
}, ...
}, },
... theme: CustomTheme
},
theme: CustomThemeType,
}; };
const AMICALE_MAIL = 'clubs@amicale-insat.fr'; type State = {
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<PropsType> { class ClubDisplayScreen extends React.Component<Props, State> {
displayData: ClubType | null;
categories: Array<ClubCategoryType> | null;
displayData: club | null;
categories: Array<category> | null;
clubId: number; clubId: number;
shouldFetchData: boolean; shouldFetchData: boolean;
constructor(props: PropsType) { state = {
imageModalVisible: false,
};
constructor(props) {
super(props); super(props);
if (props.route.params != null) { if (this.props.route.params != null) {
if ( if (this.props.route.params.data != null && this.props.route.params.categories != null) {
props.route.params.data != null && this.displayData = this.props.route.params.data;
props.route.params.categories != null this.categories = this.props.route.params.categories;
) { this.clubId = this.props.route.params.data.id;
this.displayData = props.route.params.data;
this.categories = props.route.params.categories;
this.clubId = props.route.params.data.id;
this.shouldFetchData = false; this.shouldFetchData = false;
} else if (props.route.params.clubId != null) { } else if (this.props.route.params.clubId != null) {
this.displayData = null; this.displayData = null;
this.categories = null; this.categories = null;
this.clubId = props.route.params.clubId; this.clubId = this.props.route.params.clubId;
this.shouldFetchData = true; this.shouldFetchData = true;
} }
} }
@ -77,14 +72,14 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param id The category's ID * @param id The category's ID
* @returns {string|*} * @returns {string|*}
*/ */
getCategoryName(id: number): string { getCategoryName(id: number) {
let categoryName = '';
if (this.categories !== null) { if (this.categories !== null) {
this.categories.forEach((item: ClubCategoryType) => { for (let i = 0; i < this.categories.length; i++) {
if (id === item.id) categoryName = item.name; if (id === this.categories[i].id)
}); return this.categories[i].name;
} }
return categoryName; }
return "";
} }
/** /**
@ -93,19 +88,23 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param categories The categories to display (max 2) * @param categories The categories to display (max 2)
* @returns {null|*} * @returns {null|*}
*/ */
getCategoriesRender(categories: Array<number | null>): React.Node { getCategoriesRender(categories: [number, number]) {
if (this.categories == null) return null; if (this.categories === null)
return null;
const final = []; let final = [];
categories.forEach((cat: number | null) => { for (let i = 0; i < categories.length; i++) {
if (cat != null) { let cat = categories[i];
if (cat !== null) {
final.push( final.push(
<Chip style={{marginRight: 5}} key={cat}> <Chip
style={{marginRight: 5}}
key={i.toString()}>
{this.getCategoryName(cat)} {this.getCategoryName(cat)}
</Chip>, </Chip>
); );
} }
}); }
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>; return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
} }
@ -116,39 +115,26 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param email The club contact email * @param email The club contact email
* @returns {*} * @returns {*}
*/ */
getManagersRender(managers: Array<string>, email: string | null): React.Node { getManagersRender(managers: Array<string>, email: string | null) {
const {props} = this; let managersListView = [];
const managersListView = []; for (let i = 0; i < managers.length; i++) {
managers.forEach((item: string) => { managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
managersListView.push(<Paragraph key={item}>{item}</Paragraph>); }
});
const hasManagers = managers.length > 0; const hasManagers = managers.length > 0;
return ( return (
<Card <Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title <Card.Title
title={i18n.t('screens.clubs.managers')} title={i18n.t('screens.clubs.managers')}
subtitle={ subtitle={hasManagers ? i18n.t('screens.clubs.managersSubtitle') : i18n.t('screens.clubs.managersUnavailable')}
hasManagers left={(props) => <Avatar.Icon
? i18n.t('screens.clubs.managersSubtitle') {...props}
: i18n.t('screens.clubs.managersUnavailable')
}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
color={ color={hasManagers ? this.props.theme.colors.success : this.props.theme.colors.primary}
hasManagers icon="account-tie"/>}
? props.theme.colors.success
: props.theme.colors.primary
}
icon="account-tie"
/>
)}
/> />
<Card.Content> <Card.Content>
{managersListView} {managersListView}
{ClubDisplayScreen.getEmailButton(email, hasManagers)} {this.getEmailButton(email, hasManagers)}
</Card.Content> </Card.Content>
</Card> </Card>
); );
@ -161,45 +147,51 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param hasManagers True if the club has managers * @param hasManagers True if the club has managers
* @returns {*} * @returns {*}
*/ */
static getEmailButton( getEmailButton(email: string | null, hasManagers: boolean) {
email: string | null, const destinationEmail = email != null && hasManagers
hasManagers: boolean, ? email
): React.Node { : AMICALE_MAIL;
const destinationEmail = const text = email != null && hasManagers
email != null && hasManagers ? email : AMICALE_MAIL; ? i18n.t("screens.clubs.clubContact")
const text = : i18n.t("screens.clubs.amicaleContact");
email != null && hasManagers
? i18n.t('screens.clubs.clubContact')
: i18n.t('screens.clubs.amicaleContact');
return ( return (
<Card.Actions> <Card.Actions>
<Button <Button
icon="email" icon="email"
mode="contained" mode="contained"
onPress={() => { onPress={() => Linking.openURL('mailto:' + destinationEmail)}
Linking.openURL(`mailto:${destinationEmail}`); style={{marginLeft: 'auto'}}
}} >
style={{marginLeft: 'auto'}}>
{text} {text}
</Button> </Button>
</Card.Actions> </Card.Actions>
); );
} }
getScreen = (response: Array<ApiGenericDataType | null>): React.Node => { /**
const {props} = this; * Updates the header title to match the given club
let data: ClubType | null = null; *
* @param data The club data
*/
updateHeaderTitle(data: club) {
this.props.navigation.setOptions({title: data.name})
}
getScreen = (response: Array<{ [key: string]: any } | null>) => {
let data: club | null = null;
if (response[0] != null) { if (response[0] != null) {
[data] = response; data = response[0];
this.updateHeaderTitle(data); this.updateHeaderTitle(data);
} }
if (data != null) { if (data != null) {
return ( return (
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab> <CollapsibleScrollView
style={{paddingLeft: 5, paddingRight: 5}}
hasTab={true}
>
{this.getCategoriesRender(data.category)} {this.getCategoriesRender(data.category)}
{data.logo !== null ? ( {data.logo !== null ?
<View <View style={{
style={{
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
marginTop: 10, marginTop: 10,
@ -207,7 +199,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
}}> }}>
<ImageModal <ImageModal
resizeMode="contain" resizeMode="contain"
imageBackgroundColor={props.theme.colors.background} imageBackgroundColor={this.props.theme.colors.background}
style={{ style={{
width: 300, width: 300,
height: 300, height: 300,
@ -217,59 +209,43 @@ class ClubDisplayScreen extends React.Component<PropsType> {
}} }}
/> />
</View> </View>
) : ( : <View/>}
<View />
)}
{data.description !== null ? ( {data.description !== null ?
// Surround description with div to allow text styling if the description is not html // Surround description with div to allow text styling if the description is not html
<Card.Content> <Card.Content>
<CustomHTML html={data.description} /> <CustomHTML html={data.description}/>
</Card.Content> </Card.Content>
) : ( : <View/>}
<View />
)}
{this.getManagersRender(data.responsibles, data.email)} {this.getManagersRender(data.responsibles, data.email)}
</CollapsibleScrollView> </CollapsibleScrollView>
); );
} } else
return null; return null;
}; };
/** render() {
* Updates the header title to match the given club
*
* @param data The club data
*/
updateHeaderTitle(data: ClubType) {
const {props} = this;
props.navigation.setOptions({title: data.name});
}
render(): React.Node {
const {props} = this;
if (this.shouldFetchData) if (this.shouldFetchData)
return ( return <AuthenticatedScreen
<AuthenticatedScreen {...this.props}
navigation={props.navigation}
requests={[ requests={[
{ {
link: 'clubs/info', link: 'clubs/info',
params: {id: this.clubId}, params: {'id': this.clubId},
mandatory: true, mandatory: true
}, }
]} ]}
renderFunction={this.getScreen} renderFunction={this.getScreen}
errorViewOverride={[ errorViewOverride={[
{ {
errorCode: ERROR_TYPE.BAD_INPUT, errorCode: ERROR_TYPE.BAD_INPUT,
message: i18n.t('screens.clubs.invalidClub'), message: i18n.t("screens.clubs.invalidClub"),
icon: 'account-question', icon: "account-question",
showRetryButton: false, showRetryButton: false
}, }
]} ]}
/> />;
); else
return this.getScreen([this.displayData]); return this.getScreen([this.displayData]);
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,118 +1,111 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper';
Button, import {StackNavigationProp} from "@react-navigation/stack";
Caption, import type {CustomTheme} from "../../../managers/ThemeManager";
Card, import type {Device} from "./EquipmentListScreen";
Headline, import {BackHandler, View} from "react-native";
Subheading, import * as Animatable from "react-native-animatable";
withTheme, import i18n from "i18n-js";
} from 'react-native-paper'; import {CalendarList} from "react-native-calendars";
import {StackNavigationProp} from '@react-navigation/stack'; import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
import {BackHandler, View} from 'react-native'; import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
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 { import {
generateMarkedDates, generateMarkedDates,
getFirstEquipmentAvailability, getFirstEquipmentAvailability,
getISODate, getISODate,
getRelativeDateString, getRelativeDateString,
getValidRange, getValidRange,
isEquipmentAvailable, isEquipmentAvailable
} from '../../../utils/EquipmentBooking'; } from "../../../utils/EquipmentBooking";
import ConnectionManager from '../../../managers/ConnectionManager'; import ConnectionManager from "../../../managers/ConnectionManager";
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
item?: DeviceType, item?: Device,
}, },
}, },
theme: CustomThemeType, theme: CustomTheme,
}; }
export type MarkedDatesObjectType = { type State = {
[key: string]: {startingDay: boolean, endingDay: boolean, color: string},
};
type StateType = {
dialogVisible: boolean, dialogVisible: boolean,
errorDialogVisible: boolean, errorDialogVisible: boolean,
markedDates: MarkedDatesObjectType, markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
currentError: number, currentError: number,
}; }
class EquipmentRentScreen extends React.Component<PropsType, StateType> { class EquipmentRentScreen extends React.Component<Props, State> {
item: DeviceType | null;
bookedDates: Array<string>; state = {
bookRef: {current: null | Animatable.View};
canBookEquipment: boolean;
lockedDates: {
[key: string]: {startingDay: boolean, endingDay: boolean, color: string},
};
constructor(props: PropsType) {
super(props);
this.state = {
dialogVisible: false, dialogVisible: false,
errorDialogVisible: false, errorDialogVisible: false,
markedDates: {}, markedDates: {},
currentError: 0, currentError: 0,
}; }
item: Device | null;
bookedDates: Array<string>;
bookRef: { current: null | Animatable.View }
canBookEquipment: boolean;
lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }
constructor(props: Props) {
super(props);
this.resetSelection(); this.resetSelection();
this.bookRef = React.createRef(); this.bookRef = React.createRef();
this.canBookEquipment = false; this.canBookEquipment = false;
this.bookedDates = []; this.bookedDates = [];
if (props.route.params != null) { if (this.props.route.params != null) {
if (props.route.params.item != null) this.item = props.route.params.item; if (this.props.route.params.item != null)
else this.item = null; this.item = this.props.route.params.item;
else
this.item = null;
} }
const {item} = this; const item = this.item;
if (item != null) { if (item != null) {
this.lockedDates = {}; this.lockedDates = {};
item.booked_at.forEach((date: {begin: string, end: string}) => { for (let i = 0; i < item.booked_at.length; i++) {
const range = getValidRange( const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null);
new Date(date.begin),
new Date(date.end),
null,
);
this.lockedDates = { this.lockedDates = {
...this.lockedDates, ...this.lockedDates,
...generateMarkedDates(false, props.theme, range), ...generateMarkedDates(
false,
this.props.theme,
range
)
}; };
});
} }
} }
}
/** /**
* Captures focus and blur events to hook on android back button * Captures focus and blur events to hook on android back button
*/ */
componentDidMount() { componentDidMount() {
const {navigation} = this.props; this.props.navigation.addListener(
navigation.addListener('focus', () => { 'focus',
() =>
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid, this.onBackButtonPressAndroid
)
); );
}); this.props.navigation.addListener(
navigation.addListener('blur', () => { 'blur',
() =>
BackHandler.removeEventListener( BackHandler.removeEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid, this.onBackButtonPressAndroid
)
); );
});
} }
/** /**
@ -120,88 +113,26 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
* *
* @return {boolean} * @return {boolean}
*/ */
onBackButtonPressAndroid = (): boolean => { onBackButtonPressAndroid = () => {
if (this.bookedDates.length > 0) { if (this.bookedDates.length > 0) {
this.resetSelection(); this.resetSelection();
this.updateMarkedSelection(); this.updateMarkedSelection();
return true; return true;
} } else
return false; return false;
}; };
onDialogDismiss = () => {
this.setState({dialogVisible: false});
};
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. * Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all. * If both start and end dates are already selected, unselect all.
* *
* @param day The day selected * @param day The day selected
*/ */
selectNewDate = (day: { selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => {
dateString: string,
day: number,
month: number,
timestamp: number,
year: number,
}) => {
const selected = new Date(day.dateString); const selected = new Date(day.dateString);
const start = this.getBookStartDate(); const start = this.getBookStartDate();
if (!this.lockedDates[day.dateString] != null) { if (!(this.lockedDates.hasOwnProperty(day.dateString))) {
if (start === null) { if (start === null) {
this.updateSelectionRange(selected, selected); this.updateSelectionRange(selected, selected);
this.enableBooking(); this.enableBooking();
@ -210,21 +141,39 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
} else if (this.bookedDates.length === 1) { } else if (this.bookedDates.length === 1) {
this.updateSelectionRange(start, selected); this.updateSelectionRange(start, selected);
this.enableBooking(); this.enableBooking();
} else this.resetSelection(); } else
this.resetSelection();
this.updateMarkedSelection(); this.updateMarkedSelection();
} }
}; }
showErrorDialog = (error: number) => { updateSelectionRange(start: Date, end: Date) {
this.bookedDates = getValidRange(start, end, this.item);
}
updateMarkedSelection() {
this.setState({ this.setState({
errorDialogVisible: true, markedDates: generateMarkedDates(
currentError: error, true,
this.props.theme,
this.bookedDates
),
}); });
}; }
showDialog = () => { enableBooking() {
this.setState({dialogVisible: true}); if (!this.canBookEquipment) {
}; this.showBookButton();
this.canBookEquipment = true;
}
}
resetSelection() {
if (this.canBookEquipment)
this.hideBookButton();
this.canBookEquipment = false;
this.bookedDates = [];
}
/** /**
* Shows the book button by plying a fade animation * Shows the book button by plying a fade animation
@ -244,45 +193,84 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
} }
} }
enableBooking() { showDialog = () => {
if (!this.canBookEquipment) { this.setState({dialogVisible: true});
this.showBookButton();
this.canBookEquipment = true;
}
} }
resetSelection() { showErrorDialog = (error: number) => {
if (this.canBookEquipment) this.hideBookButton();
this.canBookEquipment = false;
this.bookedDates = [];
}
updateSelectionRange(start: Date, end: Date) {
this.bookedDates = getValidRange(start, end, this.item);
}
updateMarkedSelection() {
const {theme} = this.props;
this.setState({ this.setState({
markedDates: generateMarkedDates(true, theme, this.bookedDates), errorDialogVisible: true,
currentError: error,
}); });
} }
render(): React.Node { onDialogDismiss = () => {
const {item, props, state} = this; this.setState({dialogVisible: false});
}
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<R>}
*/
onDialogAccept = () => {
return new Promise((resolve) => {
const item = this.item;
const start = this.getBookStartDate(); const start = this.getBookStartDate();
const end = this.getBookEndDate(); const end = this.getBookEndDate();
let subHeadingText; if (item != null && start != null && end != null) {
if (start == null) subHeadingText = i18n.t('screens.equipment.booking'); console.log({
else if (end != null && start.getTime() !== end.getTime()) "device": item.id,
subHeadingText = i18n.t('screens.equipment.bookingPeriod', { "begin": getISODate(start),
begin: getRelativeDateString(start), "end": getISODate(end),
end: getRelativeDateString(end), })
ConnectionManager.getInstance().authenticatedRequest(
"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)]
}); });
else resolve();
i18n.t('screens.equipment.bookingDay', { })
date: getRelativeDateString(start), .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) { if (item != null) {
const isAvailable = isEquipmentAvailable(item); const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item); const firstAvailability = getFirstEquipmentAvailability(item);
@ -292,19 +280,17 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Content> <Card.Content>
<View style={{flex: 1}}> <View style={{flex: 1}}>
<View <View style={{
style={{ marginLeft: "auto",
marginLeft: 'auto', marginRight: "auto",
marginRight: 'auto', flexDirection: "row",
flexDirection: 'row', flexWrap: "wrap",
flexWrap: 'wrap',
}}> }}>
<Headline style={{textAlign: 'center'}}> <Headline style={{textAlign: "center"}}>
{item.name} {item.name}
</Headline> </Headline>
<Caption <Caption style={{
style={{ textAlign: "center",
textAlign: 'center',
lineHeight: 35, lineHeight: 35,
marginLeft: 10, marginLeft: 10,
}}> }}>
@ -314,24 +300,30 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
</View> </View>
<Button <Button
icon={isAvailable ? 'check-circle-outline' : 'update'} icon={isAvailable ? "check-circle-outline" : "update"}
color={ color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
isAvailable mode="text"
? props.theme.colors.success >
: props.theme.colors.primary {i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)})}
}
mode="text">
{i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
})}
</Button> </Button>
<Subheading <Subheading style={{
style={{ textAlign: "center",
textAlign: 'center',
marginBottom: 10, marginBottom: 10,
minHeight: 50, minHeight: 50
}}> }}>
{subHeadingText} {
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> </Subheading>
</Card.Content> </Card.Content>
</Card> </Card>
@ -343,34 +335,35 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
// Max amount of months allowed to scroll to the future. Default = 50 // Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={3} futureScrollRange={3}
// Enable horizontal scrolling, default = false // Enable horizontal scrolling, default = false
horizontal horizontal={true}
// Enable paging on horizontal, default = false // Enable paging on horizontal, default = false
pagingEnabled pagingEnabled={true}
// Handler which gets executed on day press. Default = undefined // Handler which gets executed on day press. Default = undefined
onDayPress={this.selectNewDate} onDayPress={this.selectNewDate}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday. // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1} firstDay={1}
// Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates // Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
disableAllTouchEventsForDisabledDays disableAllTouchEventsForDisabledDays={true}
// Hide month navigation arrows. // Hide month navigation arrows.
hideArrows={false} hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple' // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType="period" markingType={'period'}
markedDates={{...this.lockedDates, ...state.markedDates}} markedDates={{...this.lockedDates, ...this.state.markedDates}}
theme={{ theme={{
backgroundColor: props.theme.colors.agendaBackgroundColor, backgroundColor: this.props.theme.colors.agendaBackgroundColor,
calendarBackground: props.theme.colors.background, calendarBackground: this.props.theme.colors.background,
textSectionTitleColor: props.theme.colors.agendaDayTextColor, textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: props.theme.colors.primary, selectedDayBackgroundColor: this.props.theme.colors.primary,
selectedDayTextColor: '#ffffff', selectedDayTextColor: '#ffffff',
todayTextColor: props.theme.colors.text, todayTextColor: this.props.theme.colors.text,
dayTextColor: props.theme.colors.text, dayTextColor: this.props.theme.colors.text,
textDisabledColor: props.theme.colors.agendaDayTextColor, textDisabledColor: this.props.theme.colors.agendaDayTextColor,
dotColor: props.theme.colors.primary, dotColor: this.props.theme.colors.primary,
selectedDotColor: '#ffffff', selectedDotColor: '#ffffff',
arrowColor: props.theme.colors.primary, arrowColor: this.props.theme.colors.primary,
monthTextColor: props.theme.colors.text, monthTextColor: this.props.theme.colors.text,
indicatorColor: props.theme.colors.primary, indicatorColor: this.props.theme.colors.primary,
textDayFontFamily: 'monospace', textDayFontFamily: 'monospace',
textMonthFontFamily: 'monospace', textMonthFontFamily: 'monospace',
textDayHeaderFontFamily: 'monospace', textDayHeaderFontFamily: 'monospace',
@ -386,14 +379,15 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
height: 34, height: 34,
width: 34, width: 34,
alignItems: 'center', alignItems: 'center',
},
}, }
}
}} }}
style={{marginBottom: 50}} style={{marginBottom: 50}}
/> />
</CollapsibleScrollView> </CollapsibleScrollView>
<LoadingConfirmDialog <LoadingConfirmDialog
visible={state.dialogVisible} visible={this.state.dialogVisible}
onDismiss={this.onDialogDismiss} onDismiss={this.onDialogDismiss}
onAccept={this.onDialogAccept} onAccept={this.onDialogAccept}
title={i18n.t('screens.equipment.dialogTitle')} title={i18n.t('screens.equipment.dialogTitle')}
@ -402,40 +396,46 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
/> />
<ErrorDialog <ErrorDialog
visible={state.errorDialogVisible} visible={this.state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss} onDismiss={this.onErrorDialogDismiss}
errorCode={state.currentError} errorCode={this.state.currentError}
/> />
<Animatable.View <Animatable.View
ref={this.bookRef} ref={this.bookRef}
style={{ style={{
position: 'absolute', position: "absolute",
bottom: 0, bottom: 0,
left: 0, left: 0,
width: '100%', width: "100%",
flex: 1, flex: 1,
transform: [{translateY: 100}], transform: [
{translateY: 100},
]
}}> }}>
<Button <Button
icon="bookmark-check" icon="bookmark-check"
mode="contained" mode="contained"
onPress={this.showDialog} onPress={this.showDialog}
style={{ style={{
width: '80%', width: "80%",
flex: 1, flex: 1,
marginLeft: 'auto', marginLeft: "auto",
marginRight: 'auto', marginRight: "auto",
marginBottom: 20, marginBottom: 20,
borderRadius: 10, borderRadius: 10
}}> }}
>
{i18n.t('screens.equipment.bookButton')} {i18n.t('screens.equipment.bookButton')}
</Button> </Button>
</Animatable.View> </Animatable.View>
</View> </View>
);
} )
return null; } else
return <View/>;
} }
} }
export default withTheme(EquipmentRentScreen); export default withTheme(EquipmentRentScreen);

View file

@ -1,33 +1,27 @@
// @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 { import {Button, Card, HelperText, TextInput, withTheme} from 'react-native-paper';
Button, import ConnectionManager from "../../managers/ConnectionManager";
Card,
HelperText,
TextInput,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import ErrorDialog from "../../components/Dialogs/ErrorDialog";
import LinearGradient from 'react-native-linear-gradient'; import type {CustomTheme} from "../../managers/ThemeManager";
import ConnectionManager from '../../managers/ConnectionManager'; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import ErrorDialog from '../../components/Dialogs/ErrorDialog'; import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomThemeType} from '../../managers/ThemeManager'; import AvailableWebsites from "../../constants/AvailableWebsites";
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import AvailableWebsites from '../../constants/AvailableWebsites'; import MascotPopup from "../../components/Mascot/MascotPopup";
import {MASCOT_STYLE} from '../../components/Mascot/Mascot'; import LinearGradient from "react-native-linear-gradient";
import MascotPopup from '../../components/Mascot/MascotPopup'; import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {nextScreen: string}}, route: { params: { nextScreen: string } },
theme: CustomThemeType, theme: CustomTheme
}; }
type StateType = { type State = {
email: string, email: string,
password: string, password: string,
isEmailValidated: boolean, isEmailValidated: boolean,
@ -36,53 +30,17 @@ type StateType = {
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 = /^.+@.+\..+$/;
const styles = StyleSheet.create({ class LoginScreen extends React.Component<Props, State> {
container: {
flex: 1,
},
card: {
marginTop: 'auto',
marginBottom: 'auto',
},
header: {
fontSize: 36,
marginBottom: 48,
},
textInput: {},
btnContainer: {
marginTop: 5,
marginBottom: 10,
},
});
class LoginScreen extends React.Component<PropsType, StateType> { state = {
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: '', email: '',
password: '', password: '',
isEmailValidated: false, isEmailValidated: false,
@ -90,27 +48,139 @@ class LoginScreen extends React.Component<PropsType, StateType> {
loading: false, loading: false,
dialogVisible: false, dialogVisible: false,
dialogError: 0, dialogError: 0,
mascotDialogVisible: AsyncStorageManager.getBool( mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.loginShowBanner.key),
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 = () => { onScreenFocus = () => {
this.handleNavigationParams(); 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 * Navigates to the Amicale website screen with the reset password link as navigation parameters
*/ */
onResetPasswordClick = () => { onResetPasswordClick = () => this.props.navigation.navigate("website", {
const {navigation} = this.props;
navigation.navigate('website', {
host: AvailableWebsites.websites.AMICALE, host: AvailableWebsites.websites.AMICALE,
path: RESET_PASSWORD_PATH, path: RESET_PASSWORD_PATH,
title: i18n.t('screens.websites.amicale'), 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. * Called when the user input changes in the email or password field.
@ -141,7 +211,7 @@ class LoginScreen extends React.Component<PropsType, StateType> {
onEmailSubmit = () => { onEmailSubmit = () => {
if (this.passwordInputRef.current != null) if (this.passwordInputRef.current != null)
this.passwordInputRef.current.focus(); this.passwordInputRef.current.focus();
}; }
/** /**
* Called when the user clicks on login or finishes to type his password. * Called when the user clicks on login or finishes to type his password.
@ -151,11 +221,9 @@ class LoginScreen extends React.Component<PropsType, StateType> {
* *
*/ */
onSubmit = () => { onSubmit = () => {
const {email, password} = this.state;
if (this.shouldEnableLogin()) { if (this.shouldEnableLogin()) {
this.setState({loading: true}); this.setState({loading: true});
ConnectionManager.getInstance() ConnectionManager.getInstance().connect(this.state.email, this.state.password)
.connect(email, password)
.then(this.handleSuccess) .then(this.handleSuccess)
.catch(this.showErrorDialog) .catch(this.showErrorDialog)
.finally(() => { .finally(() => {
@ -169,48 +237,53 @@ class LoginScreen extends React.Component<PropsType, StateType> {
* *
* @returns {*} * @returns {*}
*/ */
getFormInput(): React.Node { getFormInput() {
const {email, password} = this.state;
return ( return (
<View> <View>
<TextInput <TextInput
label={i18n.t('screens.login.email')} label={i18n.t("screens.login.email")}
mode="outlined" mode='outlined'
value={email} value={this.state.email}
onChangeText={this.onEmailChange} onChangeText={this.onEmailChange}
onBlur={this.validateEmail} onBlur={this.validateEmail}
onSubmitEditing={this.onEmailSubmit} onSubmitEditing={this.onEmailSubmit}
error={this.shouldShowEmailError()} error={this.shouldShowEmailError()}
textContentType="emailAddress" textContentType={'emailAddress'}
autoCapitalize="none" autoCapitalize={'none'}
autoCompleteType="email" autoCompleteType={'email'}
autoCorrect={false} autoCorrect={false}
keyboardType="email-address" keyboardType={'email-address'}
returnKeyType="next" returnKeyType={'next'}
secureTextEntry={false} secureTextEntry={false}
/> />
<HelperText type="error" visible={this.shouldShowEmailError()}> <HelperText
{i18n.t('screens.login.emailError')} type="error"
visible={this.shouldShowEmailError()}
>
{i18n.t("screens.login.emailError")}
</HelperText> </HelperText>
<TextInput <TextInput
ref={this.passwordInputRef} ref={this.passwordInputRef}
label={i18n.t('screens.login.password')} label={i18n.t("screens.login.password")}
mode="outlined" mode='outlined'
value={password} value={this.state.password}
onChangeText={this.onPasswordChange} onChangeText={this.onPasswordChange}
onBlur={this.validatePassword} onBlur={this.validatePassword}
onSubmitEditing={this.onSubmit} onSubmitEditing={this.onSubmit}
error={this.shouldShowPasswordError()} error={this.shouldShowPasswordError()}
textContentType="password" textContentType={'password'}
autoCapitalize="none" autoCapitalize={'none'}
autoCompleteType="password" autoCompleteType={'password'}
autoCorrect={false} autoCorrect={false}
keyboardType="default" keyboardType={'default'}
returnKeyType="done" returnKeyType={'done'}
secureTextEntry secureTextEntry={true}
/> />
<HelperText type="error" visible={this.shouldShowPasswordError()}> <HelperText
{i18n.t('screens.login.passwordError')} type="error"
visible={this.shouldShowPasswordError()}
>
{i18n.t("screens.login.passwordError")}
</HelperText> </HelperText>
</View> </View>
); );
@ -220,45 +293,43 @@ class LoginScreen extends React.Component<PropsType, StateType> {
* Gets the card containing the input form * Gets the card containing the input form
* @returns {*} * @returns {*}
*/ */
getMainCard(): React.Node { getMainCard() {
const {props, state} = this;
return ( return (
<View style={styles.card}> <View style={styles.card}>
<Card.Title <Card.Title
title={i18n.t('screens.login.title')} title={i18n.t("screens.login.title")}
titleStyle={{color: '#fff'}} titleStyle={{color: "#fff"}}
subtitle={i18n.t('screens.login.subtitle')} subtitle={i18n.t("screens.login.subtitle")}
subtitleStyle={{color: '#fff'}} subtitleStyle={{color: "#fff"}}
left={({size}: {size: number}): React.Node => ( left={(props) => <Image
<Image {...props}
source={ICON_AMICALE} source={ICON_AMICALE}
style={{ style={{
width: size, width: props.size,
height: size, height: props.size,
}} }}/>}
/>
)}
/> />
<Card.Content> <Card.Content>
{this.getFormInput()} {this.getFormInput()}
<Card.Actions style={{flexWrap: 'wrap'}}> <Card.Actions style={{flexWrap: "wrap"}}>
<Button <Button
icon="lock-question" icon="lock-question"
mode="contained" mode="contained"
onPress={this.onResetPasswordClick} onPress={this.onResetPasswordClick}
color={props.theme.colors.warning} color={this.props.theme.colors.warning}
style={{marginRight: 'auto', marginBottom: 20}}> style={{marginRight: 'auto', marginBottom: 20}}>
{i18n.t('screens.login.resetPassword')} {i18n.t("screens.login.resetPassword")}
</Button> </Button>
<Button <Button
icon="send" icon="send"
mode="contained" mode="contained"
disabled={!this.shouldEnableLogin()} disabled={!this.shouldEnableLogin()}
loading={state.loading} loading={this.state.loading}
onPress={this.onSubmit} onPress={this.onSubmit}
style={{marginLeft: 'auto'}}> style={{marginLeft: 'auto'}}>
{i18n.t('screens.login.title')} {i18n.t("screens.login.title")}
</Button> </Button>
</Card.Actions> </Card.Actions>
<Card.Actions> <Card.Actions>
<Button <Button
@ -269,7 +340,7 @@ class LoginScreen extends React.Component<PropsType, StateType> {
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}}> }}>
{i18n.t('screens.login.mascotDialog.title')} {i18n.t("screens.login.mascotDialog.title")}
</Button> </Button>
</Card.Actions> </Card.Actions>
</Card.Content> </Card.Content>
@ -277,164 +348,45 @@ class LoginScreen extends React.Component<PropsType, StateType> {
); );
} }
/** render() {
* 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 ( return (
<LinearGradient <LinearGradient
style={{ style={{
height: '100%', height: "100%"
}} }}
colors={['#9e0d18', '#530209']} colors={['#9e0d18', '#530209']}
start={{x: 0, y: 0.1}} start={{x: 0, y: 0.1}}
end={{x: 0.1, y: 1}}> end={{x: 0.1, y: 1}}>
<KeyboardAvoidingView <KeyboardAvoidingView
behavior="height" behavior={"height"}
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
style={styles.container} style={styles.container}
enabled enabled
keyboardVerticalOffset={100}> keyboardVerticalOffset={100}
>
<CollapsibleScrollView> <CollapsibleScrollView>
<View style={{height: '100%'}}>{this.getMainCard()}</View> <View style={{height: "100%"}}>
{this.getMainCard()}
</View>
<MascotPopup <MascotPopup
visible={mascotDialogVisible} visible={this.state.mascotDialogVisible}
title={i18n.t('screens.login.mascotDialog.title')} title={i18n.t("screens.login.mascotDialog.title")}
message={i18n.t('screens.login.mascotDialog.message')} message={i18n.t("screens.login.mascotDialog.message")}
icon="help" icon={"help"}
buttons={{ buttons={{
action: null, action: null,
cancel: { cancel: {
message: i18n.t('screens.login.mascotDialog.button'), message: i18n.t("screens.login.mascotDialog.button"),
icon: 'check', icon: "check",
onPress: this.hideMascotDialog, onPress: this.hideMascotDialog,
}, }
}} }}
emotion={MASCOT_STYLE.NORMAL} emotion={MASCOT_STYLE.NORMAL}
/> />
<ErrorDialog <ErrorDialog
visible={dialogVisible} visible={this.state.dialogVisible}
onDismiss={this.hideErrorDialog} onDismiss={this.hideErrorDialog}
errorCode={dialogError} errorCode={this.state.dialogError}
/> />
</CollapsibleScrollView> </CollapsibleScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
@ -443,4 +395,23 @@ class LoginScreen extends React.Component<PropsType, StateType> {
} }
} }
const styles = StyleSheet.create({
container: {
flex: 1,
},
card: {
marginTop: 'auto',
marginBottom: 'auto',
},
header: {
fontSize: 36,
marginBottom: 48
},
textInput: {},
btnContainer: {
marginTop: 5,
marginBottom: 10,
}
});
export default withTheme(LoginScreen); export default withTheme(LoginScreen);

View file

@ -1,47 +1,31 @@
// @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 { import {Avatar, Button, Card, Divider, List, Paragraph, withTheme} from 'react-native-paper';
Avatar, import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
Button,
Card,
Divider,
List,
Paragraph,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import LogoutDialog from "../../components/Amicale/LogoutDialog";
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen'; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import LogoutDialog from '../../components/Amicale/LogoutDialog'; import type {cardList} from "../../components/Lists/CardList/CardList";
import MaterialHeaderButtons, { import CardList from "../../components/Lists/CardList/CardList";
Item, import {StackNavigationProp} from "@react-navigation/stack";
} from '../../components/Overrides/CustomHeaderButton'; import type {CustomTheme} from "../../managers/ThemeManager";
import CardList from '../../components/Lists/CardList/CardList'; import AvailableWebsites from "../../constants/AvailableWebsites";
import type {CustomThemeType} from '../../managers/ThemeManager'; import Mascot, {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import AvailableWebsites from '../../constants/AvailableWebsites'; import ServicesManager, {SERVICES_KEY} from "../../managers/ServicesManager";
import Mascot, {MASCOT_STYLE} from '../../components/Mascot/Mascot'; import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
import ServicesManager, {SERVICES_KEY} from '../../managers/ServicesManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type {ServiceItemType} from '../../managers/ServicesManager';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomThemeType, theme: CustomTheme,
}; }
type StateType = { type State = {
dialogVisible: boolean, dialogVisible: boolean,
}; }
type ClubType = { type ProfileData = {
id: number,
name: string,
is_manager: boolean,
};
type ProfileDataType = {
first_name: string, first_name: string,
last_name: string, last_name: string,
email: string, email: string,
@ -50,59 +34,55 @@ type ProfileDataType = {
branch: string, branch: string,
link: string, link: string,
validity: boolean, validity: boolean,
clubs: Array<ClubType>, clubs: Array<Club>,
}; }
type Club = {
id: number,
name: string,
is_manager: boolean,
}
const styles = StyleSheet.create({ class ProfileScreen extends React.Component<Props, State> {
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
editButton: {
marginLeft: 'auto',
},
});
class ProfileScreen extends React.Component<PropsType, StateType> { state = {
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, dialogVisible: false,
}; };
data: ProfileData;
flatListData: Array<{ id: string }>;
amicaleDataset: cardList;
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() { componentDidMount() {
const {navigation} = this.props; this.props.navigation.setOptions({
navigation.setOptions({
headerRight: this.getHeaderButton, headerRight: this.getHeaderButton,
}); });
} }
showDisconnectDialog = () => this.setState({dialogVisible: true});
hideDisconnectDialog = () => this.setState({dialogVisible: false});
/** /**
* Gets the logout header button * Gets the logout header button
* *
* @returns {*} * @returns {*}
*/ */
getHeaderButton = (): React.Node => ( getHeaderButton = () => <MaterialHeaderButtons>
<MaterialHeaderButtons> <Item title="logout" iconName="logout" onPress={this.showDisconnectDialog}/>
<Item </MaterialHeaderButtons>;
title="logout"
iconName="logout"
onPress={this.showDisconnectDialog}
/>
</MaterialHeaderButtons>
);
/** /**
* Gets the main screen component with the fetched data * Gets the main screen component with the fetched data
@ -110,12 +90,10 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* @param data The data fetched from the server * @param data The data fetched from the server
* @returns {*} * @returns {*}
*/ */
getScreen = (data: Array<ProfileDataType | null>): React.Node => { getScreen = (data: Array<{ [key: string]: any } | null>) => {
const {dialogVisible} = this.state; if (data[0] != null) {
const {navigation} = this.props; this.data = data[0];
// eslint-disable-next-line prefer-destructuring }
if (data[0] != null) this.data = data[0];
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<CollapsibleFlatList <CollapsibleFlatList
@ -123,15 +101,15 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
data={this.flatListData} data={this.flatListData}
/> />
<LogoutDialog <LogoutDialog
navigation={navigation} {...this.props}
visible={dialogVisible} visible={this.state.dialogVisible}
onDismiss={this.hideDisconnectDialog} onDismiss={this.hideDisconnectDialog}
/> />
</View> </View>
); )
}; };
getRenderItem = ({item}: {item: {id: string}}): React.Node => { getRenderItem = ({item}: { item: { id: string } }) => {
switch (item.id) { switch (item.id) {
case '0': case '0':
return this.getWelcomeCard(); return this.getWelcomeCard();
@ -149,8 +127,13 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* *
* @returns {*} * @returns {*}
*/ */
getServicesList(): React.Node { getServicesList() {
return <CardList dataset={this.amicaleDataset} isHorizontal />; return (
<CardList
dataset={this.amicaleDataset}
isHorizontal={true}
/>
);
} }
/** /**
@ -158,44 +141,42 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* *
* @returns {*} * @returns {*}
*/ */
getWelcomeCard(): React.Node { getWelcomeCard() {
const {navigation} = this.props;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
title={i18n.t('screens.profile.welcomeTitle', { title={i18n.t("screens.profile.welcomeTitle", {name: this.data.first_name})}
name: this.data.first_name, left={() =>
})}
left={(): React.Node => (
<Mascot <Mascot
style={{ style={{
width: 60, width: 60
}} }}
emotion={MASCOT_STYLE.COOL} emotion={MASCOT_STYLE.COOL}
animated animated={true}
entryAnimation={{ entryAnimation={{
animation: 'bounceIn', animation: "bounceIn",
duration: 1000, duration: 1000
}} }}
/> />}
)}
titleStyle={{marginLeft: 10}} titleStyle={{marginLeft: 10}}
/> />
<Card.Content> <Card.Content>
<Divider /> <Divider/>
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph> <Paragraph>
{i18n.t("screens.profile.welcomeDescription")}
</Paragraph>
{this.getServicesList()} {this.getServicesList()}
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph> <Paragraph>
<Divider /> {i18n.t("screens.profile.welcomeFeedback")}
</Paragraph>
<Divider/>
<Card.Actions> <Card.Actions>
<Button <Button
icon="bug" icon="bug"
mode="contained" mode="contained"
onPress={() => { onPress={() => this.props.navigation.navigate('feedback')}
navigation.navigate('feedback');
}}
style={styles.editButton}> style={styles.editButton}>
{i18n.t('screens.feedback.homeButtonTitle')} {i18n.t("screens.feedback.homeButtonTitle")}
</Button> </Button>
</Card.Actions> </Card.Actions>
</Card.Content> </Card.Content>
@ -203,6 +184,16 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
); );
} }
/**
* 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. * Gets the given field value.
* If the field does not have a value, returns a placeholder text * If the field does not have a value, returns a placeholder text
@ -210,8 +201,10 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* @param field The field to get the value from * @param field The field to get the value from
* @return {*} * @return {*}
*/ */
static getFieldValue(field: ?string): string { getFieldValue(field: ?string) {
return field != null ? field : i18n.t('screens.profile.noData'); return this.isFieldAvailable(field)
? field
: i18n.t("screens.profile.noData");
} }
/** /**
@ -221,21 +214,18 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* @param icon The icon to use * @param icon The icon to use
* @return {*} * @return {*}
*/ */
getPersonalListItem(field: ?string, icon: string): React.Node { getPersonalListItem(field: ?string, icon: string) {
const {theme} = this.props; let title = this.isFieldAvailable(field) ? this.getFieldValue(field) : ':(';
const title = field != null ? ProfileScreen.getFieldValue(field) : ':('; let subtitle = this.isFieldAvailable(field) ? '' : this.getFieldValue(field);
const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
return ( return (
<List.Item <List.Item
title={title} title={title}
description={subtitle} description={subtitle}
left={({size}: {size: number}): React.Node => ( left={props => <List.Icon
<List.Icon {...props}
size={size}
icon={icon} icon={icon}
color={field != null ? null : theme.colors.textDisabled} color={this.isFieldAvailable(field) ? undefined : this.props.theme.colors.textDisabled}
/> />}
)}
/> />
); );
} }
@ -245,47 +235,40 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getPersonalCard(): React.Node { getPersonalCard() {
const {theme, navigation} = this.props;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
title={`${this.data.first_name} ${this.data.last_name}`} title={this.data.first_name + ' ' + this.data.last_name}
subtitle={this.data.email} subtitle={this.data.email}
left={({size}: {size: number}): React.Node => ( left={(props) => <Avatar.Icon
<Avatar.Icon {...props}
size={size}
icon="account" icon="account"
color={theme.colors.primary} color={this.props.theme.colors.primary}
style={styles.icon} style={styles.icon}
/> />}
)}
/> />
<Card.Content> <Card.Content>
<Divider /> <Divider/>
<List.Section> <List.Section>
<List.Subheader> <List.Subheader>{i18n.t("screens.profile.personalInformation")}</List.Subheader>
{i18n.t('screens.profile.personalInformation')} {this.getPersonalListItem(this.data.birthday, "cake-variant")}
</List.Subheader> {this.getPersonalListItem(this.data.phone, "phone")}
{this.getPersonalListItem(this.data.birthday, 'cake-variant')} {this.getPersonalListItem(this.data.email, "email")}
{this.getPersonalListItem(this.data.phone, 'phone')} {this.getPersonalListItem(this.data.branch, "school")}
{this.getPersonalListItem(this.data.email, 'email')}
{this.getPersonalListItem(this.data.branch, 'school')}
</List.Section> </List.Section>
<Divider /> <Divider/>
<Card.Actions> <Card.Actions>
<Button <Button
icon="account-edit" icon="account-edit"
mode="contained" mode="contained"
onPress={() => { onPress={() => this.props.navigation.navigate("website", {
navigation.navigate('website', {
host: AvailableWebsites.websites.AMICALE, host: AvailableWebsites.websites.AMICALE,
path: this.data.link, path: this.data.link,
title: i18n.t('screens.websites.amicale'), title: i18n.t('screens.websites.amicale')
}); })}
}}
style={styles.editButton}> style={styles.editButton}>
{i18n.t('screens.profile.editInformation')} {i18n.t("screens.profile.editInformation")}
</Button> </Button>
</Card.Actions> </Card.Actions>
</Card.Content> </Card.Content>
@ -298,24 +281,21 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getClubCard(): React.Node { getClubCard() {
const {theme} = this.props;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
title={i18n.t('screens.profile.clubs')} title={i18n.t("screens.profile.clubs")}
subtitle={i18n.t('screens.profile.clubsSubtitle')} subtitle={i18n.t("screens.profile.clubsSubtitle")}
left={({size}: {size: number}): React.Node => ( left={(props) => <Avatar.Icon
<Avatar.Icon {...props}
size={size}
icon="account-group" icon="account-group"
color={theme.colors.primary} color={this.props.theme.colors.primary}
style={styles.icon} style={styles.icon}
/> />}
)}
/> />
<Card.Content> <Card.Content>
<Divider /> <Divider/>
{this.getClubList(this.data.clubs)} {this.getClubList(this.data.clubs)}
</Card.Content> </Card.Content>
</Card> </Card>
@ -327,21 +307,18 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getMembershipCar(): React.Node { getMembershipCar() {
const {theme} = this.props;
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
title={i18n.t('screens.profile.membership')} title={i18n.t("screens.profile.membership")}
subtitle={i18n.t('screens.profile.membershipSubtitle')} subtitle={i18n.t("screens.profile.membershipSubtitle")}
left={({size}: {size: number}): React.Node => ( left={(props) => <Avatar.Icon
<Avatar.Icon {...props}
size={size}
icon="credit-card" icon="credit-card"
color={theme.colors.primary} color={this.props.theme.colors.primary}
style={styles.icon} style={styles.icon}
/> />}
)}
/> />
<Card.Content> <Card.Content>
<List.Section> <List.Section>
@ -357,106 +334,81 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
* *
* @return {*} * @return {*}
*/ */
getMembershipItem(state: boolean): React.Node { getMembershipItem(state: boolean) {
const {theme} = this.props;
return ( return (
<List.Item <List.Item
title={ title={state ? i18n.t("screens.profile.membershipPayed") : i18n.t("screens.profile.membershipNotPayed")}
state left={props => <List.Icon
? i18n.t('screens.profile.membershipPayed') {...props}
: i18n.t('screens.profile.membershipNotPayed') color={state ? this.props.theme.colors.success : this.props.theme.colors.danger}
}
left={({size}: {size: number}): React.Node => (
<List.Icon
size={size}
color={state ? theme.colors.success : theme.colors.danger}
icon={state ? 'check' : 'close'} 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 * Gets a list item for the club list
* *
* @param item The club to render * @param item The club to render
* @return {*} * @return {*}
*/ */
getClubListItem = ({item}: {item: ClubType}): React.Node => { clubListItem = ({item}: { item: Club }) => {
const {theme} = this.props; const onPress = () => this.openClubDetailsScreen(item.id);
const onPress = () => { let description = i18n.t("screens.profile.isMember");
this.openClubDetailsScreen(item.id); let icon = (props) => <List.Icon {...props} icon="chevron-right"/>;
};
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) { if (item.is_manager) {
description = i18n.t('screens.profile.isManager'); description = i18n.t("screens.profile.isManager");
icon = ({size}: {size: number}): React.Node => ( icon = (props) => <List.Icon {...props} icon="star" color={this.props.theme.colors.primary}/>;
<List.Icon size={size} icon="star" color={theme.colors.primary} />
);
} }
return ( return <List.Item
<List.Item
title={item.name} title={item.name}
description={description} description={description}
left={icon} left={icon}
onPress={onPress} 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 * Renders the list of clubs the user is part of
* *
* @param list The club list * @param list The club list
* @return {*} * @return {*}
*/ */
getClubList(list: Array<ClubType>): React.Node { getClubList(list: Array<Club>) {
list.sort(this.sortClubList); list.sort(this.sortClubList);
return ( return (
//$FlowFixMe
<FlatList <FlatList
renderItem={this.getClubListItem} renderItem={this.clubListItem}
keyExtractor={this.clubKeyExtractor} keyExtractor={this.clubKeyExtractor}
data={list} data={list}
/> />
); );
} }
clubKeyExtractor = (item: ClubType): string => item.name; render() {
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 ( return (
<AuthenticatedScreen <AuthenticatedScreen
navigation={navigation} {...this.props}
requests={[ requests={[
{ {
link: 'user/profile', link: 'user/profile',
params: {}, params: {},
mandatory: true, mandatory: true,
}, }
]} ]}
renderFunction={this.getScreen} renderFunction={this.getScreen}
/> />
@ -464,4 +416,17 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
} }
} }
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent'
},
editButton: {
marginLeft: 'auto'
}
});
export default withTheme(ProfileScreen); export default withTheme(ProfileScreen);

View file

@ -1,47 +1,46 @@
// @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 {StackNavigationProp} from '@react-navigation/stack'; import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
import i18n from 'i18n-js'; import {getTimeOnlyString, stringToDate} from "../../utils/Planning";
import {Button} from 'react-native-paper'; import VoteTease from "../../components/Amicale/Vote/VoteTease";
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen'; import VoteSelect from "../../components/Amicale/Vote/VoteSelect";
import {getTimeOnlyString, stringToDate} from '../../utils/Planning'; import VoteResults from "../../components/Amicale/Vote/VoteResults";
import VoteTease from '../../components/Amicale/Vote/VoteTease'; import VoteWait from "../../components/Amicale/Vote/VoteWait";
import VoteSelect from '../../components/Amicale/Vote/VoteSelect'; import {StackNavigationProp} from "@react-navigation/stack";
import VoteResults from '../../components/Amicale/Vote/VoteResults'; import i18n from "i18n-js";
import VoteWait from '../../components/Amicale/Vote/VoteWait'; import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import {MASCOT_STYLE} from '../../components/Mascot/Mascot'; import MascotPopup from "../../components/Mascot/MascotPopup";
import MascotPopup from '../../components/Mascot/MascotPopup'; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import AsyncStorageManager from '../../managers/AsyncStorageManager'; import {Button} from "react-native-paper";
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 VoteTeamType = { export type team = {
id: number, id: number,
name: string, name: string,
votes: number, votes: number,
}; }
type TeamResponseType = { type teamResponse = {
has_voted: boolean, has_voted: boolean,
teams: Array<VoteTeamType>, teams: Array<team>,
}; };
type VoteDatesStringType = { type stringVoteDates = {
date_begin: string, date_begin: string,
date_end: string, date_end: string,
date_result_begin: string, date_result_begin: string,
date_result_end: string, date_result_end: string,
}; }
type VoteDatesObjectType = { type objectVoteDates = {
date_begin: Date, date_begin: Date,
date_end: Date, date_end: Date,
date_result_begin: Date, date_result_begin: Date,
date_result_end: 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",
@ -93,48 +92,84 @@ type VoteDatesObjectType = {
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp
}; }
type StateType = { type State = {
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<PropsType, StateType> { export default class VoteScreen extends React.Component<Props, State> {
teams: Array<VoteTeamType>;
state = {
hasVoted: false,
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.voteShowBanner.key),
};
teams: Array<team>;
hasVoted: boolean; hasVoted: boolean;
datesString: null | stringVoteDates;
datesString: null | VoteDatesStringType; dates: null | objectVoteDates;
dates: null | VoteDatesObjectType;
today: Date; today: Date;
mainFlatListData: Array<{key: string}>; mainFlatListData: Array<{ key: string }>;
lastRefresh: Date | null; lastRefresh: Date | null;
authRef: {current: null | AuthenticatedScreen}; authRef: { current: null | AuthenticatedScreen };
constructor() { constructor() {
super(); super();
this.state = {
hasVoted: false,
mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.voteShowBanner.key,
),
};
this.hasVoted = false; this.hasVoted = false;
this.today = new Date(); this.today = new Date();
this.authRef = React.createRef(); this.authRef = React.createRef();
this.lastRefresh = null; this.lastRefresh = null;
this.mainFlatListData = [{key: 'main'}, {key: 'info'}]; this.mainFlatListData = [
{key: 'main'},
{key: 'info'},
]
}
/**
* Reloads vote data if last refresh delta is smaller than the minimum refresh time
*/
reloadData = () => {
let canRefresh;
const lastRefresh = this.lastRefresh;
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;
} }
/** /**
@ -150,43 +185,60 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
getDateString(date: Date, dateString: string): string { getDateString(date: Date, dateString: string): string {
if (this.today.getDate() === date.getDate()) { if (this.today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString); const str = getTimeOnlyString(dateString);
return str != null ? str : ''; return str != null ? str : "";
} } else
return dateString; return dateString;
} }
getMainRenderItem = ({item}: {item: {key: string}}): React.Node => { 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') if (item.key === 'info')
return ( return (
<View> <View>
<Button <Button
mode="contained" mode={"contained"}
icon="help-circle" icon={"help-circle"}
onPress={this.showMascotDialog} onPress={this.showMascotDialog}
style={{ style={{
marginLeft: 'auto', marginLeft: "auto",
marginRight: 'auto', marginRight: "auto",
marginTop: 20, marginTop: 20
}}> }}>
{i18n.t('screens.vote.mascotDialog.title')} {i18n.t("screens.vote.mascotDialog.title")}
</Button> </Button>
</View> </View>
); );
else
return this.getContent(); return this.getContent();
}; };
getScreen = (data: Array<ApiGenericDataType | null>): React.Node => { getScreen = (data: Array<{ [key: string]: any } | null>) => {
const {state} = this;
// data[0] = FAKE_TEAMS2; // data[0] = FAKE_TEAMS2;
// data[1] = FAKE_DATE; // data[1] = FAKE_DATE;
this.lastRefresh = new Date(); this.lastRefresh = new Date();
const teams: TeamResponseType | null = data[0]; const teams: teamResponse | null = data[0];
const dateStrings: VoteDatesStringType | null = data[1]; const dateStrings: stringVoteDates | null = data[1];
if (dateStrings != null && dateStrings.date_begin == null) if (dateStrings != null && dateStrings.date_begin == null)
this.datesString = null; this.datesString = null;
else this.datesString = dateStrings; else
this.datesString = dateStrings;
if (teams != null) { if (teams != null) {
this.teams = teams.teams; this.teams = teams.teams;
@ -198,173 +250,86 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
<CollapsibleFlatList <CollapsibleFlatList
data={this.mainFlatListData} data={this.mainFlatListData}
refreshControl={ refreshControl={
<RefreshControl refreshing={false} onRefresh={this.reloadData} /> <RefreshControl
refreshing={false}
onRefresh={this.reloadData}
/>
} }
extraData={state.hasVoted.toString()} extraData={this.state.hasVoted.toString()}
renderItem={this.getMainRenderItem} renderItem={this.mainRenderItem}
/> />
); );
}; };
getContent(): React.Node { getContent() {
const {state} = this; if (!this.isVoteStarted())
if (!this.isVoteStarted()) return this.getTeaseVoteCard(); return this.getTeaseVoteCard();
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) else if (this.isVoteRunning() && (!this.hasVoted && !this.state.hasVoted))
return this.getVoteCard(); return this.getVoteCard();
if (!this.isResultStarted()) return this.getWaitVoteCard(); else if (!this.isResultStarted())
if (this.isResultRunning()) return this.getVoteResultCard(); return this.getWaitVoteCard();
return <VoteNotAvailable />; else if (this.isResultRunning())
return this.getVoteResultCard();
else
return <VoteNotAvailable/>;
} }
onVoteSuccess = (): void => this.setState({hasVoted: true}); onVoteSuccess = () => this.setState({hasVoted: true});
/** /**
* The user has not voted yet, and the votes are open * The user has not voted yet, and the votes are open
*/ */
getVoteCard(): React.Node { getVoteCard() {
return ( return <VoteSelect teams={this.teams} onVoteSuccess={this.onVoteSuccess} onVoteError={this.reloadData}/>;
<VoteSelect
teams={this.teams}
onVoteSuccess={this.onVoteSuccess}
onVoteError={this.reloadData}
/>
);
} }
/** /**
* Votes have ended, results can be displayed * Votes have ended, results can be displayed
*/ */
getVoteResultCard(): React.Node { getVoteResultCard() {
if (this.dates != null && this.datesString != null) if (this.dates != null && this.datesString != null)
return ( return <VoteResults
<VoteResults
teams={this.teams} teams={this.teams}
dateEnd={this.getDateString( dateEnd={this.getDateString(
this.dates.date_result_end, this.dates.date_result_end,
this.datesString.date_result_end, this.datesString.date_result_end)}
)} />;
/> else
); return <VoteNotAvailable/>;
return <VoteNotAvailable />;
} }
/** /**
* Vote will open shortly * Vote will open shortly
*/ */
getTeaseVoteCard(): React.Node { getTeaseVoteCard() {
if (this.dates != null && this.datesString != null) if (this.dates != null && this.datesString != null)
return ( return <VoteTease
<VoteTease startDate={this.getDateString(this.dates.date_begin, this.datesString.date_begin)}/>;
startDate={this.getDateString( else
this.dates.date_begin, return <VoteNotAvailable/>;
this.datesString.date_begin,
)}
/>
);
return <VoteNotAvailable />;
} }
/** /**
* Votes have ended, or user has voted waiting for results * Votes have ended, or user has voted waiting for results
*/ */
getWaitVoteCard(): React.Node { getWaitVoteCard() {
const {state} = this;
let startDate = null; let startDate = null;
if ( if (this.dates != null && this.datesString != null && this.dates.date_result_begin != null)
this.dates != null && startDate = this.getDateString(this.dates.date_result_begin, this.datesString.date_result_begin);
this.datesString != null && return <VoteWait startDate={startDate} hasVoted={this.hasVoted || this.state.hasVoted}
this.dates.date_result_begin != null justVoted={this.state.hasVoted}
) isVoteRunning={this.isVoteRunning()}/>;
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()}
/>
);
} }
/**
* Reloads vote data if last refresh delta is smaller than the minimum refresh time
*/
reloadData = () => {
let canRefresh;
const {lastRefresh} = this;
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();
};
showMascotDialog = () => { showMascotDialog = () => {
this.setState({mascotDialogVisible: true}); this.setState({mascotDialogVisible: true})
}; };
hideMascotDialog = () => { hideMascotDialog = () => {
AsyncStorageManager.set( AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.voteShowBanner.key, false);
AsyncStorageManager.PREFERENCES.voteShowBanner.key, this.setState({mascotDialogVisible: false})
false,
);
this.setState({mascotDialogVisible: false});
}; };
isVoteStarted(): boolean {
return this.dates != null && this.today > this.dates.date_begin;
}
isResultRunning(): boolean {
return (
this.dates != null &&
this.today > this.dates.date_result_begin &&
this.today < this.dates.date_result_end
);
}
isResultStarted(): boolean {
return this.dates != null && this.today > this.dates.date_result_begin;
}
isVoteRunning(): boolean {
return (
this.dates != null &&
this.today > this.dates.date_begin &&
this.today < this.dates.date_end
);
}
/**
* 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;
}
/** /**
* Renders the authenticated screen. * Renders the authenticated screen.
* *
@ -372,12 +337,11 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
* *
* @returns {*} * @returns {*}
*/ */
render(): React.Node { render() {
const {props, state} = this;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<AuthenticatedScreen <AuthenticatedScreen
navigation={props.navigation} {...this.props}
ref={this.authRef} ref={this.authRef}
requests={[ requests={[
{ {
@ -394,17 +358,17 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
renderFunction={this.getScreen} renderFunction={this.getScreen}
/> />
<MascotPopup <MascotPopup
visible={state.mascotDialogVisible} visible={this.state.mascotDialogVisible}
title={i18n.t('screens.vote.mascotDialog.title')} title={i18n.t("screens.vote.mascotDialog.title")}
message={i18n.t('screens.vote.mascotDialog.message')} message={i18n.t("screens.vote.mascotDialog.message")}
icon="vote" icon={"vote"}
buttons={{ buttons={{
action: null, action: null,
cancel: { cancel: {
message: i18n.t('screens.vote.mascotDialog.button'), message: i18n.t("screens.vote.mascotDialog.button"),
icon: 'check', icon: "check",
onPress: this.hideMascotDialog, onPress: this.hideMascotDialog,
}, }
}} }}
emotion={MASCOT_STYLE.CUTE} emotion={MASCOT_STYLE.CUTE}
/> />

View file

@ -1,13 +1,13 @@
// @flow // @flow
import type {CustomThemeType} from '../../../managers/ThemeManager'; import type {CustomTheme} from "../../../managers/ThemeManager";
export type CoordinatesType = { export type Coordinates = {
x: number, x: number,
y: number, y: number,
}; }
export type ShapeType = Array<Array<number>>; type Shape = Array<Array<number>>;
/** /**
* Abstract class used to represent a BaseShape. * Abstract class used to represent a BaseShape.
@ -15,18 +15,16 @@ export type ShapeType = 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;
position: CoordinatesType; theme: CustomTheme;
theme: CustomThemeType;
/** /**
* Prevent instantiation if classname is BaseShape to force class to be abstract * Prevent instantiation if classname is BaseShape to force class to be abstract
*/ */
constructor(theme: CustomThemeType) { constructor(theme: CustomTheme) {
if (this.constructor === BaseShape) if (this.constructor === BaseShape)
throw new Error("Abstract class can't be instantiated"); throw new Error("Abstract class can't be instantiated");
this.theme = theme; this.theme = theme;
@ -39,7 +37,6 @@ export default class BaseShape {
* Gets this shape's color. * Gets this shape's color.
* Must be implemented by child class * Must be implemented by child class
*/ */
// eslint-disable-next-line class-methods-use-this
getColor(): string { getColor(): string {
throw new Error("Method 'getColor()' must be implemented"); throw new Error("Method 'getColor()' must be implemented");
} }
@ -50,15 +47,14 @@ export default class BaseShape {
* *
* Used by tests to read private fields * Used by tests to read private fields
*/ */
// eslint-disable-next-line class-methods-use-this getShapes(): Array<Shape> {
getShapes(): Array<ShapeType> {
throw new Error("Method 'getShapes()' must be implemented"); throw new Error("Method 'getShapes()' must be implemented");
} }
/** /**
* Gets this object's current shape. * Gets this object's current shape.
*/ */
getCurrentShape(): ShapeType { getCurrentShape(): Shape {
return this.#currentShape; return this.#currentShape;
} }
@ -67,20 +63,17 @@ export default class BaseShape {
* This will return an array of coordinates representing the positions of the cells used by this object. * 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? * @param isAbsolute Should we take into account the current position of the object?
* @return {Array<CoordinatesType>} This object cells coordinates * @return {Array<Coordinates>} This object cells coordinates
*/ */
getCellsCoordinates(isAbsolute: boolean): Array<CoordinatesType> { getCellsCoordinates(isAbsolute: boolean): Array<Coordinates> {
const coordinates = []; let coordinates = [];
for (let row = 0; row < this.#currentShape.length; row += 1) { for (let row = 0; row < this.#currentShape.length; row++) {
for (let col = 0; col < this.#currentShape[row].length; col += 1) { for (let col = 0; col < this.#currentShape[row].length; col++) {
if (this.#currentShape[row][col] === 1) { if (this.#currentShape[row][col] === 1)
if (isAbsolute) { if (isAbsolute)
coordinates.push({ coordinates.push({x: this.position.x + col, y: this.position.y + row});
x: this.position.x + col, else
y: this.position.y + row, coordinates.push({x: col, y: row});
});
} else coordinates.push({x: col, y: row});
}
} }
} }
return coordinates; return coordinates;
@ -92,10 +85,14 @@ export default class BaseShape {
* @param isForward Should we rotate clockwise? * @param isForward Should we rotate clockwise?
*/ */
rotate(isForward: boolean) { rotate(isForward: boolean) {
if (isForward) this.#rotation += 1; if (isForward)
else this.#rotation -= 1; this.#rotation++;
if (this.#rotation > 3) this.#rotation = 0; else
else if (this.#rotation < 0) this.#rotation = 3; this.#rotation--;
if (this.#rotation > 3)
this.#rotation = 0;
else if (this.#rotation < 0)
this.#rotation = 3;
this.#currentShape = this.getShapes()[this.#rotation]; this.#currentShape = this.getShapes()[this.#rotation];
} }
@ -109,4 +106,5 @@ export default class BaseShape {
this.position.x += x; this.position.x += x;
this.position.y += y; this.position.y += y;
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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