Compare commits
45 commits
05ef28f3b5
...
b378473591
| Author | SHA1 | Date | |
|---|---|---|---|
| b378473591 | |||
| 327488a470 | |||
| eef6f75414 | |||
| 5166e0b879 | |||
| 98770611ff | |||
| ee9e225dae | |||
| 05f769fe79 | |||
| 1be913c5aa | |||
| c86281cbd2 | |||
| 4cc9c61d72 | |||
| 1e81b2cd7b | |||
| cbe3777957 | |||
| 569e659779 | |||
| fcbc70956b | |||
| 3ce23726c2 | |||
| a3299c19f7 | |||
| 0a64f5fcd7 | |||
| 483970c9a8 | |||
| 3e4f2f4ac1 | |||
| 7107a8eadf | |||
| 7ac62b99f4 | |||
| aa992d20b2 | |||
| 0117b25cd8 | |||
| 4db4516296 | |||
| 7b94afadcc | |||
| 1cc0802c12 | |||
| 547af66977 | |||
| ab86c1c85c | |||
| 11b5f2ac71 | |||
| 70365136ac | |||
| 93d12b27f8 | |||
| 33d98b024b | |||
| 6b12b4cde2 | |||
| 34ccf9c4c9 | |||
| 9d92a88627 | |||
| 925bded69b | |||
| 3629c5730a | |||
| 3d9bfdea4c | |||
| 142b861ccb | |||
| 0a9e0eb0ca | |||
| 9fc02baf6d | |||
| 22eabf28d5 | |||
| c0777511a6 | |||
| be1f61b671 | |||
| b596f68abe |
145 changed files with 18112 additions and 16502 deletions
|
|
@ -13,6 +13,8 @@ module.exports = {
|
||||||
jest: true,
|
jest: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
'react/jsx-filename-extension': [1, {extensions: ['.js', '.jsx']}],
|
||||||
|
'react/static-property-placement': [2, 'static public field'],
|
||||||
'flowtype/define-flow-type': 1,
|
'flowtype/define-flow-type': 1,
|
||||||
'flowtype/no-mixed': 2,
|
'flowtype/no-mixed': 2,
|
||||||
'flowtype/no-primitive-constructor-types': 2,
|
'flowtype/no-primitive-constructor-types': 2,
|
||||||
|
|
@ -37,4 +39,8 @@ module.exports = {
|
||||||
onlyFilesWithFlowAnnotation: false,
|
onlyFilesWithFlowAnnotation: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
globals: {
|
||||||
|
fetch: false,
|
||||||
|
Headers: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="Expo" type="ReactNative" factoryName="React Native">
|
|
||||||
<node-interpreter value="project" />
|
|
||||||
<react-native value="$USER_HOME$/.nvm/versions/node/v12.4.0/lib/node_modules/react-native-cli" />
|
|
||||||
<platform value="ANDROID" />
|
|
||||||
<envs />
|
|
||||||
<only-packager />
|
|
||||||
<build-and-launch value="false" />
|
|
||||||
<browser value="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3" />
|
|
||||||
<debug-host value="127.0.0.1" />
|
|
||||||
<debug-port value="19001" />
|
|
||||||
<method v="2">
|
|
||||||
<option name="ReactNativePackager" enabled="true" />
|
|
||||||
</method>
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
||||||
207
App.js
207
App.js
|
|
@ -1,64 +1,61 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {LogBox, Platform, SafeAreaView, StatusBar, View} from 'react-native';
|
import {LogBox, Platform, SafeAreaView, View} from 'react-native';
|
||||||
import LocaleManager from './src/managers/LocaleManager';
|
|
||||||
import AsyncStorageManager from "./src/managers/AsyncStorageManager";
|
|
||||||
import CustomIntroSlider from "./src/components/Overrides/CustomIntroSlider";
|
|
||||||
import type {CustomTheme} from "./src/managers/ThemeManager";
|
|
||||||
import ThemeManager from './src/managers/ThemeManager';
|
|
||||||
import {NavigationContainer} from '@react-navigation/native';
|
import {NavigationContainer} from '@react-navigation/native';
|
||||||
import MainNavigator from './src/navigation/MainNavigator';
|
|
||||||
import {Provider as PaperProvider} from 'react-native-paper';
|
import {Provider as PaperProvider} from 'react-native-paper';
|
||||||
import AprilFoolsManager from "./src/managers/AprilFoolsManager";
|
import {setSafeBounceHeight} from 'react-navigation-collapsible';
|
||||||
import Update from "./src/constants/Update";
|
import SplashScreen from 'react-native-splash-screen';
|
||||||
import ConnectionManager from "./src/managers/ConnectionManager";
|
import {OverflowMenuProvider} from 'react-navigation-header-buttons';
|
||||||
import URLHandler from "./src/utils/URLHandler";
|
import LocaleManager from './src/managers/LocaleManager';
|
||||||
import {setSafeBounceHeight} from "react-navigation-collapsible";
|
import AsyncStorageManager from './src/managers/AsyncStorageManager';
|
||||||
import SplashScreen from 'react-native-splash-screen'
|
import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider';
|
||||||
import {OverflowMenuProvider} from "react-navigation-header-buttons";
|
import type {CustomThemeType} from './src/managers/ThemeManager';
|
||||||
|
import ThemeManager from './src/managers/ThemeManager';
|
||||||
|
import MainNavigator from './src/navigation/MainNavigator';
|
||||||
|
import AprilFoolsManager from './src/managers/AprilFoolsManager';
|
||||||
|
import Update from './src/constants/Update';
|
||||||
|
import ConnectionManager from './src/managers/ConnectionManager';
|
||||||
|
import type {ParsedUrlDataType} from './src/utils/URLHandler';
|
||||||
|
import URLHandler from './src/utils/URLHandler';
|
||||||
|
import {setupStatusBar} from './src/utils/Utils';
|
||||||
|
|
||||||
// Native optimizations https://reactnavigation.org/docs/react-native-screens
|
// Native optimizations https://reactnavigation.org/docs/react-native-screens
|
||||||
// Crashes app when navigating away from webview on android 9+
|
// Crashes app when navigating away from webview on android 9+
|
||||||
// enableScreens(true);
|
// enableScreens(true);
|
||||||
|
|
||||||
|
LogBox.ignoreLogs([
|
||||||
LogBox.ignoreLogs([ // collapsible headers cause this warning, just ignore as it is not an issue
|
// collapsible headers cause this warning, just ignore as it is not an issue
|
||||||
'Non-serializable values were found in the navigation state',
|
'Non-serializable values were found in the navigation state',
|
||||||
'Cannot update a component from inside the function body of a different component',
|
'Cannot update a component from inside the function body of a different component',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type Props = {};
|
type StateType = {
|
||||||
|
|
||||||
type State = {
|
|
||||||
isLoading: boolean,
|
isLoading: boolean,
|
||||||
showIntro: boolean,
|
showIntro: boolean,
|
||||||
showUpdate: boolean,
|
showUpdate: boolean,
|
||||||
showAprilFools: boolean,
|
showAprilFools: boolean,
|
||||||
currentTheme: CustomTheme | null,
|
currentTheme: CustomThemeType | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class App extends React.Component<Props, State> {
|
export default class App extends React.Component<null, StateType> {
|
||||||
|
navigatorRef: {current: null | NavigationContainer};
|
||||||
|
|
||||||
state = {
|
defaultHomeRoute: string | null;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
@ -66,7 +63,7 @@ export default class App extends React.Component<Props, State> {
|
||||||
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().then(() => {
|
this.loadAssetsAsync().finally(() => {
|
||||||
this.onLoadFinished();
|
this.onLoadFinished();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +74,7 @@ export default class App extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @param parsedData The data parsed from the url
|
* @param parsedData The data parsed from the url
|
||||||
*/
|
*/
|
||||||
onInitialURLParsed = (parsedData: { route: string, data: { [key: string]: any } }) => {
|
onInitialURLParsed = (parsedData: ParsedUrlDataType) => {
|
||||||
this.defaultHomeRoute = parsedData.route;
|
this.defaultHomeRoute = parsedData.route;
|
||||||
this.defaultHomeData = parsedData.data;
|
this.defaultHomeData = parsedData.data;
|
||||||
};
|
};
|
||||||
|
|
@ -88,12 +85,13 @@ export default class App extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @param parsedData The data parsed from the url
|
* @param parsedData The data parsed from the url
|
||||||
*/
|
*/
|
||||||
onDetectURL = (parsedData: { route: string, data: { [key: string]: any } }) => {
|
onDetectURL = (parsedData: ParsedUrlDataType) => {
|
||||||
// Navigate to nested navigator and pass data to the index screen
|
// Navigate to nested navigator and pass data to the index screen
|
||||||
if (this.navigatorRef.current != null) {
|
const nav = this.navigatorRef.current;
|
||||||
this.navigatorRef.current.navigate('home', {
|
if (nav != null) {
|
||||||
|
nav.navigate('home', {
|
||||||
screen: 'index',
|
screen: 'index',
|
||||||
params: {nextScreen: parsedData.route, data: parsedData.data}
|
params: {nextScreen: parsedData.route, data: parsedData.data},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -103,25 +101,11 @@ export default class App extends React.Component<Props, State> {
|
||||||
*/
|
*/
|
||||||
onUpdateTheme = () => {
|
onUpdateTheme = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentTheme: ThemeManager.getCurrentTheme()
|
currentTheme: ThemeManager.getCurrentTheme(),
|
||||||
});
|
});
|
||||||
this.setupStatusBar();
|
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
|
||||||
*/
|
*/
|
||||||
|
|
@ -131,11 +115,49 @@ export default class App extends React.Component<Props, State> {
|
||||||
showUpdate: false,
|
showUpdate: false,
|
||||||
showAprilFools: false,
|
showAprilFools: false,
|
||||||
});
|
});
|
||||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.showIntro.key, false);
|
AsyncStorageManager.set(
|
||||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.updateNumber.key, Update.number);
|
AsyncStorageManager.PREFERENCES.showIntro.key,
|
||||||
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key, false);
|
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
|
||||||
*
|
*
|
||||||
|
|
@ -143,58 +165,38 @@ export default class App extends React.Component<Props, State> {
|
||||||
*/
|
*/
|
||||||
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() {
|
render(): React.Node {
|
||||||
if (this.state.isLoading) {
|
const {state} = this;
|
||||||
|
if (state.isLoading) {
|
||||||
return null;
|
return null;
|
||||||
} else if (this.state.showIntro || this.state.showUpdate || this.state.showAprilFools) {
|
}
|
||||||
return <CustomIntroSlider
|
if (state.showIntro || state.showUpdate || state.showAprilFools) {
|
||||||
onDone={this.onIntroDone}
|
|
||||||
isUpdate={this.state.showUpdate && !this.state.showIntro}
|
|
||||||
isAprilFools={this.state.showAprilFools && !this.state.showIntro}
|
|
||||||
/>;
|
|
||||||
} else {
|
|
||||||
return (
|
return (
|
||||||
<PaperProvider theme={this.state.currentTheme}>
|
<CustomIntroSlider
|
||||||
|
onDone={this.onIntroDone}
|
||||||
|
isUpdate={state.showUpdate && !state.showIntro}
|
||||||
|
isAprilFools={state.showAprilFools && !state.showIntro}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<PaperProvider theme={state.currentTheme}>
|
||||||
<OverflowMenuProvider>
|
<OverflowMenuProvider>
|
||||||
<View style={{backgroundColor: ThemeManager.getCurrentTheme().colors.background, flex: 1}}>
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: ThemeManager.getCurrentTheme().colors.background,
|
||||||
|
flex: 1,
|
||||||
|
}}>
|
||||||
<SafeAreaView style={{flex: 1}}>
|
<SafeAreaView style={{flex: 1}}>
|
||||||
<NavigationContainer theme={this.state.currentTheme} ref={this.navigatorRef}>
|
<NavigationContainer
|
||||||
|
theme={state.currentTheme}
|
||||||
|
ref={this.navigatorRef}>
|
||||||
<MainNavigator
|
<MainNavigator
|
||||||
defaultHomeRoute={this.defaultHomeRoute}
|
defaultHomeRoute={this.defaultHomeRoute}
|
||||||
defaultHomeData={this.defaultHomeData}
|
defaultHomeData={this.defaultHomeData}
|
||||||
|
|
@ -207,4 +209,3 @@ export default class App extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
jest.mock('react-native-keychain');
|
/* eslint-disable */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ConnectionManager from "../../src/managers/ConnectionManager";
|
import ConnectionManager from '../../src/managers/ConnectionManager';
|
||||||
import {ERROR_TYPE} from "../../src/utils/WebData";
|
import {ERROR_TYPE} from '../../src/utils/WebData';
|
||||||
|
|
||||||
let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
jest.mock('react-native-keychain');
|
||||||
|
|
||||||
|
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||||
|
|
||||||
const c = ConnectionManager.getInstance();
|
const c = ConnectionManager.getInstance();
|
||||||
|
|
||||||
|
|
@ -13,132 +15,124 @@ afterEach(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isLoggedIn yes', () => {
|
test('isLoggedIn yes', () => {
|
||||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
jest
|
||||||
|
.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.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
jest
|
||||||
|
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
return expect(c.isLoggedIn()).toBe(false);
|
return expect(c.isLoggedIn()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isConnectionResponseValid", () => {
|
test('connect bad credentials', () => {
|
||||||
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'))
|
});
|
||||||
.rejects.toBe(ERROR_TYPE.BAD_CREDENTIALS);
|
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||||
|
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.toBeTruthy();
|
return expect(c.connect('email', 'password')).resolves.toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
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'))
|
});
|
||||||
.rejects.toBe(ERROR_TYPE.NO_CONSENT);
|
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||||
|
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(ERROR_TYPE.UNKNOWN);
|
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||||
|
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'))
|
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
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'))
|
});
|
||||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||||
|
ERROR_TYPE.SERVER_ERROR,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('authenticatedRequest success', () => {
|
||||||
test("authenticatedRequest success", () => {
|
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(() => {
|
||||||
|
|
@ -146,17 +140,20 @@ 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'))
|
});
|
||||||
.resolves.toStrictEqual({coucou: 'toi'});
|
return expect(
|
||||||
|
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||||
|
).resolves.toStrictEqual({coucou: 'toi'});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("authenticatedRequest error wrong token", () => {
|
test('authenticatedRequest error wrong token', () => {
|
||||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
jest
|
||||||
|
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
return 'token';
|
return 'token';
|
||||||
});
|
});
|
||||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||||
|
|
@ -164,17 +161,20 @@ 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'))
|
});
|
||||||
.rejects.toBe(ERROR_TYPE.BAD_TOKEN);
|
return expect(
|
||||||
|
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.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
jest
|
||||||
|
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
return 'token';
|
return 'token';
|
||||||
});
|
});
|
||||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||||
|
|
@ -184,27 +184,34 @@ 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'))
|
});
|
||||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
return expect(
|
||||||
|
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.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
jest
|
||||||
|
.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(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.CONNECTION_ERROR);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("authenticatedRequest error no token", () => {
|
test('authenticatedRequest error no token', () => {
|
||||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
jest
|
||||||
|
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
return expect(
|
||||||
.rejects.toBe(ERROR_TYPE.UNKNOWN);
|
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||||
|
).rejects.toBe(ERROR_TYPE.TOKEN_RETRIEVE);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,319 +1,345 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as EquipmentBooking from "../../src/utils/EquipmentBooking";
|
import * as EquipmentBooking from '../../src/utils/EquipmentBooking';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
test('getISODate', () => {
|
test('getISODate', () => {
|
||||||
let date = new Date("2020-03-05 12:00");
|
let date = new Date('2020-03-05 12:00');
|
||||||
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05");
|
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05');
|
||||||
date = new Date("2020-03-05");
|
date = new Date('2020-03-05');
|
||||||
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05");
|
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05');
|
||||||
date = new Date("2020-03-05 00:00"); // Treated as local time
|
date = new Date('2020-03-05 00:00'); // Treated as local time
|
||||||
expect(EquipmentBooking.getISODate(date)).toBe("2020-03-04"); // Treated as UTC
|
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-04'); // Treated as UTC
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getCurrentDay', () => {
|
test('getCurrentDay', () => {
|
||||||
jest.spyOn(Date, 'now')
|
jest
|
||||||
.mockImplementation(() =>
|
.spyOn(Date, 'now')
|
||||||
new Date('2020-01-14 14:50:35').getTime()
|
.mockImplementation(() => new Date('2020-01-14 14:50:35').getTime());
|
||||||
|
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(
|
||||||
|
new Date('2020-01-14').getTime(),
|
||||||
);
|
);
|
||||||
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(new Date("2020-01-14").getTime());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isEquipmentAvailable', () => {
|
test('isEquipmentAvailable', () => {
|
||||||
jest.spyOn(Date, 'now')
|
jest
|
||||||
.mockImplementation(() =>
|
.spyOn(Date, 'now')
|
||||||
new Date('2020-07-09').getTime()
|
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||||
);
|
|
||||||
let testDevice = {
|
let testDevice = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Petit barbecue",
|
name: 'Petit barbecue',
|
||||||
caution: 100,
|
caution: 100,
|
||||||
booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
|
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||||
};
|
};
|
||||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||||
|
|
||||||
testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}];
|
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
|
||||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||||
|
|
||||||
testDevice.booked_at = [{begin: "2020-07-09", end: "2020-07-10"}];
|
testDevice.booked_at = [{begin: '2020-07-09', end: '2020-07-10'}];
|
||||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||||
|
|
||||||
testDevice.booked_at = [
|
testDevice.booked_at = [
|
||||||
{begin: "2020-07-07", end: "2020-07-8"},
|
{begin: '2020-07-07', end: '2020-07-8'},
|
||||||
{begin: "2020-07-10", end: "2020-07-12"},
|
{begin: '2020-07-10', end: '2020-07-12'},
|
||||||
];
|
];
|
||||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
|
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getFirstEquipmentAvailability', () => {
|
test('getFirstEquipmentAvailability', () => {
|
||||||
jest.spyOn(Date, 'now')
|
jest
|
||||||
.mockImplementation(() =>
|
.spyOn(Date, 'now')
|
||||||
new Date('2020-07-09').getTime()
|
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||||
);
|
|
||||||
let testDevice = {
|
let testDevice = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Petit barbecue",
|
name: 'Petit barbecue',
|
||||||
caution: 100,
|
caution: 100,
|
||||||
booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
|
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||||
};
|
};
|
||||||
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-11").getTime());
|
expect(
|
||||||
testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}];
|
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||||
expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-10").getTime());
|
).toBe(new Date('2020-07-11').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(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-17").getTime());
|
expect(
|
||||||
|
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(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-13").getTime());
|
expect(
|
||||||
|
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||||
|
).toBe(new Date('2020-07-13').getTime());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getRelativeDateString', () => {
|
test('getRelativeDateString', () => {
|
||||||
jest.spyOn(Date, 'now')
|
jest
|
||||||
.mockImplementation(() =>
|
.spyOn(Date, 'now')
|
||||||
new Date('2020-07-09').getTime()
|
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||||
|
jest.spyOn(i18n, 't').mockImplementation((translationString: string) => {
|
||||||
|
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',
|
||||||
);
|
);
|
||||||
jest.spyOn(i18n, 't')
|
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-10'))).toBe(
|
||||||
.mockImplementation((translationString: string) => {
|
'3',
|
||||||
const prefix = "screens.equipment.";
|
);
|
||||||
if (translationString === prefix + "otherYear")
|
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-11'))).toBe(
|
||||||
return "0";
|
'2',
|
||||||
else if (translationString === prefix + "otherMonth")
|
);
|
||||||
return "1";
|
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-30'))).toBe(
|
||||||
else if (translationString === prefix + "thisMonth")
|
'2',
|
||||||
return "2";
|
);
|
||||||
else if (translationString === prefix + "tomorrow")
|
expect(EquipmentBooking.getRelativeDateString(new Date('2020-08-30'))).toBe(
|
||||||
return "3";
|
'1',
|
||||||
else if (translationString === prefix + "today")
|
);
|
||||||
return "4";
|
expect(EquipmentBooking.getRelativeDateString(new Date('2020-11-10'))).toBe(
|
||||||
else
|
'1',
|
||||||
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(result);
|
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||||
|
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 = [
|
result = ['2020-07-11', '2020-07-12'];
|
||||||
"2020-07-11",
|
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||||
"2020-07-12",
|
result,
|
||||||
];
|
);
|
||||||
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(result);
|
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||||
testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-12"},];
|
result,
|
||||||
result = [
|
);
|
||||||
"2020-07-13",
|
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-12'}];
|
||||||
"2020-07-14",
|
result = ['2020-07-13', '2020-07-14', '2020-07-15'];
|
||||||
"2020-07-15",
|
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||||
];
|
result,
|
||||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
|
);
|
||||||
start = new Date("2020-07-14");
|
start = new Date('2020-07-14');
|
||||||
end = new Date("2020-07-14");
|
end = new Date('2020-07-14');
|
||||||
result = [
|
result = ['2020-07-14'];
|
||||||
"2020-07-14",
|
expect(
|
||||||
];
|
EquipmentBooking.getValidRange(start, start, testDevice),
|
||||||
expect(EquipmentBooking.getValidRange(start, start, testDevice)).toStrictEqual(result);
|
).toStrictEqual(result);
|
||||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
|
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result);
|
result,
|
||||||
|
);
|
||||||
|
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
|
||||||
start = new Date("2020-07-14");
|
start = new Date('2020-07-14');
|
||||||
end = new Date("2020-07-17");
|
end = new Date('2020-07-17');
|
||||||
result = [
|
result = ['2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17'];
|
||||||
"2020-07-14",
|
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
|
||||||
"2020-07-15",
|
result,
|
||||||
"2020-07-16",
|
);
|
||||||
"2020-07-17",
|
|
||||||
];
|
|
||||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result);
|
|
||||||
|
|
||||||
testDevice.booked_at = [{begin: "2020-07-17", end: "2020-07-17"}];
|
testDevice.booked_at = [{begin: '2020-07-17', end: '2020-07-17'}];
|
||||||
result = [
|
result = ['2020-07-14', '2020-07-15', '2020-07-16'];
|
||||||
"2020-07-14",
|
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||||
"2020-07-15",
|
result,
|
||||||
"2020-07-16",
|
);
|
||||||
];
|
|
||||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
|
|
||||||
|
|
||||||
testDevice.booked_at = [
|
testDevice.booked_at = [
|
||||||
{begin: "2020-07-12", end: "2020-07-13"},
|
{begin: '2020-07-12', end: '2020-07-13'},
|
||||||
{begin: "2020-07-15", end: "2020-07-20"},
|
{begin: '2020-07-15', end: '2020-07-20'},
|
||||||
];
|
];
|
||||||
start = new Date("2020-07-11");
|
start = new Date('2020-07-11');
|
||||||
end = new Date("2020-07-23");
|
end = new Date('2020-07-23');
|
||||||
result = [
|
result = ['2020-07-21', '2020-07-22', '2020-07-23'];
|
||||||
"2020-07-21",
|
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||||
"2020-07-22",
|
result,
|
||||||
"2020-07-23",
|
);
|
||||||
];
|
|
||||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generateMarkedDates', () => {
|
test('generateMarkedDates', () => {
|
||||||
let theme = {
|
let theme = {
|
||||||
colors: {
|
colors: {
|
||||||
primary: "primary",
|
primary: 'primary',
|
||||||
danger: "primary",
|
danger: 'primary',
|
||||||
textDisabled: "primary",
|
textDisabled: 'primary',
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
let testDevice = {
|
let testDevice = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "Petit barbecue",
|
name: 'Petit barbecue',
|
||||||
caution: 100,
|
caution: 100,
|
||||||
booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
|
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||||
};
|
};
|
||||||
let start = new Date("2020-07-11");
|
let start = new Date('2020-07-11');
|
||||||
let end = new Date("2020-07-13");
|
let end = new Date('2020-07-13');
|
||||||
let range = EquipmentBooking.getValidRange(start, end, testDevice);
|
let range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||||
let result = {
|
let result = {
|
||||||
"2020-07-11": {
|
'2020-07-11': {
|
||||||
startingDay: true,
|
startingDay: true,
|
||||||
endingDay: false,
|
endingDay: false,
|
||||||
color: theme.colors.primary
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
"2020-07-12": {
|
'2020-07-12': {
|
||||||
startingDay: false,
|
startingDay: false,
|
||||||
endingDay: false,
|
endingDay: false,
|
||||||
color: theme.colors.danger
|
color: theme.colors.danger,
|
||||||
},
|
},
|
||||||
"2020-07-13": {
|
'2020-07-13': {
|
||||||
startingDay: false,
|
startingDay: false,
|
||||||
endingDay: true,
|
endingDay: true,
|
||||||
color: theme.colors.primary
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
expect(
|
||||||
|
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(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
|
expect(
|
||||||
|
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(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
|
expect(
|
||||||
|
EquipmentBooking.generateMarkedDates(false, theme, range),
|
||||||
|
).toStrictEqual(result);
|
||||||
|
|
||||||
testDevice.booked_at = [{begin: "2020-07-13", end: "2020-07-15"},];
|
testDevice.booked_at = [{begin: '2020-07-13', end: '2020-07-15'}];
|
||||||
result = {
|
result = {
|
||||||
"2020-07-11": {
|
'2020-07-11': {
|
||||||
startingDay: true,
|
startingDay: true,
|
||||||
endingDay: false,
|
endingDay: false,
|
||||||
color: theme.colors.primary
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
"2020-07-12": {
|
'2020-07-12': {
|
||||||
startingDay: false,
|
startingDay: false,
|
||||||
endingDay: true,
|
endingDay: true,
|
||||||
color: theme.colors.primary
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
expect(
|
||||||
|
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||||
|
).toStrictEqual(result);
|
||||||
|
|
||||||
testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"},];
|
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
|
||||||
result = {
|
result = {
|
||||||
"2020-07-11": {
|
'2020-07-11': {
|
||||||
startingDay: true,
|
startingDay: true,
|
||||||
endingDay: true,
|
endingDay: true,
|
||||||
color: theme.colors.primary
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
expect(
|
||||||
|
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||||
|
).toStrictEqual(result);
|
||||||
|
|
||||||
testDevice.booked_at = [
|
testDevice.booked_at = [
|
||||||
{begin: "2020-07-12", end: "2020-07-13"},
|
{begin: '2020-07-12', end: '2020-07-13'},
|
||||||
{begin: "2020-07-15", end: "2020-07-20"},
|
{begin: '2020-07-15', end: '2020-07-20'},
|
||||||
];
|
];
|
||||||
start = new Date("2020-07-11");
|
start = new Date('2020-07-11');
|
||||||
end = new Date("2020-07-23");
|
end = new Date('2020-07-23');
|
||||||
result = {
|
result = {
|
||||||
"2020-07-11": {
|
'2020-07-11': {
|
||||||
startingDay: true,
|
startingDay: true,
|
||||||
endingDay: true,
|
endingDay: true,
|
||||||
color: theme.colors.primary
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
expect(
|
||||||
|
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||||
|
).toStrictEqual(result);
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"2020-07-21": {
|
'2020-07-21': {
|
||||||
startingDay: true,
|
startingDay: true,
|
||||||
endingDay: false,
|
endingDay: false,
|
||||||
color: theme.colors.primary
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
"2020-07-22": {
|
'2020-07-22': {
|
||||||
startingDay: false,
|
startingDay: false,
|
||||||
endingDay: false,
|
endingDay: false,
|
||||||
color: theme.colors.danger
|
color: theme.colors.danger,
|
||||||
},
|
},
|
||||||
"2020-07-23": {
|
'2020-07-23': {
|
||||||
startingDay: false,
|
startingDay: false,
|
||||||
endingDay: true,
|
endingDay: true,
|
||||||
color: theme.colors.primary
|
color: theme.colors.primary,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||||
expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
|
expect(
|
||||||
|
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||||
|
).toStrictEqual(result);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,41 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as Planning from "../../src/utils/Planning";
|
import * as Planning from '../../src/utils/Planning';
|
||||||
|
|
||||||
test('isDescriptionEmpty', () => {
|
test('isDescriptionEmpty', () => {
|
||||||
expect(Planning.isDescriptionEmpty("")).toBeTrue();
|
expect(Planning.isDescriptionEmpty('')).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty(" ")).toBeTrue();
|
expect(Planning.isDescriptionEmpty(' ')).toBeTrue();
|
||||||
// noinspection CheckTagEmptyBody
|
// noinspection CheckTagEmptyBody
|
||||||
expect(Planning.isDescriptionEmpty("<p></p>")).toBeTrue();
|
expect(Planning.isDescriptionEmpty('<p></p>')).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty("<p> </p>")).toBeTrue();
|
expect(Planning.isDescriptionEmpty('<p> </p>')).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty("<p><br></p>")).toBeTrue();
|
expect(Planning.isDescriptionEmpty('<p><br></p>')).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty("<p><br></p><p><br></p>")).toBeTrue();
|
expect(Planning.isDescriptionEmpty('<p><br></p><p><br></p>')).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty("<p><br><br><br></p>")).toBeTrue();
|
expect(Planning.isDescriptionEmpty('<p><br><br><br></p>')).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty("<p><br>")).toBeTrue();
|
expect(Planning.isDescriptionEmpty('<p><br>')).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty(null)).toBeTrue();
|
expect(Planning.isDescriptionEmpty(null)).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
|
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
|
||||||
expect(Planning.isDescriptionEmpty("coucou")).toBeFalse();
|
expect(Planning.isDescriptionEmpty('coucou')).toBeFalse();
|
||||||
expect(Planning.isDescriptionEmpty("<p>coucou</p>")).toBeFalse();
|
expect(Planning.isDescriptionEmpty('<p>coucou</p>')).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isEventDateStringFormatValid', () => {
|
test('isEventDateStringFormatValid', () => {
|
||||||
expect(Planning.isEventDateStringFormatValid("2020-03-21 09:00")).toBeTrue();
|
expect(Planning.isEventDateStringFormatValid('2020-03-21 09:00')).toBeTrue();
|
||||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16")).toBeTrue();
|
expect(Planning.isEventDateStringFormatValid('3214-64-12 01:16')).toBeTrue();
|
||||||
|
|
||||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16:00")).toBeFalse();
|
expect(
|
||||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16")).toBeFalse();
|
Planning.isEventDateStringFormatValid('3214-64-12 01:16:00'),
|
||||||
expect(Planning.isEventDateStringFormatValid("3214-f4-12 01:16")).toBeFalse();
|
).toBeFalse();
|
||||||
expect(Planning.isEventDateStringFormatValid("sqdd 09:00")).toBeFalse();
|
expect(Planning.isEventDateStringFormatValid('3214-64-12 1:16')).toBeFalse();
|
||||||
expect(Planning.isEventDateStringFormatValid("2020-03-21")).toBeFalse();
|
expect(Planning.isEventDateStringFormatValid('3214-f4-12 01:16')).toBeFalse();
|
||||||
expect(Planning.isEventDateStringFormatValid("2020-03-21 truc")).toBeFalse();
|
expect(Planning.isEventDateStringFormatValid('sqdd 09:00')).toBeFalse();
|
||||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16:65")).toBeFalse();
|
expect(Planning.isEventDateStringFormatValid('2020-03-21')).toBeFalse();
|
||||||
expect(Planning.isEventDateStringFormatValid("garbage")).toBeFalse();
|
expect(Planning.isEventDateStringFormatValid('2020-03-21 truc')).toBeFalse();
|
||||||
expect(Planning.isEventDateStringFormatValid("")).toBeFalse();
|
expect(
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
|
|
@ -37,136 +43,144 @@ 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))
|
expect(Planning.getFormattedEventTime(null, null)).toBe('/ - /');
|
||||||
.toBe('/ - /');
|
expect(Planning.getFormattedEventTime(undefined, undefined)).toBe('/ - /');
|
||||||
expect(Planning.getFormattedEventTime(undefined, undefined))
|
expect(Planning.getFormattedEventTime('20:30', '23:00')).toBe('/ - /');
|
||||||
.toBe('/ - /');
|
expect(Planning.getFormattedEventTime('2020-03-30', '2020-03-31')).toBe(
|
||||||
expect(Planning.getFormattedEventTime("20:30", "23:00"))
|
'/ - /',
|
||||||
.toBe('/ - /');
|
);
|
||||||
expect(Planning.getFormattedEventTime("2020-03-30", "2020-03-31"))
|
|
||||||
.toBe('/ - /');
|
|
||||||
|
|
||||||
|
expect(
|
||||||
expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-21 09:00"))
|
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-21 09:00'),
|
||||||
.toBe('09:00');
|
).toBe('09:00');
|
||||||
expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-22 17:00"))
|
expect(
|
||||||
.toBe('09:00 - 23:59');
|
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-22 17:00'),
|
||||||
expect(Planning.getFormattedEventTime("2020-03-30 20:30", "2020-03-30 23:00"))
|
).toBe('09:00 - 23:59');
|
||||||
.toBe('20:30 - 23:00');
|
expect(
|
||||||
|
Planning.getFormattedEventTime('2020-03-30 20:30', '2020-03-30 23:00'),
|
||||||
|
).toBe('20:30 - 23:00');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getDateOnlyString', () => {
|
test('getDateOnlyString', () => {
|
||||||
expect(Planning.getDateOnlyString("2020-03-21 09:00")).toBe("2020-03-21");
|
expect(Planning.getDateOnlyString('2020-03-21 09:00')).toBe('2020-03-21');
|
||||||
expect(Planning.getDateOnlyString("2021-12-15 09:00")).toBe("2021-12-15");
|
expect(Planning.getDateOnlyString('2021-12-15 09:00')).toBe('2021-12-15');
|
||||||
expect(Planning.getDateOnlyString("2021-12-o5 09:00")).toBeNull();
|
expect(Planning.getDateOnlyString('2021-12-o5 09:00')).toBeNull();
|
||||||
expect(Planning.getDateOnlyString("2021-12-15 09:")).toBeNull();
|
expect(Planning.getDateOnlyString('2021-12-15 09:')).toBeNull();
|
||||||
expect(Planning.getDateOnlyString("2021-12-15")).toBeNull();
|
expect(Planning.getDateOnlyString('2021-12-15')).toBeNull();
|
||||||
expect(Planning.getDateOnlyString("garbage")).toBeNull();
|
expect(Planning.getDateOnlyString('garbage')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isEventBefore', () => {
|
test('isEventBefore', () => {
|
||||||
expect(Planning.isEventBefore(
|
expect(
|
||||||
"2020-03-21 09:00", "2020-03-21 10:00")).toBeTrue();
|
Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00'),
|
||||||
expect(Planning.isEventBefore(
|
).toBeTrue();
|
||||||
"2020-03-21 10:00", "2020-03-21 10:15")).toBeTrue();
|
expect(
|
||||||
expect(Planning.isEventBefore(
|
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:15'),
|
||||||
"2020-03-21 10:15", "2021-03-21 10:15")).toBeTrue();
|
).toBeTrue();
|
||||||
expect(Planning.isEventBefore(
|
expect(
|
||||||
"2020-03-21 10:15", "2020-05-21 10:15")).toBeTrue();
|
Planning.isEventBefore('2020-03-21 10:15', '2021-03-21 10:15'),
|
||||||
expect(Planning.isEventBefore(
|
).toBeTrue();
|
||||||
"2020-03-21 10:15", "2020-03-30 10:15")).toBeTrue();
|
expect(
|
||||||
|
Planning.isEventBefore('2020-03-21 10:15', '2020-05-21 10:15'),
|
||||||
|
).toBeTrue();
|
||||||
|
expect(
|
||||||
|
Planning.isEventBefore('2020-03-21 10:15', '2020-03-30 10:15'),
|
||||||
|
).toBeTrue();
|
||||||
|
|
||||||
expect(Planning.isEventBefore(
|
expect(
|
||||||
"2020-03-21 10:00", "2020-03-21 10:00")).toBeFalse();
|
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:00'),
|
||||||
expect(Planning.isEventBefore(
|
).toBeFalse();
|
||||||
"2020-03-21 10:00", "2020-03-21 09:00")).toBeFalse();
|
expect(
|
||||||
expect(Planning.isEventBefore(
|
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 09:00'),
|
||||||
"2020-03-21 10:15", "2020-03-21 10:00")).toBeFalse();
|
).toBeFalse();
|
||||||
expect(Planning.isEventBefore(
|
expect(
|
||||||
"2021-03-21 10:15", "2020-03-21 10:15")).toBeFalse();
|
Planning.isEventBefore('2020-03-21 10:15', '2020-03-21 10:00'),
|
||||||
expect(Planning.isEventBefore(
|
).toBeFalse();
|
||||||
"2020-05-21 10:15", "2020-03-21 10:15")).toBeFalse();
|
expect(
|
||||||
expect(Planning.isEventBefore(
|
Planning.isEventBefore('2021-03-21 10:15', '2020-03-21 10:15'),
|
||||||
"2020-03-30 10:15", "2020-03-21 10:15")).toBeFalse();
|
).toBeFalse();
|
||||||
|
expect(
|
||||||
|
Planning.isEventBefore('2020-05-21 10:15', '2020-03-21 10:15'),
|
||||||
|
).toBeFalse();
|
||||||
|
expect(
|
||||||
|
Planning.isEventBefore('2020-03-30 10:15', '2020-03-21 10:15'),
|
||||||
|
).toBeFalse();
|
||||||
|
|
||||||
expect(Planning.isEventBefore(
|
expect(Planning.isEventBefore('garbage', '2020-03-21 10:15')).toBeFalse();
|
||||||
"garbage", "2020-03-21 10:15")).toBeFalse();
|
expect(Planning.isEventBefore(undefined, undefined)).toBeFalse();
|
||||||
expect(Planning.isEventBefore(
|
|
||||||
undefined, undefined)).toBeFalse();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dateToString', () => {
|
test('dateToString', () => {
|
||||||
let testDate = new Date();
|
let testDate = new Date();
|
||||||
testDate.setFullYear(2020, 2, 21);
|
testDate.setFullYear(2020, 2, 21);
|
||||||
testDate.setHours(9, 0, 0, 0);
|
testDate.setHours(9, 0, 0, 0);
|
||||||
expect(Planning.dateToString(testDate)).toBe("2020-03-21 09:00");
|
expect(Planning.dateToString(testDate)).toBe('2020-03-21 09:00');
|
||||||
testDate.setFullYear(2021, 0, 12);
|
testDate.setFullYear(2021, 0, 12);
|
||||||
testDate.setHours(9, 10, 0, 0);
|
testDate.setHours(9, 10, 0, 0);
|
||||||
expect(Planning.dateToString(testDate)).toBe("2021-01-12 09:10");
|
expect(Planning.dateToString(testDate)).toBe('2021-01-12 09:10');
|
||||||
testDate.setFullYear(2022, 11, 31);
|
testDate.setFullYear(2022, 11, 31);
|
||||||
testDate.setHours(9, 10, 15, 0);
|
testDate.setHours(9, 10, 15, 0);
|
||||||
expect(Planning.dateToString(testDate)).toBe("2022-12-31 09:10");
|
expect(Planning.dateToString(testDate)).toBe('2022-12-31 09:10');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generateEmptyCalendar', () => {
|
test('generateEmptyCalendar', () => {
|
||||||
jest.spyOn(Date, 'now')
|
jest
|
||||||
.mockImplementation(() =>
|
.spyOn(Date, 'now')
|
||||||
new Date('2020-01-14T00:00:00.000Z').getTime()
|
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
|
||||||
);
|
|
||||||
let calendar = Planning.generateEmptyCalendar(1);
|
let calendar = Planning.generateEmptyCalendar(1);
|
||||||
expect(calendar).toHaveProperty("2020-01-14");
|
expect(calendar).toHaveProperty('2020-01-14');
|
||||||
expect(calendar).toHaveProperty("2020-01-20");
|
expect(calendar).toHaveProperty('2020-01-20');
|
||||||
expect(calendar).toHaveProperty("2020-02-10");
|
expect(calendar).toHaveProperty('2020-02-10');
|
||||||
expect(Object.keys(calendar).length).toBe(32);
|
expect(Object.keys(calendar).length).toBe(32);
|
||||||
calendar = Planning.generateEmptyCalendar(3);
|
calendar = Planning.generateEmptyCalendar(3);
|
||||||
expect(calendar).toHaveProperty("2020-01-14");
|
expect(calendar).toHaveProperty('2020-01-14');
|
||||||
expect(calendar).toHaveProperty("2020-01-20");
|
expect(calendar).toHaveProperty('2020-01-20');
|
||||||
expect(calendar).toHaveProperty("2020-02-10");
|
expect(calendar).toHaveProperty('2020-02-10');
|
||||||
expect(calendar).toHaveProperty("2020-02-14");
|
expect(calendar).toHaveProperty('2020-02-14');
|
||||||
expect(calendar).toHaveProperty("2020-03-20");
|
expect(calendar).toHaveProperty('2020-03-20');
|
||||||
expect(calendar).toHaveProperty("2020-04-12");
|
expect(calendar).toHaveProperty('2020-04-12');
|
||||||
expect(Object.keys(calendar).length).toBe(92);
|
expect(Object.keys(calendar).length).toBe(92);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pushEventInOrder', () => {
|
test('pushEventInOrder', () => {
|
||||||
let eventArray = [];
|
let eventArray = [];
|
||||||
let event1 = {date_begin: "2020-01-14 09:15"};
|
let event1 = {date_begin: '2020-01-14 09:15'};
|
||||||
Planning.pushEventInOrder(eventArray, event1);
|
Planning.pushEventInOrder(eventArray, event1);
|
||||||
expect(eventArray.length).toBe(1);
|
expect(eventArray.length).toBe(1);
|
||||||
expect(eventArray[0]).toBe(event1);
|
expect(eventArray[0]).toBe(event1);
|
||||||
|
|
||||||
let event2 = {date_begin: "2020-01-14 10:15"};
|
let event2 = {date_begin: '2020-01-14 10:15'};
|
||||||
Planning.pushEventInOrder(eventArray, event2);
|
Planning.pushEventInOrder(eventArray, event2);
|
||||||
expect(eventArray.length).toBe(2);
|
expect(eventArray.length).toBe(2);
|
||||||
expect(eventArray[0]).toBe(event1);
|
expect(eventArray[0]).toBe(event1);
|
||||||
expect(eventArray[1]).toBe(event2);
|
expect(eventArray[1]).toBe(event2);
|
||||||
|
|
||||||
let event3 = {date_begin: "2020-01-14 10:15", title: "garbage"};
|
let event3 = {date_begin: '2020-01-14 10:15', title: 'garbage'};
|
||||||
Planning.pushEventInOrder(eventArray, event3);
|
Planning.pushEventInOrder(eventArray, event3);
|
||||||
expect(eventArray.length).toBe(3);
|
expect(eventArray.length).toBe(3);
|
||||||
expect(eventArray[0]).toBe(event1);
|
expect(eventArray[0]).toBe(event1);
|
||||||
expect(eventArray[1]).toBe(event2);
|
expect(eventArray[1]).toBe(event2);
|
||||||
expect(eventArray[2]).toBe(event3);
|
expect(eventArray[2]).toBe(event3);
|
||||||
|
|
||||||
let event4 = {date_begin: "2020-01-13 09:00"};
|
let event4 = {date_begin: '2020-01-13 09:00'};
|
||||||
Planning.pushEventInOrder(eventArray, event4);
|
Planning.pushEventInOrder(eventArray, event4);
|
||||||
expect(eventArray.length).toBe(4);
|
expect(eventArray.length).toBe(4);
|
||||||
expect(eventArray[0]).toBe(event4);
|
expect(eventArray[0]).toBe(event4);
|
||||||
|
|
@ -176,31 +190,29 @@ test('pushEventInOrder', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('generateEventAgenda', () => {
|
test('generateEventAgenda', () => {
|
||||||
jest.spyOn(Date, 'now')
|
jest
|
||||||
.mockImplementation(() =>
|
.spyOn(Date, 'now')
|
||||||
new Date('2020-01-14T00:00:00.000Z').getTime()
|
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
|
||||||
);
|
|
||||||
let eventList = [
|
let eventList = [
|
||||||
{date_begin: "2020-01-14 09:15"},
|
{date_begin: '2020-01-14 09:15'},
|
||||||
{date_begin: "2020-02-01 09:15"},
|
{date_begin: '2020-02-01 09:15'},
|
||||||
{date_begin: "2020-01-15 09:15"},
|
{date_begin: '2020-01-15 09:15'},
|
||||||
{date_begin: "2020-02-01 09:30"},
|
{date_begin: '2020-02-01 09:30'},
|
||||||
{date_begin: "2020-02-01 08:30"},
|
{date_begin: '2020-02-01 08:30'},
|
||||||
];
|
];
|
||||||
const calendar = Planning.generateEventAgenda(eventList, 2);
|
const calendar = Planning.generateEventAgenda(eventList, 2);
|
||||||
expect(calendar["2020-01-14"].length).toBe(1);
|
expect(calendar['2020-01-14'].length).toBe(1);
|
||||||
expect(calendar["2020-01-14"][0]).toBe(eventList[0]);
|
expect(calendar['2020-01-14'][0]).toBe(eventList[0]);
|
||||||
expect(calendar["2020-01-15"].length).toBe(1);
|
expect(calendar['2020-01-15'].length).toBe(1);
|
||||||
expect(calendar["2020-01-15"][0]).toBe(eventList[2]);
|
expect(calendar['2020-01-15'][0]).toBe(eventList[2]);
|
||||||
expect(calendar["2020-02-01"].length).toBe(3);
|
expect(calendar['2020-02-01'].length).toBe(3);
|
||||||
expect(calendar["2020-02-01"][0]).toBe(eventList[4]);
|
expect(calendar['2020-02-01'][0]).toBe(eventList[4]);
|
||||||
expect(calendar["2020-02-01"][1]).toBe(eventList[1]);
|
expect(calendar['2020-02-01'][1]).toBe(eventList[1]);
|
||||||
expect(calendar["2020-02-01"][2]).toBe(eventList[3]);
|
expect(calendar['2020-02-01'][2]).toBe(eventList[3]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getCurrentDateString', () => {
|
test('getCurrentDateString', () => {
|
||||||
jest.spyOn(Date, 'now')
|
jest.spyOn(Date, 'now').mockImplementation(() => {
|
||||||
.mockImplementation(() => {
|
|
||||||
let date = new Date();
|
let date = new Date();
|
||||||
date.setFullYear(2020, 0, 14);
|
date.setFullYear(2020, 0, 14);
|
||||||
date.setHours(15, 30, 54, 65);
|
date.setHours(15, 30, 54, 65);
|
||||||
|
|
|
||||||
|
|
@ -1,142 +1,167 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {getCleanedMachineWatched, getMachineEndDate, getMachineOfId, isMachineWatched} from "../../src/utils/Proxiwash";
|
import {
|
||||||
|
getCleanedMachineWatched,
|
||||||
|
getMachineEndDate,
|
||||||
|
getMachineOfId,
|
||||||
|
isMachineWatched,
|
||||||
|
} from '../../src/utils/Proxiwash';
|
||||||
|
|
||||||
test('getMachineEndDate', () => {
|
test('getMachineEndDate', () => {
|
||||||
jest.spyOn(Date, 'now')
|
jest
|
||||||
.mockImplementation(() =>
|
.spyOn(Date, 'now')
|
||||||
new Date('2020-01-14T15:00:00.000Z').getTime()
|
.mockImplementation(() => new Date('2020-01-14T15:00:00.000Z').getTime());
|
||||||
);
|
|
||||||
let expectDate = new Date('2020-01-14T15:00:00.000Z');
|
let expectDate = new Date('2020-01-14T15:00:00.000Z');
|
||||||
expectDate.setHours(23);
|
expectDate.setHours(23);
|
||||||
expectDate.setMinutes(10);
|
expectDate.setMinutes(10);
|
||||||
expect(getMachineEndDate({endTime: "23:10"}).getTime()).toBe(expectDate.getTime());
|
expect(getMachineEndDate({endTime: '23:10'}).getTime()).toBe(
|
||||||
|
expectDate.getTime(),
|
||||||
|
);
|
||||||
|
|
||||||
expectDate.setHours(16);
|
expectDate.setHours(16);
|
||||||
expectDate.setMinutes(30);
|
expectDate.setMinutes(30);
|
||||||
expect(getMachineEndDate({endTime: "16:30"}).getTime()).toBe(expectDate.getTime());
|
expect(getMachineEndDate({endTime: '16:30'}).getTime()).toBe(
|
||||||
|
expectDate.getTime(),
|
||||||
expect(getMachineEndDate({endTime: "15:30"})).toBeNull();
|
|
||||||
|
|
||||||
expect(getMachineEndDate({endTime: "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(expectDate.getTime());
|
expect(getMachineEndDate({endTime: '00:30'}).getTime()).toBe(
|
||||||
|
expectDate.getTime(),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isMachineWatched', () => {
|
test('isMachineWatched', () => {
|
||||||
let machineList = [
|
let machineList = [
|
||||||
{
|
{
|
||||||
number: "0",
|
number: '0',
|
||||||
endTime: "23:30",
|
endTime: '23:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "1",
|
number: '1',
|
||||||
endTime: "20:30",
|
endTime: '20:30',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(isMachineWatched({number: "0", endTime: "23:30"}, machineList)).toBeTrue();
|
expect(
|
||||||
expect(isMachineWatched({number: "1", endTime: "20:30"}, machineList)).toBeTrue();
|
isMachineWatched({number: '0', endTime: '23:30'}, machineList),
|
||||||
expect(isMachineWatched({number: "3", endTime: "20:30"}, machineList)).toBeFalse();
|
).toBeTrue();
|
||||||
expect(isMachineWatched({number: "1", endTime: "23:30"}, machineList)).toBeFalse();
|
expect(
|
||||||
|
isMachineWatched({number: '1', endTime: '20:30'}, machineList),
|
||||||
|
).toBeTrue();
|
||||||
|
expect(
|
||||||
|
isMachineWatched({number: '3', endTime: '20:30'}, machineList),
|
||||||
|
).toBeFalse();
|
||||||
|
expect(
|
||||||
|
isMachineWatched({number: '1', endTime: '23:30'}, machineList),
|
||||||
|
).toBeFalse();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getMachineOfId', () => {
|
test('getMachineOfId', () => {
|
||||||
let machineList = [
|
let machineList = [
|
||||||
{
|
{
|
||||||
number: "0",
|
number: '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "1",
|
number: '1',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(getMachineOfId("0", machineList)).toStrictEqual({number: "0"});
|
expect(getMachineOfId('0', machineList)).toStrictEqual({number: '0'});
|
||||||
expect(getMachineOfId("1", machineList)).toStrictEqual({number: "1"});
|
expect(getMachineOfId('1', machineList)).toStrictEqual({number: '1'});
|
||||||
expect(getMachineOfId("3", machineList)).toBeNull();
|
expect(getMachineOfId('3', machineList)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('getCleanedMachineWatched', () => {
|
test('getCleanedMachineWatched', () => {
|
||||||
let machineList = [
|
let machineList = [
|
||||||
{
|
{
|
||||||
number: "0",
|
number: '0',
|
||||||
endTime: "23:30",
|
endTime: '23:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "1",
|
number: '1',
|
||||||
endTime: "20:30",
|
endTime: '20:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "2",
|
number: '2',
|
||||||
endTime: "",
|
endTime: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let watchList = [
|
let watchList = [
|
||||||
{
|
{
|
||||||
number: "0",
|
number: '0',
|
||||||
endTime: "23:30",
|
endTime: '23:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "1",
|
number: '1',
|
||||||
endTime: "20:30",
|
endTime: '20:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "2",
|
number: '2',
|
||||||
endTime: "",
|
endTime: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let cleanedList = watchList;
|
let cleanedList = watchList;
|
||||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||||
|
cleanedList,
|
||||||
|
);
|
||||||
|
|
||||||
watchList = [
|
watchList = [
|
||||||
{
|
{
|
||||||
number: "0",
|
number: '0',
|
||||||
endTime: "23:30",
|
endTime: '23:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "1",
|
number: '1',
|
||||||
endTime: "20:30",
|
endTime: '20:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "2",
|
number: '2',
|
||||||
endTime: "15:30",
|
endTime: '15:30',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
cleanedList = [
|
cleanedList = [
|
||||||
{
|
{
|
||||||
number: "0",
|
number: '0',
|
||||||
endTime: "23:30",
|
endTime: '23:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "1",
|
number: '1',
|
||||||
endTime: "20:30",
|
endTime: '20:30',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||||
|
cleanedList,
|
||||||
|
);
|
||||||
|
|
||||||
watchList = [
|
watchList = [
|
||||||
{
|
{
|
||||||
number: "0",
|
number: '0',
|
||||||
endTime: "23:30",
|
endTime: '23:30',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "1",
|
number: '1',
|
||||||
endTime: "20:31",
|
endTime: '20:31',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "3",
|
number: '3',
|
||||||
endTime: "15:30",
|
endTime: '15:30',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
cleanedList = [
|
cleanedList = [
|
||||||
{
|
{
|
||||||
number: "0",
|
number: '0',
|
||||||
endTime: "23:30",
|
endTime: '23:30',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||||
|
cleanedList,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import {isResponseValid} from "../../src/utils/WebData";
|
|
||||||
|
|
||||||
let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
|
||||||
|
|
||||||
test('isRequestResponseValid', () => {
|
|
||||||
let json = {
|
|
||||||
error: 0,
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
expect(isResponseValid(json)).toBeTrue();
|
|
||||||
json = {
|
|
||||||
error: 1,
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
expect(isResponseValid(json)).toBeTrue();
|
|
||||||
json = {
|
|
||||||
error: 50,
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
expect(isResponseValid(json)).toBeTrue();
|
|
||||||
json = {
|
|
||||||
error: 50,
|
|
||||||
data: {truc: 'machin'}
|
|
||||||
};
|
|
||||||
expect(isResponseValid(json)).toBeTrue();
|
|
||||||
json = {
|
|
||||||
message: 'coucou'
|
|
||||||
};
|
|
||||||
expect(isResponseValid(json)).toBeFalse();
|
|
||||||
json = {
|
|
||||||
error: 'coucou',
|
|
||||||
data: {truc: 'machin'}
|
|
||||||
};
|
|
||||||
expect(isResponseValid(json)).toBeFalse();
|
|
||||||
json = {
|
|
||||||
error: 0,
|
|
||||||
data: 'coucou'
|
|
||||||
};
|
|
||||||
expect(isResponseValid(json)).toBeFalse();
|
|
||||||
json = {
|
|
||||||
error: 0,
|
|
||||||
};
|
|
||||||
expect(isResponseValid(json)).toBeFalse();
|
|
||||||
});
|
|
||||||
47
__tests__/utils/WebData.test.js
Normal file
47
__tests__/utils/WebData.test.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {isApiResponseValid} from '../../src/utils/WebData';
|
||||||
|
|
||||||
|
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||||
|
|
||||||
|
test('isRequestResponseValid', () => {
|
||||||
|
let json = {
|
||||||
|
error: 0,
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
expect(isApiResponseValid(json)).toBeTrue();
|
||||||
|
json = {
|
||||||
|
error: 1,
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
expect(isApiResponseValid(json)).toBeTrue();
|
||||||
|
json = {
|
||||||
|
error: 50,
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
expect(isApiResponseValid(json)).toBeTrue();
|
||||||
|
json = {
|
||||||
|
error: 50,
|
||||||
|
data: {truc: 'machin'},
|
||||||
|
};
|
||||||
|
expect(isApiResponseValid(json)).toBeTrue();
|
||||||
|
json = {
|
||||||
|
message: 'coucou',
|
||||||
|
};
|
||||||
|
expect(isApiResponseValid(json)).toBeFalse();
|
||||||
|
json = {
|
||||||
|
error: 'coucou',
|
||||||
|
data: {truc: 'machin'},
|
||||||
|
};
|
||||||
|
expect(isApiResponseValid(json)).toBeFalse();
|
||||||
|
json = {
|
||||||
|
error: 0,
|
||||||
|
data: 'coucou',
|
||||||
|
};
|
||||||
|
expect(isApiResponseValid(json)).toBeFalse();
|
||||||
|
json = {
|
||||||
|
error: 0,
|
||||||
|
};
|
||||||
|
expect(isApiResponseValid(json)).toBeFalse();
|
||||||
|
});
|
||||||
|
|
@ -136,8 +136,8 @@ android {
|
||||||
applicationId 'fr.amicaleinsat.application'
|
applicationId 'fr.amicaleinsat.application'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 32
|
versionCode 34
|
||||||
versionName "3.1.4"
|
versionName "4.0.1"
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
splits {
|
splits {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: ['module:metro-react-native-babel-preset'],
|
presets: ['module:metro-react-native-babel-preset', '@babel/preset-flow'],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
1
index.js
1
index.js
|
|
@ -6,4 +6,5 @@ import {AppRegistry} from 'react-native';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import {name as appName} from './app.json';
|
import {name as appName} from './app.json';
|
||||||
|
|
||||||
|
// eslint-disable-next-line flowtype/require-return-type
|
||||||
AppRegistry.registerComponent(appName, () => App);
|
AppRegistry.registerComponent(appName, () => App);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transformer: {
|
transformer: {
|
||||||
|
// eslint-disable-next-line flowtype/require-return-type
|
||||||
getTransformOptions: async () => ({
|
getTransformOptions: async () => ({
|
||||||
transform: {
|
transform: {
|
||||||
experimentalImportSupport: false,
|
experimentalImportSupport: false,
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,55 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import ConnectionManager from "../../managers/ConnectionManager";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import {ERROR_TYPE} from "../../utils/WebData";
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
import ErrorView from "../Screens/ErrorView";
|
import type {ApiGenericDataType} from '../../utils/WebData';
|
||||||
import BasicLoadingScreen from "../Screens/BasicLoadingScreen";
|
import {ERROR_TYPE} from '../../utils/WebData';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import ErrorView from '../Screens/ErrorView';
|
||||||
|
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
requests: Array<{
|
requests: Array<{
|
||||||
link: string,
|
link: string,
|
||||||
params: Object,
|
params: {...},
|
||||||
mandatory: boolean
|
mandatory: boolean,
|
||||||
}>,
|
}>,
|
||||||
renderFunction: (Array<{ [key: string]: any } | null>) => React.Node,
|
renderFunction: (Array<ApiGenericDataType | null>) => React.Node,
|
||||||
errorViewOverride?: Array<{
|
errorViewOverride?: Array<{
|
||||||
errorCode: number,
|
errorCode: number,
|
||||||
message: string,
|
message: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
showRetryButton: boolean
|
showRetryButton: boolean,
|
||||||
}>,
|
}> | null,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
class AuthenticatedScreen extends React.Component<Props, State> {
|
class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
||||||
|
static defaultProps = {
|
||||||
state = {
|
errorViewOverride: null,
|
||||||
loading: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
currentUserToken: string | null;
|
currentUserToken: string | null;
|
||||||
connectionManager: ConnectionManager;
|
|
||||||
errors: Array<number>;
|
|
||||||
fetchedData: Array<{ [key: string]: any } | null>;
|
|
||||||
|
|
||||||
constructor(props: Object) {
|
connectionManager: ConnectionManager;
|
||||||
|
|
||||||
|
errors: Array<number>;
|
||||||
|
|
||||||
|
fetchedData: Array<ApiGenericDataType | null>;
|
||||||
|
|
||||||
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
};
|
||||||
this.connectionManager = ConnectionManager.getInstance();
|
this.connectionManager = ConnectionManager.getInstance();
|
||||||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
props.navigation.addListener('focus', this.onScreenFocus);
|
||||||
this.fetchedData = new Array(this.props.requests.length);
|
this.fetchedData = new Array(props.requests.length);
|
||||||
this.errors = new Array(this.props.requests.length);
|
this.errors = new Array(props.requests.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -56,35 +62,6 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
||||||
|
|
@ -95,51 +72,20 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
||||||
* @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(data: { [key: string]: any } | null, index: number, error: number) {
|
onRequestFinished(
|
||||||
if (index >= 0 && index < this.props.requests.length) {
|
data: ApiGenericDataType | null,
|
||||||
|
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;
|
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
|
||||||
}
|
}
|
||||||
|
// Token expired, logout user
|
||||||
|
if (error === ERROR_TYPE.BAD_TOKEN) this.connectionManager.disconnect();
|
||||||
|
|
||||||
if (error === ERROR_TYPE.BAD_TOKEN) // Token expired, logout user
|
if (this.allRequestsFinished()) this.setState({loading: false});
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -149,9 +95,13 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @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() {
|
getError(): number {
|
||||||
for (let i = 0; i < this.errors.length; i++) {
|
const {props} = this;
|
||||||
if (this.errors[i] !== 0 && this.props.requests[i].mandatory) {
|
for (let i = 0; i < this.errors.length; i += 1) {
|
||||||
|
if (
|
||||||
|
this.errors[i] !== ERROR_TYPE.SUCCESS &&
|
||||||
|
props.requests[i].mandatory
|
||||||
|
) {
|
||||||
return this.errors[i];
|
return this.errors[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -163,13 +113,14 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getErrorRender() {
|
getErrorRender(): React.Node {
|
||||||
|
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 = this.props.errorViewOverride;
|
const overrideList = props.errorViewOverride;
|
||||||
if (overrideList != null) {
|
if (overrideList != null) {
|
||||||
for (let i = 0; i < overrideList.length; i++) {
|
for (let i = 0; i < overrideList.length; i += 1) {
|
||||||
if (overrideList[i].errorCode === errorCode) {
|
if (overrideList[i].errorCode === errorCode) {
|
||||||
shouldOverride = true;
|
shouldOverride = true;
|
||||||
override = overrideList[i];
|
override = overrideList[i];
|
||||||
|
|
@ -177,25 +128,62 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (
|
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
|
||||||
<ErrorView
|
|
||||||
{...this.props}
|
|
||||||
errorCode={errorCode}
|
|
||||||
onRefresh={this.fetchData}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the data from the server.
|
||||||
|
*
|
||||||
|
* If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
|
||||||
|
*
|
||||||
|
* If the user is logged in, send all requests.
|
||||||
|
*/
|
||||||
|
fetchData = () => {
|
||||||
|
const {state, props} = this;
|
||||||
|
if (!state.loading) this.setState({loading: true});
|
||||||
|
|
||||||
|
if (this.connectionManager.isLoggedIn()) {
|
||||||
|
for (let i = 0; i < props.requests.length; i += 1) {
|
||||||
|
this.connectionManager
|
||||||
|
.authenticatedRequest(
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -205,14 +193,12 @@ class AuthenticatedScreen extends React.Component<Props, State> {
|
||||||
this.fetchData();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
return (
|
const {state, props} = this;
|
||||||
this.state.loading
|
if (state.loading) return <BasicLoadingScreen />;
|
||||||
? <BasicLoadingScreen/>
|
if (this.getError() === ERROR_TYPE.SUCCESS)
|
||||||
: (this.allRequestsValid()
|
return props.renderFunction(this.fetchedData);
|
||||||
? this.props.renderFunction(this.fetchedData)
|
return this.getErrorRender();
|
||||||
: this.getErrorRender())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,42 +2,43 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import LoadingConfirmDialog from "../Dialogs/LoadingConfirmDialog";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import ConnectionManager from "../../managers/ConnectionManager";
|
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
onDismiss: () => void,
|
onDismiss: () => void,
|
||||||
}
|
};
|
||||||
|
|
||||||
class LogoutDialog extends React.PureComponent<Props> {
|
class LogoutDialog extends React.PureComponent<PropsType> {
|
||||||
|
onClickAccept = async (): Promise<void> => {
|
||||||
onClickAccept = async () => {
|
const {props} = this;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve: () => void) => {
|
||||||
ConnectionManager.getInstance().disconnect()
|
ConnectionManager.getInstance()
|
||||||
|
.disconnect()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.props.navigation.reset({
|
props.navigation.reset({
|
||||||
index: 0,
|
index: 0,
|
||||||
routes: [{name: 'main'}],
|
routes: [{name: 'main'}],
|
||||||
});
|
});
|
||||||
this.props.onDismiss();
|
props.onDismiss();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<LoadingConfirmDialog
|
<LoadingConfirmDialog
|
||||||
{...this.props}
|
visible={props.visible}
|
||||||
visible={this.props.visible}
|
onDismiss={props.onDismiss}
|
||||||
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')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,35 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
import {Headline, withTheme} from "react-native-paper";
|
import {Headline, withTheme} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
theme: CustomTheme
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
class VoteNotAvailable extends React.Component<Props> {
|
class VoteNotAvailable extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View
|
||||||
width: "100%",
|
style={{
|
||||||
|
width: '100%',
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
}}>
|
}}>
|
||||||
<Headline
|
<Headline
|
||||||
style={{
|
style={{
|
||||||
color: this.props.theme.colors.textDisabled,
|
color: props.theme.colors.textDisabled,
|
||||||
textAlign: "center",
|
textAlign: 'center',
|
||||||
}}
|
}}>
|
||||||
>{i18n.t("screens.vote.noVote")}</Headline>
|
{i18n.t('screens.vote.noVote')}
|
||||||
|
</Headline>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,127 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Card, List, ProgressBar, Subheading, withTheme} from "react-native-paper";
|
import {
|
||||||
import {FlatList, StyleSheet} from "react-native";
|
Avatar,
|
||||||
|
Card,
|
||||||
|
List,
|
||||||
|
ProgressBar,
|
||||||
|
Subheading,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import {FlatList, StyleSheet} from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {team} from "../../../screens/Amicale/VoteScreen";
|
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
type Props = {
|
teams: Array<VoteTeamType>,
|
||||||
teams: Array<team>,
|
|
||||||
dateEnd: string,
|
dateEnd: string,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
class VoteResults extends React.Component<Props> {
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class VoteResults extends React.Component<PropsType> {
|
||||||
totalVotes: number;
|
totalVotes: number;
|
||||||
|
|
||||||
winnerIds: Array<number>;
|
winnerIds: Array<number>;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: PropsType) {
|
||||||
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() {
|
shouldComponentUpdate(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sortByVotes = (a: team, b: team) => b.votes - a.votes;
|
getTotalVotes(teams: Array<VoteTeamType>) {
|
||||||
|
|
||||||
getTotalVotes(teams: Array<team>) {
|
|
||||||
this.totalVotes = 0;
|
this.totalVotes = 0;
|
||||||
for (let i = 0; i < teams.length; i++) {
|
for (let i = 0; i < teams.length; i += 1) {
|
||||||
this.totalVotes += teams[i].votes;
|
this.totalVotes += teams[i].votes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getWinnerIds(teams: Array<team>) {
|
getWinnerIds(teams: Array<VoteTeamType>) {
|
||||||
let max = teams[0].votes;
|
const max = teams[0].votes;
|
||||||
this.winnerIds = [];
|
this.winnerIds = [];
|
||||||
for (let i = 0; i < teams.length; i++) {
|
for (let i = 0; i < teams.length; i += 1) {
|
||||||
if (teams[i].votes === max)
|
if (teams[i].votes === max) this.winnerIds.push(teams[i].id);
|
||||||
this.winnerIds.push(teams[i].id);
|
else break;
|
||||||
else
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
voteKeyExtractor = (item: team) => item.id.toString();
|
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
|
||||||
|
|
||||||
resultRenderItem = ({item}: { item: team }) => {
|
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||||
|
|
||||||
|
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 colors = this.props.theme.colors;
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<Card style={{
|
<Card
|
||||||
|
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={props => isWinner
|
left={({size}: {size: number}): React.Node =>
|
||||||
? <List.Icon {...props} icon={isDraw ? "trophy-outline" : "trophy"} color={colors.primary}/>
|
isWinner ? (
|
||||||
: null}
|
<List.Icon
|
||||||
|
size={size}
|
||||||
|
icon={isDraw ? 'trophy-outline' : 'trophy'}
|
||||||
|
color={props.theme.colors.primary}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
titleStyle={{
|
titleStyle={{
|
||||||
color: isWinner
|
color: isWinner
|
||||||
? colors.primary
|
? props.theme.colors.primary
|
||||||
: colors.text
|
: props.theme.colors.text,
|
||||||
}}
|
}}
|
||||||
style={{padding: 0}}
|
style={{padding: 0}}
|
||||||
/>
|
/>
|
||||||
<ProgressBar progress={item.votes / this.totalVotes} color={colors.primary}/>
|
<ProgressBar
|
||||||
|
progress={item.votes / this.totalVotes}
|
||||||
|
color={props.theme.colors.primary}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<Card style={styles.card}>
|
<Card style={styles.card}>
|
||||||
<Card.Title
|
<Card.Title
|
||||||
title={i18n.t('screens.vote.results.title')}
|
title={i18n.t('screens.vote.results.title')}
|
||||||
subtitle={i18n.t('screens.vote.results.subtitle') + ' ' + this.props.dateEnd}
|
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
|
||||||
left={(props) => <Avatar.Icon
|
props.dateEnd
|
||||||
{...props}
|
}`}
|
||||||
icon={"podium-gold"}
|
left={({size}: {size: number}): React.Node => (
|
||||||
/>}
|
<Avatar.Icon size={size} icon="podium-gold" />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Subheading>{i18n.t('screens.vote.results.totalVotes') + ' ' + this.totalVotes}</Subheading>
|
<Subheading>{`${i18n.t('screens.vote.results.totalVotes')} ${
|
||||||
|
this.totalVotes
|
||||||
|
}`}</Subheading>
|
||||||
{/* $FlowFixMe */}
|
{/* $FlowFixMe */}
|
||||||
<FlatList
|
<FlatList
|
||||||
data={this.props.teams}
|
data={props.teams}
|
||||||
keyExtractor={this.voteKeyExtractor}
|
keyExtractor={this.voteKeyExtractor}
|
||||||
renderItem={this.resultRenderItem}
|
renderItem={this.resultRenderItem}
|
||||||
/>
|
/>
|
||||||
|
|
@ -104,13 +131,4 @@ class VoteResults extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
card: {
|
|
||||||
margin: 10,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withTheme(VoteResults);
|
export default withTheme(VoteResults);
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,74 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Button, Card, RadioButton} from "react-native-paper";
|
import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
|
||||||
import {FlatList, StyleSheet, View} from "react-native";
|
import {FlatList, StyleSheet, View} from 'react-native';
|
||||||
import ConnectionManager from "../../../managers/ConnectionManager";
|
|
||||||
import LoadingConfirmDialog from "../../Dialogs/LoadingConfirmDialog";
|
|
||||||
import ErrorDialog from "../../Dialogs/ErrorDialog";
|
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {team} from "../../../screens/Amicale/VoteScreen";
|
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||||
|
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
|
||||||
|
import ErrorDialog from '../../Dialogs/ErrorDialog';
|
||||||
|
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
teams: Array<team>,
|
teams: Array<VoteTeamType>,
|
||||||
onVoteSuccess: () => void,
|
onVoteSuccess: () => void,
|
||||||
onVoteError: () => void,
|
onVoteError: () => void,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
selectedTeam: string,
|
selectedTeam: string,
|
||||||
voteDialogVisible: boolean,
|
voteDialogVisible: boolean,
|
||||||
errorDialogVisible: boolean,
|
errorDialogVisible: boolean,
|
||||||
currentError: number,
|
currentError: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default class VoteSelect extends React.PureComponent<Props, State> {
|
export default class VoteSelect extends React.PureComponent<
|
||||||
|
PropsType,
|
||||||
state = {
|
StateType,
|
||||||
selectedTeam: "none",
|
> {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
selectedTeam: 'none',
|
||||||
voteDialogVisible: false,
|
voteDialogVisible: false,
|
||||||
errorDialogVisible: false,
|
errorDialogVisible: false,
|
||||||
currentError: 0,
|
currentError: 0,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
onVoteSelectionChange = (team: string) => this.setState({selectedTeam: team});
|
onVoteSelectionChange = (teamName: string): void =>
|
||||||
|
this.setState({selectedTeam: teamName});
|
||||||
|
|
||||||
voteKeyExtractor = (item: team) => item.id.toString();
|
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||||
|
|
||||||
voteRenderItem = ({item}: { item: team }) => <RadioButton.Item label={item.name} value={item.id.toString()}/>;
|
voteRenderItem = ({item}: {item: VoteTeamType}): React.Node => (
|
||||||
|
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
||||||
|
);
|
||||||
|
|
||||||
showVoteDialog = () => this.setState({voteDialogVisible: true});
|
showVoteDialog = (): void => this.setState({voteDialogVisible: true});
|
||||||
|
|
||||||
onVoteDialogDismiss = () => this.setState({voteDialogVisible: false,});
|
onVoteDialogDismiss = (): void => this.setState({voteDialogVisible: false});
|
||||||
|
|
||||||
onVoteDialogAccept = async () => {
|
onVoteDialogAccept = async (): Promise<void> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve: () => void) => {
|
||||||
ConnectionManager.getInstance().authenticatedRequest(
|
const {state} = this;
|
||||||
"elections/vote",
|
ConnectionManager.getInstance()
|
||||||
{"team": parseInt(this.state.selectedTeam)})
|
.authenticatedRequest('elections/vote', {
|
||||||
|
team: parseInt(state.selectedTeam, 10),
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.onVoteDialogDismiss();
|
this.onVoteDialogDismiss();
|
||||||
this.props.onVoteSuccess();
|
const {props} = this;
|
||||||
|
props.onVoteSuccess();
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error: number) => {
|
.catch((error: number) => {
|
||||||
|
|
@ -60,39 +79,39 @@ export default class VoteSelect extends React.PureComponent<Props, State> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
showErrorDialog = (error: number) => this.setState({
|
showErrorDialog = (error: number): void =>
|
||||||
|
this.setState({
|
||||||
errorDialogVisible: true,
|
errorDialogVisible: true,
|
||||||
currentError: error,
|
currentError: error,
|
||||||
});
|
});
|
||||||
|
|
||||||
onErrorDialogDismiss = () => {
|
onErrorDialogDismiss = () => {
|
||||||
this.setState({errorDialogVisible: false});
|
this.setState({errorDialogVisible: false});
|
||||||
this.props.onVoteError();
|
const {props} = this;
|
||||||
|
props.onVoteError();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
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={(props) =>
|
left={({size}: {size: number}): React.Node => (
|
||||||
<Avatar.Icon
|
<Avatar.Icon size={size} icon="alert-decagram" />
|
||||||
{...props}
|
)}
|
||||||
icon={"alert-decagram"}
|
|
||||||
/>}
|
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<RadioButton.Group
|
<RadioButton.Group
|
||||||
onValueChange={this.onVoteSelectionChange}
|
onValueChange={this.onVoteSelectionChange}
|
||||||
value={this.state.selectedTeam}
|
value={state.selectedTeam}>
|
||||||
>
|
|
||||||
{/* $FlowFixMe */}
|
{/* $FlowFixMe */}
|
||||||
<FlatList
|
<FlatList
|
||||||
data={this.props.teams}
|
data={props.teams}
|
||||||
keyExtractor={this.voteKeyExtractor}
|
keyExtractor={this.voteKeyExtractor}
|
||||||
extraData={this.state.selectedTeam}
|
extraData={state.selectedTeam}
|
||||||
renderItem={this.voteRenderItem}
|
renderItem={this.voteRenderItem}
|
||||||
/>
|
/>
|
||||||
</RadioButton.Group>
|
</RadioButton.Group>
|
||||||
|
|
@ -103,14 +122,13 @@ export default class VoteSelect extends React.PureComponent<Props, State> {
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={this.showVoteDialog}
|
onPress={this.showVoteDialog}
|
||||||
style={{marginLeft: 'auto'}}
|
style={{marginLeft: 'auto'}}
|
||||||
disabled={this.state.selectedTeam === "none"}
|
disabled={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={this.state.voteDialogVisible}
|
visible={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')}
|
||||||
|
|
@ -118,20 +136,11 @@ export default class VoteSelect extends React.PureComponent<Props, State> {
|
||||||
message={i18n.t('screens.vote.select.dialogMessage')}
|
message={i18n.t('screens.vote.select.dialogMessage')}
|
||||||
/>
|
/>
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
visible={this.state.errorDialogVisible}
|
visible={state.errorDialogVisible}
|
||||||
onDismiss={this.onErrorDialogDismiss}
|
onDismiss={this.onErrorDialogDismiss}
|
||||||
errorCode={this.state.currentError}
|
errorCode={state.currentError}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
card: {
|
|
||||||
margin: 10,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,45 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Card, Paragraph} from "react-native-paper";
|
import {Avatar, Card, Paragraph} from 'react-native-paper';
|
||||||
import {StyleSheet} from "react-native";
|
import {StyleSheet} from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
startDate: string,
|
startDate: string,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default class VoteTease extends React.Component<Props> {
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Card style={styles.card}>
|
|
||||||
<Card.Title
|
|
||||||
title={i18n.t('screens.vote.tease.title')}
|
|
||||||
subtitle={i18n.t('screens.vote.tease.subtitle')}
|
|
||||||
left={props => <Avatar.Icon
|
|
||||||
{...props}
|
|
||||||
icon="vote"/>}
|
|
||||||
/>
|
|
||||||
<Card.Content>
|
|
||||||
<Paragraph>
|
|
||||||
{i18n.t('screens.vote.tease.message') + ' ' + this.props.startDate}
|
|
||||||
</Paragraph>
|
|
||||||
</Card.Content>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
margin: 10,
|
margin: 10,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default class VoteTease extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
|
return (
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Card.Title
|
||||||
|
title={i18n.t('screens.vote.tease.title')}
|
||||||
|
subtitle={i18n.t('screens.vote.tease.subtitle')}
|
||||||
|
left={({size}: {size: number}): React.Node => (
|
||||||
|
<Avatar.Icon size={size} icon="vote" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
<Paragraph>
|
||||||
|
{`${i18n.t('screens.vote.tease.message')} ${props.startDate}`}
|
||||||
|
</Paragraph>
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,78 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {ActivityIndicator, Card, Paragraph, withTheme} from "react-native-paper";
|
import {
|
||||||
import {StyleSheet} from "react-native";
|
ActivityIndicator,
|
||||||
|
Card,
|
||||||
|
Paragraph,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import {StyleSheet} from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
startDate: string | null,
|
startDate: string | null,
|
||||||
justVoted: boolean,
|
justVoted: boolean,
|
||||||
hasVoted: boolean,
|
hasVoted: boolean,
|
||||||
isVoteRunning: boolean,
|
isVoteRunning: boolean,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
class VoteWait extends React.Component<Props> {
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const colors = this.props.theme.colors;
|
|
||||||
const startDate = this.props.startDate;
|
|
||||||
return (
|
|
||||||
<Card style={styles.card}>
|
|
||||||
<Card.Title
|
|
||||||
title={this.props.isVoteRunning
|
|
||||||
? i18n.t('screens.vote.wait.titleSubmitted')
|
|
||||||
: i18n.t('screens.vote.wait.titleEnded')}
|
|
||||||
subtitle={i18n.t('screens.vote.wait.subtitle')}
|
|
||||||
left={(props) => <ActivityIndicator {...props}/>}
|
|
||||||
/>
|
|
||||||
<Card.Content>
|
|
||||||
{
|
|
||||||
this.props.justVoted
|
|
||||||
? <Paragraph style={{color: colors.success}}>
|
|
||||||
{i18n.t('screens.vote.wait.messageSubmitted')}
|
|
||||||
</Paragraph>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.props.hasVoted
|
|
||||||
? <Paragraph style={{color: colors.success}}>
|
|
||||||
{i18n.t('screens.vote.wait.messageVoted')}
|
|
||||||
</Paragraph>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
startDate != null
|
|
||||||
? <Paragraph>
|
|
||||||
{i18n.t('screens.vote.wait.messageDate') + ' ' + startDate}
|
|
||||||
</Paragraph>
|
|
||||||
: <Paragraph>{i18n.t('screens.vote.wait.messageDateUndefined')}</Paragraph>
|
|
||||||
}
|
|
||||||
</Card.Content>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
margin: 10,
|
margin: 10,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class VoteWait extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
|
const {startDate} = props;
|
||||||
|
return (
|
||||||
|
<Card style={styles.card}>
|
||||||
|
<Card.Title
|
||||||
|
title={
|
||||||
|
props.isVoteRunning
|
||||||
|
? i18n.t('screens.vote.wait.titleSubmitted')
|
||||||
|
: i18n.t('screens.vote.wait.titleEnded')
|
||||||
|
}
|
||||||
|
subtitle={i18n.t('screens.vote.wait.subtitle')}
|
||||||
|
left={({size}: {size: number}): React.Node => (
|
||||||
|
<ActivityIndicator size={size} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
{props.justVoted ? (
|
||||||
|
<Paragraph style={{color: props.theme.colors.success}}>
|
||||||
|
{i18n.t('screens.vote.wait.messageSubmitted')}
|
||||||
|
</Paragraph>
|
||||||
|
) : null}
|
||||||
|
{props.hasVoted ? (
|
||||||
|
<Paragraph style={{color: props.theme.colors.success}}>
|
||||||
|
{i18n.t('screens.vote.wait.messageVoted')}
|
||||||
|
</Paragraph>
|
||||||
|
) : null}
|
||||||
|
{startDate != null ? (
|
||||||
|
<Paragraph>
|
||||||
|
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
|
||||||
|
</Paragraph>
|
||||||
|
) : (
|
||||||
|
<Paragraph>
|
||||||
|
{i18n.t('screens.vote.wait.messageDateUndefined')}
|
||||||
|
</Paragraph>
|
||||||
|
)}
|
||||||
|
</Card.Content>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default withTheme(VoteWait);
|
export default withTheme(VoteWait);
|
||||||
|
|
|
||||||
|
|
@ -1,101 +1,117 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import {List, withTheme} from 'react-native-paper';
|
import {List, withTheme} from 'react-native-paper';
|
||||||
import Collapsible from "react-native-collapsible";
|
import Collapsible from 'react-native-collapsible';
|
||||||
import * as Animatable from "react-native-animatable";
|
import * as Animatable from 'react-native-animatable';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
title: string,
|
title: string,
|
||||||
subtitle?: string,
|
subtitle?: string,
|
||||||
left?: (props: { [keys: string]: any }) => React.Node,
|
left?: () => React.Node,
|
||||||
opened?: boolean,
|
opened?: boolean,
|
||||||
unmountWhenCollapsed: boolean,
|
unmountWhenCollapsed?: boolean,
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
expanded: boolean,
|
expanded: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
|
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
|
||||||
|
|
||||||
class AnimatedAccordion extends React.Component<Props, State> {
|
class AnimatedAccordion extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
static defaultProps = {
|
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;
|
||||||
|
|
||||||
state = {
|
constructor(props: PropsType) {
|
||||||
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() {
|
||||||
if (this.state.expanded) {
|
const {expanded} = this.state;
|
||||||
this.chevronIcon = "chevron-up";
|
if (expanded) {
|
||||||
this.animStart = "180deg";
|
this.chevronIcon = 'chevron-up';
|
||||||
this.animEnd = "0deg";
|
this.animStart = '180deg';
|
||||||
|
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({rotate: this.state.expanded ? this.animStart : this.animEnd});
|
this.chevronRef.current.transitionTo({
|
||||||
this.setState({expanded: !this.state.expanded})
|
rotate: expanded ? this.animStart : this.animEnd,
|
||||||
|
});
|
||||||
|
this.setState((prevState: StateType): {expanded: boolean} => ({
|
||||||
|
expanded: !prevState.expanded,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
|
render(): React.Node {
|
||||||
if (nextProps.opened != null && nextProps.opened !== this.props.opened)
|
const {props, state} = this;
|
||||||
this.state.expanded = nextProps.opened;
|
const {colors} = props.theme;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const colors = this.props.theme.colors;
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<List.Item
|
<List.Item
|
||||||
{...this.props}
|
title={props.title}
|
||||||
title={this.props.title}
|
subtitle={props.subtitle}
|
||||||
subtitle={this.props.subtitle}
|
titleStyle={state.expanded ? {color: colors.primary} : null}
|
||||||
titleStyle={this.state.expanded ? {color: colors.primary} : undefined}
|
|
||||||
onPress={this.toggleAccordion}
|
onPress={this.toggleAccordion}
|
||||||
right={(props) => <AnimatedListIcon
|
right={({size}: {size: number}): React.Node => (
|
||||||
|
<AnimatedListIcon
|
||||||
ref={this.chevronRef}
|
ref={this.chevronRef}
|
||||||
{...props}
|
size={size}
|
||||||
icon={this.chevronIcon}
|
icon={this.chevronIcon}
|
||||||
color={this.state.expanded ? colors.primary : undefined}
|
color={state.expanded ? colors.primary : null}
|
||||||
useNativeDriver
|
useNativeDriver
|
||||||
/>}
|
style={{rotate: '0deg'}}
|
||||||
left={this.props.left}
|
|
||||||
/>
|
/>
|
||||||
<Collapsible collapsed={!this.state.expanded}>
|
)}
|
||||||
{!this.props.unmountWhenCollapsed || (this.props.unmountWhenCollapsed && this.state.expanded)
|
left={props.left}
|
||||||
? 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);
|
||||||
|
|
@ -1,143 +1,34 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {StyleSheet, View} from "react-native";
|
import {StyleSheet, View} from 'react-native';
|
||||||
import {FAB, IconButton, Surface, withTheme} from "react-native-paper";
|
import {FAB, IconButton, Surface, withTheme} from 'react-native-paper';
|
||||||
import AutoHideHandler from "../../utils/AutoHideHandler";
|
|
||||||
import * as Animatable from 'react-native-animatable';
|
import * as Animatable from 'react-native-animatable';
|
||||||
import CustomTabBar from "../Tabbar/CustomTabBar";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
import type {OnScrollType} from '../../utils/AutoHideHandler';
|
||||||
|
|
||||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
onPress: (action: string, data: any) => void,
|
onPress: (action: string, data?: string) => void,
|
||||||
seekAttention: boolean,
|
seekAttention: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
currentMode: string,
|
currentMode: string,
|
||||||
}
|
};
|
||||||
|
|
||||||
const DISPLAY_MODES = {
|
const DISPLAY_MODES = {
|
||||||
DAY: "agendaDay",
|
DAY: 'agendaDay',
|
||||||
WEEK: "agendaWeek",
|
WEEK: 'agendaWeek',
|
||||||
MONTH: "month",
|
MONTH: 'month',
|
||||||
}
|
|
||||||
|
|
||||||
class AnimatedBottomBar extends React.Component<Props, State> {
|
|
||||||
|
|
||||||
ref: { current: null | Animatable.View };
|
|
||||||
hideHandler: AutoHideHandler;
|
|
||||||
|
|
||||||
displayModeIcons: { [key: string]: string };
|
|
||||||
|
|
||||||
state = {
|
|
||||||
currentMode: DISPLAY_MODES.WEEK,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.ref = React.createRef();
|
|
||||||
this.hideHandler = new AutoHideHandler(false);
|
|
||||||
this.hideHandler.addListener(this.onHideChange);
|
|
||||||
|
|
||||||
this.displayModeIcons = {};
|
|
||||||
this.displayModeIcons[DISPLAY_MODES.DAY] = "calendar-text";
|
|
||||||
this.displayModeIcons[DISPLAY_MODES.WEEK] = "calendar-week";
|
|
||||||
this.displayModeIcons[DISPLAY_MODES.MONTH] = "calendar-range";
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props, nextState: State) {
|
|
||||||
return (nextProps.seekAttention !== this.props.seekAttention)
|
|
||||||
|| (nextState.currentMode !== this.state.currentMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
onHideChange = (shouldHide: boolean) => {
|
|
||||||
if (this.ref.current != null) {
|
|
||||||
if (shouldHide)
|
|
||||||
this.ref.current.fadeOutDown(500);
|
|
||||||
else
|
|
||||||
this.ref.current.fadeInUp(500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
|
||||||
this.hideHandler.onScroll(event);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
changeDisplayMode = () => {
|
|
||||||
let newMode;
|
|
||||||
switch (this.state.currentMode) {
|
|
||||||
case DISPLAY_MODES.DAY:
|
|
||||||
newMode = DISPLAY_MODES.WEEK;
|
|
||||||
break;
|
|
||||||
case DISPLAY_MODES.WEEK:
|
|
||||||
newMode = DISPLAY_MODES.MONTH;
|
|
||||||
|
|
||||||
break;
|
|
||||||
case DISPLAY_MODES.MONTH:
|
|
||||||
newMode = DISPLAY_MODES.DAY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.setState({currentMode: newMode});
|
|
||||||
this.props.onPress("changeView", newMode);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const buttonColor = this.props.theme.colors.primary;
|
|
||||||
return (
|
|
||||||
<Animatable.View
|
|
||||||
ref={this.ref}
|
|
||||||
useNativeDriver
|
|
||||||
style={{
|
|
||||||
...styles.container,
|
|
||||||
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT
|
|
||||||
}}>
|
|
||||||
<Surface style={styles.surface}>
|
|
||||||
<View style={styles.fabContainer}>
|
|
||||||
<AnimatedFAB
|
|
||||||
animation={this.props.seekAttention ? "bounce" : undefined}
|
|
||||||
easing="ease-out"
|
|
||||||
iterationDelay={500}
|
|
||||||
iterationCount="infinite"
|
|
||||||
useNativeDriver
|
|
||||||
style={styles.fab}
|
|
||||||
icon="account-clock"
|
|
||||||
onPress={() => this.props.navigation.navigate('group-select')}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={{flexDirection: 'row'}}>
|
|
||||||
<IconButton
|
|
||||||
icon={this.displayModeIcons[this.state.currentMode]}
|
|
||||||
color={buttonColor}
|
|
||||||
onPress={this.changeDisplayMode}/>
|
|
||||||
<IconButton
|
|
||||||
icon="clock-in"
|
|
||||||
color={buttonColor}
|
|
||||||
style={{marginLeft: 5}}
|
|
||||||
onPress={() => this.props.onPress('today', undefined)}/>
|
|
||||||
</View>
|
|
||||||
<View style={{flexDirection: 'row'}}>
|
|
||||||
<IconButton
|
|
||||||
icon="chevron-left"
|
|
||||||
color={buttonColor}
|
|
||||||
onPress={() => this.props.onPress('prev', undefined)}/>
|
|
||||||
<IconButton
|
|
||||||
icon="chevron-right"
|
|
||||||
color={buttonColor}
|
|
||||||
style={{marginLeft: 5}}
|
|
||||||
onPress={() => this.props.onPress('next', undefined)}/>
|
|
||||||
</View>
|
|
||||||
</Surface>
|
|
||||||
</Animatable.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -153,18 +44,136 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,30 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {StyleSheet} from "react-native";
|
import {StyleSheet} from 'react-native';
|
||||||
import {FAB} from "react-native-paper";
|
import {FAB} from 'react-native-paper';
|
||||||
import AutoHideHandler from "../../utils/AutoHideHandler";
|
|
||||||
import * as Animatable from 'react-native-animatable';
|
import * as Animatable from 'react-native-animatable';
|
||||||
import CustomTabBar from "../Tabbar/CustomTabBar";
|
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
|
||||||
icon: string,
|
icon: string,
|
||||||
onPress: () => void,
|
onPress: () => void,
|
||||||
}
|
};
|
||||||
|
|
||||||
const AnimatedFab = Animatable.createAnimatableComponent(FAB);
|
const AnimatedFab = Animatable.createAnimatableComponent(FAB);
|
||||||
|
|
||||||
export default class AnimatedFAB extends React.Component<Props> {
|
const styles = StyleSheet.create({
|
||||||
|
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() {
|
||||||
|
|
@ -34,33 +40,24 @@ export default class AnimatedFAB extends React.Component<Props> {
|
||||||
|
|
||||||
onHideChange = (shouldHide: boolean) => {
|
onHideChange = (shouldHide: boolean) => {
|
||||||
if (this.ref.current != null) {
|
if (this.ref.current != null) {
|
||||||
if (shouldHide)
|
if (shouldHide) this.ref.current.bounceOutDown(1000);
|
||||||
this.ref.current.bounceOutDown(1000);
|
else this.ref.current.bounceInUp(1000);
|
||||||
else
|
|
||||||
this.ref.current.bounceInUp(1000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<AnimatedFab
|
<AnimatedFab
|
||||||
ref={this.ref}
|
ref={this.ref}
|
||||||
useNativeDriver
|
useNativeDriver
|
||||||
icon={this.props.icon}
|
icon={props.icon}
|
||||||
onPress={this.props.onPress}
|
onPress={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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,56 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {withCollapsible} from "../../utils/withCollapsible";
|
import {Collapsible} from 'react-navigation-collapsible';
|
||||||
import {Collapsible} from "react-navigation-collapsible";
|
import withCollapsible from '../../utils/withCollapsible';
|
||||||
import CustomTabBar from "../Tabbar/CustomTabBar";
|
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||||
|
|
||||||
export type CollapsibleComponentProps = {
|
export type CollapsibleComponentPropsType = {
|
||||||
children?: React.Node,
|
children?: React.Node,
|
||||||
hasTab?: boolean,
|
hasTab?: boolean,
|
||||||
onScroll?: (event: SyntheticEvent<EventTarget>) => void,
|
onScroll?: (event: SyntheticEvent<EventTarget>) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
...CollapsibleComponentProps,
|
...CollapsibleComponentPropsType,
|
||||||
collapsibleStack: Collapsible,
|
collapsibleStack: Collapsible,
|
||||||
|
// 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>) => {
|
||||||
if (this.props.onScroll)
|
const {props} = this;
|
||||||
this.props.onScroll(event);
|
if (props.onScroll) 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
|
||||||
{...this.props}
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...props}
|
||||||
onScroll={onScrollWithListener(this.onScroll)}
|
onScroll={onScrollWithListener(this.onScroll)}
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
paddingTop: containerPaddingTop,
|
paddingTop: containerPaddingTop,
|
||||||
paddingBottom: this.props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
|
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
|
||||||
minHeight: '100%'
|
minHeight: '100%',
|
||||||
}}
|
}}
|
||||||
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
|
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
|
||||||
>
|
{props.children}
|
||||||
{this.props.children}
|
|
||||||
</Comp>
|
</Comp>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {CollapsibleComponentProps} from "./CollapsibleComponent";
|
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||||
import CollapsibleComponent from "./CollapsibleComponent";
|
import CollapsibleComponent from './CollapsibleComponent';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
...CollapsibleComponentProps
|
...CollapsibleComponentPropsType,
|
||||||
}
|
};
|
||||||
|
|
||||||
class CollapsibleFlatList extends React.Component<Props> {
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
|
class CollapsibleFlatList extends React.Component<PropsType> {
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<CollapsibleComponent
|
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...this.props}
|
{...props}
|
||||||
component={Animated.FlatList}
|
component={Animated.FlatList}>
|
||||||
>
|
{props.children}
|
||||||
{this.props.children}
|
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {CollapsibleComponentProps} from "./CollapsibleComponent";
|
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||||
import CollapsibleComponent from "./CollapsibleComponent";
|
import CollapsibleComponent from './CollapsibleComponent';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
...CollapsibleComponentProps
|
...CollapsibleComponentPropsType,
|
||||||
}
|
};
|
||||||
|
|
||||||
class CollapsibleScrollView extends React.Component<Props> {
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
|
class CollapsibleScrollView extends React.Component<PropsType> {
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<CollapsibleComponent
|
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...this.props}
|
{...props}
|
||||||
component={Animated.ScrollView}
|
component={Animated.ScrollView}>
|
||||||
>
|
{props.children}
|
||||||
{this.props.children}
|
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {CollapsibleComponentProps} from "./CollapsibleComponent";
|
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||||
import CollapsibleComponent from "./CollapsibleComponent";
|
import CollapsibleComponent from './CollapsibleComponent';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
...CollapsibleComponentProps
|
...CollapsibleComponentPropsType,
|
||||||
}
|
};
|
||||||
|
|
||||||
class CollapsibleSectionList extends React.Component<Props> {
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
|
class CollapsibleSectionList extends React.Component<PropsType> {
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<CollapsibleComponent
|
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...this.props}
|
{...props}
|
||||||
component={Animated.SectionList}
|
component={Animated.SectionList}>
|
||||||
>
|
{props.children}
|
||||||
{this.props.children}
|
|
||||||
</CollapsibleComponent>
|
</CollapsibleComponent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,29 +2,27 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
onDismiss: () => void,
|
onDismiss: () => void,
|
||||||
title: string,
|
title: string,
|
||||||
message: string,
|
message: string,
|
||||||
}
|
};
|
||||||
|
|
||||||
class AlertDialog extends React.PureComponent<Props> {
|
class AlertDialog extends React.PureComponent<PropsType> {
|
||||||
|
render(): React.Node {
|
||||||
render() {
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<Dialog
|
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
|
||||||
visible={this.props.visible}
|
<Dialog.Title>{props.title}</Dialog.Title>
|
||||||
onDismiss={this.props.onDismiss}>
|
|
||||||
<Dialog.Title>{this.props.title}</Dialog.Title>
|
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Paragraph>{this.props.message}</Paragraph>
|
<Paragraph>{props.message}</Paragraph>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
<Dialog.Actions>
|
<Dialog.Actions>
|
||||||
<Button onPress={this.props.onDismiss}>{i18n.t("dialog.ok")}</Button>
|
<Button onPress={props.onDismiss}>{i18n.t('dialog.ok')}</Button>
|
||||||
</Dialog.Actions>
|
</Dialog.Actions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Portal>
|
</Portal>
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,69 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
import {ERROR_TYPE} from "../../utils/WebData";
|
import {ERROR_TYPE} from '../../utils/WebData';
|
||||||
import AlertDialog from "./AlertDialog";
|
import AlertDialog from './AlertDialog';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
onDismiss: () => void,
|
onDismiss: () => void,
|
||||||
errorCode: number,
|
errorCode: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
class ErrorDialog extends React.PureComponent<Props> {
|
|
||||||
|
|
||||||
|
class ErrorDialog extends React.PureComponent<PropsType> {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
message: string;
|
message: string;
|
||||||
|
|
||||||
generateMessage() {
|
generateMessage() {
|
||||||
this.title = i18n.t("errors.title");
|
const {props} = this;
|
||||||
switch (this.props.errorCode) {
|
this.title = i18n.t('errors.title');
|
||||||
|
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 " + this.props.errorCode;
|
this.message += `\n\nCode ${props.errorCode}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
this.generateMessage();
|
this.generateMessage();
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<AlertDialog {...this.props} title={this.title} message={this.message}/>
|
<AlertDialog
|
||||||
|
visible={props.visible}
|
||||||
|
onDismiss={props.onDismiss}
|
||||||
|
title={this.title}
|
||||||
|
message={this.message}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,88 +1,102 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {ActivityIndicator, Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
import {
|
||||||
import i18n from "i18n-js";
|
ActivityIndicator,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
Paragraph,
|
||||||
|
Portal,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
onDismiss: () => void,
|
onDismiss?: () => void,
|
||||||
onAccept: () => Promise<void>, // async function to be executed
|
onAccept?: () => Promise<void>, // async function to be executed
|
||||||
title: string,
|
title?: string,
|
||||||
titleLoading: string,
|
titleLoading?: string,
|
||||||
message: string,
|
message?: string,
|
||||||
startLoading: boolean,
|
startLoading?: boolean,
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
loading: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
class LoadingConfirmDialog extends React.PureComponent<Props, State> {
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
title: '',
|
|
||||||
message: '',
|
|
||||||
onDismiss: () => {},
|
|
||||||
onAccept: () => {return Promise.resolve()},
|
|
||||||
startLoading: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
loading: this.props.startLoading,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type StateType = {
|
||||||
|
loading: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
|
||||||
|
static defaultProps = {
|
||||||
|
onDismiss: () => {},
|
||||||
|
onAccept: (): Promise<void> => {
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
title: '',
|
||||||
|
titleLoading: '',
|
||||||
|
message: '',
|
||||||
|
startLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: PropsType) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading:
|
||||||
|
props.startLoading != null
|
||||||
|
? props.startLoading
|
||||||
|
: LoadingConfirmDialog.defaultProps.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});
|
||||||
this.props.onAccept().then(this.hideLoading);
|
if (props.onAccept != null) props.onAccept().then(this.hideLoading);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Waits for fade out animations to finish before hiding loading
|
* Waits for fade out animations to finish before hiding loading
|
||||||
* @returns {TimeoutID}
|
* @returns {TimeoutID}
|
||||||
*/
|
*/
|
||||||
hideLoading = () => setTimeout(() => {
|
hideLoading = (): TimeoutID =>
|
||||||
this.setState({loading: false})
|
setTimeout(() => {
|
||||||
|
this.setState({loading: false});
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hide the dialog if it is not loading
|
* Hide the dialog if it is not loading
|
||||||
*/
|
*/
|
||||||
onDismiss = () => {
|
onDismiss = () => {
|
||||||
if (!this.state.loading)
|
const {state, props} = this;
|
||||||
this.props.onDismiss();
|
if (!state.loading && props.onDismiss != null) props.onDismiss();
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {state, props} = this;
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<Dialog
|
<Dialog visible={props.visible} onDismiss={this.onDismiss}>
|
||||||
visible={this.props.visible}
|
|
||||||
onDismiss={this.onDismiss}>
|
|
||||||
<Dialog.Title>
|
<Dialog.Title>
|
||||||
{this.state.loading
|
{state.loading ? props.titleLoading : props.title}
|
||||||
? this.props.titleLoading
|
|
||||||
: this.props.title}
|
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
{this.state.loading
|
{state.loading ? (
|
||||||
? <ActivityIndicator
|
<ActivityIndicator animating size="large" />
|
||||||
animating={true}
|
) : (
|
||||||
size={'large'}/>
|
<Paragraph>{props.message}</Paragraph>
|
||||||
: <Paragraph>{this.props.message}</Paragraph>
|
)}
|
||||||
}
|
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
{this.state.loading
|
{state.loading ? null : (
|
||||||
? null
|
<Dialog.Actions>
|
||||||
: <Dialog.Actions>
|
<Button onPress={this.onDismiss} style={{marginRight: 10}}>
|
||||||
<Button onPress={this.onDismiss}
|
{i18n.t('dialog.cancel')}
|
||||||
style={{marginRight: 10}}>{i18n.t("dialog.cancel")}</Button>
|
</Button>
|
||||||
<Button onPress={this.onClickAccept}>{i18n.t("dialog.yes")}</Button>
|
<Button onPress={this.onClickAccept}>
|
||||||
|
{i18n.t('dialog.yes')}
|
||||||
|
</Button>
|
||||||
</Dialog.Actions>
|
</Dialog.Actions>
|
||||||
}
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,49 +2,44 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
||||||
import {FlatList} from "react-native";
|
import {FlatList} from 'react-native';
|
||||||
|
|
||||||
export type OptionsDialogButton = {
|
export type OptionsDialogButtonType = {
|
||||||
title: string,
|
title: string,
|
||||||
onPress: () => void,
|
onPress: () => void,
|
||||||
}
|
};
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
title: string,
|
title: string,
|
||||||
message: string,
|
message: string,
|
||||||
buttons: Array<OptionsDialogButton>,
|
buttons: Array<OptionsDialogButtonType>,
|
||||||
onDismiss: () => void,
|
onDismiss: () => void,
|
||||||
}
|
};
|
||||||
|
|
||||||
class OptionsDialog extends React.PureComponent<Props> {
|
class OptionsDialog extends React.PureComponent<PropsType> {
|
||||||
|
getButtonRender = ({item}: {item: OptionsDialogButtonType}): React.Node => {
|
||||||
|
return <Button onPress={item.onPress}>{item.title}</Button>;
|
||||||
|
};
|
||||||
|
|
||||||
getButtonRender = ({item}: { item: OptionsDialogButton }) => {
|
keyExtractor = (item: OptionsDialogButtonType): string => item.title;
|
||||||
return <Button
|
|
||||||
onPress={item.onPress}>
|
|
||||||
{item.title}
|
|
||||||
</Button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
keyExtractor = (item: OptionsDialogButton) => item.title;
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<Dialog
|
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
|
||||||
visible={this.props.visible}
|
<Dialog.Title>{props.title}</Dialog.Title>
|
||||||
onDismiss={this.props.onDismiss}>
|
|
||||||
<Dialog.Title>{this.props.title}</Dialog.Title>
|
|
||||||
<Dialog.Content>
|
<Dialog.Content>
|
||||||
<Paragraph>{this.props.message}</Paragraph>
|
<Paragraph>{props.message}</Paragraph>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
<Dialog.Actions>
|
<Dialog.Actions>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={this.props.buttons}
|
data={props.buttons}
|
||||||
renderItem={this.getButtonRender}
|
renderItem={this.getButtonRender}
|
||||||
keyExtractor={this.keyExtractor}
|
keyExtractor={this.keyExtractor}
|
||||||
horizontal={true}
|
horizontal
|
||||||
inverted={true}
|
inverted
|
||||||
/>
|
/>
|
||||||
</Dialog.Actions>
|
</Dialog.Actions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,44 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {List, withTheme} from 'react-native-paper';
|
import {List, withTheme} from 'react-native-paper';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ActionsDashBoardItem extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
|
const {props} = this;
|
||||||
|
return nextProps.theme.dark !== props.theme.dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActionsDashBoardItem extends React.Component<Props> {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
shouldComponentUpdate(nextProps: Props): boolean {
|
|
||||||
return (nextProps.theme.dark !== this.props.theme.dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
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={props => <List.Icon {...props} icon={"comment-quote"}/>}
|
left={({size}: {size: number}): React.Node => (
|
||||||
right={props => <List.Icon {...props} icon={"chevron-right"}/>}
|
<List.Icon size={size} icon="comment-quote" />
|
||||||
onPress={() => this.props.navigation.navigate("feedback")}
|
)}
|
||||||
style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}}
|
right={({size}: {size: number}): React.Node => (
|
||||||
|
<List.Icon size={size} icon="chevron-right" />
|
||||||
|
)}
|
||||||
|
onPress={(): void => props.navigation.navigate('feedback')}
|
||||||
|
style={{
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
marginLeft: 10,
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,79 +1,23 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Card, Text, TouchableRipple, withTheme} from 'react-native-paper';
|
import {
|
||||||
import {StyleSheet, View} from "react-native";
|
Avatar,
|
||||||
import i18n from "i18n-js";
|
Card,
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
Text,
|
||||||
|
TouchableRipple,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
eventNumber: number;
|
eventNumber: number,
|
||||||
clickAction: () => void,
|
clickAction: () => void,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
children?: React.Node
|
children?: React.Node,
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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: {
|
||||||
|
|
@ -84,8 +28,69 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,47 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
|
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
|
||||||
import {Image, View} from "react-native";
|
import {Image, View} from 'react-native';
|
||||||
import Autolink from "react-native-autolink";
|
import Autolink from 'react-native-autolink';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
import ImageModal from 'react-native-image-modal';
|
import ImageModal from 'react-native-image-modal';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import type {FeedItemType} from '../../screens/Home/HomeScreen';
|
||||||
import type {feedItem} from "../../screens/Home/HomeScreen";
|
|
||||||
|
|
||||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
item: FeedItemType,
|
||||||
item: feedItem,
|
|
||||||
title: string,
|
title: string,
|
||||||
subtitle: string,
|
subtitle: string,
|
||||||
height: number,
|
height: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component used to display a feed item
|
* Component used to display a feed item
|
||||||
*/
|
*/
|
||||||
class FeedItem extends React.Component<Props> {
|
class FeedItem extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
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 = () => {
|
||||||
this.props.navigation.navigate(
|
const {props} = this;
|
||||||
'feed-information',
|
props.navigation.navigate('feed-information', {
|
||||||
{
|
data: props.item,
|
||||||
data: this.props.item,
|
date: props.subtitle,
|
||||||
date: this.props.subtitle
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
const item = this.props.item;
|
const {props} = this;
|
||||||
const hasImage = item.full_picture !== '' && item.full_picture !== undefined;
|
const {item} = props;
|
||||||
|
const hasImage =
|
||||||
|
item.full_picture !== '' && item.full_picture !== undefined;
|
||||||
|
|
||||||
const cardMargin = 10;
|
const cardMargin = 10;
|
||||||
const cardHeight = this.props.height - 2 * cardMargin;
|
const cardHeight = props.height - 2 * cardMargin;
|
||||||
const imageSize = 250;
|
const imageSize = 250;
|
||||||
const titleHeight = 80;
|
const titleHeight = 80;
|
||||||
const actionsHeight = 60;
|
const actionsHeight = 60;
|
||||||
|
|
@ -74,23 +54,29 @@ class FeedItem extends React.Component<Props> {
|
||||||
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={this.props.title}
|
title={props.title}
|
||||||
subtitle={this.props.subtitle}
|
subtitle={props.subtitle}
|
||||||
left={this.getAvatar}
|
left={(): React.Node => (
|
||||||
|
<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,
|
||||||
|
|
@ -98,21 +84,23 @@ class FeedItem extends React.Component<Props> {
|
||||||
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>
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,21 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {StyleSheet, View} from "react-native";
|
import {StyleSheet, View} from 'react-native';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
|
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
|
||||||
import {getFormattedEventTime, isDescriptionEmpty} from "../../utils/Planning";
|
import {getFormattedEventTime, isDescriptionEmpty} from '../../utils/Planning';
|
||||||
import CustomHTML from "../Overrides/CustomHTML";
|
import CustomHTML from '../Overrides/CustomHTML';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import type {EventType} from '../../screens/Home/HomeScreen';
|
||||||
import type {event} from "../../screens/Home/HomeScreen";
|
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
event?: event,
|
event?: EventType | null,
|
||||||
clickAction: () => void,
|
clickAction: () => void,
|
||||||
theme?: CustomTheme,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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,
|
||||||
|
|
@ -84,11 +24,77 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
|
import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
|
||||||
import {Dimensions, Image, View} from "react-native";
|
import {Dimensions, Image, View} from 'react-native';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import * as Animatable from 'react-native-animatable';
|
||||||
import * as Animatable from "react-native-animatable";
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
image: string,
|
image: string | null,
|
||||||
onPress: () => void,
|
onPress: () => void | null,
|
||||||
badgeCount: number | null,
|
badgeCount: number | null,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
};
|
};
|
||||||
|
|
||||||
const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
|
const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
|
||||||
|
|
@ -18,50 +18,51 @@ const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
|
||||||
/**
|
/**
|
||||||
* Component used to render a small dashboard item
|
* Component used to render a small dashboard item
|
||||||
*/
|
*/
|
||||||
class SmallDashboardItem extends React.Component<Props> {
|
class SmallDashboardItem extends React.Component<PropsType> {
|
||||||
|
|
||||||
itemSize: number;
|
itemSize: number;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.itemSize = Dimensions.get('window').width / 8;
|
this.itemSize = Dimensions.get('window').width / 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props) {
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
return (nextProps.theme.dark !== this.props.theme.dark)
|
const {props} = this;
|
||||||
|| (nextProps.badgeCount !== this.props.badgeCount);
|
return (
|
||||||
|
nextProps.theme.dark !== props.theme.dark ||
|
||||||
|
nextProps.badgeCount !== props.badgeCount
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
const props = this.props;
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
onPress={this.props.onPress}
|
onPress={props.onPress}
|
||||||
borderless={true}
|
borderless
|
||||||
style={{
|
style={{
|
||||||
marginLeft: this.itemSize / 6,
|
marginLeft: this.itemSize / 6,
|
||||||
marginRight: this.itemSize / 6,
|
marginRight: this.itemSize / 6,
|
||||||
}}
|
}}>
|
||||||
>
|
<View
|
||||||
<View style={{
|
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={{
|
||||||
|
|
@ -73,14 +74,12 @@ class SmallDashboardItem extends React.Component<Props> {
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
}}>
|
}}>
|
||||||
{props.badgeCount}
|
{props.badgeCount}
|
||||||
</AnimatableBadge> : null
|
</AnimatableBadge>
|
||||||
}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</TouchableRipple>
|
</TouchableRipple>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(SmallDashboardItem);
|
export default withTheme(SmallDashboardItem);
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,53 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Animated, Dimensions} from "react-native";
|
import {Animated, Dimensions} from 'react-native';
|
||||||
import ImageListItem from "./ImageListItem";
|
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||||
import CardListItem from "./CardListItem";
|
import ImageListItem from './ImageListItem';
|
||||||
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
|
import CardListItem from './CardListItem';
|
||||||
|
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
dataset: Array<cardItem>,
|
dataset: Array<ServiceItemType>,
|
||||||
isHorizontal: boolean,
|
isHorizontal?: boolean,
|
||||||
contentContainerStyle?: ViewStyle,
|
contentContainerStyle?: ViewStyle | null,
|
||||||
}
|
|
||||||
|
|
||||||
export type cardItem = {
|
|
||||||
key: string,
|
|
||||||
title: string,
|
|
||||||
subtitle: string,
|
|
||||||
image: string | number,
|
|
||||||
onPress: () => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type cardList = Array<cardItem>;
|
export default class CardList extends React.Component<PropsType> {
|
||||||
|
|
||||||
|
|
||||||
export default class CardList extends React.Component<Props> {
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
isHorizontal: false,
|
isHorizontal: false,
|
||||||
}
|
contentContainerStyle: null,
|
||||||
|
};
|
||||||
|
|
||||||
windowWidth: number;
|
windowWidth: number;
|
||||||
|
|
||||||
horizontalItemSize: number;
|
horizontalItemSize: number;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.windowWidth = Dimensions.get('window').width;
|
this.windowWidth = Dimensions.get('window').width;
|
||||||
this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll
|
this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
renderItem = ({item}: { item: cardItem }) => {
|
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => {
|
||||||
if (this.props.isHorizontal)
|
const {props} = this;
|
||||||
return <ImageListItem item={item} key={item.title} width={this.horizontalItemSize}/>;
|
if (props.isHorizontal)
|
||||||
else
|
return (
|
||||||
|
<ImageListItem
|
||||||
|
item={item}
|
||||||
|
key={item.title}
|
||||||
|
width={this.horizontalItemSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
return <CardListItem item={item} key={item.title} />;
|
return <CardListItem item={item} key={item.title} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
keyExtractor = (item: cardItem) => item.key;
|
keyExtractor = (item: ServiceItemType): string => item.key;
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
let containerStyle = {};
|
let containerStyle = {};
|
||||||
if (this.props.isHorizontal) {
|
if (props.isHorizontal) {
|
||||||
containerStyle = {
|
containerStyle = {
|
||||||
height: this.horizontalItemSize + 50,
|
height: this.horizontalItemSize + 50,
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
|
|
@ -57,15 +55,18 @@ export default class CardList extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Animated.FlatList
|
<Animated.FlatList
|
||||||
{...this.props}
|
data={props.dataset}
|
||||||
data={this.props.dataset}
|
renderItem={this.getRenderItem}
|
||||||
renderItem={this.renderItem}
|
|
||||||
keyExtractor={this.keyExtractor}
|
keyExtractor={this.keyExtractor}
|
||||||
numColumns={this.props.isHorizontal ? undefined : 2}
|
numColumns={props.isHorizontal ? undefined : 2}
|
||||||
horizontal={this.props.isHorizontal}
|
horizontal={props.isHorizontal}
|
||||||
contentContainerStyle={this.props.isHorizontal ? containerStyle : this.props.contentContainerStyle}
|
contentContainerStyle={
|
||||||
pagingEnabled={this.props.isHorizontal}
|
props.isHorizontal ? containerStyle : props.contentContainerStyle
|
||||||
snapToInterval={this.props.isHorizontal ? (this.horizontalItemSize+5)*3 : null}
|
}
|
||||||
|
pagingEnabled={props.isHorizontal}
|
||||||
|
snapToInterval={
|
||||||
|
props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,23 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
|
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import type {cardItem} from "./CardList";
|
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
item: cardItem,
|
item: ServiceItemType,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default class CardListItem extends React.Component<Props> {
|
export default class CardListItem extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
const props = this.props;
|
const {props} = this;
|
||||||
const item = 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 (
|
||||||
<Card
|
<Card
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -28,23 +26,16 @@ export default class CardListItem extends React.Component<Props> {
|
||||||
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
|
<Card.Cover style={{height: 80}} source={source} />
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,48 +3,47 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Text, TouchableRipple} from 'react-native-paper';
|
import {Text, TouchableRipple} from 'react-native-paper';
|
||||||
import {Image, View} from 'react-native';
|
import {Image, View} from 'react-native';
|
||||||
import type {cardItem} from "./CardList";
|
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
item: cardItem,
|
item: ServiceItemType,
|
||||||
width: number,
|
width: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
export default class ImageListItem extends React.Component<Props> {
|
export default class ImageListItem extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
const item = this.props.item;
|
const {props} = this;
|
||||||
const source = typeof item.image === "number"
|
const {item} = props;
|
||||||
? item.image
|
const source =
|
||||||
: {uri: item.image};
|
typeof item.image === 'number' ? item.image : {uri: item.image};
|
||||||
return (
|
return (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
style={{
|
style={{
|
||||||
width: this.props.width,
|
width: props.width,
|
||||||
height: this.props.width + 40,
|
height: props.width + 40,
|
||||||
margin: 5,
|
margin: 5,
|
||||||
}}
|
}}
|
||||||
onPress={item.onPress}
|
onPress={item.onPress}>
|
||||||
>
|
|
||||||
<View>
|
<View>
|
||||||
<Image
|
<Image
|
||||||
style={{
|
style={{
|
||||||
width: this.props.width - 20,
|
width: props.width - 20,
|
||||||
height: this.props.width - 20,
|
height: props.width - 20,
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
}}
|
}}
|
||||||
source={source}
|
source={source}
|
||||||
/>
|
/>
|
||||||
<Text style={{
|
<Text
|
||||||
|
style={{
|
||||||
marginTop: 5,
|
marginTop: 5,
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
textAlign: 'center'
|
textAlign: 'center',
|
||||||
}}>
|
}}>
|
||||||
{item.title}
|
{item.title}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -2,67 +2,21 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Card, Chip, List, Text} from 'react-native-paper';
|
import {Card, Chip, List, Text} from 'react-native-paper';
|
||||||
import {StyleSheet, View} from "react-native";
|
import {StyleSheet, View} from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
|
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||||
import {isItemInCategoryFilter} from "../../../utils/Search";
|
import {isItemInCategoryFilter} from '../../../utils/Search';
|
||||||
import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen";
|
import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
categories: Array<category>,
|
categories: Array<ClubCategoryType>,
|
||||||
onChipSelect: (id: number) => void,
|
onChipSelect: (id: number) => void,
|
||||||
selectedCategories: Array<number>,
|
selectedCategories: Array<number>,
|
||||||
}
|
|
||||||
|
|
||||||
class ClubListHeader extends React.Component<Props> {
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props) {
|
|
||||||
return nextProps.selectedCategories.length !== this.props.selectedCategories.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
getChipRender = (category: category, key: string) => {
|
|
||||||
const onPress = () => this.props.onChipSelect(category.id);
|
|
||||||
return <Chip
|
|
||||||
selected={isItemInCategoryFilter(this.props.selectedCategories, [category.id])}
|
|
||||||
mode={'outlined'}
|
|
||||||
onPress={onPress}
|
|
||||||
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
|
|
||||||
key={key}
|
|
||||||
>
|
|
||||||
{category.name}
|
|
||||||
</Chip>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
getCategoriesRender() {
|
|
||||||
let final = [];
|
|
||||||
for (let i = 0; i < this.props.categories.length; i++) {
|
|
||||||
final.push(this.getChipRender(this.props.categories[i], this.props.categories[i].id.toString()));
|
|
||||||
}
|
|
||||||
return final;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Card style={styles.card}>
|
|
||||||
<AnimatedAccordion
|
|
||||||
title={i18n.t("screens.clubs.categories")}
|
|
||||||
left={props => <List.Icon {...props} icon="star"/>}
|
|
||||||
opened={true}
|
|
||||||
>
|
|
||||||
<Text style={styles.text}>{i18n.t("screens.clubs.categoriesFilterMessage")}</Text>
|
|
||||||
<View style={styles.chipContainer}>
|
|
||||||
{this.getCategoriesRender()}
|
|
||||||
</View>
|
|
||||||
</AnimatedAccordion>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
margin: 5
|
margin: 5,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
paddingLeft: 0,
|
paddingLeft: 0,
|
||||||
|
|
@ -80,4 +34,58 @@ 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;
|
||||||
|
|
|
||||||
|
|
@ -2,79 +2,88 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
|
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import type {category, club} from "../../../screens/Amicale/Clubs/ClubListScreen";
|
import type {
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
ClubCategoryType,
|
||||||
|
ClubType,
|
||||||
|
} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||||
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
onPress: () => void,
|
onPress: () => void,
|
||||||
categoryTranslator: (id: number) => category,
|
categoryTranslator: (id: number) => ClubCategoryType,
|
||||||
item: club,
|
item: ClubType,
|
||||||
height: number,
|
height: number,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
class ClubListItem extends React.Component<Props> {
|
|
||||||
|
|
||||||
|
class ClubListItem extends React.Component<PropsType> {
|
||||||
hasManagers: boolean;
|
hasManagers: boolean;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.hasManagers = props.item.responsibles.length > 0;
|
this.hasManagers = props.item.responsibles.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
shouldComponentUpdate(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getCategoriesRender(categories: Array<number | null>) {
|
getCategoriesRender(categories: Array<number | null>): React.Node {
|
||||||
let final = [];
|
const {props} = this;
|
||||||
for (let i = 0; i < categories.length; i++) {
|
const final = [];
|
||||||
if (categories[i] !== null) {
|
categories.forEach((cat: number | null) => {
|
||||||
const category: category = this.props.categoryTranslator(categories[i]);
|
if (cat != null) {
|
||||||
|
const category: ClubCategoryType = props.categoryTranslator(cat);
|
||||||
final.push(
|
final.push(
|
||||||
<Chip
|
<Chip
|
||||||
style={{marginRight: 5, marginBottom: 5}}
|
style={{marginRight: 5, marginBottom: 5}}
|
||||||
key={this.props.item.id + ':' + category.id}
|
key={`${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() {
|
render(): React.Node {
|
||||||
const categoriesRender = this.getCategoriesRender.bind(this, this.props.item.category);
|
const {props} = this;
|
||||||
const colors = this.props.theme.colors;
|
const categoriesRender = (): React.Node =>
|
||||||
|
this.getCategoriesRender(props.item.category);
|
||||||
|
const {colors} = props.theme;
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title={this.props.item.name}
|
title={props.item.name}
|
||||||
description={categoriesRender}
|
description={categoriesRender}
|
||||||
onPress={this.props.onPress}
|
onPress={props.onPress}
|
||||||
left={(props) => <Avatar.Image
|
left={(): React.Node => (
|
||||||
{...props}
|
<Avatar.Image
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
}}
|
}}
|
||||||
size={64}
|
size={64}
|
||||||
source={{uri: this.props.item.logo}}/>}
|
source={{uri: 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={this.hasManagers ? "check-circle-outline" : "alert-circle-outline"}
|
icon={
|
||||||
|
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: this.props.height,
|
height: props.height,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,66 +2,83 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {withTheme} from 'react-native-paper';
|
import {withTheme} from 'react-native-paper';
|
||||||
import {FlatList, Image, View} from "react-native";
|
import {FlatList, Image, View} from 'react-native';
|
||||||
import DashboardEditItem from "./DashboardEditItem";
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
|
import DashboardEditItem from './DashboardEditItem';
|
||||||
import type {ServiceCategory, ServiceItem} from "../../../managers/ServicesManager";
|
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
import type {
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
ServiceCategoryType,
|
||||||
|
ServiceItemType,
|
||||||
|
} from '../../../managers/ServicesManager';
|
||||||
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
item: ServiceCategory,
|
item: ServiceCategoryType,
|
||||||
activeDashboard: Array<string>,
|
activeDashboard: Array<string>,
|
||||||
onPress: (service: ServiceItem) => void,
|
onPress: (service: ServiceItemType) => void,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
const LIST_ITEM_HEIGHT = 64;
|
const LIST_ITEM_HEIGHT = 64;
|
||||||
|
|
||||||
class DashboardEditAccordion extends React.Component<Props> {
|
class DashboardEditAccordion extends React.Component<PropsType> {
|
||||||
|
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => {
|
||||||
renderItem = ({item}: { item: ServiceItem }) => {
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<DashboardEditItem
|
<DashboardEditItem
|
||||||
height={LIST_ITEM_HEIGHT}
|
height={LIST_ITEM_HEIGHT}
|
||||||
item={item}
|
item={item}
|
||||||
isActive={this.props.activeDashboard.includes(item.key)}
|
isActive={props.activeDashboard.includes(item.key)}
|
||||||
onPress={() => this.props.onPress(item)}/>
|
onPress={() => {
|
||||||
|
props.onPress(item);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
|
getItemLayout = (
|
||||||
|
data: ?Array<ServiceItemType>,
|
||||||
|
index: number,
|
||||||
|
): {length: number, offset: number, index: number} => ({
|
||||||
|
length: LIST_ITEM_HEIGHT,
|
||||||
|
offset: LIST_ITEM_HEIGHT * index,
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
const item = this.props.item;
|
const {props} = this;
|
||||||
|
const {item} = props;
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<AnimatedAccordion
|
<AnimatedAccordion
|
||||||
title={item.title}
|
title={item.title}
|
||||||
left={props => typeof item.image === "number"
|
left={(): React.Node =>
|
||||||
? <Image
|
typeof item.image === 'number' ? (
|
||||||
{...props}
|
<Image
|
||||||
source={item.image}
|
source={item.image}
|
||||||
style={{
|
style={{
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40
|
height: 40,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
: <MaterialCommunityIcons
|
) : (
|
||||||
|
<MaterialCommunityIcons
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
name={item.image}
|
name={item.image}
|
||||||
color={this.props.theme.colors.primary}
|
color={props.theme.colors.primary}
|
||||||
size={40}/>}
|
size={40}
|
||||||
>
|
/>
|
||||||
|
)
|
||||||
|
}>
|
||||||
{/* $FlowFixMe */}
|
{/* $FlowFixMe */}
|
||||||
<FlatList
|
<FlatList
|
||||||
data={item.content}
|
data={item.content}
|
||||||
extraData={this.props.activeDashboard.toString()}
|
extraData={props.activeDashboard.toString()}
|
||||||
renderItem={this.renderItem}
|
renderItem={this.getRenderItem}
|
||||||
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.itemLayout}
|
getItemLayout={this.getItemLayout}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews
|
||||||
/>
|
/>
|
||||||
</AnimatedAccordion>
|
</AnimatedAccordion>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -69,4 +86,4 @@ class DashboardEditAccordion extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(DashboardEditAccordion)
|
export default withTheme(DashboardEditAccordion);
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,57 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Image} from "react-native";
|
import {Image} from 'react-native';
|
||||||
import {List, withTheme} from 'react-native-paper';
|
import {List, withTheme} from 'react-native-paper';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
import type {ServiceItem} from "../../../managers/ServicesManager";
|
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
item: ServiceItem,
|
item: ServiceItemType,
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
height: number,
|
height: number,
|
||||||
onPress: () => void,
|
onPress: () => void,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
|
};
|
||||||
|
|
||||||
|
class DashboardEditItem extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
|
const {isActive} = this.props;
|
||||||
|
return nextProps.isActive !== isActive;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DashboardEditItem extends React.Component<Props> {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
shouldComponentUpdate(nextProps: Props) {
|
|
||||||
return (nextProps.isActive !== this.props.isActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title={this.props.item.title}
|
title={props.item.title}
|
||||||
description={this.props.item.subtitle}
|
description={props.item.subtitle}
|
||||||
onPress={this.props.isActive ? null : this.props.onPress}
|
onPress={props.isActive ? null : props.onPress}
|
||||||
left={props =>
|
left={(): React.Node => (
|
||||||
<Image
|
<Image
|
||||||
{...props}
|
source={{uri: props.item.image}}
|
||||||
source={{uri: this.props.item.image}}
|
|
||||||
style={{
|
style={{
|
||||||
width: 40,
|
width: 40,
|
||||||
height: 40
|
height: 40,
|
||||||
}}
|
}}
|
||||||
/>}
|
/>
|
||||||
right={props => this.props.isActive
|
)}
|
||||||
? <List.Icon
|
right={({size}: {size: number}): React.Node =>
|
||||||
{...props}
|
props.isActive ? (
|
||||||
icon={"check"}
|
<List.Icon
|
||||||
color={this.props.theme.colors.success}
|
size={size}
|
||||||
/> : null}
|
icon="check"
|
||||||
|
color={props.theme.colors.success}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
height: this.props.height,
|
height: props.height,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
paddingLeft: 30,
|
paddingLeft: 30,
|
||||||
backgroundColor: this.props.isActive ? this.props.theme.colors.proxiwashFinishedColor : "transparent"
|
backgroundColor: props.isActive
|
||||||
|
? props.theme.colors.proxiwashFinishedColor
|
||||||
|
: 'transparent',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,57 +2,57 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {TouchableRipple, withTheme} from 'react-native-paper';
|
import {TouchableRipple, withTheme} from 'react-native-paper';
|
||||||
import {Dimensions, Image, View} from "react-native";
|
import {Dimensions, Image, View} from 'react-native';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
image: string,
|
image: string,
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
onPress: () => void,
|
onPress: () => void,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component used to render a small dashboard item
|
* Component used to render a small dashboard item
|
||||||
*/
|
*/
|
||||||
class DashboardEditPreviewItem extends React.Component<Props> {
|
class DashboardEditPreviewItem extends React.Component<PropsType> {
|
||||||
|
|
||||||
itemSize: number;
|
itemSize: number;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.itemSize = Dimensions.get('window').width / 8;
|
this.itemSize = Dimensions.get('window').width / 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
const props = this.props;
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
onPress={this.props.onPress}
|
onPress={props.onPress}
|
||||||
borderless={true}
|
borderless
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 5,
|
marginLeft: 5,
|
||||||
marginRight: 5,
|
marginRight: 5,
|
||||||
backgroundColor: this.props.isActive ? this.props.theme.colors.textDisabled : "transparent",
|
backgroundColor: props.isActive
|
||||||
borderRadius: 5
|
? props.theme.colors.textDisabled
|
||||||
}}
|
: '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);
|
||||||
|
|
|
||||||
|
|
@ -2,46 +2,48 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, List, withTheme} from 'react-native-paper';
|
import {Avatar, List, withTheme} from 'react-native-paper';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import i18n from 'i18n-js';
|
||||||
import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import i18n from "i18n-js";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
|
||||||
import {
|
import {
|
||||||
getFirstEquipmentAvailability,
|
getFirstEquipmentAvailability,
|
||||||
getRelativeDateString,
|
getRelativeDateString,
|
||||||
isEquipmentAvailable
|
isEquipmentAvailable,
|
||||||
} from "../../../utils/EquipmentBooking";
|
} from '../../../utils/EquipmentBooking';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
userDeviceRentDates: [string, string],
|
userDeviceRentDates: [string, string],
|
||||||
item: Device,
|
item: DeviceType,
|
||||||
height: number,
|
height: number,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
|
};
|
||||||
|
|
||||||
|
class EquipmentListItem extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
|
const {userDeviceRentDates} = this.props;
|
||||||
|
return nextProps.userDeviceRentDates !== userDeviceRentDates;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EquipmentListItem extends React.Component<Props> {
|
render(): React.Node {
|
||||||
|
const {item, userDeviceRentDates, navigation, height, theme} = this.props;
|
||||||
shouldComponentUpdate(nextProps: Props): boolean {
|
|
||||||
return nextProps.userDeviceRentDates !== this.props.userDeviceRentDates;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
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 = () => this.props.navigation.navigate("equipment-confirm", {
|
onPress = () => {
|
||||||
item: item,
|
navigation.navigate('equipment-confirm', {
|
||||||
dates: userDeviceRentDates
|
item,
|
||||||
|
dates: userDeviceRentDates,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
else
|
else
|
||||||
onPress = () => this.props.navigation.navigate("equipment-rent", {item: item});
|
onPress = () => {
|
||||||
|
navigation.navigate('equipment-rent', {item});
|
||||||
|
};
|
||||||
|
|
||||||
let description;
|
let description;
|
||||||
if (isRented) {
|
if (isRented) {
|
||||||
|
|
@ -50,58 +52,57 @@ class EquipmentListItem extends React.Component<Props> {
|
||||||
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', {date: getRelativeDateString(firstAvailability)});
|
description = i18n.t('screens.equipment.available', {
|
||||||
|
date: getRelativeDateString(firstAvailability),
|
||||||
|
});
|
||||||
|
|
||||||
let icon;
|
let icon;
|
||||||
if (isRented)
|
if (isRented) icon = 'bookmark-check';
|
||||||
icon = "bookmark-check";
|
else if (isAvailable) icon = 'check-circle-outline';
|
||||||
else if (isAvailable)
|
else icon = 'update';
|
||||||
icon = "check-circle-outline";
|
|
||||||
else
|
|
||||||
icon = "update";
|
|
||||||
|
|
||||||
let color;
|
let color;
|
||||||
if (isRented)
|
if (isRented) color = theme.colors.warning;
|
||||||
color = colors.warning;
|
else if (isAvailable) color = theme.colors.success;
|
||||||
else if (isAvailable)
|
else color = theme.colors.primary;
|
||||||
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={(props) => <Avatar.Icon
|
left={({size}: {size: number}): React.Node => (
|
||||||
{...props}
|
<Avatar.Icon
|
||||||
|
size={size}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
}}
|
}}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
color={color}
|
color={color}
|
||||||
/>}
|
/>
|
||||||
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={"chevron-right"}
|
icon="chevron-right"
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
height: this.props.height,
|
height,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,91 +2,111 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {List, withTheme} from 'react-native-paper';
|
import {List, withTheme} from 'react-native-paper';
|
||||||
import {FlatList, View} from "react-native";
|
import {FlatList, View} from 'react-native';
|
||||||
import {stringMatchQuery} from "../../../utils/Search";
|
import {stringMatchQuery} from '../../../utils/Search';
|
||||||
import GroupListItem from "./GroupListItem";
|
import GroupListItem from './GroupListItem';
|
||||||
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
|
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||||
import type {group, groupCategory} from "../../../screens/Planex/GroupSelectionScreen";
|
import type {
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
PlanexGroupType,
|
||||||
|
PlanexGroupCategoryType,
|
||||||
|
} from '../../../screens/Planex/GroupSelectionScreen';
|
||||||
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
item: groupCategory,
|
item: PlanexGroupCategoryType,
|
||||||
onGroupPress: (group) => void,
|
favorites: Array<PlanexGroupType>,
|
||||||
onFavoritePress: (group) => void,
|
onGroupPress: (PlanexGroupType) => void,
|
||||||
|
onFavoritePress: (PlanexGroupType) => void,
|
||||||
currentSearchString: string,
|
currentSearchString: string,
|
||||||
favoriteNumber: number,
|
|
||||||
height: number,
|
height: number,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
const LIST_ITEM_HEIGHT = 64;
|
const LIST_ITEM_HEIGHT = 64;
|
||||||
|
const REPLACE_REGEX = /_/g;
|
||||||
|
|
||||||
class GroupListAccordion extends React.Component<Props> {
|
class GroupListAccordion extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
shouldComponentUpdate(nextProps: Props) {
|
const {props} = this;
|
||||||
return (nextProps.currentSearchString !== this.props.currentSearchString)
|
return (
|
||||||
|| (nextProps.favoriteNumber !== this.props.favoriteNumber)
|
nextProps.currentSearchString !== props.currentSearchString ||
|
||||||
|| (nextProps.item.content.length !== this.props.item.content.length);
|
nextProps.favorites.length !== props.favorites.length ||
|
||||||
|
nextProps.item.content.length !== props.item.content.length
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
keyExtractor = (item: group) => item.id.toString();
|
getRenderItem = ({item}: {item: PlanexGroupType}): React.Node => {
|
||||||
|
const {props} = this;
|
||||||
renderItem = ({item}: { item: group }) => {
|
const onPress = () => {
|
||||||
const onPress = () => this.props.onGroupPress(item);
|
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() {
|
getData(): Array<PlanexGroupType> {
|
||||||
const originalData = this.props.item.content;
|
const {props} = this;
|
||||||
let displayData = [];
|
const originalData = props.item.content;
|
||||||
for (let i = 0; i < originalData.length; i++) {
|
const displayData = [];
|
||||||
if (stringMatchQuery(originalData[i].name, this.props.currentSearchString))
|
originalData.forEach((data: PlanexGroupType) => {
|
||||||
displayData.push(originalData[i]);
|
if (stringMatchQuery(data.name, props.currentSearchString))
|
||||||
}
|
displayData.push(data);
|
||||||
|
});
|
||||||
return displayData;
|
return displayData;
|
||||||
}
|
}
|
||||||
|
|
||||||
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
|
itemLayout = (
|
||||||
|
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() {
|
render(): React.Node {
|
||||||
const item = this.props.item;
|
const {props} = this;
|
||||||
|
const {item} = this.props;
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<AnimatedAccordion
|
<AnimatedAccordion
|
||||||
title={item.name}
|
title={item.name.replace(REPLACE_REGEX, ' ')}
|
||||||
style={{
|
style={{
|
||||||
height: this.props.height,
|
height: props.height,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
left={props =>
|
left={({size}: {size: number}): React.Node =>
|
||||||
item.id === 0
|
item.id === 0 ? (
|
||||||
? <List.Icon
|
<List.Icon
|
||||||
{...props}
|
size={size}
|
||||||
icon={"star"}
|
icon="star"
|
||||||
color={this.props.theme.colors.tetrisScore}
|
color={props.theme.colors.tetrisScore}
|
||||||
/>
|
/>
|
||||||
: null}
|
) : null
|
||||||
unmountWhenCollapsed={true}// Only render list if expanded for increased performance
|
}
|
||||||
opened={this.props.item.id === 0 || this.props.currentSearchString.length > 0}
|
unmountWhenCollapsed={item.id !== 0} // Only render list if expanded for increased performance
|
||||||
>
|
opened={props.currentSearchString.length > 0}>
|
||||||
{/*$FlowFixMe*/}
|
|
||||||
<FlatList
|
<FlatList
|
||||||
data={this.getData()}
|
data={this.getData()}
|
||||||
extraData={this.props.currentSearchString}
|
extraData={props.currentSearchString + props.favorites.length}
|
||||||
renderItem={this.renderItem}
|
renderItem={this.getRenderItem}
|
||||||
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={true}
|
removeClippedSubviews
|
||||||
/>
|
/>
|
||||||
</AnimatedAccordion>
|
</AnimatedAccordion>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -94,4 +114,4 @@ class GroupListAccordion extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(GroupListAccordion)
|
export default withTheme(GroupListAccordion);
|
||||||
|
|
|
||||||
|
|
@ -2,60 +2,80 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {IconButton, List, withTheme} from 'react-native-paper';
|
import {IconButton, List, withTheme} from 'react-native-paper';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import * as Animatable from 'react-native-animatable';
|
||||||
import type {group} from "../../../screens/Planex/GroupSelectionScreen";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
onPress: () => void,
|
onPress: () => void,
|
||||||
onStarPress: () => void,
|
onStarPress: () => void,
|
||||||
item: group,
|
item: PlanexGroupType,
|
||||||
|
favorites: Array<PlanexGroupType>,
|
||||||
height: number,
|
height: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
const REPLACE_REGEX = /_/g;
|
||||||
isFav: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
class GroupListItem extends React.Component<Props, State> {
|
class GroupListItem extends React.Component<PropsType> {
|
||||||
|
isFav: boolean;
|
||||||
|
|
||||||
constructor(props) {
|
starRef = {current: null | IconButton};
|
||||||
|
|
||||||
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.isFav = this.isGroupInFavorites(props.favorites);
|
||||||
isFav: (props.item.isFav !== undefined && props.item.isFav),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(prevProps: Props, prevState: State) {
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
return (prevState.isFav !== this.state.isFav);
|
const {favorites} = this.props;
|
||||||
|
const favChanged = favorites.length !== nextProps.favorites.length;
|
||||||
|
let newFavState = this.isFav;
|
||||||
|
if (favChanged) newFavState = this.isGroupInFavorites(nextProps.favorites);
|
||||||
|
const shouldUpdate = this.isFav !== newFavState;
|
||||||
|
this.isFav = newFavState;
|
||||||
|
return shouldUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
isGroupInFavorites(favorites: Array<PlanexGroupType>): boolean {
|
||||||
|
const {item} = this.props;
|
||||||
|
for (let i = 0; i < favorites.length; i += 1) {
|
||||||
|
if (favorites[i].id === item.id) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
onStarPress = () => {
|
onStarPress = () => {
|
||||||
this.setState({isFav: !this.state.isFav});
|
const {props} = this;
|
||||||
this.props.onStarPress();
|
if (this.starRef.current != null) {
|
||||||
|
if (this.isFav) this.starRef.current.rubberBand();
|
||||||
|
else this.starRef.current.swing();
|
||||||
}
|
}
|
||||||
|
props.onStarPress();
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
const colors = this.props.theme.colors;
|
const {props} = this;
|
||||||
|
const {colors} = props.theme;
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title={this.props.item.name}
|
title={props.item.name.replace(REPLACE_REGEX, ' ')}
|
||||||
onPress={this.props.onPress}
|
onPress={props.onPress}
|
||||||
left={props =>
|
left={({size}: {size: number}): React.Node => (
|
||||||
<List.Icon
|
<List.Icon size={size} icon="chevron-right" />
|
||||||
{...props}
|
)}
|
||||||
icon={"chevron-right"}/>}
|
right={({size, color}: {size: number, color: string}): React.Node => (
|
||||||
right={props =>
|
<Animatable.View ref={this.starRef} useNativeDriver>
|
||||||
<IconButton
|
<IconButton
|
||||||
{...props}
|
size={size}
|
||||||
icon={"star"}
|
icon="star"
|
||||||
onPress={this.onStarPress}
|
onPress={this.onStarPress}
|
||||||
color={this.state.isFav
|
color={this.isFav ? colors.tetrisScore : color}
|
||||||
? colors.tetrisScore
|
/>
|
||||||
: props.color}
|
</Animatable.View>
|
||||||
/>}
|
)}
|
||||||
style={{
|
style={{
|
||||||
height: this.props.height,
|
height: props.height,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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 Props = {
|
type PropsType = {
|
||||||
onPress: Function,
|
onPress: () => void,
|
||||||
color: string,
|
color: string,
|
||||||
item: Object,
|
item: ProximoArticleType,
|
||||||
height: number,
|
height: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
class ProximoListItem extends React.Component<Props> {
|
class ProximoListItem extends React.Component<PropsType> {
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
colors: Object;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.colors = props.theme.colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title={this.props.item.name}
|
title={props.item.name}
|
||||||
description={this.props.item.quantity + ' ' + i18n.t('screens.proximo.inStock')}
|
description={`${props.item.quantity} ${i18n.t(
|
||||||
descriptionStyle={{color: this.props.color}}
|
'screens.proximo.inStock',
|
||||||
onPress={this.props.onPress}
|
)}`}
|
||||||
left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64}
|
descriptionStyle={{color: props.color}}
|
||||||
source={{uri: this.props.item.image}}/>}
|
onPress={props.onPress}
|
||||||
right={() =>
|
left={(): React.Node => (
|
||||||
<Text style={{fontWeight: "bold"}}>
|
<Avatar.Image
|
||||||
{this.props.item.price}€
|
style={{backgroundColor: 'transparent'}}
|
||||||
</Text>}
|
size={64}
|
||||||
|
source={{uri: props.item.image}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
right={(): React.Node => (
|
||||||
|
<Text style={{fontWeight: 'bold'}}>{props.item.price}€</Text>
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
height: this.props.height,
|
height: props.height,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,188 +1,46 @@
|
||||||
import * as React from 'react';
|
// @flow
|
||||||
import {Avatar, Caption, List, ProgressBar, Surface, Text, withTheme} from 'react-native-paper';
|
|
||||||
import {StyleSheet, View} from "react-native";
|
|
||||||
import ProxiwashConstants from "../../../constants/ProxiwashConstants";
|
|
||||||
import i18n from "i18n-js";
|
|
||||||
import AprilFoolsManager from "../../../managers/AprilFoolsManager";
|
|
||||||
import * as Animatable from "react-native-animatable";
|
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
|
||||||
import type {Machine} from "../../../screens/Proxiwash/ProxiwashScreen";
|
|
||||||
|
|
||||||
type Props = {
|
import * as React from 'react';
|
||||||
item: Machine,
|
import {
|
||||||
theme: CustomTheme,
|
Avatar,
|
||||||
onPress: Function,
|
Caption,
|
||||||
|
List,
|
||||||
|
ProgressBar,
|
||||||
|
Surface,
|
||||||
|
Text,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import {StyleSheet, View} from 'react-native';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import * as Animatable from 'react-native-animatable';
|
||||||
|
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
|
||||||
|
import AprilFoolsManager from '../../../managers/AprilFoolsManager';
|
||||||
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {ProxiwashMachineType} from '../../../screens/Proxiwash/ProxiwashScreen';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
item: ProxiwashMachineType,
|
||||||
|
theme: CustomThemeType,
|
||||||
|
onPress: (
|
||||||
|
title: string,
|
||||||
|
item: ProxiwashMachineType,
|
||||||
|
isDryer: boolean,
|
||||||
|
) => 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);
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component used to display a proxiwash item, showing machine progression and state
|
|
||||||
*/
|
|
||||||
class ProxiwashListItem extends React.Component<Props> {
|
|
||||||
|
|
||||||
stateColors: Object;
|
|
||||||
stateStrings: Object;
|
|
||||||
|
|
||||||
title: string;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.stateColors = {};
|
|
||||||
this.stateStrings = {};
|
|
||||||
|
|
||||||
this.updateStateStrings();
|
|
||||||
|
|
||||||
let displayNumber = props.item.number;
|
|
||||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
|
|
||||||
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(parseInt(props.item.number));
|
|
||||||
|
|
||||||
this.title = props.isDryer
|
|
||||||
? i18n.t('screens.proxiwash.dryer')
|
|
||||||
: i18n.t('screens.proxiwash.washer');
|
|
||||||
this.title += ' n°' + displayNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props): boolean {
|
|
||||||
const props = this.props;
|
|
||||||
return (nextProps.theme.dark !== props.theme.dark)
|
|
||||||
|| (nextProps.item.state !== props.item.state)
|
|
||||||
|| (nextProps.item.donePercent !== props.item.donePercent)
|
|
||||||
|| (nextProps.isWatched !== props.isWatched);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStateStrings() {
|
|
||||||
this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t('screens.proxiwash.states.ready');
|
|
||||||
this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t('screens.proxiwash.states.running');
|
|
||||||
this.stateStrings[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = i18n.t('screens.proxiwash.states.runningNotStarted');
|
|
||||||
this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t('screens.proxiwash.states.finished');
|
|
||||||
this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t('screens.proxiwash.states.broken');
|
|
||||||
this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t('screens.proxiwash.states.error');
|
|
||||||
this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t('screens.proxiwash.states.unknown');
|
|
||||||
}
|
|
||||||
|
|
||||||
updateStateColors() {
|
|
||||||
const colors = this.props.theme.colors;
|
|
||||||
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] = colors.proxiwashReadyColor;
|
|
||||||
this.stateColors[ProxiwashConstants.machineStates.RUNNING] = colors.proxiwashRunningColor;
|
|
||||||
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] = colors.proxiwashRunningNotStartedColor;
|
|
||||||
this.stateColors[ProxiwashConstants.machineStates.FINISHED] = colors.proxiwashFinishedColor;
|
|
||||||
this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] = colors.proxiwashBrokenColor;
|
|
||||||
this.stateColors[ProxiwashConstants.machineStates.ERROR] = colors.proxiwashErrorColor;
|
|
||||||
this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] = colors.proxiwashUnknownColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
onListItemPress = () => this.props.onPress(this.title, this.props.item, this.props.isDryer);
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const props = this.props;
|
|
||||||
const colors = props.theme.colors;
|
|
||||||
const machineState = props.item.state;
|
|
||||||
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING;
|
|
||||||
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE;
|
|
||||||
const description = isRunning ? props.item.startTime + '/' + props.item.endTime : '';
|
|
||||||
const stateIcon = ProxiwashConstants.stateIcons[machineState];
|
|
||||||
const stateString = this.stateStrings[machineState];
|
|
||||||
const progress = isRunning
|
|
||||||
? props.item.donePercent !== ''
|
|
||||||
? parseFloat(props.item.donePercent) / 100
|
|
||||||
: 0
|
|
||||||
: 1;
|
|
||||||
|
|
||||||
const icon = props.isWatched
|
|
||||||
? <AnimatedIcon
|
|
||||||
icon={'bell-ring'}
|
|
||||||
animation={"rubberBand"}
|
|
||||||
useNativeDriver
|
|
||||||
size={50}
|
|
||||||
color={colors.primary}
|
|
||||||
style={styles.icon}
|
|
||||||
/>
|
|
||||||
: <AnimatedIcon
|
|
||||||
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
|
|
||||||
animation={isRunning ? "pulse" : undefined}
|
|
||||||
iterationCount={"infinite"}
|
|
||||||
easing={"linear"}
|
|
||||||
duration={1000}
|
|
||||||
useNativeDriver
|
|
||||||
size={40}
|
|
||||||
color={colors.text}
|
|
||||||
style={styles.icon}
|
|
||||||
/>;
|
|
||||||
this.updateStateColors();
|
|
||||||
return (
|
|
||||||
<Surface
|
|
||||||
style={{
|
|
||||||
...styles.container,
|
|
||||||
height: props.height,
|
|
||||||
borderRadius: 4,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!isReady
|
|
||||||
? <ProgressBar
|
|
||||||
style={{
|
|
||||||
...styles.progressBar,
|
|
||||||
height: props.height
|
|
||||||
}}
|
|
||||||
progress={progress}
|
|
||||||
color={this.stateColors[machineState]}
|
|
||||||
/>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
<List.Item
|
|
||||||
title={this.title}
|
|
||||||
description={description}
|
|
||||||
style={{
|
|
||||||
height: props.height,
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
onPress={this.onListItemPress}
|
|
||||||
left={() => icon}
|
|
||||||
right={() => (
|
|
||||||
<View style={{flexDirection: 'row',}}>
|
|
||||||
<View style={{justifyContent: 'center',}}>
|
|
||||||
<Text style={
|
|
||||||
machineState === ProxiwashConstants.machineStates.FINISHED ?
|
|
||||||
{fontWeight: 'bold',} : {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{stateString}
|
|
||||||
</Text>
|
|
||||||
{
|
|
||||||
machineState === ProxiwashConstants.machineStates.RUNNING
|
|
||||||
? <Caption>{props.item.remainingTime} min</Caption>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
|
|
||||||
</View>
|
|
||||||
<View style={{justifyContent: 'center',}}>
|
|
||||||
<Avatar.Icon
|
|
||||||
icon={stateIcon}
|
|
||||||
color={colors.text}
|
|
||||||
size={30}
|
|
||||||
style={styles.icon}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>)}
|
|
||||||
/>
|
|
||||||
</Surface>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
margin: 5,
|
margin: 5,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
elevation: 1
|
elevation: 1,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
backgroundColor: 'transparent'
|
backgroundColor: 'transparent',
|
||||||
},
|
},
|
||||||
progressBar: {
|
progressBar: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -191,4 +49,188 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component used to display a proxiwash item, showing machine progression and state
|
||||||
|
*/
|
||||||
|
class ProxiwashListItem extends React.Component<PropsType> {
|
||||||
|
stateColors: {[key: string]: string};
|
||||||
|
|
||||||
|
stateStrings: {[key: string]: string};
|
||||||
|
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
constructor(props: PropsType) {
|
||||||
|
super(props);
|
||||||
|
this.stateColors = {};
|
||||||
|
this.stateStrings = {};
|
||||||
|
|
||||||
|
this.updateStateStrings();
|
||||||
|
|
||||||
|
let displayNumber = props.item.number;
|
||||||
|
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
|
||||||
|
displayNumber = AprilFoolsManager.getProxiwashMachineDisplayNumber(
|
||||||
|
parseInt(props.item.number, 10),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.title = props.isDryer
|
||||||
|
? i18n.t('screens.proxiwash.dryer')
|
||||||
|
: i18n.t('screens.proxiwash.washer');
|
||||||
|
this.title += ` n°${displayNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
|
const {props} = this;
|
||||||
|
return (
|
||||||
|
nextProps.theme.dark !== props.theme.dark ||
|
||||||
|
nextProps.item.state !== props.item.state ||
|
||||||
|
nextProps.item.donePercent !== props.item.donePercent ||
|
||||||
|
nextProps.isWatched !== props.isWatched
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onListItemPress = () => {
|
||||||
|
const {props} = this;
|
||||||
|
props.onPress(this.title, props.item, props.isDryer);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateStateStrings() {
|
||||||
|
this.stateStrings[ProxiwashConstants.machineStates.AVAILABLE] = i18n.t(
|
||||||
|
'screens.proxiwash.states.ready',
|
||||||
|
);
|
||||||
|
this.stateStrings[ProxiwashConstants.machineStates.RUNNING] = i18n.t(
|
||||||
|
'screens.proxiwash.states.running',
|
||||||
|
);
|
||||||
|
this.stateStrings[
|
||||||
|
ProxiwashConstants.machineStates.RUNNING_NOT_STARTED
|
||||||
|
] = i18n.t('screens.proxiwash.states.runningNotStarted');
|
||||||
|
this.stateStrings[ProxiwashConstants.machineStates.FINISHED] = i18n.t(
|
||||||
|
'screens.proxiwash.states.finished',
|
||||||
|
);
|
||||||
|
this.stateStrings[ProxiwashConstants.machineStates.UNAVAILABLE] = i18n.t(
|
||||||
|
'screens.proxiwash.states.broken',
|
||||||
|
);
|
||||||
|
this.stateStrings[ProxiwashConstants.machineStates.ERROR] = i18n.t(
|
||||||
|
'screens.proxiwash.states.error',
|
||||||
|
);
|
||||||
|
this.stateStrings[ProxiwashConstants.machineStates.UNKNOWN] = i18n.t(
|
||||||
|
'screens.proxiwash.states.unknown',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStateColors() {
|
||||||
|
const {props} = this;
|
||||||
|
const {colors} = props.theme;
|
||||||
|
this.stateColors[ProxiwashConstants.machineStates.AVAILABLE] =
|
||||||
|
colors.proxiwashReadyColor;
|
||||||
|
this.stateColors[ProxiwashConstants.machineStates.RUNNING] =
|
||||||
|
colors.proxiwashRunningColor;
|
||||||
|
this.stateColors[ProxiwashConstants.machineStates.RUNNING_NOT_STARTED] =
|
||||||
|
colors.proxiwashRunningNotStartedColor;
|
||||||
|
this.stateColors[ProxiwashConstants.machineStates.FINISHED] =
|
||||||
|
colors.proxiwashFinishedColor;
|
||||||
|
this.stateColors[ProxiwashConstants.machineStates.UNAVAILABLE] =
|
||||||
|
colors.proxiwashBrokenColor;
|
||||||
|
this.stateColors[ProxiwashConstants.machineStates.ERROR] =
|
||||||
|
colors.proxiwashErrorColor;
|
||||||
|
this.stateColors[ProxiwashConstants.machineStates.UNKNOWN] =
|
||||||
|
colors.proxiwashUnknownColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
|
const {colors} = props.theme;
|
||||||
|
const machineState = props.item.state;
|
||||||
|
const isRunning = machineState === ProxiwashConstants.machineStates.RUNNING;
|
||||||
|
const isReady = machineState === ProxiwashConstants.machineStates.AVAILABLE;
|
||||||
|
const description = isRunning
|
||||||
|
? `${props.item.startTime}/${props.item.endTime}`
|
||||||
|
: '';
|
||||||
|
const stateIcon = ProxiwashConstants.stateIcons[machineState];
|
||||||
|
const stateString = this.stateStrings[machineState];
|
||||||
|
let progress;
|
||||||
|
if (isRunning && props.item.donePercent !== '')
|
||||||
|
progress = parseFloat(props.item.donePercent) / 100;
|
||||||
|
else if (isRunning) progress = 0;
|
||||||
|
else progress = 1;
|
||||||
|
|
||||||
|
const icon = props.isWatched ? (
|
||||||
|
<AnimatedIcon
|
||||||
|
icon="bell-ring"
|
||||||
|
animation="rubberBand"
|
||||||
|
useNativeDriver
|
||||||
|
size={50}
|
||||||
|
color={colors.primary}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AnimatedIcon
|
||||||
|
icon={props.isDryer ? 'tumble-dryer' : 'washing-machine'}
|
||||||
|
animation={isRunning ? 'pulse' : undefined}
|
||||||
|
iterationCount="infinite"
|
||||||
|
easing="linear"
|
||||||
|
duration={1000}
|
||||||
|
useNativeDriver
|
||||||
|
size={40}
|
||||||
|
color={colors.text}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
this.updateStateColors();
|
||||||
|
return (
|
||||||
|
<Surface
|
||||||
|
style={{
|
||||||
|
...styles.container,
|
||||||
|
height: props.height,
|
||||||
|
borderRadius: 4,
|
||||||
|
}}>
|
||||||
|
{!isReady ? (
|
||||||
|
<ProgressBar
|
||||||
|
style={{
|
||||||
|
...styles.progressBar,
|
||||||
|
height: props.height,
|
||||||
|
}}
|
||||||
|
progress={progress}
|
||||||
|
color={this.stateColors[machineState]}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<List.Item
|
||||||
|
title={this.title}
|
||||||
|
description={description}
|
||||||
|
style={{
|
||||||
|
height: props.height,
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
onPress={this.onListItemPress}
|
||||||
|
left={(): React.Node => icon}
|
||||||
|
right={(): React.Node => (
|
||||||
|
<View style={{flexDirection: 'row'}}>
|
||||||
|
<View style={{justifyContent: 'center'}}>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
machineState === ProxiwashConstants.machineStates.FINISHED
|
||||||
|
? {fontWeight: 'bold'}
|
||||||
|
: {}
|
||||||
|
}>
|
||||||
|
{stateString}
|
||||||
|
</Text>
|
||||||
|
{machineState === ProxiwashConstants.machineStates.RUNNING ? (
|
||||||
|
<Caption>{props.item.remainingTime} min</Caption>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
<View style={{justifyContent: 'center'}}>
|
||||||
|
<Avatar.Icon
|
||||||
|
icon={stateIcon}
|
||||||
|
color={colors.text}
|
||||||
|
size={30}
|
||||||
|
style={styles.icon}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Surface>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default withTheme(ProxiwashListItem);
|
export default withTheme(ProxiwashListItem);
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,17 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Text, withTheme} from 'react-native-paper';
|
import {Avatar, Text, withTheme} from 'react-native-paper';
|
||||||
import {StyleSheet, View} from "react-native";
|
import {StyleSheet, View} from 'react-native';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
|
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: {
|
||||||
|
|
@ -61,12 +22,51 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
type Props = {
|
export type AnimatableViewRefType = {current: null | Animatable.View};
|
||||||
style?: ViewStyle,
|
|
||||||
emotion: number,
|
|
||||||
animated: boolean,
|
|
||||||
entryAnimation: Animatable.AnimatableProperties | null,
|
|
||||||
loopAnimation: Animatable.AnimatableProperties | null,
|
|
||||||
onPress?: (viewRef: AnimatableViewRef) => null,
|
|
||||||
onLongPress?: (viewRef: AnimatableViewRef) => null,
|
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
type PropsType = {
|
||||||
|
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,
|
||||||
}
|
};
|
||||||
|
|
||||||
export type AnimatableViewRef = {current: null | Animatable.View};
|
const MASCOT_IMAGE = require('../../../assets/mascot/mascot.png');
|
||||||
|
const MASCOT_EYES_NORMAL = require('../../../assets/mascot/mascot_eyes_normal.png');
|
||||||
const MASCOT_IMAGE = require("../../../assets/mascot/mascot.png");
|
const MASCOT_EYES_GIRLY = require('../../../assets/mascot/mascot_eyes_girly.png');
|
||||||
const MASCOT_EYES_NORMAL = require("../../../assets/mascot/mascot_eyes_normal.png");
|
const MASCOT_EYES_CUTE = require('../../../assets/mascot/mascot_eyes_cute.png');
|
||||||
const MASCOT_EYES_GIRLY = require("../../../assets/mascot/mascot_eyes_girly.png");
|
const MASCOT_EYES_WINK = require('../../../assets/mascot/mascot_eyes_wink.png');
|
||||||
const MASCOT_EYES_CUTE = require("../../../assets/mascot/mascot_eyes_cute.png");
|
const MASCOT_EYES_HEART = require('../../../assets/mascot/mascot_eyes_heart.png');
|
||||||
const MASCOT_EYES_WINK = require("../../../assets/mascot/mascot_eyes_wink.png");
|
const MASCOT_EYES_ANGRY = require('../../../assets/mascot/mascot_eyes_angry.png');
|
||||||
const MASCOT_EYES_HEART = require("../../../assets/mascot/mascot_eyes_heart.png");
|
const MASCOT_GLASSES = require('../../../assets/mascot/mascot_glasses.png');
|
||||||
const MASCOT_EYES_ANGRY = require("../../../assets/mascot/mascot_eyes_angry.png");
|
const MASCOT_SUNGLASSES = require('../../../assets/mascot/mascot_sunglasses.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',
|
||||||
},
|
},
|
||||||
clickAnimation: {
|
onPress: null,
|
||||||
useNativeDriver: true,
|
onLongPress: null,
|
||||||
animation: "rubberBand",
|
};
|
||||||
duration: 2000,
|
|
||||||
},
|
viewRef: AnimatableViewRefType;
|
||||||
}
|
|
||||||
|
|
||||||
viewRef: AnimatableViewRef;
|
|
||||||
eyeList: {[key: number]: number | string};
|
eyeList: {[key: number]: number | string};
|
||||||
|
|
||||||
glassesList: {[key: number]: number | string};
|
glassesList: {[key: number]: number | string};
|
||||||
|
|
||||||
onPress: (viewRef: AnimatableViewRef) => null;
|
onPress: (viewRef: AnimatableViewRefType) => void;
|
||||||
onLongPress: (viewRef: AnimatableViewRef) => null;
|
|
||||||
|
onLongPress: (viewRef: AnimatableViewRefType) => void;
|
||||||
|
|
||||||
initialEmotion: number;
|
initialEmotion: number;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.viewRef = React.createRef();
|
this.viewRef = React.createRef();
|
||||||
this.eyeList = {};
|
this.eyeList = {};
|
||||||
|
|
@ -106,87 +106,94 @@ class Mascot extends React.Component<Props, State> {
|
||||||
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.props.emotion;
|
this.initialEmotion =
|
||||||
|
props.emotion != null ? props.emotion : Mascot.defaultProps.emotion;
|
||||||
|
|
||||||
if (this.initialEmotion === MASCOT_STYLE.RANDOM)
|
if (this.initialEmotion === MASCOT_STYLE.RANDOM)
|
||||||
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
|
this.initialEmotion = Math.floor(Math.random() * MASCOT_STYLE.ANGRY) + 1;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
currentEmotion: this.initialEmotion
|
currentEmotion: this.initialEmotion,
|
||||||
}
|
};
|
||||||
|
|
||||||
if (this.props.onPress == null) {
|
if (props.onPress == null) {
|
||||||
this.onPress = (viewRef: AnimatableViewRef) => {
|
this.onPress = (viewRef: AnimatableViewRefType) => {
|
||||||
let ref = viewRef.current;
|
const ref = viewRef.current;
|
||||||
if (ref != null) {
|
if (ref != null) {
|
||||||
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
|
this.setState({currentEmotion: MASCOT_STYLE.LOVE});
|
||||||
ref.rubberBand(1500).then(() => {
|
ref.rubberBand(1500).then(() => {
|
||||||
this.setState({currentEmotion: this.initialEmotion});
|
this.setState({currentEmotion: this.initialEmotion});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
return null;
|
};
|
||||||
}
|
} else this.onPress = props.onPress;
|
||||||
} else
|
|
||||||
this.onPress = this.props.onPress;
|
|
||||||
|
|
||||||
if (this.props.onLongPress == null) {
|
if (props.onLongPress == null) {
|
||||||
this.onLongPress = (viewRef: AnimatableViewRef) => {
|
this.onLongPress = (viewRef: AnimatableViewRefType) => {
|
||||||
let ref = viewRef.current;
|
const ref = viewRef.current;
|
||||||
if (ref != null) {
|
if (ref != null) {
|
||||||
this.setState({currentEmotion: MASCOT_STYLE.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) {
|
getGlasses(style: number): React.Node {
|
||||||
const glasses = this.glassesList[style];
|
const glasses = this.glassesList[style];
|
||||||
return <Image
|
return (
|
||||||
key={"glasses"}
|
<Image
|
||||||
source={glasses != null ? glasses : this.glassesList[GLASSES_STYLE.NORMAL]}
|
key="glasses"
|
||||||
|
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(style: number, isRight: boolean, rotation: string="0deg") {
|
getEye(
|
||||||
|
style: number,
|
||||||
|
isRight: boolean,
|
||||||
|
rotation: string = '0deg',
|
||||||
|
): React.Node {
|
||||||
const eye = this.eyeList[style];
|
const eye = this.eyeList[style];
|
||||||
return <Image
|
return (
|
||||||
key={isRight ? "right" : "left"}
|
<Image
|
||||||
|
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) {
|
getEyes(emotion: number): React.Node {
|
||||||
let final = [];
|
const final = [];
|
||||||
final.push(<View
|
final.push(
|
||||||
key={"container"}
|
<View
|
||||||
|
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));
|
||||||
|
|
@ -204,7 +211,7 @@ class Mascot extends React.Component<Props, State> {
|
||||||
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 {
|
||||||
|
|
@ -212,42 +219,45 @@ class Mascot extends React.Component<Props, State> {
|
||||||
final.push(this.getEye(EYE_STYLE.NORMAL, false));
|
final.push(this.getEye(EYE_STYLE.NORMAL, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emotion === MASCOT_STYLE.INTELLO) { // Needs to have normal eyes behind the glasses
|
if (emotion === MASCOT_STYLE.INTELLO) {
|
||||||
|
// 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() {
|
render(): React.Node {
|
||||||
const entryAnimation = this.props.animated ? this.props.entryAnimation : null;
|
const {props, state} = this;
|
||||||
const loopAnimation = this.props.animated ? this.props.loopAnimation : null;
|
const entryAnimation = props.animated ? props.entryAnimation : null;
|
||||||
|
const loopAnimation = props.animated ? props.loopAnimation : null;
|
||||||
return (
|
return (
|
||||||
<Animatable.View
|
<Animatable.View
|
||||||
style={{
|
style={{
|
||||||
aspectRatio: 1,
|
aspectRatio: 1,
|
||||||
...this.props.style
|
...props.style,
|
||||||
}}
|
}}
|
||||||
{...entryAnimation}
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
>
|
{...entryAnimation}>
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
onPress={() => this.onPress(this.viewRef)}
|
onPress={() => {
|
||||||
onLongPress={() => this.onLongPress(this.viewRef)}
|
this.onPress(this.viewRef);
|
||||||
>
|
}}
|
||||||
|
onLongPress={() => {
|
||||||
|
this.onLongPress(this.viewRef);
|
||||||
|
}}>
|
||||||
|
<Animatable.View ref={this.viewRef}>
|
||||||
<Animatable.View
|
<Animatable.View
|
||||||
ref={this.viewRef}
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
>
|
{...loopAnimation}>
|
||||||
<Animatable.View
|
|
||||||
{...loopAnimation}
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
source={MASCOT_IMAGE}
|
source={MASCOT_IMAGE}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height:"100%",
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{this.getEyes(this.state.currentEmotion)}
|
{this.getEyes(state.currentEmotion)}
|
||||||
</Animatable.View>
|
</Animatable.View>
|
||||||
</Animatable.View>
|
</Animatable.View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,29 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Avatar, Button, Card, Paragraph, Portal, withTheme} from 'react-native-paper';
|
import {
|
||||||
import Mascot from "./Mascot";
|
Avatar,
|
||||||
import * as Animatable from "react-native-animatable";
|
Button,
|
||||||
import {BackHandler, Dimensions, ScrollView, TouchableWithoutFeedback, View} from "react-native";
|
Card,
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
Paragraph,
|
||||||
import SpeechArrow from "./SpeechArrow";
|
Portal,
|
||||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import * as Animatable from 'react-native-animatable';
|
||||||
|
import {
|
||||||
|
BackHandler,
|
||||||
|
Dimensions,
|
||||||
|
ScrollView,
|
||||||
|
TouchableWithoutFeedback,
|
||||||
|
View,
|
||||||
|
} from 'react-native';
|
||||||
|
import Mascot from './Mascot';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
import SpeechArrow from './SpeechArrow';
|
||||||
|
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
icon: string,
|
icon: string,
|
||||||
title: string,
|
title: string,
|
||||||
message: string,
|
message: string,
|
||||||
|
|
@ -26,28 +39,34 @@ type Props = {
|
||||||
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 State = {
|
type StateType = {
|
||||||
shouldRenderDialog: boolean, // Used to stop rendering after hide animation
|
shouldRenderDialog: boolean, // Used to stop rendering after hide animation
|
||||||
dialogVisible: boolean,
|
dialogVisible: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component used to display a popup with the mascot.
|
* Component used to display a popup with the mascot.
|
||||||
*/
|
*/
|
||||||
class MascotPopup extends React.Component<Props, State> {
|
class MascotPopup extends React.Component<PropsType, StateType> {
|
||||||
|
static defaultProps = {
|
||||||
|
visible: null,
|
||||||
|
prefKey: null,
|
||||||
|
};
|
||||||
|
|
||||||
mascotSize: number;
|
mascotSize: number;
|
||||||
|
|
||||||
windowWidth: number;
|
windowWidth: number;
|
||||||
|
|
||||||
windowHeight: number;
|
windowHeight: number;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.windowWidth = Dimensions.get('window').width;
|
this.windowWidth = Dimensions.get('window').width;
|
||||||
|
|
@ -55,13 +74,13 @@ class MascotPopup extends React.Component<Props, State> {
|
||||||
|
|
||||||
this.mascotSize = Dimensions.get('window').height / 6;
|
this.mascotSize = Dimensions.get('window').height / 6;
|
||||||
|
|
||||||
if (this.props.visible != null) {
|
if (props.visible != null) {
|
||||||
this.state = {
|
this.state = {
|
||||||
shouldRenderDialog: this.props.visible,
|
shouldRenderDialog: props.visible,
|
||||||
dialogVisible: this.props.visible,
|
dialogVisible: props.visible,
|
||||||
};
|
};
|
||||||
} else if (this.props.prefKey != null) {
|
} else if (props.prefKey != null) {
|
||||||
const visible = AsyncStorageManager.getBool(this.props.prefKey);
|
const visible = AsyncStorageManager.getBool(props.prefKey);
|
||||||
this.state = {
|
this.state = {
|
||||||
shouldRenderDialog: visible,
|
shouldRenderDialog: visible,
|
||||||
dialogVisible: visible,
|
dialogVisible: visible,
|
||||||
|
|
@ -72,90 +91,92 @@ class MascotPopup extends React.Component<Props, State> {
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onAnimationEnd = () => {
|
componentDidMount(): * {
|
||||||
this.setState({
|
BackHandler.addEventListener(
|
||||||
shouldRenderDialog: false,
|
'hardwareBackPress',
|
||||||
})
|
this.onBackButtonPressAndroid,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
|
shouldComponentUpdate(nextProps: PropsType, nextState: StateType): 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 (nextProps.visible !== this.props.visible
|
} else if (
|
||||||
|| (!nextState.dialogVisible && nextState.dialogVisible !== this.state.dialogVisible)) {
|
nextProps.visible !== props.visible ||
|
||||||
|
(!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;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): * {
|
onAnimationEnd = () => {
|
||||||
BackHandler.addEventListener(
|
this.setState({
|
||||||
'hardwareBackPress',
|
shouldRenderDialog: false,
|
||||||
this.onBackButtonPressAndroid
|
});
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getSpeechBubble() {
|
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;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
getSpeechBubble(): React.Node {
|
||||||
|
const {state, props} = this;
|
||||||
return (
|
return (
|
||||||
<Animatable.View
|
<Animatable.View
|
||||||
style={{
|
style={{
|
||||||
marginLeft: "10%",
|
marginLeft: '10%',
|
||||||
marginRight: "10%",
|
marginRight: '10%',
|
||||||
}}
|
}}
|
||||||
useNativeDriver={true}
|
useNativeDriver
|
||||||
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
|
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
|
||||||
duration={this.state.dialogVisible ? 1000 : 300}
|
duration={state.dialogVisible ? 1000 : 300}>
|
||||||
>
|
|
||||||
<SpeechArrow
|
<SpeechArrow
|
||||||
style={{marginLeft: this.mascotSize / 3}}
|
style={{marginLeft: this.mascotSize / 3}}
|
||||||
size={20}
|
size={20}
|
||||||
color={this.props.theme.colors.mascotMessageArrow}
|
color={props.theme.colors.mascotMessageArrow}
|
||||||
/>
|
/>
|
||||||
<Card style={{
|
<Card
|
||||||
borderColor: this.props.theme.colors.mascotMessageArrow,
|
style={{
|
||||||
|
borderColor: props.theme.colors.mascotMessageArrow,
|
||||||
borderWidth: 4,
|
borderWidth: 4,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
}}>
|
}}>
|
||||||
<Card.Title
|
<Card.Title
|
||||||
title={this.props.title}
|
title={props.title}
|
||||||
left={this.props.icon != null ?
|
left={
|
||||||
(props) => <Avatar.Icon
|
props.icon != null
|
||||||
{...props}
|
? (): React.Node => (
|
||||||
|
<Avatar.Icon
|
||||||
size={48}
|
size={48}
|
||||||
style={{backgroundColor: "transparent"}}
|
style={{backgroundColor: 'transparent'}}
|
||||||
color={this.props.theme.colors.primary}
|
color={props.theme.colors.primary}
|
||||||
icon={this.props.icon}
|
icon={props.icon}
|
||||||
/>
|
/>
|
||||||
|
)
|
||||||
: null}
|
: null
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
<Card.Content
|
||||||
<Card.Content style={{
|
style={{
|
||||||
maxHeight: this.windowHeight / 3
|
maxHeight: this.windowHeight / 3,
|
||||||
}}>
|
}}>
|
||||||
<ScrollView>
|
<ScrollView>
|
||||||
<Paragraph style={{marginBottom: 10}}>
|
<Paragraph style={{marginBottom: 10}}>{props.message}</Paragraph>
|
||||||
{this.props.message}
|
|
||||||
</Paragraph>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
|
|
||||||
|
|
@ -167,116 +188,124 @@ class MascotPopup extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMascot() {
|
getMascot(): React.Node {
|
||||||
|
const {props, state} = this;
|
||||||
return (
|
return (
|
||||||
<Animatable.View
|
<Animatable.View
|
||||||
useNativeDriver={true}
|
useNativeDriver
|
||||||
animation={this.state.dialogVisible ? "bounceInLeft" : "bounceOutLeft"}
|
animation={state.dialogVisible ? 'bounceInLeft' : 'bounceOutLeft'}
|
||||||
duration={this.state.dialogVisible ? 1500 : 200}
|
duration={state.dialogVisible ? 1500 : 200}>
|
||||||
>
|
|
||||||
<Mascot
|
<Mascot
|
||||||
style={{width: this.mascotSize}}
|
style={{width: this.mascotSize}}
|
||||||
animated={true}
|
animated
|
||||||
emotion={this.props.emotion}
|
emotion={props.emotion}
|
||||||
/>
|
/>
|
||||||
</Animatable.View>
|
</Animatable.View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getButtons() {
|
getButtons(): React.Node {
|
||||||
const action = this.props.buttons.action;
|
const {props} = this;
|
||||||
const cancel = this.props.buttons.cancel;
|
const {action} = props.buttons;
|
||||||
|
const {cancel} = props.buttons;
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View
|
||||||
marginLeft: "auto",
|
style={{
|
||||||
marginRight: "auto",
|
marginLeft: 'auto',
|
||||||
marginTop: "auto",
|
marginRight: 'auto',
|
||||||
marginBottom: "auto",
|
marginTop: '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={() => this.onDismiss(action.onPress)}
|
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={() => this.onDismiss(cancel.onPress)}
|
onPress={() => {
|
||||||
>
|
this.onDismiss(cancel.onPress);
|
||||||
|
}}>
|
||||||
{cancel.message}
|
{cancel.message}
|
||||||
</Button>
|
</Button>
|
||||||
: null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBackground() {
|
getBackground(): React.Node {
|
||||||
|
const {props, state} = this;
|
||||||
return (
|
return (
|
||||||
<TouchableWithoutFeedback onPress={() => this.onDismiss(this.props.buttons.cancel.onPress)}>
|
<TouchableWithoutFeedback
|
||||||
|
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={true}
|
useNativeDriver
|
||||||
animation={this.state.dialogVisible ? "fadeIn" : "fadeOut"}
|
animation={state.dialogVisible ? 'fadeIn' : 'fadeOut'}
|
||||||
duration={this.state.dialogVisible ? 300 : 300}
|
duration={state.dialogVisible ? 300 : 300}
|
||||||
/>
|
/>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDismiss = (callback?: () => void) => {
|
onDismiss = (callback?: () => void) => {
|
||||||
if (this.props.prefKey != null) {
|
const {prefKey} = this.props;
|
||||||
AsyncStorageManager.set(this.props.prefKey, false);
|
if (prefKey != null) {
|
||||||
|
AsyncStorageManager.set(prefKey, false);
|
||||||
this.setState({dialogVisible: false});
|
this.setState({dialogVisible: false});
|
||||||
}
|
}
|
||||||
if (callback != null)
|
if (callback != null) callback();
|
||||||
callback();
|
};
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
if (this.state.shouldRenderDialog) {
|
const {shouldRenderDialog} = this.state;
|
||||||
|
if (shouldRenderDialog) {
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
{this.getBackground()}
|
{this.getBackground()}
|
||||||
<View style={{
|
<View
|
||||||
marginTop: "auto",
|
style={{
|
||||||
marginBottom: "auto",
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
}}>
|
}}>
|
||||||
<View style={{
|
<View
|
||||||
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,42 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
|
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
style?: ViewStyle,
|
style?: ViewStyle | null,
|
||||||
size: number,
|
size: number,
|
||||||
color: string,
|
color: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class SpeechArrow extends React.Component<PropsType> {
|
||||||
|
static defaultProps = {
|
||||||
|
style: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
shouldComponentUpdate(): boolean {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SpeechArrow extends React.Component<Props> {
|
render(): React.Node {
|
||||||
|
const {props} = this;
|
||||||
render() {
|
|
||||||
return (
|
return (
|
||||||
<View style={this.props.style}>
|
<View style={props.style}>
|
||||||
<View style={{
|
<View
|
||||||
|
style={{
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
borderLeftWidth: 0,
|
borderLeftWidth: 0,
|
||||||
borderRightWidth: this.props.size,
|
borderRightWidth: props.size,
|
||||||
borderBottomWidth: this.props.size,
|
borderBottomWidth: props.size,
|
||||||
borderStyle: 'solid',
|
borderStyle: 'solid',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
borderLeftColor: 'transparent',
|
borderLeftColor: 'transparent',
|
||||||
borderRightColor: 'transparent',
|
borderRightColor: 'transparent',
|
||||||
borderBottomColor: this.props.color,
|
borderBottomColor: props.color,
|
||||||
}}/>
|
}}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,61 @@
|
||||||
import * as React from 'react';
|
// @flow
|
||||||
import {View} from "react-native";
|
|
||||||
import {withTheme} from 'react-native-paper';
|
|
||||||
import {Agenda} from "react-native-calendars";
|
|
||||||
|
|
||||||
type Props = {
|
import * as React from 'react';
|
||||||
theme: Object,
|
import {View} from 'react-native';
|
||||||
}
|
import {withTheme} from 'react-native-paper';
|
||||||
|
import {Agenda} from 'react-native-calendars';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
theme: CustomThemeType,
|
||||||
|
onRef: (ref: Agenda) => void,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstraction layer for Agenda component, using custom configuration
|
* Abstraction layer for Agenda component, using custom configuration
|
||||||
*/
|
*/
|
||||||
class CustomAgenda extends React.Component<Props> {
|
class CustomAgenda extends React.Component<PropsType> {
|
||||||
|
getAgenda(): React.Node {
|
||||||
getAgenda() {
|
const {props} = this;
|
||||||
return <Agenda
|
return (
|
||||||
{...this.props}
|
<Agenda
|
||||||
ref={this.props.onRef}
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...props}
|
||||||
|
ref={props.onRef}
|
||||||
theme={{
|
theme={{
|
||||||
backgroundColor: this.props.theme.colors.agendaBackgroundColor,
|
backgroundColor: props.theme.colors.agendaBackgroundColor,
|
||||||
calendarBackground: this.props.theme.colors.background,
|
calendarBackground: props.theme.colors.background,
|
||||||
textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
|
textSectionTitleColor: props.theme.colors.agendaDayTextColor,
|
||||||
selectedDayBackgroundColor: this.props.theme.colors.primary,
|
selectedDayBackgroundColor: props.theme.colors.primary,
|
||||||
selectedDayTextColor: '#ffffff',
|
selectedDayTextColor: '#ffffff',
|
||||||
todayTextColor: this.props.theme.colors.primary,
|
todayTextColor: props.theme.colors.primary,
|
||||||
dayTextColor: this.props.theme.colors.text,
|
dayTextColor: props.theme.colors.text,
|
||||||
textDisabledColor: this.props.theme.colors.agendaDayTextColor,
|
textDisabledColor: props.theme.colors.agendaDayTextColor,
|
||||||
dotColor: this.props.theme.colors.primary,
|
dotColor: props.theme.colors.primary,
|
||||||
selectedDotColor: '#ffffff',
|
selectedDotColor: '#ffffff',
|
||||||
arrowColor: 'orange',
|
arrowColor: 'orange',
|
||||||
monthTextColor: this.props.theme.colors.primary,
|
monthTextColor: props.theme.colors.primary,
|
||||||
indicatorColor: this.props.theme.colors.primary,
|
indicatorColor: 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: this.props.theme.colors.agendaDayTextColor,
|
agendaDayTextColor: props.theme.colors.agendaDayTextColor,
|
||||||
agendaDayNumColor: this.props.theme.colors.agendaDayTextColor,
|
agendaDayNumColor: props.theme.colors.agendaDayTextColor,
|
||||||
agendaTodayColor: this.props.theme.colors.primary,
|
agendaTodayColor: props.theme.colors.primary,
|
||||||
agendaKnobColor: this.props.theme.colors.primary,
|
agendaKnobColor: props.theme.colors.primary,
|
||||||
}}
|
}}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
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 (this.props.theme.dark)
|
if (props.theme.dark)
|
||||||
return (
|
return <View style={{flex: 1}}>{this.getAgenda()}</View>;
|
||||||
<View style={{flex: 1}}>
|
|
||||||
{this.getAgenda()}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
else
|
|
||||||
return this.getAgenda();
|
return this.getAgenda();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,57 @@
|
||||||
|
/* eslint-disable flowtype/require-parameter-type */
|
||||||
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Text, withTheme} from 'react-native-paper';
|
import {Text, withTheme} from 'react-native-paper';
|
||||||
import HTML from "react-native-render-html";
|
import HTML from 'react-native-render-html';
|
||||||
import {Linking} from "react-native";
|
import {Linking} from 'react-native';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
theme: Object,
|
theme: CustomThemeType,
|
||||||
html: string,
|
html: string,
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstraction layer for Agenda component, using custom configuration
|
* Abstraction layer for Agenda component, using custom configuration
|
||||||
*/
|
*/
|
||||||
class CustomHTML extends React.Component<Props> {
|
class CustomHTML extends React.Component<PropsType> {
|
||||||
|
openWebLink = (event: {...}, link: string) => {
|
||||||
openWebLink = (event, link) => {
|
Linking.openURL(link);
|
||||||
Linking.openURL(link).catch((err) => console.error('Error opening link', err));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
getBasicText = (htmlAttribs, children, convertedCSSStyles, passProps) => {
|
getBasicText = (
|
||||||
|
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 = (htmlAttribs, children, convertedCSSStyles, passProps) => {
|
getListBullet = (): React.Node => {
|
||||||
return (
|
return <Text>- </Text>;
|
||||||
<Text>- </Text>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
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 <HTML
|
return (
|
||||||
html={"<p>" + this.props.html + "</p>"}
|
<HTML
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,39 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import {HeaderButton, HeaderButtons} from 'react-navigation-header-buttons';
|
import {HeaderButton, HeaderButtons} from 'react-navigation-header-buttons';
|
||||||
import {withTheme} from "react-native-paper";
|
import {withTheme} from 'react-native-paper';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
const MaterialHeaderButton = (props: Object) =>
|
const MaterialHeaderButton = (props: {
|
||||||
|
theme: CustomThemeType,
|
||||||
|
color: string,
|
||||||
|
}): React.Node => {
|
||||||
|
const {color, theme} = props;
|
||||||
|
return (
|
||||||
|
// $FlowFixMe
|
||||||
<HeaderButton
|
<HeaderButton
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
IconComponent={MaterialCommunityIcons}
|
IconComponent={MaterialCommunityIcons}
|
||||||
iconSize={26}
|
iconSize={26}
|
||||||
color={props.color != null ? props.color : props.theme.colors.text}
|
color={color != null ? color : theme.colors.text}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const MaterialHeaderButtons = (props: Object) => {
|
const MaterialHeaderButtons = (props: {...}): React.Node => {
|
||||||
return (
|
return (
|
||||||
|
// $FlowFixMe
|
||||||
<HeaderButtons
|
<HeaderButtons
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
|
HeaderButtonComponent={withTheme(MaterialHeaderButton)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withTheme(MaterialHeaderButtons);
|
export default MaterialHeaderButtons;
|
||||||
|
|
||||||
export {Item} from 'react-navigation-header-buttons';
|
export {Item} from 'react-navigation-header-buttons';
|
||||||
|
|
|
||||||
|
|
@ -1,390 +1,37 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Platform, StatusBar, StyleSheet, View} from "react-native";
|
import {Platform, StatusBar, StyleSheet, View} from 'react-native';
|
||||||
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
|
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import AppIntroSlider from "react-native-app-intro-slider";
|
import AppIntroSlider from 'react-native-app-intro-slider';
|
||||||
import Update from "../../constants/Update";
|
|
||||||
import ThemeManager from "../../managers/ThemeManager";
|
|
||||||
import LinearGradient from 'react-native-linear-gradient';
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import Mascot, {MASCOT_STYLE} from "../Mascot/Mascot";
|
import * as Animatable from 'react-native-animatable';
|
||||||
import * as Animatable from "react-native-animatable";
|
import {Card} from 'react-native-paper';
|
||||||
import {Card} from "react-native-paper";
|
import Update from '../../constants/Update';
|
||||||
|
import ThemeManager from '../../managers/ThemeManager';
|
||||||
|
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
onDone: Function,
|
onDone: () => void,
|
||||||
isUpdate: boolean,
|
isUpdate: boolean,
|
||||||
isAprilFools: boolean,
|
isAprilFools: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
currentSlide: number,
|
currentSlide: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
type Slide = {
|
type IntroSlideType = {
|
||||||
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,
|
||||||
|
|
@ -409,3 +56,348 @@ 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {withTheme} from 'react-native-paper';
|
import {withTheme} from 'react-native-paper';
|
||||||
import {Modalize} from "react-native-modalize";
|
import {Modalize} from 'react-native-modalize';
|
||||||
import {View} from "react-native-animatable";
|
import {View} from 'react-native-animatable';
|
||||||
import CustomTabBar from "../Tabbar/CustomTabBar";
|
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstraction layer for Modalize component, using custom configuration
|
* Abstraction layer for Modalize component, using custom configuration
|
||||||
|
|
@ -12,25 +13,29 @@ import CustomTabBar from "../Tabbar/CustomTabBar";
|
||||||
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
|
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
function CustomModal(props) {
|
function CustomModal(props: {
|
||||||
const {colors} = props.theme;
|
theme: CustomThemeType,
|
||||||
|
onRef: (re: Modalize) => void,
|
||||||
|
children?: React.Node,
|
||||||
|
}): React.Node {
|
||||||
|
const {theme, onRef, children} = props;
|
||||||
return (
|
return (
|
||||||
<Modalize
|
<Modalize
|
||||||
ref={props.onRef}
|
ref={onRef}
|
||||||
adjustToContentHeight
|
adjustToContentHeight
|
||||||
handlePosition={'inside'}
|
handlePosition="inside"
|
||||||
modalStyle={{backgroundColor: colors.card}}
|
modalStyle={{backgroundColor: theme.colors.card}}
|
||||||
handleStyle={{backgroundColor: colors.primary}}
|
handleStyle={{backgroundColor: theme.colors.primary}}>
|
||||||
>
|
<View
|
||||||
<View style={{
|
style={{
|
||||||
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT
|
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||||
}}>
|
}}>
|
||||||
{props.children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
</Modalize>
|
</Modalize>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(CustomModal);
|
CustomModal.defaultProps = {children: null};
|
||||||
|
|
||||||
|
export default withTheme(CustomModal);
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,19 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Text, withTheme} from 'react-native-paper';
|
import {Text, withTheme} from 'react-native-paper';
|
||||||
import {View} from "react-native-animatable";
|
import {View} from 'react-native-animatable';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import Slider, {SliderProps} from '@react-native-community/slider';
|
||||||
import Slider, {SliderProps} from "@react-native-community/slider";
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
valueSuffix: string,
|
valueSuffix?: string,
|
||||||
...SliderProps
|
...SliderProps,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
currentValue: number,
|
currentValue: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstraction layer for Modalize component, using custom configuration
|
* Abstraction layer for Modalize component, using custom configuration
|
||||||
|
|
@ -22,37 +22,44 @@ type State = {
|
||||||
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
|
* @param props Props to pass to the element. Must specify an onRef prop to get an Modalize ref.
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
class CustomSlider extends React.Component<Props, State> {
|
class CustomSlider extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
valueSuffix: "",
|
valueSuffix: '',
|
||||||
}
|
};
|
||||||
|
|
||||||
state = {
|
constructor(props: PropsType) {
|
||||||
currentValue: this.props.value,
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
currentValue: props.value,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onValueChange = (value: number) => {
|
onValueChange = (value: number) => {
|
||||||
|
const {props} = this;
|
||||||
this.setState({currentValue: value});
|
this.setState({currentValue: value});
|
||||||
if (this.props.onValueChange != null)
|
if (props.onValueChange != null) props.onValueChange(value);
|
||||||
this.props.onValueChange(value);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props, state} = this;
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||||
<Text style={{marginHorizontal: 10, marginTop: 'auto', marginBottom: 'auto'}}>
|
<Text
|
||||||
{this.state.currentValue}min
|
style={{
|
||||||
|
marginHorizontal: 10,
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
}}>
|
||||||
|
{state.currentValue}min
|
||||||
</Text>
|
</Text>
|
||||||
<Slider
|
<Slider
|
||||||
{...this.props}
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...props}
|
||||||
onValueChange={this.onValueChange}
|
onValueChange={this.onValueChange}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(CustomSlider);
|
export default withTheme(CustomSlider);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {View} from 'react-native';
|
import {View} from 'react-native';
|
||||||
import {ActivityIndicator, withTheme} from 'react-native-paper';
|
import {ActivityIndicator, withTheme} from 'react-native-paper';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component used to display a header button
|
* Component used to display a header button
|
||||||
|
|
@ -10,26 +11,27 @@ import {ActivityIndicator, withTheme} from 'react-native-paper';
|
||||||
* @param props Props to pass to the component
|
* @param props Props to pass to the component
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
function BasicLoadingScreen(props) {
|
function BasicLoadingScreen(props: {
|
||||||
const {colors} = props.theme;
|
theme: CustomThemeType,
|
||||||
let position = undefined;
|
isAbsolute: boolean,
|
||||||
if (props.isAbsolute !== undefined && props.isAbsolute)
|
}): React.Node {
|
||||||
position = 'absolute';
|
const {theme, isAbsolute} = props;
|
||||||
|
const {colors} = theme;
|
||||||
|
let position;
|
||||||
|
if (isAbsolute != null && isAbsolute) position = 'absolute';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View
|
||||||
|
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
|
<ActivityIndicator animating size="large" color={colors.primary} />
|
||||||
animating={true}
|
|
||||||
size={'large'}
|
|
||||||
color={colors.primary}/>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,168 +2,25 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Button, Subheading, withTheme} from 'react-native-paper';
|
import {Button, Subheading, withTheme} from 'react-native-paper';
|
||||||
import {StyleSheet, View} from "react-native";
|
import {StyleSheet, View} from 'react-native';
|
||||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import {ERROR_TYPE} from "../../utils/WebData";
|
|
||||||
import * as Animatable from 'react-native-animatable';
|
import * as Animatable from 'react-native-animatable';
|
||||||
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
|
import {ERROR_TYPE} from '../../utils/WebData';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: Object,
|
navigation: StackNavigationProp,
|
||||||
route: Object,
|
theme: CustomThemeType,
|
||||||
errorCode: number,
|
route: {name: string},
|
||||||
onRefresh: Function,
|
onRefresh?: () => void,
|
||||||
icon: string,
|
errorCode?: number,
|
||||||
message: string,
|
icon?: string,
|
||||||
showRetryButton: boolean,
|
message?: string,
|
||||||
}
|
showRetryButton?: boolean,
|
||||||
|
|
||||||
type State = {
|
|
||||||
refreshing: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ErrorView extends React.PureComponent<Props, State> {
|
|
||||||
|
|
||||||
colors: Object;
|
|
||||||
|
|
||||||
message: string;
|
|
||||||
icon: string;
|
|
||||||
|
|
||||||
showLoginButton: boolean;
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
errorCode: 0,
|
|
||||||
icon: '',
|
|
||||||
message: '',
|
|
||||||
showRetryButton: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
state = {
|
|
||||||
refreshing: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.colors = props.theme.colors;
|
|
||||||
this.icon = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
generateMessage() {
|
|
||||||
this.showLoginButton = false;
|
|
||||||
if (this.props.errorCode !== 0) {
|
|
||||||
switch (this.props.errorCode) {
|
|
||||||
case ERROR_TYPE.BAD_CREDENTIALS:
|
|
||||||
this.message = i18n.t("errors.badCredentials");
|
|
||||||
this.icon = "account-alert-outline";
|
|
||||||
break;
|
|
||||||
case ERROR_TYPE.BAD_TOKEN:
|
|
||||||
this.message = i18n.t("errors.badToken");
|
|
||||||
this.icon = "account-alert-outline";
|
|
||||||
this.showLoginButton = true;
|
|
||||||
break;
|
|
||||||
case ERROR_TYPE.NO_CONSENT:
|
|
||||||
this.message = i18n.t("errors.noConsent");
|
|
||||||
this.icon = "account-remove-outline";
|
|
||||||
break;
|
|
||||||
case ERROR_TYPE.TOKEN_SAVE:
|
|
||||||
this.message = i18n.t("errors.tokenSave");
|
|
||||||
this.icon = "alert-circle-outline";
|
|
||||||
break;
|
|
||||||
case ERROR_TYPE.BAD_INPUT:
|
|
||||||
this.message = i18n.t("errors.badInput");
|
|
||||||
this.icon = "alert-circle-outline";
|
|
||||||
break;
|
|
||||||
case ERROR_TYPE.FORBIDDEN:
|
|
||||||
this.message = i18n.t("errors.forbidden");
|
|
||||||
this.icon = "lock";
|
|
||||||
break;
|
|
||||||
case ERROR_TYPE.CONNECTION_ERROR:
|
|
||||||
this.message = i18n.t("errors.connectionError");
|
|
||||||
this.icon = "access-point-network-off";
|
|
||||||
break;
|
|
||||||
case ERROR_TYPE.SERVER_ERROR:
|
|
||||||
this.message = i18n.t("errors.serverError");
|
|
||||||
this.icon = "server-network-off";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.message = i18n.t("errors.unknown");
|
|
||||||
this.icon = "alert-circle-outline";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.message += "\n\nCode " + this.props.errorCode;
|
|
||||||
} else {
|
|
||||||
this.message = this.props.message;
|
|
||||||
this.icon = this.props.icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getRetryButton() {
|
|
||||||
return <Button
|
|
||||||
mode={'contained'}
|
|
||||||
icon={'refresh'}
|
|
||||||
onPress={this.props.onRefresh}
|
|
||||||
style={styles.button}
|
|
||||||
>
|
|
||||||
{i18n.t("general.retry")}
|
|
||||||
</Button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
goToLogin = () => {
|
|
||||||
this.props.navigation.navigate("login",
|
|
||||||
{
|
|
||||||
screen: 'login',
|
|
||||||
params: {nextScreen: this.props.route.name}
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
getLoginButton() {
|
|
||||||
return <Button
|
|
||||||
mode={'contained'}
|
|
||||||
icon={'login'}
|
|
||||||
onPress={this.goToLogin}
|
|
||||||
style={styles.button}
|
|
||||||
>
|
|
||||||
{i18n.t("screens.login.title")}
|
|
||||||
</Button>;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.generateMessage();
|
|
||||||
return (
|
|
||||||
<Animatable.View
|
|
||||||
style={{
|
|
||||||
...styles.outer,
|
|
||||||
backgroundColor: this.colors.background
|
|
||||||
}}
|
|
||||||
animation={"zoomIn"}
|
|
||||||
duration={200}
|
|
||||||
useNativeDriver
|
|
||||||
>
|
|
||||||
<View style={styles.inner}>
|
|
||||||
<View style={styles.iconContainer}>
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name={this.icon}
|
|
||||||
size={150}
|
|
||||||
color={this.colors.textDisabled}/>
|
|
||||||
</View>
|
|
||||||
<Subheading style={{
|
|
||||||
...styles.subheading,
|
|
||||||
color: this.colors.textDisabled
|
|
||||||
}}>
|
|
||||||
{this.message}
|
|
||||||
</Subheading>
|
|
||||||
{this.props.showRetryButton
|
|
||||||
? (this.showLoginButton
|
|
||||||
? this.getLoginButton()
|
|
||||||
: this.getRetryButton())
|
|
||||||
: null}
|
|
||||||
</View>
|
|
||||||
</Animatable.View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
outer: {
|
outer: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|
@ -175,18 +32,162 @@ 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);
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,55 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {ERROR_TYPE, readData} from "../../utils/WebData";
|
import i18n from 'i18n-js';
|
||||||
import i18n from "i18n-js";
|
|
||||||
import {Snackbar} from 'react-native-paper';
|
import {Snackbar} from 'react-native-paper';
|
||||||
import {RefreshControl, View} from "react-native";
|
import {RefreshControl, View} from 'react-native';
|
||||||
import ErrorView from "./ErrorView";
|
|
||||||
import BasicLoadingScreen from "./BasicLoadingScreen";
|
|
||||||
import {withCollapsible} from "../../utils/withCollapsible";
|
|
||||||
import * as Animatable from 'react-native-animatable';
|
import * as Animatable from 'react-native-animatable';
|
||||||
import CustomTabBar from "../Tabbar/CustomTabBar";
|
import {Collapsible} from 'react-navigation-collapsible';
|
||||||
import {Collapsible} from "react-navigation-collapsible";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import ErrorView from './ErrorView';
|
||||||
import CollapsibleSectionList from "../Collapsible/CollapsibleSectionList";
|
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||||
|
import withCollapsible from '../../utils/withCollapsible';
|
||||||
|
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||||
|
import {ERROR_TYPE, readData} from '../../utils/WebData';
|
||||||
|
import CollapsibleSectionList from '../Collapsible/CollapsibleSectionList';
|
||||||
|
import type {ApiGenericDataType} from '../../utils/WebData';
|
||||||
|
|
||||||
type Props = {
|
export type SectionListDataType<T> = Array<{
|
||||||
|
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: { [key: string]: any }) => React.Node,
|
renderItem: (data: {item: T}) => React.Node,
|
||||||
createDataset: (data: { [key: string]: any } | null, isLoading?: boolean) => Array<Object>,
|
createDataset: (
|
||||||
|
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,
|
itemHeight?: number | null,
|
||||||
updateData?: number,
|
updateData?: number,
|
||||||
renderListHeaderComponent?: (data: { [key: string]: any } | null) => React.Node,
|
renderListHeaderComponent?: (data: ApiGenericDataType | null) => React.Node,
|
||||||
renderSectionHeader?: (data: { section: { [key: string]: any } }, isLoading?: boolean) => React.Node,
|
renderSectionHeader?: (
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
@ -48,31 +59,37 @@ const MIN_REFRESH_TIME = 5 * 1000;
|
||||||
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
|
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
|
||||||
* To force the component to update, change the value of updateData.
|
* To force the component to update, change the value of updateData.
|
||||||
*/
|
*/
|
||||||
class WebSectionList extends React.PureComponent<Props, State> {
|
class WebSectionList<T> extends React.PureComponent<PropsType<T>, StateType> {
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
stickyHeader: false,
|
|
||||||
updateData: 0,
|
|
||||||
showError: true,
|
showError: true,
|
||||||
|
itemHeight: null,
|
||||||
|
updateData: 0,
|
||||||
|
renderListHeaderComponent: (): React.Node => null,
|
||||||
|
renderSectionHeader: (): React.Node => null,
|
||||||
|
stickyHeader: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
refreshInterval: IntervalID;
|
refreshInterval: IntervalID;
|
||||||
|
|
||||||
lastRefresh: Date | null;
|
lastRefresh: Date | null;
|
||||||
|
|
||||||
state = {
|
constructor() {
|
||||||
|
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() {
|
||||||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
const {navigation} = this.props;
|
||||||
this.props.navigation.addListener('blur', this.onScreenBlur);
|
navigation.addListener('focus', this.onScreenFocus);
|
||||||
|
navigation.addListener('blur', this.onScreenBlur);
|
||||||
this.lastRefresh = null;
|
this.lastRefresh = null;
|
||||||
this.onRefresh();
|
this.onRefresh();
|
||||||
}
|
}
|
||||||
|
|
@ -81,19 +98,18 @@ class WebSectionList extends React.PureComponent<Props, State> {
|
||||||
* 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 = () => {
|
||||||
if (this.props.refreshOnFocus && this.lastRefresh)
|
const {props} = this;
|
||||||
this.onRefresh();
|
if (props.refreshOnFocus && this.lastRefresh) this.onRefresh();
|
||||||
if (this.props.autoRefreshTime > 0)
|
if (props.autoRefreshTime > 0)
|
||||||
this.refreshInterval = setInterval(this.onRefresh, this.props.autoRefreshTime)
|
this.refreshInterval = setInterval(this.onRefresh, 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.
|
||||||
|
|
@ -101,11 +117,10 @@ class WebSectionList extends React.PureComponent<Props, State> {
|
||||||
*
|
*
|
||||||
* @param fetchedData The newly fetched data
|
* @param fetchedData The newly fetched data
|
||||||
*/
|
*/
|
||||||
onFetchSuccess = (fetchedData: { [key: string]: any }) => {
|
onFetchSuccess = (fetchedData: ApiGenericDataType) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
fetchedData: fetchedData,
|
fetchedData,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
firstLoading: false
|
|
||||||
});
|
});
|
||||||
this.lastRefresh = new Date();
|
this.lastRefresh = new Date();
|
||||||
};
|
};
|
||||||
|
|
@ -118,7 +133,6 @@ class WebSectionList extends React.PureComponent<Props, State> {
|
||||||
this.setState({
|
this.setState({
|
||||||
fetchedData: null,
|
fetchedData: null,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
firstLoading: false
|
|
||||||
});
|
});
|
||||||
this.showSnackBar();
|
this.showSnackBar();
|
||||||
};
|
};
|
||||||
|
|
@ -127,128 +141,130 @@ class WebSectionList extends React.PureComponent<Props, State> {
|
||||||
* 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
|
} else canRefresh = true;
|
||||||
canRefresh = true;
|
|
||||||
if (canRefresh) {
|
if (canRefresh) {
|
||||||
this.setState({refreshing: true});
|
this.setState({refreshing: true});
|
||||||
readData(this.props.fetchUrl)
|
readData(fetchUrl).then(this.onFetchSuccess).catch(this.onFetchError);
|
||||||
.then(this.onFetchSuccess)
|
|
||||||
.catch(this.onFetchError);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the error popup
|
* Shows the error popup
|
||||||
*/
|
*/
|
||||||
showSnackBar = () => this.setState({snackbarVisible: true});
|
showSnackBar = () => {
|
||||||
|
this.setState({snackbarVisible: true});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hides the error popup
|
* Hides the error popup
|
||||||
*/
|
*/
|
||||||
hideSnackBar = () => this.setState({snackbarVisible: false});
|
hideSnackBar = () => {
|
||||||
|
this.setState({snackbarVisible: false});
|
||||||
itemLayout = (data: { [key: string]: any }, index: number) => {
|
|
||||||
const height = this.props.itemHeight;
|
|
||||||
if (height == null)
|
|
||||||
return undefined;
|
|
||||||
return {
|
|
||||||
length: height,
|
|
||||||
offset: height * index,
|
|
||||||
index
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderSectionHeader = (data: { section: { [key: string]: any } }) => {
|
getItemLayout = (
|
||||||
if (this.props.renderSectionHeader != null) {
|
data: T,
|
||||||
return (
|
|
||||||
<Animatable.View
|
|
||||||
animation={"fadeInUp"}
|
|
||||||
duration={500}
|
|
||||||
useNativeDriver
|
|
||||||
>
|
|
||||||
{this.props.renderSectionHeader(data, this.state.refreshing)}
|
|
||||||
</Animatable.View>
|
|
||||||
);
|
|
||||||
} else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderItem = (data: {
|
|
||||||
item: { [key: string]: any },
|
|
||||||
index: number,
|
index: number,
|
||||||
section: { [key: string]: any },
|
): {length: number, offset: number, index: number} | null => {
|
||||||
separators: { [key: string]: any },
|
const {itemHeight} = this.props;
|
||||||
}) => {
|
if (itemHeight == null) return null;
|
||||||
|
return {
|
||||||
|
length: itemHeight,
|
||||||
|
offset: itemHeight * index,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
getRenderSectionHeader = (data: {section: {title: string}}): React.Node => {
|
||||||
|
const {renderSectionHeader} = this.props;
|
||||||
|
const {refreshing} = this.state;
|
||||||
|
if (renderSectionHeader != null) {
|
||||||
return (
|
return (
|
||||||
<Animatable.View
|
<Animatable.View animation="fadeInUp" duration={500} useNativeDriver>
|
||||||
animation={"fadeInUp"}
|
{renderSectionHeader(data, refreshing)}
|
||||||
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>) => {
|
||||||
if (this.props.onScroll)
|
const {onScroll} = this.props;
|
||||||
this.props.onScroll(event);
|
if (onScroll != null) onScroll(event);
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props, state} = this;
|
||||||
let dataset = [];
|
let dataset = [];
|
||||||
if (this.state.fetchedData != null || (this.state.fetchedData == null && !this.props.showError)) {
|
if (
|
||||||
dataset = this.props.createDataset(this.state.fetchedData, this.state.refreshing);
|
state.fetchedData != null ||
|
||||||
}
|
(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={this.props.updateData}
|
extraData={props.updateData}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
progressViewOffset={containerPaddingTop}
|
progressViewOffset={containerPaddingTop}
|
||||||
refreshing={this.state.refreshing}
|
refreshing={state.refreshing}
|
||||||
onRefresh={this.onRefresh}
|
onRefresh={this.onRefresh}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
renderSectionHeader={this.renderSectionHeader}
|
renderSectionHeader={this.getRenderSectionHeader}
|
||||||
renderItem={this.renderItem}
|
renderItem={this.getRenderItem}
|
||||||
stickySectionHeadersEnabled={this.props.stickyHeader}
|
stickySectionHeadersEnabled={props.stickyHeader}
|
||||||
style={{minHeight: '100%'}}
|
style={{minHeight: '100%'}}
|
||||||
ListHeaderComponent={this.props.renderListHeaderComponent != null
|
ListHeaderComponent={
|
||||||
? this.props.renderListHeaderComponent(this.state.fetchedData)
|
props.renderListHeaderComponent != null
|
||||||
: null}
|
? props.renderListHeaderComponent(state.fetchedData)
|
||||||
ListEmptyComponent={this.state.refreshing
|
: null
|
||||||
? <BasicLoadingScreen/>
|
|
||||||
: <ErrorView
|
|
||||||
{...this.props}
|
|
||||||
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
|
||||||
onRefresh={this.onRefresh}/>
|
|
||||||
}
|
}
|
||||||
getItemLayout={this.props.itemHeight != null ? this.itemLayout : undefined}
|
ListEmptyComponent={
|
||||||
|
state.refreshing ? (
|
||||||
|
<BasicLoadingScreen />
|
||||||
|
) : (
|
||||||
|
<ErrorView
|
||||||
|
navigation={props.navigation}
|
||||||
|
errorCode={ERROR_TYPE.CONNECTION_ERROR}
|
||||||
|
onRefresh={this.onRefresh}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
getItemLayout={props.itemHeight != null ? this.getItemLayout : null}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
hasTab={true}
|
hasTab
|
||||||
/>
|
/>
|
||||||
<Snackbar
|
<Snackbar
|
||||||
visible={this.state.snackbarVisible}
|
visible={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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,43 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import WebView from "react-native-webview";
|
import WebView from 'react-native-webview';
|
||||||
import BasicLoadingScreen from "./BasicLoadingScreen";
|
import {
|
||||||
import ErrorView from "./ErrorView";
|
Divider,
|
||||||
import {ERROR_TYPE} from "../../utils/WebData";
|
HiddenItem,
|
||||||
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
|
OverflowMenu,
|
||||||
import {Divider, HiddenItem, OverflowMenu} from "react-navigation-header-buttons";
|
} from 'react-navigation-header-buttons';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import {Animated, BackHandler, Linking} from "react-native";
|
import {Animated, BackHandler, Linking} from 'react-native';
|
||||||
import {withCollapsible} from "../../utils/withCollapsible";
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
import {withTheme} from 'react-native-paper';
|
||||||
import {withTheme} from "react-native-paper";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import {Collapsible} from 'react-navigation-collapsible';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
import {Collapsible} from "react-navigation-collapsible";
|
import withCollapsible from '../../utils/withCollapsible';
|
||||||
|
import MaterialHeaderButtons, {Item} from '../Overrides/CustomHeaderButton';
|
||||||
|
import {ERROR_TYPE} from '../../utils/WebData';
|
||||||
|
import ErrorView from './ErrorView';
|
||||||
|
import BasicLoadingScreen from './BasicLoadingScreen';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
url: string,
|
url: string,
|
||||||
customJS: string,
|
|
||||||
customPaddingFunction: null | (padding: number) => string,
|
|
||||||
collapsibleStack: Collapsible,
|
collapsibleStack: Collapsible,
|
||||||
onMessage: Function,
|
onMessage: (event: {nativeEvent: {data: string}}) => void,
|
||||||
onScroll: Function,
|
onScroll: (event: SyntheticEvent<EventTarget>) => void,
|
||||||
showAdvancedControls: boolean,
|
customJS?: string,
|
||||||
}
|
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<Props> {
|
class WebViewScreen extends React.PureComponent<PropsType> {
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
customJS: '',
|
customJS: '',
|
||||||
showAdvancedControls: true,
|
showAdvancedControls: true,
|
||||||
|
|
@ -55,27 +58,24 @@ class WebViewScreen extends React.PureComponent<Props> {
|
||||||
* Creates header buttons and listens to events after mounting
|
* Creates header buttons and listens to events after mounting
|
||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.navigation.setOptions({
|
const {props} = this;
|
||||||
headerRight: this.props.showAdvancedControls
|
props.navigation.setOptions({
|
||||||
|
headerRight: props.showAdvancedControls
|
||||||
? this.getAdvancedButtons
|
? this.getAdvancedButtons
|
||||||
: this.getBasicButton,
|
: this.getBasicButton,
|
||||||
});
|
});
|
||||||
this.props.navigation.addListener(
|
props.navigation.addListener('focus', () => {
|
||||||
'focus',
|
|
||||||
() =>
|
|
||||||
BackHandler.addEventListener(
|
BackHandler.addEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
this.onBackButtonPressAndroid
|
this.onBackButtonPressAndroid,
|
||||||
)
|
|
||||||
);
|
);
|
||||||
this.props.navigation.addListener(
|
});
|
||||||
'blur',
|
props.navigation.addListener('blur', () => {
|
||||||
() =>
|
|
||||||
BackHandler.removeEventListener(
|
BackHandler.removeEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
this.onBackButtonPressAndroid
|
this.onBackButtonPressAndroid,
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -83,7 +83,7 @@ class WebViewScreen extends React.PureComponent<Props> {
|
||||||
*
|
*
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
onBackButtonPressAndroid = () => {
|
onBackButtonPressAndroid = (): boolean => {
|
||||||
if (this.canGoBack) {
|
if (this.canGoBack) {
|
||||||
this.onGoBackClicked();
|
this.onGoBackClicked();
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -96,17 +96,19 @@ class WebViewScreen extends React.PureComponent<Props> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getBasicButton = () => {
|
getBasicButton = (): React.Node => {
|
||||||
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -117,7 +119,8 @@ class WebViewScreen extends React.PureComponent<Props> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getAdvancedButtons = () => {
|
getAdvancedButtons = (): React.Node => {
|
||||||
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<MaterialHeaderButtons>
|
<MaterialHeaderButtons>
|
||||||
<Item
|
<Item
|
||||||
|
|
@ -131,40 +134,74 @@ class WebViewScreen extends React.PureComponent<Props> {
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="dots-vertical"
|
name="dots-vertical"
|
||||||
size={26}
|
size={26}
|
||||||
color={this.props.theme.colors.text}
|
color={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)
|
if (this.webviewRef.current != null) this.webviewRef.current.reload();
|
||||||
this.webviewRef.current.reload();
|
};
|
||||||
}
|
|
||||||
onGoBackClicked = () => {
|
onGoBackClicked = () => {
|
||||||
if (this.webviewRef.current != null)
|
if (this.webviewRef.current != null) this.webviewRef.current.goBack();
|
||||||
this.webviewRef.current.goBack();
|
};
|
||||||
}
|
|
||||||
onGoForwardClicked = () => {
|
onGoForwardClicked = () => {
|
||||||
if (this.webviewRef.current != null)
|
if (this.webviewRef.current != null) this.webviewRef.current.goForward();
|
||||||
this.webviewRef.current.goForward();
|
};
|
||||||
}
|
|
||||||
onOpenClicked = () => Linking.openURL(this.props.url);
|
onOpenClicked = () => {
|
||||||
|
const {url} = this.props;
|
||||||
|
Linking.openURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||||
|
const {onScroll} = this.props;
|
||||||
|
if (onScroll) onScroll(event);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injects the given javascript string into the web page
|
* Injects the given javascript string into the web page
|
||||||
|
|
@ -174,55 +211,32 @@ class WebViewScreen extends React.PureComponent<Props> {
|
||||||
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 {
|
||||||
* Gets the loading indicator
|
const {props} = this;
|
||||||
*
|
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: this.props.url}}
|
source={{uri: props.url}}
|
||||||
startInLoadingState={true}
|
startInLoadingState
|
||||||
injectedJavaScript={this.props.customJS}
|
injectedJavaScript={props.customJS}
|
||||||
javaScriptEnabled={true}
|
javaScriptEnabled
|
||||||
renderLoading={this.getRenderLoading}
|
renderLoading={this.getRenderLoading}
|
||||||
renderError={() => <ErrorView
|
renderError={(): React.Node => (
|
||||||
|
<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={this.props.onMessage}
|
onMessage={props.onMessage}
|
||||||
onLoad={() => this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop))}
|
onLoad={() => {
|
||||||
|
this.injectJavaScript(this.getJavascriptPadding(containerPaddingTop));
|
||||||
|
}}
|
||||||
// Animations
|
// Animations
|
||||||
onScroll={onScrollWithListener(this.onScroll)}
|
onScroll={onScrollWithListener(this.onScroll)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,45 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {withTheme} from 'react-native-paper';
|
|
||||||
import TabIcon from "./TabIcon";
|
|
||||||
import TabHomeIcon from "./TabHomeIcon";
|
|
||||||
import {Animated} from 'react-native';
|
import {Animated} from 'react-native';
|
||||||
import {Collapsible} from "react-navigation-collapsible";
|
import {withTheme} from 'react-native-paper';
|
||||||
|
import {Collapsible} from 'react-navigation-collapsible';
|
||||||
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
|
import TabIcon from './TabIcon';
|
||||||
|
import TabHomeIcon from './TabHomeIcon';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type RouteType = {
|
||||||
state: Object,
|
name: string,
|
||||||
descriptors: Object,
|
key: string,
|
||||||
navigation: Object,
|
params: {collapsible: Collapsible},
|
||||||
theme: Object,
|
state: {
|
||||||
collapsibleStack: Object,
|
index: number,
|
||||||
}
|
routes: Array<RouteType>,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
type State = {
|
type PropsType = {
|
||||||
translateY: AnimatedValue,
|
state: {
|
||||||
barSynced: boolean,
|
index: number,
|
||||||
}
|
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',
|
||||||
|
|
@ -27,29 +48,15 @@ const TAB_ICONS = {
|
||||||
planex: 'clock',
|
planex: 'clock',
|
||||||
};
|
};
|
||||||
|
|
||||||
class CustomTabBar extends React.Component<Props, State> {
|
class CustomTabBar extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
static TAB_BAR_HEIGHT = 48;
|
static TAB_BAR_HEIGHT = 48;
|
||||||
|
|
||||||
state = {
|
constructor() {
|
||||||
|
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
|
||||||
|
|
@ -58,14 +65,15 @@ class CustomTabBar extends React.Component<Props, State> {
|
||||||
* @param currentIndex The current route index
|
* @param currentIndex The current route index
|
||||||
* @param destIndex The destination route index
|
* @param destIndex The destination route index
|
||||||
*/
|
*/
|
||||||
onItemPress(route: Object, currentIndex: number, destIndex: number) {
|
onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
|
||||||
const event = this.props.navigation.emit({
|
const {navigation} = this.props;
|
||||||
|
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)
|
||||||
this.props.navigation.navigate(route.name);
|
navigation.navigate(route.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,16 +81,25 @@ class CustomTabBar extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @param route
|
* @param route
|
||||||
*/
|
*/
|
||||||
onItemLongPress(route: Object) {
|
onItemLongPress(route: RouteType) {
|
||||||
const event = this.props.navigation.emit({
|
const {navigation} = this.props;
|
||||||
|
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)
|
||||||
this.props.navigation.navigate('game-start');
|
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
|
||||||
*
|
*
|
||||||
|
|
@ -90,22 +107,13 @@ class CustomTabBar extends React.Component<Props, State> {
|
||||||
* @param focused
|
* @param focused
|
||||||
* @returns {null}
|
* @returns {null}
|
||||||
*/
|
*/
|
||||||
tabBarIcon = (route, focused) => {
|
getTabBarIcon = (route: RouteType, focused: boolean): React.Node => {
|
||||||
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")
|
if (route.name !== 'home') return icon;
|
||||||
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
|
||||||
|
|
@ -114,50 +122,80 @@ class CustomTabBar extends React.Component<Props, State> {
|
||||||
* @param index The index of the current route
|
* @param index The index of the current route
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
renderIcon = (route, index) => {
|
getRenderIcon = (route: RouteType, index: number): React.Node => {
|
||||||
const state = this.props.state;
|
const {props} = this;
|
||||||
const {options} = this.props.descriptors[route.key];
|
const {state} = props;
|
||||||
const label =
|
const {options} = props.descriptors[route.key];
|
||||||
options.tabBarLabel != null
|
let label;
|
||||||
? options.tabBarLabel
|
if (options.tabBarLabel != null) label = options.tabBarLabel;
|
||||||
: options.title != null
|
else if (options.title != null) label = options.title;
|
||||||
? options.title
|
else label = route.name;
|
||||||
: route.name;
|
|
||||||
|
|
||||||
const onPress = () => this.onItemPress(route, state.index, index);
|
const onPress = () => {
|
||||||
const onLongPress = () => this.onItemLongPress(route);
|
this.onItemPress(route, state.index, index);
|
||||||
|
};
|
||||||
|
const onLongPress = () => {
|
||||||
|
this.onItemLongPress(route);
|
||||||
|
};
|
||||||
const isFocused = state.index === index;
|
const isFocused = state.index === index;
|
||||||
|
|
||||||
const color = isFocused ? this.props.theme.colors.primary : this.props.theme.colors.tabIcon;
|
const color = isFocused
|
||||||
if (route.name !== "home") {
|
? props.theme.colors.primary
|
||||||
return <TabIcon
|
: props.theme.colors.tabIcon;
|
||||||
|
if (route.name !== 'home') {
|
||||||
|
return (
|
||||||
|
<TabIcon
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
icon={this.tabBarIcon(route, isFocused)}
|
icon={this.getTabBarIcon(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() {
|
getIcons(): React.Node {
|
||||||
return this.props.state.routes.map(this.renderIcon);
|
const {props} = this;
|
||||||
|
return props.state.routes.map(this.getRenderIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
syncTabBar = (route: RouteType, index: number) => {
|
||||||
this.props.navigation.addListener('state', this.onRouteChange);
|
const {state} = this.props;
|
||||||
|
const isFocused = state.index === index;
|
||||||
|
if (isFocused) {
|
||||||
|
const stackState = route.state;
|
||||||
|
const stackRoute =
|
||||||
|
stackState != null ? stackState.routes[stackState.index] : null;
|
||||||
|
const params: {collapsible: Collapsible} | null =
|
||||||
|
stackRoute != null ? stackRoute.params : null;
|
||||||
|
const collapsible = params != null ? params.collapsible : null;
|
||||||
|
if (collapsible != null) {
|
||||||
|
this.setState({
|
||||||
|
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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={{
|
||||||
|
|
@ -167,10 +205,9 @@ class CustomTabBar extends React.Component<Props, State> {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
backgroundColor: this.props.theme.colors.surface,
|
backgroundColor: props.theme.colors.surface,
|
||||||
transform: [{translateY: this.state.translateY}],
|
transform: [{translateY: state.translateY}],
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
{icons}
|
{icons}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,70 +1,95 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Image, Platform, View} from "react-native";
|
import {Image, Platform, View} from 'react-native';
|
||||||
import {FAB, TouchableRipple, withTheme} from 'react-native-paper';
|
import {FAB, TouchableRipple, withTheme} from 'react-native-paper';
|
||||||
import * as Animatable from "react-native-animatable";
|
import * as Animatable from 'react-native-animatable';
|
||||||
|
import FOCUSED_ICON from '../../../assets/tab-icon.png';
|
||||||
|
import UNFOCUSED_ICON from '../../../assets/tab-icon-outline.png';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
focused: boolean,
|
focused: boolean,
|
||||||
onPress: Function,
|
onPress: () => void,
|
||||||
onLongPress: Function,
|
onLongPress: () => void,
|
||||||
theme: Object,
|
theme: CustomThemeType,
|
||||||
tabBarHeight: number,
|
tabBarHeight: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstraction layer for Agenda component, using custom configuration
|
* Abstraction layer for Agenda component, using custom configuration
|
||||||
*/
|
*/
|
||||||
class TabHomeIcon extends React.Component<Props> {
|
class TabHomeIcon extends React.Component<PropsType> {
|
||||||
|
constructor(props: PropsType) {
|
||||||
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, translateY: 0
|
scale: 1,
|
||||||
|
translateY: 0,
|
||||||
},
|
},
|
||||||
"0.9": {
|
'0.9': {
|
||||||
scale: 1.2, translateY: -9
|
scale: 1.2,
|
||||||
|
translateY: -9,
|
||||||
},
|
},
|
||||||
"1": {
|
'1': {
|
||||||
scale: 1.1, translateY: -7
|
scale: 1.1,
|
||||||
|
translateY: -7,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fabFocusOut: {
|
fabFocusOut: {
|
||||||
"0": {
|
'0': {
|
||||||
scale: 1.1, translateY: -6
|
scale: 1.1,
|
||||||
|
translateY: -6,
|
||||||
|
},
|
||||||
|
'1': {
|
||||||
|
scale: 1,
|
||||||
|
translateY: 0,
|
||||||
},
|
},
|
||||||
"1": {
|
|
||||||
scale: 1, translateY: 0
|
|
||||||
},
|
},
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
iconRender = ({size, color}) =>
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
this.props.focused
|
const {focused} = this.props;
|
||||||
? <Image
|
return nextProps.focused !== focused;
|
||||||
source={this.focusedIcon}
|
|
||||||
style={{width: size, height: size, tintColor: color}}
|
|
||||||
/>
|
|
||||||
: <Image
|
|
||||||
source={this.unFocusedIcon}
|
|
||||||
style={{width: size, height: size, tintColor: color}}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props): boolean {
|
|
||||||
return (nextProps.focused !== this.props.focused);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React$Node {
|
getIconRender = ({
|
||||||
const props = this.props;
|
size,
|
||||||
|
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={{
|
||||||
|
|
@ -74,33 +99,35 @@ class TabHomeIcon extends React.Component<Props> {
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
onPress={props.onPress}
|
onPress={props.onPress}
|
||||||
onLongPress={props.onLongPress}
|
onLongPress={props.onLongPress}
|
||||||
borderless={true}
|
borderless
|
||||||
rippleColor={Platform.OS === 'android' ? this.props.theme.colors.primary : 'transparent'}
|
rippleColor={
|
||||||
|
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: this.props.tabBarHeight + 30,
|
height: 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.iconRender}
|
icon={this.getIconRender}
|
||||||
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);
|
||||||
|
|
@ -1,53 +1,57 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import {TouchableRipple, withTheme} from 'react-native-paper';
|
import {TouchableRipple, withTheme} from 'react-native-paper';
|
||||||
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
|
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
|
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import * as Animatable from "react-native-animatable";
|
import * as Animatable from 'react-native-animatable';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
focused: boolean,
|
focused: boolean,
|
||||||
color: string,
|
color: string,
|
||||||
label: string,
|
label: string,
|
||||||
icon: MaterialCommunityIconsGlyphs,
|
icon: MaterialCommunityIconsGlyphs,
|
||||||
onPress: Function,
|
onPress: () => void,
|
||||||
onLongPress: Function,
|
onLongPress: () => void,
|
||||||
theme: Object,
|
theme: CustomThemeType,
|
||||||
extraData: any,
|
extraData: null | boolean | number | string,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstraction layer for Agenda component, using custom configuration
|
* Abstraction layer for Agenda component, using custom configuration
|
||||||
*/
|
*/
|
||||||
class TabIcon extends React.Component<Props> {
|
class TabIcon extends React.Component<PropsType> {
|
||||||
|
|
||||||
firstRender: boolean;
|
firstRender: boolean;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
Animatable.initializeRegistryWithDefinitions({
|
Animatable.initializeRegistryWithDefinitions({
|
||||||
focusIn: {
|
focusIn: {
|
||||||
"0": {
|
'0': {
|
||||||
scale: 1, translateY: 0
|
scale: 1,
|
||||||
|
translateY: 0,
|
||||||
},
|
},
|
||||||
"0.9": {
|
'0.9': {
|
||||||
scale: 1.3, translateY: 7
|
scale: 1.3,
|
||||||
|
translateY: 7,
|
||||||
},
|
},
|
||||||
"1": {
|
'1': {
|
||||||
scale: 1.2, translateY: 6
|
scale: 1.2,
|
||||||
|
translateY: 6,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
focusOut: {
|
focusOut: {
|
||||||
"0": {
|
'0': {
|
||||||
scale: 1.2, translateY: 6
|
scale: 1.2,
|
||||||
|
translateY: 6,
|
||||||
|
},
|
||||||
|
'1': {
|
||||||
|
scale: 1,
|
||||||
|
translateY: 0,
|
||||||
},
|
},
|
||||||
"1": {
|
|
||||||
scale: 1, translateY: 0
|
|
||||||
},
|
},
|
||||||
}
|
|
||||||
});
|
});
|
||||||
this.firstRender = true;
|
this.firstRender = true;
|
||||||
}
|
}
|
||||||
|
|
@ -56,32 +60,33 @@ class TabIcon extends React.Component<Props> {
|
||||||
this.firstRender = false;
|
this.firstRender = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: Props): boolean {
|
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||||
return (nextProps.focused !== this.props.focused)
|
const {props} = this;
|
||||||
|| (nextProps.theme.dark !== this.props.theme.dark)
|
return (
|
||||||
|| (nextProps.extraData !== this.props.extraData);
|
nextProps.focused !== props.focused ||
|
||||||
|
nextProps.theme.dark !== props.theme.dark ||
|
||||||
|
nextProps.extraData !== props.extraData
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): React$Node {
|
render(): React.Node {
|
||||||
const props = this.props;
|
const {props} = this;
|
||||||
return (
|
return (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
onPress={props.onPress}
|
onPress={props.onPress}
|
||||||
onLongPress={props.onLongPress}
|
onLongPress={props.onLongPress}
|
||||||
borderless={true}
|
borderless
|
||||||
rippleColor={this.props.theme.colors.primary}
|
rippleColor={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}
|
||||||
|
|
@ -93,16 +98,14 @@ class TabIcon extends React.Component<Props> {
|
||||||
/>
|
/>
|
||||||
</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>
|
||||||
|
|
|
||||||
|
|
@ -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/',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,28 +14,26 @@ 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 = [
|
static iconList = ['star', 'clock', 'qrcode-scan', 'account'];
|
||||||
'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>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -44,9 +42,9 @@ export default class Update {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.titleList = [];
|
this.titleList = [];
|
||||||
this.descriptionList = [];
|
this.descriptionList = [];
|
||||||
for (let i = 0; i < Update.slidesNumber; i++) {
|
for (let i = 0; i < Update.slidesNumber; i += 1) {
|
||||||
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`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,9 +54,7 @@ export default class Update {
|
||||||
* @returns {Update}
|
* @returns {Update}
|
||||||
*/
|
*/
|
||||||
static getInstance(): Update {
|
static getInstance(): Update {
|
||||||
return Update.instance === null ?
|
if (Update.instance == null) Update.instance = new Update();
|
||||||
Update.instance = new Update() :
|
return Update.instance;
|
||||||
Update.instance;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,36 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {Machine} from "../screens/Proxiwash/ProxiwashScreen";
|
import type {ProxiwashMachineType} from '../screens/Proxiwash/ProxiwashScreen';
|
||||||
|
import type {CustomThemeType} from './ThemeManager';
|
||||||
|
import type {RuFoodCategoryType} from '../screens/Services/SelfMenuScreen';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton class used to manage april fools
|
* Singleton class used to manage april fools
|
||||||
*/
|
*/
|
||||||
export default class AprilFoolsManager {
|
export default class AprilFoolsManager {
|
||||||
|
|
||||||
static instance: AprilFoolsManager | null = null;
|
static instance: AprilFoolsManager | null = null;
|
||||||
|
|
||||||
static fakeMachineNumber = [
|
static fakeMachineNumber = [
|
||||||
"",
|
'',
|
||||||
"cos(ln(1))",
|
'cos(ln(1))',
|
||||||
"0,5⁻¹",
|
'0,5⁻¹',
|
||||||
"567/189",
|
'567/189',
|
||||||
"√2×√8",
|
'√2×√8',
|
||||||
"√50×sin(9π/4)",
|
'√50×sin(9π/4)',
|
||||||
"⌈π+e⌉",
|
'⌈π+e⌉',
|
||||||
"div(rot(B))+7",
|
'div(rot(B))+7',
|
||||||
"4×cosh(0)+4",
|
'4×cosh(0)+4',
|
||||||
"8-(-i)²",
|
'8-(-i)²',
|
||||||
"|5√2+5√2i|",
|
'|5√2+5√2i|',
|
||||||
"1×10¹+1×10⁰",
|
'1×10¹+1×10⁰',
|
||||||
"Re(√192e^(iπ/6))",
|
'Re(√192e^(iπ/6))',
|
||||||
];
|
];
|
||||||
|
|
||||||
aprilFoolsEnabled: boolean;
|
aprilFoolsEnabled: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let today = new Date();
|
const today = new Date();
|
||||||
this.aprilFoolsEnabled = (today.getDate() === 1 && today.getMonth() === 3);
|
this.aprilFoolsEnabled = today.getDate() === 1 && today.getMonth() === 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -35,9 +38,9 @@ export default class AprilFoolsManager {
|
||||||
* @returns {ThemeManager}
|
* @returns {ThemeManager}
|
||||||
*/
|
*/
|
||||||
static getInstance(): AprilFoolsManager {
|
static getInstance(): AprilFoolsManager {
|
||||||
return AprilFoolsManager.instance === null ?
|
if (AprilFoolsManager.instance == null)
|
||||||
AprilFoolsManager.instance = new AprilFoolsManager() :
|
AprilFoolsManager.instance = new AprilFoolsManager();
|
||||||
AprilFoolsManager.instance;
|
return AprilFoolsManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -46,12 +49,14 @@ export default class AprilFoolsManager {
|
||||||
* @param menu
|
* @param menu
|
||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
static getFakeMenuItem(menu: Array<{dishes: Array<{name: string}>}>) {
|
static getFakeMenuItem(
|
||||||
menu[1]["dishes"].splice(4, 0, {name: "Coq au vin"});
|
menu: Array<RuFoodCategoryType>,
|
||||||
menu[1]["dishes"].splice(2, 0, {name: "Bat'Soupe"});
|
): Array<RuFoodCategoryType> {
|
||||||
menu[1]["dishes"].splice(1, 0, {name: "Pave de loup"});
|
menu[1].dishes.splice(4, 0, {name: 'Coq au vin'});
|
||||||
menu[1]["dishes"].splice(0, 0, {name: "Béranger à point"});
|
menu[1].dishes.splice(2, 0, {name: "Bat'Soupe"});
|
||||||
menu[1]["dishes"].splice(0, 0, {name: "Pieds d'Arnaud"});
|
menu[1].dishes.splice(1, 0, {name: 'Pave de loup'});
|
||||||
|
menu[1].dishes.splice(0, 0, {name: 'Béranger à point'});
|
||||||
|
menu[1].dishes.splice(0, 0, {name: "Pieds d'Arnaud"});
|
||||||
return menu;
|
return menu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,9 +65,11 @@ export default class AprilFoolsManager {
|
||||||
*
|
*
|
||||||
* @param dryers
|
* @param dryers
|
||||||
*/
|
*/
|
||||||
static getNewProxiwashDryerOrderedList(dryers: Array<Machine> | null) {
|
static getNewProxiwashDryerOrderedList(
|
||||||
|
dryers: Array<ProxiwashMachineType> | null,
|
||||||
|
) {
|
||||||
if (dryers != null) {
|
if (dryers != null) {
|
||||||
let second = dryers[1];
|
const second = dryers[1];
|
||||||
dryers.splice(1, 1);
|
dryers.splice(1, 1);
|
||||||
dryers.push(second);
|
dryers.push(second);
|
||||||
}
|
}
|
||||||
|
|
@ -73,12 +80,14 @@ export default class AprilFoolsManager {
|
||||||
*
|
*
|
||||||
* @param washers
|
* @param washers
|
||||||
*/
|
*/
|
||||||
static getNewProxiwashWasherOrderedList(washers: Array<Machine> | null) {
|
static getNewProxiwashWasherOrderedList(
|
||||||
|
washers: Array<ProxiwashMachineType> | null,
|
||||||
|
) {
|
||||||
if (washers != null) {
|
if (washers != null) {
|
||||||
let first = washers[0];
|
const first = washers[0];
|
||||||
let second = washers[1];
|
const second = washers[1];
|
||||||
let fifth = washers[4];
|
const fifth = washers[4];
|
||||||
let ninth = washers[8];
|
const 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);
|
||||||
|
|
@ -92,7 +101,7 @@ export default class AprilFoolsManager {
|
||||||
* @param number
|
* @param number
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
static getProxiwashMachineDisplayNumber(number: number) {
|
static getProxiwashMachineDisplayNumber(number: number): string {
|
||||||
return AprilFoolsManager.fakeMachineNumber[number];
|
return AprilFoolsManager.fakeMachineNumber[number];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +111,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: Object) {
|
static getAprilFoolsTheme(currentTheme: CustomThemeType): CustomThemeType {
|
||||||
return {
|
return {
|
||||||
...currentTheme,
|
...currentTheme,
|
||||||
colors: {
|
colors: {
|
||||||
|
|
@ -110,9 +119,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',
|
||||||
|
|
||||||
|
|
@ -123,8 +132,7 @@ export default class AprilFoolsManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
isAprilFoolsEnabled() {
|
isAprilFoolsEnabled(): boolean {
|
||||||
return this.aprilFoolsEnabled;
|
return this.aprilFoolsEnabled;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +10,6 @@ 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 = {
|
||||||
|
|
@ -108,7 +107,7 @@ export default class AsyncStorageManager {
|
||||||
key: 'gameScores',
|
key: 'gameScores',
|
||||||
default: '[]',
|
default: '[]',
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
#currentPreferences: {[key: string]: string};
|
#currentPreferences: {[key: string]: string};
|
||||||
|
|
||||||
|
|
@ -121,9 +120,66 @@ export default class AsyncStorageManager {
|
||||||
* @returns {AsyncStorageManager}
|
* @returns {AsyncStorageManager}
|
||||||
*/
|
*/
|
||||||
static getInstance(): AsyncStorageManager {
|
static getInstance(): AsyncStorageManager {
|
||||||
return AsyncStorageManager.instance === null ?
|
if (AsyncStorageManager.instance == null)
|
||||||
AsyncStorageManager.instance = new AsyncStorageManager() :
|
AsyncStorageManager.instance = new AsyncStorageManager();
|
||||||
AsyncStorageManager.instance;
|
return 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -133,21 +189,20 @@ export default class AsyncStorageManager {
|
||||||
* @return {Promise<void>}
|
* @return {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async loadPreferences() {
|
async loadPreferences() {
|
||||||
let prefKeys = [];
|
const prefKeys = [];
|
||||||
// Get all available keys
|
// Get all available keys
|
||||||
for (let key in AsyncStorageManager.PREFERENCES) {
|
Object.keys(AsyncStorageManager.PREFERENCES).forEach((key: string) => {
|
||||||
prefKeys.push(key);
|
prefKeys.push(key);
|
||||||
}
|
});
|
||||||
// Get corresponding values
|
// Get corresponding values
|
||||||
let resultArray: Array<Array<string>> = await AsyncStorage.multiGet(prefKeys);
|
const resultArray = await AsyncStorage.multiGet(prefKeys);
|
||||||
// Save those values for later use
|
// Save those values for later use
|
||||||
for (let i = 0; i < resultArray.length; i++) {
|
resultArray.forEach((item: [string, string | null]) => {
|
||||||
let key: string = resultArray[i][0];
|
const key = item[0];
|
||||||
let val: string | null = resultArray[i][1];
|
let val = item[1];
|
||||||
if (val === null)
|
if (val === null) val = AsyncStorageManager.PREFERENCES[key].default;
|
||||||
val = AsyncStorageManager.PREFERENCES[key].default;
|
|
||||||
this.#currentPreferences[key] = val;
|
this.#currentPreferences[key] = val;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -157,15 +212,17 @@ export default class AsyncStorageManager {
|
||||||
* @param key
|
* @param key
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
setPreference(key: string, value: any) {
|
setPreference(
|
||||||
|
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")
|
if (typeof value === 'string') convertedValue = value;
|
||||||
convertedValue = value;
|
else if (typeof value === 'boolean' || typeof value === 'number')
|
||||||
else if (typeof value === "boolean" || typeof value === "number")
|
|
||||||
convertedValue = value.toString();
|
convertedValue = value.toString();
|
||||||
else
|
else convertedValue = JSON.stringify(value);
|
||||||
convertedValue = JSON.stringify(value);
|
|
||||||
this.#currentPreferences[key] = convertedValue;
|
this.#currentPreferences[key] = convertedValue;
|
||||||
AsyncStorage.setItem(key, convertedValue);
|
AsyncStorage.setItem(key, convertedValue);
|
||||||
}
|
}
|
||||||
|
|
@ -178,59 +235,7 @@ export default class AsyncStorageManager {
|
||||||
* @param key
|
* @param key
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
getPreference(key: string) {
|
getPreference(key: string): string | null {
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as Keychain from 'react-native-keychain';
|
import * as Keychain from 'react-native-keychain';
|
||||||
import {apiRequest, ERROR_TYPE} from "../utils/WebData";
|
import type {ApiDataLoginType, ApiGenericDataType} from '../utils/WebData';
|
||||||
|
import {apiRequest, ERROR_TYPE} from '../utils/WebData';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* champ: error
|
* champ: error
|
||||||
|
|
@ -14,13 +15,14 @@ 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() {
|
||||||
|
|
@ -33,9 +35,9 @@ export default class ConnectionManager {
|
||||||
* @returns {ConnectionManager}
|
* @returns {ConnectionManager}
|
||||||
*/
|
*/
|
||||||
static getInstance(): ConnectionManager {
|
static getInstance(): ConnectionManager {
|
||||||
return ConnectionManager.instance === null ?
|
if (ConnectionManager.instance == null)
|
||||||
ConnectionManager.instance = new ConnectionManager() :
|
ConnectionManager.instance = new ConnectionManager();
|
||||||
ConnectionManager.instance;
|
return ConnectionManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -50,26 +52,29 @@ export default class ConnectionManager {
|
||||||
/**
|
/**
|
||||||
* Tries to recover login token from the secure keychain
|
* Tries to recover login token from the secure keychain
|
||||||
*
|
*
|
||||||
* @returns {Promise<R>}
|
* @returns Promise<string>
|
||||||
*/
|
*/
|
||||||
async recoverLogin() {
|
async recoverLogin(): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(
|
||||||
if (this.getToken() !== null)
|
(resolve: (token: string) => void, reject: () => void) => {
|
||||||
resolve(this.getToken());
|
const token = this.getToken();
|
||||||
|
if (token != null) resolve(token);
|
||||||
else {
|
else {
|
||||||
Keychain.getInternetCredentials(SERVER_NAME)
|
Keychain.getInternetCredentials(SERVER_NAME)
|
||||||
.then((data) => {
|
.then((data: Keychain.UserCredentials | false) => {
|
||||||
if (data) {
|
if (
|
||||||
|
data != null &&
|
||||||
|
data.password != null &&
|
||||||
|
typeof data.password === 'string'
|
||||||
|
) {
|
||||||
this.#token = data.password;
|
this.#token = data.password;
|
||||||
resolve(this.#token);
|
resolve(this.#token);
|
||||||
} else
|
} else reject();
|
||||||
reject(false);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((): void => reject());
|
||||||
reject(false);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,7 +82,7 @@ export default class ConnectionManager {
|
||||||
*
|
*
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isLoggedIn() {
|
isLoggedIn(): boolean {
|
||||||
return this.getToken() !== null;
|
return this.getToken() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,41 +91,36 @@ export default class ConnectionManager {
|
||||||
*
|
*
|
||||||
* @param email
|
* @param email
|
||||||
* @param token
|
* @param token
|
||||||
* @returns {Promise<R>}
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
async saveLogin(email: string, token: string) {
|
async saveLogin(email: string, token: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve: () => void, reject: () => void) => {
|
||||||
Keychain.setInternetCredentials(SERVER_NAME, 'token', token)
|
Keychain.setInternetCredentials(SERVER_NAME, 'token', token)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.#token = token;
|
this.#token = token;
|
||||||
this.#email = email;
|
this.#email = email;
|
||||||
resolve(true);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((): void => reject());
|
||||||
reject(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the login token from the keychain
|
* Deletes the login token from the keychain
|
||||||
*
|
*
|
||||||
* @returns {Promise<R>}
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
async disconnect() {
|
async disconnect(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve: () => void, reject: () => void) => {
|
||||||
Keychain.resetInternetCredentials(SERVER_NAME)
|
Keychain.resetInternetCredentials(SERVER_NAME)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.#token = null;
|
this.#token = null;
|
||||||
resolve(true);
|
resolve();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((): void => reject());
|
||||||
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<R>}
|
* @returns Promise<void>
|
||||||
*/
|
*/
|
||||||
async connect(email: string, password: string) {
|
async connect(email: string, password: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(
|
||||||
|
(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) => {
|
.then((response: ApiDataLoginType) => {
|
||||||
|
if (response.token != null) {
|
||||||
this.saveLogin(email, response.token)
|
this.saveLogin(email, response.token)
|
||||||
.then(() => {
|
.then((): void => resolve())
|
||||||
resolve(true);
|
.catch((): void => reject(ERROR_TYPE.TOKEN_SAVE));
|
||||||
|
} else reject(ERROR_TYPE.SERVER_ERROR);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error: number): void => reject(error));
|
||||||
reject(ERROR_TYPE.TOKEN_SAVE);
|
},
|
||||||
});
|
);
|
||||||
})
|
|
||||||
.catch((error) => reject(error));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -155,20 +155,27 @@ export default class ConnectionManager {
|
||||||
*
|
*
|
||||||
* @param path
|
* @param path
|
||||||
* @param params
|
* @param params
|
||||||
* @returns {Promise<R>}
|
* @returns Promise<ApiGenericDataType>
|
||||||
*/
|
*/
|
||||||
async authenticatedRequest(path: string, params: Object) {
|
async authenticatedRequest(
|
||||||
return new Promise((resolve, reject) => {
|
path: string,
|
||||||
|
params: {...},
|
||||||
|
): Promise<ApiGenericDataType> {
|
||||||
|
return new Promise(
|
||||||
|
(
|
||||||
|
resolve: (response: ApiGenericDataType) => void,
|
||||||
|
reject: (error: number) => void,
|
||||||
|
) => {
|
||||||
if (this.getToken() !== null) {
|
if (this.getToken() !== null) {
|
||||||
let data = {
|
const data = {
|
||||||
|
...params,
|
||||||
token: this.getToken(),
|
token: this.getToken(),
|
||||||
...params
|
|
||||||
};
|
};
|
||||||
apiRequest(path, 'POST', data)
|
apiRequest(path, 'POST', data)
|
||||||
.then((response) => resolve(response))
|
.then((response: ApiGenericDataType): void => resolve(response))
|
||||||
.catch((error) => reject(error));
|
.catch((error: number): void => reject(error));
|
||||||
} else
|
} else reject(ERROR_TYPE.TOKEN_RETRIEVE);
|
||||||
reject(ERROR_TYPE.TOKEN_RETRIEVE);
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,15 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {ServiceItem} from "./ServicesManager";
|
import type {ServiceItemType} from './ServicesManager';
|
||||||
import ServicesManager from "./ServicesManager";
|
import ServicesManager from './ServicesManager';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import {getSublistWithIds} from '../utils/Utils';
|
||||||
import {getSublistWithIds} from "../utils/Utils";
|
import AsyncStorageManager from './AsyncStorageManager';
|
||||||
import AsyncStorageManager from "./AsyncStorageManager";
|
|
||||||
|
|
||||||
|
|
||||||
export default class DashboardManager extends ServicesManager {
|
export default class DashboardManager extends ServicesManager {
|
||||||
|
getCurrentDashboard(): Array<ServiceItemType | null> {
|
||||||
constructor(nav: StackNavigationProp) {
|
const dashboardIdList = AsyncStorageManager.getObject(
|
||||||
super(nav)
|
AsyncStorageManager.PREFERENCES.dashboardItems.key,
|
||||||
}
|
);
|
||||||
|
|
||||||
getCurrentDashboard(): Array<ServiceItem> {
|
|
||||||
const dashboardIdList = AsyncStorageManager
|
|
||||||
.getObject(AsyncStorageManager.PREFERENCES.dashboardItems.key);
|
|
||||||
const allDatasets = [
|
const allDatasets = [
|
||||||
...this.amicaleDataset,
|
...this.amicaleDataset,
|
||||||
...this.studentsDataset,
|
...this.studentsDataset,
|
||||||
|
|
|
||||||
|
|
@ -10,29 +10,30 @@ 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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,16 +41,15 @@ export default class DateManager {
|
||||||
* @returns {DateManager}
|
* @returns {DateManager}
|
||||||
*/
|
*/
|
||||||
static getInstance(): DateManager {
|
static getInstance(): DateManager {
|
||||||
return DateManager.instance === null ?
|
if (DateManager.instance == null) DateManager.instance = new DateManager();
|
||||||
DateManager.instance = new DateManager() :
|
return DateManager.instance;
|
||||||
DateManager.instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static isWeekend(date: Date) {
|
static isWeekend(date: Date): boolean {
|
||||||
return date.getDay() === 6 || date.getDay() === 0;
|
return date.getDay() === 6 || date.getDay() === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMonthsOfYear() {
|
getMonthsOfYear(): Array<string> {
|
||||||
return this.monthsOfYear;
|
return this.monthsOfYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,11 +59,16 @@ 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) {
|
getTranslatedDate(dateString: string): string {
|
||||||
let dateArray = dateString.split('-');
|
const dateArray = dateString.split('-');
|
||||||
let date = new Date();
|
const date = new Date();
|
||||||
date.setFullYear(parseInt(dateArray[0]), parseInt(dateArray[1]) - 1, parseInt(dateArray[2]));
|
date.setFullYear(
|
||||||
return this.daysOfWeek[date.getDay()] + " " + date.getDate() + " " + this.monthsOfYear[date.getMonth()] + " " + date.getFullYear();
|
parseInt(dateArray[0], 10),
|
||||||
|
parseInt(dateArray[1], 10) - 1,
|
||||||
|
parseInt(dateArray[2], 10),
|
||||||
|
);
|
||||||
|
return `${this.daysOfWeek[date.getDay()]} ${date.getDate()} ${
|
||||||
|
this.monthsOfYear[date.getMonth()]
|
||||||
|
} ${date.getFullYear()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,24 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import * as RNLocalize from "react-native-localize";
|
import * as RNLocalize from 'react-native-localize';
|
||||||
|
|
||||||
import en from '../../locales/en';
|
import en from '../../locales/en.json';
|
||||||
import fr from '../../locales/fr.json';
|
import fr from '../../locales/fr.json';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static class used to manage locales
|
* Static class used to manage locales
|
||||||
*/
|
*/
|
||||||
export default class LocaleManager {
|
export default class LocaleManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize translations using language files
|
* Initialize translations using language files
|
||||||
*/
|
*/
|
||||||
static initTranslations() {
|
static initTranslations() {
|
||||||
i18n.fallbacks = true;
|
i18n.fallbacks = true;
|
||||||
i18n.translations = {fr, en};
|
i18n.translations = {fr, en};
|
||||||
i18n.locale = RNLocalize.findBestAvailableLanguage(["en", "fr"]).languageTag;
|
i18n.locale = RNLocalize.findBestAvailableLanguage([
|
||||||
|
'en',
|
||||||
|
'fr',
|
||||||
|
]).languageTag;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,107 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
import AvailableWebsites from "../constants/AvailableWebsites";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import AvailableWebsites from '../constants/AvailableWebsites';
|
||||||
import ConnectionManager from "./ConnectionManager";
|
import ConnectionManager from './ConnectionManager';
|
||||||
import type {fullDashboard} from "../screens/Home/HomeScreen";
|
import type {FullDashboardType} from '../screens/Home/HomeScreen';
|
||||||
|
import getStrippedServicesList from '../utils/Services';
|
||||||
|
|
||||||
// AMICALE
|
// AMICALE
|
||||||
const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png";
|
const CLUBS_IMAGE =
|
||||||
const PROFILE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png";
|
'https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png';
|
||||||
const EQUIPMENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png";
|
const PROFILE_IMAGE =
|
||||||
const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png";
|
'https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png';
|
||||||
const AMICALE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png";
|
const EQUIPMENT_IMAGE =
|
||||||
|
'https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png';
|
||||||
|
const VOTE_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png';
|
||||||
|
const AMICALE_IMAGE =
|
||||||
|
'https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png';
|
||||||
|
|
||||||
// STUDENTS
|
// STUDENTS
|
||||||
const PROXIMO_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png"
|
const PROXIMO_IMAGE =
|
||||||
const WIKETUD_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png";
|
'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png';
|
||||||
const EE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png";
|
const WIKETUD_IMAGE =
|
||||||
const TUTORINSA_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.png";
|
'https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png';
|
||||||
|
const EE_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png';
|
||||||
|
const TUTORINSA_IMAGE =
|
||||||
|
'https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.png';
|
||||||
|
|
||||||
// INSA
|
// INSA
|
||||||
const BIB_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bib.png";
|
const BIB_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/Bib.png';
|
||||||
const RU_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/RU.png";
|
const RU_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/RU.png';
|
||||||
const ROOM_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png";
|
const ROOM_IMAGE =
|
||||||
const EMAIL_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png";
|
'https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png';
|
||||||
const ENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png";
|
const EMAIL_IMAGE =
|
||||||
const ACCOUNT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Account.png";
|
'https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png';
|
||||||
|
const ENT_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png';
|
||||||
|
const ACCOUNT_IMAGE =
|
||||||
|
'https://etud.insa-toulouse.fr/~amicale_app/images/Account.png';
|
||||||
|
|
||||||
// SPECIAL
|
// SPECIAL
|
||||||
const WASHER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png";
|
const WASHER_IMAGE =
|
||||||
const DRYER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png";
|
'https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png';
|
||||||
|
const DRYER_IMAGE =
|
||||||
|
'https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png';
|
||||||
|
|
||||||
const AMICALE_LOGO = require("../../assets/amicale.png");
|
const AMICALE_LOGO = require('../../assets/amicale.png');
|
||||||
|
|
||||||
export const SERVICES_KEY = {
|
export const SERVICES_KEY = {
|
||||||
CLUBS: "clubs",
|
CLUBS: 'clubs',
|
||||||
PROFILE: "profile",
|
PROFILE: 'profile',
|
||||||
EQUIPMENT: "equipment",
|
EQUIPMENT: 'equipment',
|
||||||
AMICALE_WEBSITE: "amicale_website",
|
AMICALE_WEBSITE: 'amicale_website',
|
||||||
VOTE: "vote",
|
VOTE: 'vote',
|
||||||
PROXIMO: "proximo",
|
PROXIMO: 'proximo',
|
||||||
WIKETUD: "wiketud",
|
WIKETUD: 'wiketud',
|
||||||
ELUS_ETUDIANTS: "elus_etudiants",
|
ELUS_ETUDIANTS: 'elus_etudiants',
|
||||||
TUTOR_INSA: "tutor_insa",
|
TUTOR_INSA: 'tutor_insa',
|
||||||
RU: "ru",
|
RU: 'ru',
|
||||||
AVAILABLE_ROOMS: "available_rooms",
|
AVAILABLE_ROOMS: 'available_rooms',
|
||||||
BIB: "bib",
|
BIB: 'bib',
|
||||||
EMAIL: "email",
|
EMAIL: 'email',
|
||||||
ENT: "ent",
|
ENT: 'ent',
|
||||||
INSA_ACCOUNT: "insa_account",
|
INSA_ACCOUNT: 'insa_account',
|
||||||
WASHERS: "washers",
|
WASHERS: 'washers',
|
||||||
DRYERS: "dryers",
|
DRYERS: 'dryers',
|
||||||
}
|
};
|
||||||
|
|
||||||
export const SERVICES_CATEGORIES_KEY = {
|
export const SERVICES_CATEGORIES_KEY = {
|
||||||
AMICALE: "amicale",
|
AMICALE: 'amicale',
|
||||||
STUDENTS: "students",
|
STUDENTS: 'students',
|
||||||
INSA: "insa",
|
INSA: 'insa',
|
||||||
SPECIAL: "special",
|
SPECIAL: 'special',
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type ServiceItemType = {
|
||||||
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: fullDashboard) => number,
|
badgeFunction?: (dashboard: FullDashboardType) => number,
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ServiceCategory = {
|
export type ServiceCategoryType = {
|
||||||
key: string,
|
key: string,
|
||||||
title: string,
|
title: string,
|
||||||
subtitle: string,
|
subtitle: string,
|
||||||
image: string | number,
|
image: string | number,
|
||||||
content: Array<ServiceItem>
|
content: Array<ServiceItemType>,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export default class ServicesManager {
|
export default class ServicesManager {
|
||||||
|
|
||||||
navigation: StackNavigationProp;
|
navigation: StackNavigationProp;
|
||||||
|
|
||||||
amicaleDataset: Array<ServiceItem>;
|
amicaleDataset: Array<ServiceItemType>;
|
||||||
studentsDataset: Array<ServiceItem>;
|
|
||||||
insaDataset: Array<ServiceItem>;
|
|
||||||
specialDataset: Array<ServiceItem>;
|
|
||||||
|
|
||||||
categoriesDataset: Array<ServiceCategory>;
|
studentsDataset: Array<ServiceItemType>;
|
||||||
|
|
||||||
|
insaDataset: Array<ServiceItemType>;
|
||||||
|
|
||||||
|
specialDataset: Array<ServiceItemType>;
|
||||||
|
|
||||||
|
categoriesDataset: Array<ServiceCategoryType>;
|
||||||
|
|
||||||
constructor(nav: StackNavigationProp) {
|
constructor(nav: StackNavigationProp) {
|
||||||
this.navigation = nav;
|
this.navigation = nav;
|
||||||
|
|
@ -98,30 +111,31 @@ 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: () => this.onAmicaleServicePress("club-list"),
|
onPress: (): void => 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: () => this.onAmicaleServicePress("profile"),
|
onPress: (): void => 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: () => this.onAmicaleServicePress("equipment-list"),
|
onPress: (): void => 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: () => nav.navigate("website", {
|
onPress: (): void =>
|
||||||
|
nav.navigate('website', {
|
||||||
host: AvailableWebsites.websites.AMICALE,
|
host: AvailableWebsites.websites.AMICALE,
|
||||||
title: i18n.t('screens.websites.amicale')
|
title: i18n.t('screens.websites.amicale'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -129,7 +143,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: () => this.onAmicaleServicePress("vote"),
|
onPress: (): void => this.onAmicaleServicePress('vote'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.studentsDataset = [
|
this.studentsDataset = [
|
||||||
|
|
@ -138,24 +152,30 @@ 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: () => nav.navigate("proximo"),
|
onPress: (): void => nav.navigate('proximo'),
|
||||||
badgeFunction: (dashboard: fullDashboard) => dashboard.proximo_articles
|
badgeFunction: (dashboard: FullDashboardType): number =>
|
||||||
|
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: () => nav.navigate("website", {host: AvailableWebsites.websites.WIKETUD, title: "Wiketud"}),
|
onPress: (): void =>
|
||||||
|
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: () => nav.navigate("website", {
|
onPress: (): void =>
|
||||||
|
nav.navigate('website', {
|
||||||
host: AvailableWebsites.websites.ELUS_ETUDIANTS,
|
host: AvailableWebsites.websites.ELUS_ETUDIANTS,
|
||||||
title: "Élus Étudiants"
|
title: 'Élus Étudiants',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -163,11 +183,13 @@ 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: () => nav.navigate("website", {
|
onPress: (): void =>
|
||||||
|
nav.navigate('website', {
|
||||||
host: AvailableWebsites.websites.TUTOR_INSA,
|
host: AvailableWebsites.websites.TUTOR_INSA,
|
||||||
title: "Tutor'INSA"
|
title: "Tutor'INSA",
|
||||||
}),
|
}),
|
||||||
badgeFunction: (dashboard: fullDashboard) => dashboard.available_tutorials
|
badgeFunction: (dashboard: FullDashboardType): number =>
|
||||||
|
dashboard.available_tutorials,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.insaDataset = [
|
this.insaDataset = [
|
||||||
|
|
@ -176,17 +198,19 @@ 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: () => nav.navigate("self-menu"),
|
onPress: (): void => nav.navigate('self-menu'),
|
||||||
badgeFunction: (dashboard: fullDashboard) => dashboard.today_menu.length
|
badgeFunction: (dashboard: FullDashboardType): number =>
|
||||||
|
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: () => nav.navigate("website", {
|
onPress: (): void =>
|
||||||
|
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'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -194,9 +218,10 @@ 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: () => nav.navigate("website", {
|
onPress: (): void =>
|
||||||
|
nav.navigate('website', {
|
||||||
host: AvailableWebsites.websites.BIB,
|
host: AvailableWebsites.websites.BIB,
|
||||||
title: i18n.t('screens.websites.bib')
|
title: i18n.t('screens.websites.bib'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -204,9 +229,10 @@ 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: () => nav.navigate("website", {
|
onPress: (): void =>
|
||||||
|
nav.navigate('website', {
|
||||||
host: AvailableWebsites.websites.BLUEMIND,
|
host: AvailableWebsites.websites.BLUEMIND,
|
||||||
title: i18n.t('screens.websites.mails')
|
title: i18n.t('screens.websites.mails'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -214,9 +240,10 @@ 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: () => nav.navigate("website", {
|
onPress: (): void =>
|
||||||
|
nav.navigate('website', {
|
||||||
host: AvailableWebsites.websites.ENT,
|
host: AvailableWebsites.websites.ENT,
|
||||||
title: i18n.t('screens.websites.ent')
|
title: i18n.t('screens.websites.ent'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -224,9 +251,10 @@ 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: () => nav.navigate("website", {
|
onPress: (): void =>
|
||||||
|
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'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -236,46 +264,48 @@ 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: () => nav.navigate("proxiwash"),
|
onPress: (): void => nav.navigate('proxiwash'),
|
||||||
badgeFunction: (dashboard: fullDashboard) => dashboard.available_washers
|
badgeFunction: (dashboard: FullDashboardType): number =>
|
||||||
|
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: () => nav.navigate("proxiwash"),
|
onPress: (): void => nav.navigate('proxiwash'),
|
||||||
badgeFunction: (dashboard: fullDashboard) => dashboard.available_dryers
|
badgeFunction: (dashboard: FullDashboardType): number =>
|
||||||
}
|
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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -289,37 +319,18 @@ 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
|
else this.navigation.navigate('login', {nextScreen: route});
|
||||||
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<ServiceItem>}
|
* @returns {Array<ServiceItemType>}
|
||||||
*/
|
*/
|
||||||
getAmicaleServices(excludedItems?: Array<string>) {
|
getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> {
|
||||||
if (excludedItems != null)
|
if (excludedItems != null)
|
||||||
return this.getStrippedList(excludedItems, this.amicaleDataset)
|
return getStrippedServicesList(excludedItems, this.amicaleDataset);
|
||||||
else
|
|
||||||
return this.amicaleDataset;
|
return this.amicaleDataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -327,12 +338,11 @@ 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<ServiceItem>}
|
* @returns {Array<ServiceItemType>}
|
||||||
*/
|
*/
|
||||||
getStudentServices(excludedItems?: Array<string>) {
|
getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> {
|
||||||
if (excludedItems != null)
|
if (excludedItems != null)
|
||||||
return this.getStrippedList(excludedItems, this.studentsDataset)
|
return getStrippedServicesList(excludedItems, this.studentsDataset);
|
||||||
else
|
|
||||||
return this.studentsDataset;
|
return this.studentsDataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,12 +350,11 @@ 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<ServiceItem>}
|
* @returns {Array<ServiceItemType>}
|
||||||
*/
|
*/
|
||||||
getINSAServices(excludedItems?: Array<string>) {
|
getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> {
|
||||||
if (excludedItems != null)
|
if (excludedItems != null)
|
||||||
return this.getStrippedList(excludedItems, this.insaDataset)
|
return getStrippedServicesList(excludedItems, this.insaDataset);
|
||||||
else
|
|
||||||
return this.insaDataset;
|
return this.insaDataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -353,12 +362,11 @@ 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<ServiceItem>}
|
* @returns {Array<ServiceItemType>}
|
||||||
*/
|
*/
|
||||||
getSpecialServices(excludedItems?: Array<string>) {
|
getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> {
|
||||||
if (excludedItems != null)
|
if (excludedItems != null)
|
||||||
return this.getStrippedList(excludedItems, this.specialDataset)
|
return getStrippedServicesList(excludedItems, this.specialDataset);
|
||||||
else
|
|
||||||
return this.specialDataset;
|
return this.specialDataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -366,13 +374,11 @@ 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<ServiceCategory>}
|
* @returns {Array<ServiceCategoryType>}
|
||||||
*/
|
*/
|
||||||
getCategories(excludedItems?: Array<string>) {
|
getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> {
|
||||||
if (excludedItems != null)
|
if (excludedItems != null)
|
||||||
return this.getStrippedList(excludedItems, this.categoriesDataset)
|
return getStrippedServicesList(excludedItems, this.categoriesDataset);
|
||||||
else
|
|
||||||
return this.categoriesDataset;
|
return this.categoriesDataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 CustomTheme = {
|
export type CustomThemeType = {
|
||||||
...DefaultTheme,
|
...DefaultTheme,
|
||||||
colors: {
|
colors: {
|
||||||
primary: string,
|
primary: string,
|
||||||
|
|
@ -63,15 +63,15 @@ export type CustomTheme = {
|
||||||
// Mascot Popup
|
// Mascot Popup
|
||||||
mascotMessageArrow: string,
|
mascotMessageArrow: string,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton class used to manage themes
|
* Singleton class used to manage themes
|
||||||
*/
|
*/
|
||||||
export default class ThemeManager {
|
export default class ThemeManager {
|
||||||
|
|
||||||
static instance: ThemeManager | null = null;
|
static instance: ThemeManager | null = null;
|
||||||
updateThemeCallback: Function;
|
|
||||||
|
updateThemeCallback: null | (() => void);
|
||||||
|
|
||||||
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 {CustomTheme} Object containing theme variables
|
* @return {CustomThemeType} Object containing theme variables
|
||||||
* */
|
* */
|
||||||
static getWhiteTheme(): CustomTheme {
|
static getWhiteTheme(): CustomThemeType {
|
||||||
return {
|
return {
|
||||||
...DefaultTheme,
|
...DefaultTheme,
|
||||||
colors: {
|
colors: {
|
||||||
...DefaultTheme.colors,
|
...DefaultTheme.colors,
|
||||||
primary: '#be1522',
|
primary: '#be1522',
|
||||||
accent: '#be1522',
|
accent: '#be1522',
|
||||||
tabIcon: "#929292",
|
tabIcon: '#929292',
|
||||||
card: "#fff",
|
card: '#fff',
|
||||||
dividerBackground: '#e2e2e2',
|
dividerBackground: '#e2e2e2',
|
||||||
ripple: "rgba(0,0,0,0.2)",
|
ripple: 'rgba(0,0,0,0.2)',
|
||||||
textDisabled: '#c1c1c1',
|
textDisabled: '#c1c1c1',
|
||||||
icon: '#5d5d5d',
|
icon: '#5d5d5d',
|
||||||
subtitle: '#707070',
|
subtitle: '#707070',
|
||||||
success: "#5cb85c",
|
success: '#5cb85c',
|
||||||
warning: "#f0ad4e",
|
warning: '#f0ad4e',
|
||||||
danger: "#d9534f",
|
danger: '#d9534f',
|
||||||
cc: 'dst',
|
cc: 'dst',
|
||||||
|
|
||||||
// Calendar/Agenda
|
// Calendar/Agenda
|
||||||
|
|
@ -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 {CustomTheme} Object containing theme variables
|
* @return {CustomThemeType} Object containing theme variables
|
||||||
* */
|
* */
|
||||||
static getDarkTheme(): CustomTheme {
|
static getDarkTheme(): CustomThemeType {
|
||||||
return {
|
return {
|
||||||
...DarkTheme,
|
...DarkTheme,
|
||||||
colors: {
|
colors: {
|
||||||
...DarkTheme.colors,
|
...DarkTheme.colors,
|
||||||
primary: '#be1522',
|
primary: '#be1522',
|
||||||
accent: '#be1522',
|
accent: '#be1522',
|
||||||
tabBackground: "#181818",
|
tabBackground: '#181818',
|
||||||
tabIcon: "#6d6d6d",
|
tabIcon: '#6d6d6d',
|
||||||
card: "rgb(18,18,18)",
|
card: 'rgb(18,18,18)',
|
||||||
dividerBackground: '#222222',
|
dividerBackground: '#222222',
|
||||||
ripple: "rgba(255,255,255,0.2)",
|
ripple: 'rgba(255,255,255,0.2)',
|
||||||
textDisabled: '#5b5b5b',
|
textDisabled: '#5b5b5b',
|
||||||
icon: '#b3b3b3',
|
icon: '#b3b3b3',
|
||||||
subtitle: '#aaaaaa',
|
subtitle: '#aaaaaa',
|
||||||
success: "#5cb85c",
|
success: '#5cb85c',
|
||||||
warning: "#f0ad4e",
|
warning: '#f0ad4e',
|
||||||
danger: "#d9534f",
|
danger: '#d9534f',
|
||||||
|
|
||||||
// Calendar/Agenda
|
// Calendar/Agenda
|
||||||
agendaBackgroundColor: '#171717',
|
agendaBackgroundColor: '#171717',
|
||||||
agendaDayTextColor: '#6d6d6d',
|
agendaDayTextColor: '#6d6d6d',
|
||||||
|
|
||||||
// PROXIWASH
|
// PROXIWASH
|
||||||
proxiwashFinishedColor: "#31682c",
|
proxiwashFinishedColor: '#31682c',
|
||||||
proxiwashReadyColor: "transparent",
|
proxiwashReadyColor: 'transparent',
|
||||||
proxiwashRunningColor: "#213c79",
|
proxiwashRunningColor: '#213c79',
|
||||||
proxiwashRunningNotStartedColor: "#1e263e",
|
proxiwashRunningNotStartedColor: '#1e263e',
|
||||||
proxiwashRunningBgColor: "#1a2033",
|
proxiwashRunningBgColor: '#1a2033',
|
||||||
proxiwashBrokenColor: "#7e2e2f",
|
proxiwashBrokenColor: '#7e2e2f',
|
||||||
proxiwashErrorColor: "#7e2e2f",
|
proxiwashErrorColor: '#7e2e2f',
|
||||||
proxiwashUnknownColor: "#535353",
|
proxiwashUnknownColor: '#535353',
|
||||||
|
|
||||||
// Screens
|
// Screens
|
||||||
planningColor: '#d99e09',
|
planningColor: '#d99e09',
|
||||||
|
|
@ -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 {
|
||||||
return ThemeManager.instance === null ?
|
if (ThemeManager.instance == null)
|
||||||
ThemeManager.instance = new ThemeManager() :
|
ThemeManager.instance = new ThemeManager();
|
||||||
ThemeManager.instance;
|
return ThemeManager.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -228,34 +228,39 @@ export default class ThemeManager {
|
||||||
* @returns {boolean} Night mode state
|
* @returns {boolean} Night mode state
|
||||||
*/
|
*/
|
||||||
static getNightMode(): boolean {
|
static getNightMode(): boolean {
|
||||||
return (AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightMode.key) &&
|
return (
|
||||||
(!AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
|
(AsyncStorageManager.getBool(
|
||||||
|| colorScheme === 'no-preference')) ||
|
AsyncStorageManager.PREFERENCES.nightMode.key,
|
||||||
(AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key)
|
) &&
|
||||||
&& colorScheme === 'dark');
|
(!AsyncStorageManager.getBool(
|
||||||
|
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
|
||||||
|
) ||
|
||||||
|
colorScheme === 'no-preference')) ||
|
||||||
|
(AsyncStorageManager.getBool(
|
||||||
|
AsyncStorageManager.PREFERENCES.nightModeFollowSystem.key,
|
||||||
|
) &&
|
||||||
|
colorScheme === 'dark')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current theme based on night mode and events
|
* Get the current theme based on night mode and events
|
||||||
*
|
*
|
||||||
* @returns {CustomTheme} The current theme
|
* @returns {CustomThemeType} The current theme
|
||||||
*/
|
*/
|
||||||
static getCurrentTheme(): CustomTheme {
|
static getCurrentTheme(): CustomThemeType {
|
||||||
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
|
if (AprilFoolsManager.getInstance().isAprilFoolsEnabled())
|
||||||
return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme());
|
return AprilFoolsManager.getAprilFoolsTheme(ThemeManager.getWhiteTheme());
|
||||||
else
|
return ThemeManager.getBaseTheme();
|
||||||
return ThemeManager.getBaseTheme()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the theme based on night mode
|
* Get the theme based on night mode
|
||||||
*
|
*
|
||||||
* @return {CustomTheme} The theme
|
* @return {CustomThemeType} The theme
|
||||||
*/
|
*/
|
||||||
static getBaseTheme(): CustomTheme {
|
static getBaseTheme(): CustomThemeType {
|
||||||
if (ThemeManager.getNightMode())
|
if (ThemeManager.getNightMode()) return ThemeManager.getDarkTheme();
|
||||||
return ThemeManager.getDarkTheme();
|
|
||||||
else
|
|
||||||
return ThemeManager.getWhiteTheme();
|
return ThemeManager.getWhiteTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -274,9 +279,10 @@ 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.PREFERENCES.nightMode.key, isNightMode);
|
AsyncStorageManager.set(
|
||||||
if (this.updateThemeCallback != null)
|
AsyncStorageManager.PREFERENCES.nightMode.key,
|
||||||
this.updateThemeCallback();
|
isNightMode,
|
||||||
|
);
|
||||||
|
if (this.updateThemeCallback != null) this.updateThemeCallback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,41 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import {Platform} from 'react-native';
|
||||||
import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
|
import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
|
||||||
import AboutScreen from '../screens/About/AboutScreen';
|
import AboutScreen from '../screens/About/AboutScreen';
|
||||||
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
|
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
|
||||||
import DebugScreen from '../screens/About/DebugScreen';
|
import DebugScreen from '../screens/About/DebugScreen';
|
||||||
import {createStackNavigator, TransitionPresets} from "@react-navigation/stack";
|
import TabNavigator from './TabNavigator';
|
||||||
import i18n from "i18n-js";
|
import GameMainScreen from '../screens/Game/screens/GameMainScreen';
|
||||||
import TabNavigator from "./TabNavigator";
|
import VoteScreen from '../screens/Amicale/VoteScreen';
|
||||||
import GameMainScreen from "../screens/Game/screens/GameMainScreen";
|
import LoginScreen from '../screens/Amicale/LoginScreen';
|
||||||
import VoteScreen from "../screens/Amicale/VoteScreen";
|
import SelfMenuScreen from '../screens/Services/SelfMenuScreen';
|
||||||
import LoginScreen from "../screens/Amicale/LoginScreen";
|
import ProximoMainScreen from '../screens/Services/Proximo/ProximoMainScreen';
|
||||||
import {Platform} from "react-native";
|
import ProximoListScreen from '../screens/Services/Proximo/ProximoListScreen';
|
||||||
import SelfMenuScreen from "../screens/Services/SelfMenuScreen";
|
import ProximoAboutScreen from '../screens/Services/Proximo/ProximoAboutScreen';
|
||||||
import ProximoMainScreen from "../screens/Services/Proximo/ProximoMainScreen";
|
import ProfileScreen from '../screens/Amicale/ProfileScreen';
|
||||||
import ProximoListScreen from "../screens/Services/Proximo/ProximoListScreen";
|
import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
|
||||||
import ProximoAboutScreen from "../screens/Services/Proximo/ProximoAboutScreen";
|
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
|
||||||
import ProfileScreen from "../screens/Amicale/ProfileScreen";
|
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
|
||||||
import ClubListScreen from "../screens/Amicale/Clubs/ClubListScreen";
|
import {
|
||||||
import ClubAboutScreen from "../screens/Amicale/Clubs/ClubAboutScreen";
|
createScreenCollapsibleStack,
|
||||||
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
|
getWebsiteStack,
|
||||||
import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils";
|
} from '../utils/CollapsibleUtils';
|
||||||
import BugReportScreen from "../screens/Other/FeedbackScreen";
|
import BugReportScreen from '../screens/Other/FeedbackScreen';
|
||||||
import WebsiteScreen from "../screens/Services/WebsiteScreen";
|
import WebsiteScreen from '../screens/Services/WebsiteScreen';
|
||||||
import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen";
|
import EquipmentScreen from '../screens/Amicale/Equipment/EquipmentListScreen';
|
||||||
import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen";
|
import EquipmentLendScreen from '../screens/Amicale/Equipment/EquipmentRentScreen';
|
||||||
import EquipmentConfirmScreen from "../screens/Amicale/Equipment/EquipmentConfirmScreen";
|
import EquipmentConfirmScreen from '../screens/Amicale/Equipment/EquipmentConfirmScreen';
|
||||||
import DashboardEditScreen from "../screens/Other/Settings/DashboardEditScreen";
|
import DashboardEditScreen from '../screens/Other/Settings/DashboardEditScreen';
|
||||||
import GameStartScreen from "../screens/Game/screens/GameStartScreen";
|
import GameStartScreen from '../screens/Game/screens/GameStartScreen';
|
||||||
|
|
||||||
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
|
const modalTransition =
|
||||||
|
Platform.OS === 'ios'
|
||||||
|
? TransitionPresets.ModalPresentationIOS
|
||||||
|
: TransitionPresets.ModalTransition;
|
||||||
|
|
||||||
const defaultScreenOptions = {
|
const defaultScreenOptions = {
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
|
|
@ -37,91 +43,100 @@ const defaultScreenOptions = {
|
||||||
...TransitionPresets.SlideFromRightIOS,
|
...TransitionPresets.SlideFromRightIOS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const MainStack = createStackNavigator();
|
const MainStack = createStackNavigator();
|
||||||
|
|
||||||
function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
|
function MainStackComponent(props: {
|
||||||
|
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={props.createTabNavigator}
|
component={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'),
|
||||||
|
|
@ -130,75 +145,87 @@ function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{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 Props = {
|
type PropsType = {
|
||||||
defaultHomeRoute: string | null,
|
defaultHomeRoute: string | null,
|
||||||
defaultHomeData: { [key: string]: any }
|
// eslint-disable-next-line flowtype/no-weak-types
|
||||||
}
|
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: Props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.createTabNavigator = () => <TabNavigator {...props}/>
|
this.createTabNavigator = (): React.Node => (
|
||||||
}
|
<TabNavigator
|
||||||
|
defaultHomeRoute={props.defaultHomeRoute}
|
||||||
render() {
|
defaultHomeData={props.defaultHomeData}
|
||||||
return (
|
/>
|
||||||
<MainStackComponent createTabNavigator={this.createTabNavigator}/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render(): React.Node {
|
||||||
|
return <MainStackComponent createTabNavigator={this.createTabNavigator} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,39 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
|
import {createStackNavigator, TransitionPresets} from '@react-navigation/stack';
|
||||||
import {createBottomTabNavigator} from "@react-navigation/bottom-tabs";
|
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
|
||||||
|
|
||||||
|
import {Title, useTheme} from 'react-native-paper';
|
||||||
|
import {Platform} from 'react-native';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import {createCollapsibleStack} from 'react-navigation-collapsible';
|
||||||
|
import {View} from 'react-native-animatable';
|
||||||
import HomeScreen from '../screens/Home/HomeScreen';
|
import HomeScreen from '../screens/Home/HomeScreen';
|
||||||
import PlanningScreen from '../screens/Planning/PlanningScreen';
|
import PlanningScreen from '../screens/Planning/PlanningScreen';
|
||||||
import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
|
import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
|
||||||
import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
|
import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
|
||||||
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
|
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
|
||||||
import PlanexScreen from '../screens/Planex/PlanexScreen';
|
import PlanexScreen from '../screens/Planex/PlanexScreen';
|
||||||
import AsyncStorageManager from "../managers/AsyncStorageManager";
|
import AsyncStorageManager from '../managers/AsyncStorageManager';
|
||||||
import {Title, useTheme} from 'react-native-paper';
|
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
|
||||||
import {Platform} from 'react-native';
|
import ScannerScreen from '../screens/Home/ScannerScreen';
|
||||||
import i18n from "i18n-js";
|
import FeedItemScreen from '../screens/Home/FeedItemScreen';
|
||||||
import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
|
import GroupSelectionScreen from '../screens/Planex/GroupSelectionScreen';
|
||||||
import ScannerScreen from "../screens/Home/ScannerScreen";
|
import CustomTabBar from '../components/Tabbar/CustomTabBar';
|
||||||
import FeedItemScreen from "../screens/Home/FeedItemScreen";
|
import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
|
||||||
import {createCollapsibleStack} from "react-navigation-collapsible";
|
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
|
||||||
import GroupSelectionScreen from "../screens/Planex/GroupSelectionScreen";
|
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
|
||||||
import CustomTabBar from "../components/Tabbar/CustomTabBar";
|
import {
|
||||||
import WebsitesHomeScreen from "../screens/Services/ServicesScreen";
|
createScreenCollapsibleStack,
|
||||||
import ServicesSectionScreen from "../screens/Services/ServicesSectionScreen";
|
getWebsiteStack,
|
||||||
import AmicaleContactScreen from "../screens/Amicale/AmicaleContactScreen";
|
} from '../utils/CollapsibleUtils';
|
||||||
import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils";
|
import Mascot, {MASCOT_STYLE} from '../components/Mascot/Mascot';
|
||||||
import {View} from "react-native-animatable";
|
|
||||||
import Mascot, {MASCOT_STYLE} from "../components/Mascot/Mascot";
|
|
||||||
|
|
||||||
const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
|
|
||||||
|
|
||||||
|
const modalTransition =
|
||||||
|
Platform.OS === 'ios'
|
||||||
|
? TransitionPresets.ModalPresentationIOS
|
||||||
|
: TransitionPresets.ModalTransition;
|
||||||
|
|
||||||
const defaultScreenOptions = {
|
const defaultScreenOptions = {
|
||||||
gestureEnabled: true,
|
gestureEnabled: true,
|
||||||
|
|
@ -34,94 +41,98 @@ const defaultScreenOptions = {
|
||||||
...modalTransition,
|
...modalTransition,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const ServicesStack = createStackNavigator();
|
const ServicesStack = createStackNavigator();
|
||||||
|
|
||||||
function ServicesStackComponent() {
|
function ServicesStackComponent(): React.Node {
|
||||||
return (
|
return (
|
||||||
<ServicesStack.Navigator
|
<ServicesStack.Navigator
|
||||||
initialRouteName="index"
|
initialRouteName="index"
|
||||||
headerMode={"screen"}
|
headerMode="screen"
|
||||||
screenOptions={defaultScreenOptions}
|
screenOptions={defaultScreenOptions}>
|
||||||
>
|
|
||||||
{createScreenCollapsibleStack(
|
{createScreenCollapsibleStack(
|
||||||
"index",
|
'index',
|
||||||
ServicesStack,
|
ServicesStack,
|
||||||
WebsitesHomeScreen,
|
WebsitesHomeScreen,
|
||||||
i18n.t('screens.services.title'))}
|
i18n.t('screens.services.title'),
|
||||||
|
)}
|
||||||
{createScreenCollapsibleStack(
|
{createScreenCollapsibleStack(
|
||||||
"services-section",
|
'services-section',
|
||||||
ServicesStack,
|
ServicesStack,
|
||||||
ServicesSectionScreen,
|
ServicesSectionScreen,
|
||||||
"SECTION")}
|
'SECTION',
|
||||||
|
)}
|
||||||
{createScreenCollapsibleStack(
|
{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() {
|
function ProxiwashStackComponent(): React.Node {
|
||||||
return (
|
return (
|
||||||
<ProxiwashStack.Navigator
|
<ProxiwashStack.Navigator
|
||||||
initialRouteName="index"
|
initialRouteName="index"
|
||||||
headerMode={"screen"}
|
headerMode="screen"
|
||||||
screenOptions={defaultScreenOptions}
|
screenOptions={defaultScreenOptions}>
|
||||||
>
|
|
||||||
{createScreenCollapsibleStack(
|
{createScreenCollapsibleStack(
|
||||||
"index",
|
'index',
|
||||||
ProxiwashStack,
|
ProxiwashStack,
|
||||||
ProxiwashScreen,
|
ProxiwashScreen,
|
||||||
i18n.t('screens.proxiwash.title'))}
|
i18n.t('screens.proxiwash.title'),
|
||||||
|
)}
|
||||||
{createScreenCollapsibleStack(
|
{createScreenCollapsibleStack(
|
||||||
"proxiwash-about",
|
'proxiwash-about',
|
||||||
ProxiwashStack,
|
ProxiwashStack,
|
||||||
ProxiwashAboutScreen,
|
ProxiwashAboutScreen,
|
||||||
i18n.t('screens.proxiwash.title'))}
|
i18n.t('screens.proxiwash.title'),
|
||||||
|
)}
|
||||||
</ProxiwashStack.Navigator>
|
</ProxiwashStack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlanningStack = createStackNavigator();
|
const PlanningStack = createStackNavigator();
|
||||||
|
|
||||||
function PlanningStackComponent() {
|
function PlanningStackComponent(): React.Node {
|
||||||
return (
|
return (
|
||||||
<PlanningStack.Navigator
|
<PlanningStack.Navigator
|
||||||
initialRouteName="index"
|
initialRouteName="index"
|
||||||
headerMode={"screen"}
|
headerMode="screen"
|
||||||
screenOptions={defaultScreenOptions}
|
screenOptions={defaultScreenOptions}>
|
||||||
>
|
|
||||||
<PlanningStack.Screen
|
<PlanningStack.Screen
|
||||||
name="index"
|
name="index"
|
||||||
component={PlanningScreen}
|
component={PlanningScreen}
|
||||||
options={{title: i18n.t('screens.planning.title'),}}
|
options={{title: i18n.t('screens.planning.title')}}
|
||||||
/>
|
/>
|
||||||
{createScreenCollapsibleStack(
|
{createScreenCollapsibleStack(
|
||||||
"planning-information",
|
'planning-information',
|
||||||
PlanningStack,
|
PlanningStack,
|
||||||
PlanningDisplayScreen,
|
PlanningDisplayScreen,
|
||||||
i18n.t('screens.planning.eventDetails'))}
|
i18n.t('screens.planning.eventDetails'),
|
||||||
|
)}
|
||||||
</PlanningStack.Navigator>
|
</PlanningStack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const HomeStack = createStackNavigator();
|
const HomeStack = createStackNavigator();
|
||||||
|
|
||||||
function HomeStackComponent(initialRoute: string | null, defaultData: { [key: string]: any }) {
|
function HomeStackComponent(
|
||||||
let params = undefined;
|
initialRoute: string | null,
|
||||||
|
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"
|
||||||
|
|
@ -131,113 +142,123 @@ function HomeStackComponent(initialRoute: string | null, defaultData: { [key: st
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: colors.surface,
|
backgroundColor: colors.surface,
|
||||||
},
|
},
|
||||||
headerTitle: () =>
|
headerTitle: (): React.Node => (
|
||||||
<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={true}
|
animated
|
||||||
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 style={{
|
<Title
|
||||||
|
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() {
|
function PlanexStackComponent(): React.Node {
|
||||||
return (
|
return (
|
||||||
<PlanexStack.Navigator
|
<PlanexStack.Navigator
|
||||||
initialRouteName="index"
|
initialRouteName="index"
|
||||||
headerMode={"screen"}
|
headerMode="screen"
|
||||||
screenOptions={defaultScreenOptions}
|
screenOptions={defaultScreenOptions}>
|
||||||
>
|
|
||||||
{getWebsiteStack(
|
{getWebsiteStack(
|
||||||
"index",
|
'index',
|
||||||
PlanexStack,
|
PlanexStack,
|
||||||
PlanexScreen,
|
PlanexScreen,
|
||||||
i18n.t("screens.planex.title"))}
|
i18n.t('screens.planex.title'),
|
||||||
|
)}
|
||||||
{createScreenCollapsibleStack(
|
{createScreenCollapsibleStack(
|
||||||
"group-select",
|
'group-select',
|
||||||
PlanexStack,
|
PlanexStack,
|
||||||
GroupSelectionScreen,
|
GroupSelectionScreen,
|
||||||
"")}
|
'',
|
||||||
|
)}
|
||||||
</PlanexStack.Navigator>
|
</PlanexStack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
defaultHomeRoute: string | null,
|
defaultHomeRoute: string | null,
|
||||||
defaultHomeData: { [key: string]: any }
|
defaultHomeData: {[key: string]: string},
|
||||||
}
|
};
|
||||||
|
|
||||||
export default class TabNavigator extends React.Component<Props> {
|
export default class TabNavigator extends React.Component<PropsType> {
|
||||||
|
createHomeStackComponent: () => React.Node;
|
||||||
|
|
||||||
createHomeStackComponent: () => HomeStackComponent;
|
|
||||||
defaultRoute: string;
|
defaultRoute: string;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
if (props.defaultHomeRoute != null)
|
if (props.defaultHomeRoute != null) this.defaultRoute = 'home';
|
||||||
this.defaultRoute = 'home';
|
|
||||||
else
|
else
|
||||||
this.defaultRoute = AsyncStorageManager.getString(AsyncStorageManager.PREFERENCES.defaultStartScreen.key).toLowerCase();
|
this.defaultRoute = AsyncStorageManager.getString(
|
||||||
this.createHomeStackComponent = () => HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
|
AsyncStorageManager.PREFERENCES.defaultStartScreen.key,
|
||||||
|
).toLowerCase();
|
||||||
|
this.createHomeStackComponent = (): React.Node =>
|
||||||
|
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName={this.defaultRoute}
|
initialRouteName={this.defaultRoute}
|
||||||
tabBar={props => <CustomTabBar {...props} />}
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
>
|
tabBar={(props: {...}): React.Node => <CustomTabBar {...props} />}>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="services"
|
name="services"
|
||||||
option
|
option
|
||||||
|
|
@ -263,7 +284,7 @@ export default class TabNavigator extends React.Component<Props> {
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,73 +1,75 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import packageJson from '../../../package';
|
|
||||||
import {List} from 'react-native-paper';
|
import {List} from 'react-native-paper';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import {View} from 'react-native-animatable';
|
||||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||||
import {View} from "react-native-animatable";
|
import packageJson from '../../../package.json';
|
||||||
|
|
||||||
type listItem = {
|
type ListItemType = {
|
||||||
name: string,
|
name: string,
|
||||||
version: string
|
version: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the dependencies list from the raw json
|
* Generates the dependencies list from the raw json
|
||||||
*
|
*
|
||||||
* @param object The raw json
|
* @param object The raw json
|
||||||
* @return {Array<listItem>}
|
* @return {Array<ListItemType>}
|
||||||
*/
|
*/
|
||||||
function generateListFromObject(object: { [key: string]: string }): Array<listItem> {
|
function generateListFromObject(object: {
|
||||||
let list = [];
|
[key: string]: string,
|
||||||
let keys = Object.keys(object);
|
}): Array<ListItemType> {
|
||||||
let values = Object.values(object);
|
const list = [];
|
||||||
for (let i = 0; i < keys.length; i++) {
|
const keys = Object.keys(object);
|
||||||
list.push({name: keys[i], version: values[i]});
|
keys.forEach((key: string) => {
|
||||||
}
|
list.push({name: key, version: object[key]});
|
||||||
//$FlowFixMe
|
});
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
|
||||||
navigation: StackNavigationProp,
|
|
||||||
}
|
|
||||||
|
|
||||||
const LIST_ITEM_HEIGHT = 64;
|
const LIST_ITEM_HEIGHT = 64;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class defining a screen showing the list of libraries used by the app, taken from package.json
|
* Class defining a screen showing the list of libraries used by the app, taken from package.json
|
||||||
*/
|
*/
|
||||||
export default class AboutDependenciesScreen extends React.Component<Props> {
|
export default class AboutDependenciesScreen extends React.Component<null> {
|
||||||
|
data: Array<ListItemType>;
|
||||||
data: Array<listItem>;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.data = generateListFromObject(packageJson.dependencies);
|
this.data = generateListFromObject(packageJson.dependencies);
|
||||||
}
|
}
|
||||||
|
|
||||||
keyExtractor = (item: listItem) => item.name;
|
keyExtractor = (item: ListItemType): string => item.name;
|
||||||
|
|
||||||
renderItem = ({item}: { item: listItem }) =>
|
getRenderItem = ({item}: {item: ListItemType}): React.Node => (
|
||||||
<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}}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
itemLayout = (data: any, index: number) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
|
getItemLayout = (
|
||||||
|
data: ListItemType,
|
||||||
|
index: number,
|
||||||
|
): {length: number, offset: number, index: number} => ({
|
||||||
|
length: LIST_ITEM_HEIGHT,
|
||||||
|
offset: LIST_ITEM_HEIGHT * index,
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<CollapsibleFlatList
|
<CollapsibleFlatList
|
||||||
data={this.data}
|
data={this.data}
|
||||||
keyExtractor={this.keyExtractor}
|
keyExtractor={this.keyExtractor}
|
||||||
renderItem={this.renderItem}
|
renderItem={this.getRenderItem}
|
||||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews
|
||||||
getItemLayout={this.itemLayout}
|
getItemLayout={this.getItemLayout}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,50 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {FlatList, Linking, Platform, View} from 'react-native';
|
import {FlatList, Linking, Platform} from 'react-native';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
import {Avatar, Card, List, Title, withTheme} from 'react-native-paper';
|
import {Avatar, Card, List, Title, withTheme} from 'react-native-paper';
|
||||||
import packageJson from "../../../package.json";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import packageJson from '../../../package.json';
|
||||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||||
|
import APP_LOGO from '../../../assets/android.icon.png';
|
||||||
|
|
||||||
type ListItem = {
|
type ListItemType = {
|
||||||
onPressCallback: () => void,
|
onPressCallback: () => void,
|
||||||
icon: string,
|
icon: string,
|
||||||
text: string,
|
text: string,
|
||||||
showChevron: boolean
|
showChevron: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const links = {
|
const links = {
|
||||||
appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148',
|
appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148',
|
||||||
playstore: 'https://play.google.com/store/apps/details?id=fr.amicaleinsat.application',
|
playstore:
|
||||||
git: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/README.md',
|
'https://play.google.com/store/apps/details?id=fr.amicaleinsat.application',
|
||||||
changelog: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/Changelog.md',
|
git:
|
||||||
license: 'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/LICENSE',
|
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/README.md',
|
||||||
authorMail: "mailto:vergnet@etud.insa-toulouse.fr?" +
|
changelog:
|
||||||
"subject=" +
|
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/Changelog.md',
|
||||||
"Application Amicale INSA Toulouse" +
|
license:
|
||||||
"&body=" +
|
'https://git.etud.insa-toulouse.fr/vergnet/application-amicale/src/branch/master/LICENSE',
|
||||||
"Coucou !\n\n",
|
authorMail:
|
||||||
|
'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: "mailto:ysimard@etud.insa-toulouse.fr?" +
|
yohanMail:
|
||||||
"subject=" +
|
'mailto:ysimard@etud.insa-toulouse.fr?' +
|
||||||
"Application Amicale INSA Toulouse" +
|
'subject=' +
|
||||||
"&body=" +
|
'Application Amicale INSA Toulouse' +
|
||||||
"Coucou !\n\n",
|
'&body=' +
|
||||||
|
'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 Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -45,113 +52,145 @@ type Props = {
|
||||||
* Opens a link in the device's browser
|
* Opens a link in the device's browser
|
||||||
* @param link The link to open
|
* @param link The link to open
|
||||||
*/
|
*/
|
||||||
function openWebLink(link) {
|
function openWebLink(link: string) {
|
||||||
Linking.openURL(link).catch((err) => console.error('Error opening link', err));
|
Linking.openURL(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class defining an about screen. This screen shows the user information about the app and it's author.
|
* Class defining an about screen. This screen shows the user information about the app and it's author.
|
||||||
*/
|
*/
|
||||||
class AboutScreen extends React.Component<Props> {
|
class AboutScreen extends React.Component<PropsType> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data to be displayed in the app card
|
* Data to be displayed in the app card
|
||||||
*/
|
*/
|
||||||
appData = [
|
appData = [
|
||||||
{
|
{
|
||||||
onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore),
|
onPressCallback: () => {
|
||||||
icon: Platform.OS === "ios" ? 'apple' : 'google-play',
|
openWebLink(Platform.OS === 'ios' ? links.appstore : links.playstore);
|
||||||
text: Platform.OS === "ios" ? i18n.t('screens.about.appstore') : i18n.t('screens.about.playstore'),
|
},
|
||||||
showChevron: true
|
icon: Platform.OS === 'ios' ? 'apple' : 'google-play',
|
||||||
|
text:
|
||||||
|
Platform.OS === 'ios'
|
||||||
|
? i18n.t('screens.about.appstore')
|
||||||
|
: i18n.t('screens.about.playstore'),
|
||||||
|
showChevron: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onPressCallback: () => this.props.navigation.navigate("feedback"),
|
onPressCallback: () => {
|
||||||
|
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: () => openWebLink(links.git),
|
onPressCallback: () => {
|
||||||
|
openWebLink(links.git);
|
||||||
|
},
|
||||||
icon: 'git',
|
icon: 'git',
|
||||||
text: 'Git',
|
text: 'Git',
|
||||||
showChevron: true
|
showChevron: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onPressCallback: () => openWebLink(links.changelog),
|
onPressCallback: () => {
|
||||||
|
openWebLink(links.changelog);
|
||||||
|
},
|
||||||
icon: 'refresh',
|
icon: 'refresh',
|
||||||
text: i18n.t('screens.about.changelog'),
|
text: i18n.t('screens.about.changelog'),
|
||||||
showChevron: true
|
showChevron: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onPressCallback: () => openWebLink(links.license),
|
onPressCallback: () => {
|
||||||
|
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: () => openWebLink(links.meme),
|
onPressCallback: () => {
|
||||||
|
openWebLink(links.meme);
|
||||||
|
},
|
||||||
icon: 'account-circle',
|
icon: 'account-circle',
|
||||||
text: 'Arnaud VERGNET',
|
text: 'Arnaud VERGNET',
|
||||||
showChevron: false
|
showChevron: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onPressCallback: () => openWebLink(links.authorMail),
|
onPressCallback: () => {
|
||||||
|
openWebLink(links.authorMail);
|
||||||
|
},
|
||||||
icon: 'email',
|
icon: 'email',
|
||||||
text: i18n.t('screens.about.authorMail'),
|
text: i18n.t('screens.about.authorMail'),
|
||||||
showChevron: true
|
showChevron: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onPressCallback: () => openWebLink(links.authorLinkedin),
|
onPressCallback: () => {
|
||||||
|
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: () => console.log('Meme this'),
|
onPressCallback: () => {},
|
||||||
icon: 'account',
|
icon: 'account',
|
||||||
text: 'Yohan SIMARD',
|
text: 'Yohan SIMARD',
|
||||||
showChevron: false
|
showChevron: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onPressCallback: () => openWebLink(links.yohanMail),
|
onPressCallback: () => {
|
||||||
|
openWebLink(links.yohanMail);
|
||||||
|
},
|
||||||
icon: 'email',
|
icon: 'email',
|
||||||
text: i18n.t('screens.about.authorMail'),
|
text: i18n.t('screens.about.authorMail'),
|
||||||
showChevron: true
|
showChevron: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onPressCallback: () => openWebLink(links.yohanLinkedin),
|
onPressCallback: () => {
|
||||||
|
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: () => openWebLink(links.react),
|
onPressCallback: () => {
|
||||||
|
openWebLink(links.react);
|
||||||
|
},
|
||||||
icon: 'react',
|
icon: 'react',
|
||||||
text: i18n.t('screens.about.reactNative'),
|
text: i18n.t('screens.about.reactNative'),
|
||||||
showChevron: true
|
showChevron: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onPressCallback: () => this.props.navigation.navigate('dependencies'),
|
onPressCallback: () => {
|
||||||
|
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
|
||||||
*/
|
*/
|
||||||
|
|
@ -167,44 +206,25 @@ class AboutScreen extends React.Component<Props> {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* 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() {
|
getAppCard(): React.Node {
|
||||||
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={this.getAppIcon}/>
|
left={({size}: {size: number}): React.Node => (
|
||||||
|
<Avatar.Image
|
||||||
|
size={size}
|
||||||
|
source={APP_LOGO}
|
||||||
|
style={{backgroundColor: 'transparent'}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={this.appData}
|
data={this.appData}
|
||||||
|
|
@ -221,25 +241,28 @@ class AboutScreen extends React.Component<Props> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getTeamCard() {
|
getTeamCard(): React.Node {
|
||||||
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={(props) => <Avatar.Icon {...props} icon={'account-multiple'}/>}/>
|
left={({size, color}: {size: number, color: string}): React.Node => (
|
||||||
|
<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>
|
||||||
|
|
@ -252,7 +275,7 @@ class AboutScreen extends React.Component<Props> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getTechnoCard() {
|
getTechnoCard(): React.Node {
|
||||||
return (
|
return (
|
||||||
<Card style={{marginBottom: 10}}>
|
<Card style={{marginBottom: 10}}>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
|
|
@ -273,10 +296,14 @@ class AboutScreen extends React.Component<Props> {
|
||||||
* @param props
|
* @param props
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getChevronIcon(props) {
|
static getChevronIcon({
|
||||||
return (
|
size,
|
||||||
<List.Icon {...props} icon={'chevron-right'}/>
|
color,
|
||||||
);
|
}: {
|
||||||
|
size: number,
|
||||||
|
color: string,
|
||||||
|
}): React.Node {
|
||||||
|
return <List.Icon size={size} color={color} icon="chevron-right" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -286,10 +313,11 @@ class AboutScreen extends React.Component<Props> {
|
||||||
* @param props
|
* @param props
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getItemIcon(item: ListItem, props) {
|
static getItemIcon(
|
||||||
return (
|
item: ListItemType,
|
||||||
<List.Icon {...props} icon={item.icon}/>
|
{size, color}: {size: number, color: string},
|
||||||
);
|
): React.Node {
|
||||||
|
return <List.Icon size={size} color={color} icon={item.icon} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -297,18 +325,19 @@ class AboutScreen extends React.Component<Props> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getCardItem = ({item}: { item: ListItem }) => {
|
getCardItem = ({item}: {item: ListItemType}): React.Node => {
|
||||||
const getItemIcon = this.getItemIcon.bind(this, item);
|
const getItemIcon = (props: {size: number, color: string}): React.Node =>
|
||||||
|
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={this.getChevronIcon}
|
right={AboutScreen.getChevronIcon}
|
||||||
onPress={item.onPressCallback}
|
onPress={item.onPressCallback}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title={item.text}
|
title={item.text}
|
||||||
|
|
@ -316,7 +345,6 @@ class AboutScreen extends React.Component<Props> {
|
||||||
onPress={item.onPressCallback}
|
onPress={item.onPressCallback}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -325,7 +353,7 @@ class AboutScreen extends React.Component<Props> {
|
||||||
* @param item The item to show
|
* @param item The item to show
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getMainCard = ({item}: { item: { id: string } }) => {
|
getMainCard = ({item}: {item: {id: string}}): React.Node => {
|
||||||
switch (item.id) {
|
switch (item.id) {
|
||||||
case 'app':
|
case 'app':
|
||||||
return this.getAppCard();
|
return this.getAppCard();
|
||||||
|
|
@ -333,11 +361,20 @@ class AboutScreen extends React.Component<Props> {
|
||||||
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}}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,43 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
import {
|
||||||
import CustomModal from "../../components/Overrides/CustomModal";
|
Button,
|
||||||
import {Button, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper';
|
List,
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
Subheading,
|
||||||
import {Modalize} from "react-native-modalize";
|
TextInput,
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
Title,
|
||||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
|
import {Modalize} from 'react-native-modalize';
|
||||||
|
import CustomModal from '../../components/Overrides/CustomModal';
|
||||||
|
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||||
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
|
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||||
|
|
||||||
type PreferenceItem = {
|
type PreferenceItemType = {
|
||||||
key: string,
|
key: string,
|
||||||
default: string,
|
default: string,
|
||||||
current: string,
|
current: string,
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
navigation: StackNavigationProp,
|
|
||||||
theme: CustomTheme
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type PropsType = {
|
||||||
modalCurrentDisplayItem: PreferenceItem,
|
theme: CustomThemeType,
|
||||||
currentPreferences: Array<PreferenceItem>,
|
};
|
||||||
}
|
|
||||||
|
type StateType = {
|
||||||
|
modalCurrentDisplayItem: PreferenceItemType,
|
||||||
|
currentPreferences: Array<PreferenceItemType>,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class defining the Debug screen.
|
* Class defining the Debug screen.
|
||||||
* This screen allows the user to get and modify information on the app/device.
|
* This screen allows the user to get and modify information on the app/device.
|
||||||
*/
|
*/
|
||||||
class DebugScreen extends React.Component<Props, State> {
|
class DebugScreen extends React.Component<PropsType, StateType> {
|
||||||
|
|
||||||
modalRef: Modalize;
|
modalRef: Modalize;
|
||||||
|
|
||||||
modalInputValue: string;
|
modalInputValue: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,87 +45,126 @@ class DebugScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @param props
|
* @param props
|
||||||
*/
|
*/
|
||||||
constructor(props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.modalInputValue = "";
|
this.modalInputValue = '';
|
||||||
let currentPreferences : Array<PreferenceItem> = [];
|
const currentPreferences: Array<PreferenceItemType> = [];
|
||||||
Object.values(AsyncStorageManager.PREFERENCES).map((object: any) => {
|
// eslint-disable-next-line flowtype/no-weak-types
|
||||||
let newObject: PreferenceItem = {...object};
|
Object.values(AsyncStorageManager.PREFERENCES).forEach((object: any) => {
|
||||||
|
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() {
|
getModalContent(): React.Node {
|
||||||
|
const {props, state} = this;
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View
|
||||||
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: 20
|
padding: 20,
|
||||||
}}>
|
}}>
|
||||||
<Title>{this.state.modalCurrentDisplayItem.key}</Title>
|
<Title>{state.modalCurrentDisplayItem.key}</Title>
|
||||||
<Subheading>Default: {this.state.modalCurrentDisplayItem.default}</Subheading>
|
<Subheading>
|
||||||
<Subheading>Current: {this.state.modalCurrentDisplayItem.current}</Subheading>
|
Default: {state.modalCurrentDisplayItem.default}
|
||||||
|
</Subheading>
|
||||||
|
<Subheading>
|
||||||
|
Current: {state.modalCurrentDisplayItem.current}
|
||||||
|
</Subheading>
|
||||||
<TextInput
|
<TextInput
|
||||||
label='New Value'
|
label="New Value"
|
||||||
onChangeText={(text) => this.modalInputValue = text}
|
onChangeText={(text: string) => {
|
||||||
|
this.modalInputValue = text;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={{
|
<View
|
||||||
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
}}>
|
}}>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
dark={true}
|
dark
|
||||||
color={this.props.theme.colors.success}
|
color={props.theme.colors.success}
|
||||||
onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.modalInputValue)}>
|
onPress={() => {
|
||||||
|
this.saveNewPrefs(
|
||||||
|
state.modalCurrentDisplayItem.key,
|
||||||
|
this.modalInputValue,
|
||||||
|
);
|
||||||
|
}}>
|
||||||
Save new value
|
Save new value
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
dark={true}
|
dark
|
||||||
color={this.props.theme.colors.danger}
|
color={props.theme.colors.danger}
|
||||||
onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.state.modalCurrentDisplayItem.default)}>
|
onPress={() => {
|
||||||
|
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) {
|
findIndexOfKey(key: string): number {
|
||||||
|
const {currentPreferences} = this.state;
|
||||||
let index = -1;
|
let index = -1;
|
||||||
for (let i = 0; i < this.state.currentPreferences.length; i++) {
|
for (let i = 0; i < currentPreferences.length; i += 1) {
|
||||||
if (this.state.currentPreferences[i].key === key) {
|
if (currentPreferences[i].key === key) {
|
||||||
index = i;
|
index = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -135,8 +179,10 @@ class DebugScreen extends React.Component<Props, State> {
|
||||||
* @param value The pref value
|
* @param value The pref value
|
||||||
*/
|
*/
|
||||||
saveNewPrefs(key: string, value: string) {
|
saveNewPrefs(key: string, value: string) {
|
||||||
this.setState((prevState) => {
|
this.setState((prevState: StateType): {
|
||||||
let currentPreferences = [...prevState.currentPreferences];
|
currentPreferences: Array<PreferenceItemType>,
|
||||||
|
} => {
|
||||||
|
const currentPreferences = [...prevState.currentPreferences];
|
||||||
currentPreferences[this.findIndexOfKey(key)].current = value;
|
currentPreferences[this.findIndexOfKey(key)].current = value;
|
||||||
return {currentPreferences};
|
return {currentPreferences};
|
||||||
});
|
});
|
||||||
|
|
@ -144,26 +190,8 @@ class DebugScreen extends React.Component<Props, State> {
|
||||||
this.modalRef.close();
|
this.modalRef.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
render(): React.Node {
|
||||||
* Callback used when receiving the modal ref
|
const {state} = this;
|
||||||
*
|
|
||||||
* @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}>
|
||||||
|
|
@ -171,9 +199,9 @@ class DebugScreen extends React.Component<Props, State> {
|
||||||
</CustomModal>
|
</CustomModal>
|
||||||
{/* $FlowFixMe */}
|
{/* $FlowFixMe */}
|
||||||
<CollapsibleFlatList
|
<CollapsibleFlatList
|
||||||
data={this.state.currentPreferences}
|
data={state.currentPreferences}
|
||||||
extraData={this.state.currentPreferences}
|
extraData={state.currentPreferences}
|
||||||
renderItem={this.renderItem}
|
renderItem={this.getRenderItem}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,121 +4,141 @@ import * as React from 'react';
|
||||||
import {FlatList, Image, Linking, View} from 'react-native';
|
import {FlatList, Image, Linking, View} from 'react-native';
|
||||||
import {Card, List, Text, withTheme} from 'react-native-paper';
|
import {Card, List, Text, withTheme} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons";
|
import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||||
|
import AMICALE_LOGO from '../../../assets/amicale.png';
|
||||||
|
|
||||||
type Props = {
|
type DatasetItemType = {
|
||||||
};
|
|
||||||
|
|
||||||
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<Props> {
|
class AmicaleContactScreen extends React.Component<null> {
|
||||||
|
|
||||||
// Dataset containing information about contacts
|
// Dataset containing information about contacts
|
||||||
CONTACT_DATASET: Array<DatasetItem>;
|
CONTACT_DATASET: Array<DatasetItemType>;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor() {
|
||||||
super(props);
|
super();
|
||||||
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: DatasetItem) => item.email;
|
keyExtractor = (item: DatasetItemType): string => item.email;
|
||||||
|
|
||||||
getChevronIcon = (props) => <List.Icon {...props} icon={'chevron-right'}/>;
|
getChevronIcon = ({
|
||||||
|
size,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
size: number,
|
||||||
|
color: string,
|
||||||
|
}): React.Node => (
|
||||||
|
<List.Icon size={size} color={color} icon="chevron-right" />
|
||||||
|
);
|
||||||
|
|
||||||
renderItem = ({item}: { item: DatasetItem }) => {
|
getRenderItem = ({item}: {item: DatasetItemType}): React.Node => {
|
||||||
const onPress = () => Linking.openURL('mailto:' + item.email);
|
const onPress = () => {
|
||||||
return <List.Item
|
Linking.openURL(`mailto:${item.email}`);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
title={item.name}
|
title={item.name}
|
||||||
description={item.email}
|
description={item.email}
|
||||||
left={(props) => <List.Icon {...props} icon={item.icon}/>}
|
left={({size, color}: {size: number, color: string}): React.Node => (
|
||||||
|
<List.Icon size={size} color={color} icon={item.icon} />
|
||||||
|
)}
|
||||||
right={this.getChevronIcon}
|
right={this.getChevronIcon}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getScreen = () => {
|
getScreen = (): React.Node => {
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View style={{
|
<View
|
||||||
|
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={require('../../../assets/amicale.png')}
|
source={AMICALE_LOGO}
|
||||||
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={props => <List.Icon {...props} icon={'information'}/>}
|
left={({
|
||||||
|
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.renderItem}
|
renderItem={this.getRenderItem}
|
||||||
/>
|
/>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -126,12 +146,12 @@ class AmicaleContactScreen extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
return (
|
return (
|
||||||
<CollapsibleFlatList
|
<CollapsibleFlatList
|
||||||
data={[{key: "1"}]}
|
data={[{key: '1'}]}
|
||||||
renderItem={this.getScreen}
|
renderItem={this.getScreen}
|
||||||
hasTab={true}
|
hasTab
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
class ClubAboutScreen extends React.Component<Props> {
|
// eslint-disable-next-line react/prefer-stateless-function
|
||||||
|
class ClubAboutScreen extends React.Component<null> {
|
||||||
render() {
|
render(): React.Node {
|
||||||
return (
|
return (
|
||||||
<CollapsibleScrollView style={{padding: 5}}>
|
<CollapsibleScrollView style={{padding: 5}}>
|
||||||
<View style={{
|
<View
|
||||||
|
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={require('../../../../assets/amicale.png')}
|
source={AMICALE_ICON}
|
||||||
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={props => <List.Icon {...props} icon={'information'}/>}
|
left={({size}: {size: number}): React.Node => (
|
||||||
|
<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
|
<Autolink text={CONTACT_LINK} component={Text} />
|
||||||
text={CONTACT_LINK}
|
|
||||||
component={Text}
|
|
||||||
/>
|
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
</CollapsibleScrollView>
|
</CollapsibleScrollView>
|
||||||
|
|
|
||||||
|
|
@ -2,65 +2,70 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Linking, View} from 'react-native';
|
import {Linking, View} from 'react-native';
|
||||||
import {Avatar, Button, Card, Chip, Paragraph, withTheme} from 'react-native-paper';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Chip,
|
||||||
|
Paragraph,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
import ImageModal from 'react-native-image-modal';
|
import ImageModal from 'react-native-image-modal';
|
||||||
import i18n from "i18n-js";
|
import i18n from 'i18n-js';
|
||||||
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import CustomHTML from "../../../components/Overrides/CustomHTML";
|
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
|
||||||
import CustomTabBar from "../../../components/Tabbar/CustomTabBar";
|
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
||||||
import type {category, club} from "./ClubListScreen";
|
import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {ClubCategoryType, ClubType} from './ClubListScreen';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
import {ERROR_TYPE} from "../../../utils/WebData";
|
import {ERROR_TYPE} from '../../../utils/WebData';
|
||||||
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||||
|
import type {ApiGenericDataType} from '../../../utils/WebData';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
route: {
|
route: {
|
||||||
params?: {
|
params?: {
|
||||||
data?: club,
|
data?: ClubType,
|
||||||
categories?: Array<category>,
|
categories?: Array<ClubCategoryType>,
|
||||||
clubId?: number,
|
clubId?: number,
|
||||||
}, ...
|
|
||||||
},
|
},
|
||||||
theme: CustomTheme
|
...
|
||||||
|
},
|
||||||
|
theme: CustomThemeType,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
const AMICALE_MAIL = 'clubs@amicale-insat.fr';
|
||||||
imageModalVisible: boolean,
|
|
||||||
};
|
|
||||||
|
|
||||||
const AMICALE_MAIL = "clubs@amicale-insat.fr";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class defining a club event information page.
|
* Class defining a club event information page.
|
||||||
* If called with data and categories navigation parameters, will use those to display the data.
|
* If called with data and categories navigation parameters, will use those to display the data.
|
||||||
* If called with clubId parameter, will fetch the information on the server
|
* If called with clubId parameter, will fetch the information on the server
|
||||||
*/
|
*/
|
||||||
class ClubDisplayScreen extends React.Component<Props, State> {
|
class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
|
displayData: ClubType | null;
|
||||||
|
|
||||||
|
categories: Array<ClubCategoryType> | null;
|
||||||
|
|
||||||
displayData: club | null;
|
|
||||||
categories: Array<category> | null;
|
|
||||||
clubId: number;
|
clubId: number;
|
||||||
|
|
||||||
shouldFetchData: boolean;
|
shouldFetchData: boolean;
|
||||||
|
|
||||||
state = {
|
constructor(props: PropsType) {
|
||||||
imageModalVisible: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
if (this.props.route.params != null) {
|
if (props.route.params != null) {
|
||||||
if (this.props.route.params.data != null && this.props.route.params.categories != null) {
|
if (
|
||||||
this.displayData = this.props.route.params.data;
|
props.route.params.data != null &&
|
||||||
this.categories = this.props.route.params.categories;
|
props.route.params.categories != null
|
||||||
this.clubId = this.props.route.params.data.id;
|
) {
|
||||||
|
this.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 (this.props.route.params.clubId != null) {
|
} else if (props.route.params.clubId != null) {
|
||||||
this.displayData = null;
|
this.displayData = null;
|
||||||
this.categories = null;
|
this.categories = null;
|
||||||
this.clubId = this.props.route.params.clubId;
|
this.clubId = props.route.params.clubId;
|
||||||
this.shouldFetchData = true;
|
this.shouldFetchData = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -72,14 +77,14 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
||||||
* @param id The category's ID
|
* @param id The category's ID
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
*/
|
*/
|
||||||
getCategoryName(id: number) {
|
getCategoryName(id: number): string {
|
||||||
|
let categoryName = '';
|
||||||
if (this.categories !== null) {
|
if (this.categories !== null) {
|
||||||
for (let i = 0; i < this.categories.length; i++) {
|
this.categories.forEach((item: ClubCategoryType) => {
|
||||||
if (id === this.categories[i].id)
|
if (id === item.id) categoryName = item.name;
|
||||||
return this.categories[i].name;
|
});
|
||||||
}
|
}
|
||||||
}
|
return categoryName;
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,23 +93,19 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
||||||
* @param categories The categories to display (max 2)
|
* @param categories The categories to display (max 2)
|
||||||
* @returns {null|*}
|
* @returns {null|*}
|
||||||
*/
|
*/
|
||||||
getCategoriesRender(categories: [number, number]) {
|
getCategoriesRender(categories: Array<number | null>): React.Node {
|
||||||
if (this.categories === null)
|
if (this.categories == null) return null;
|
||||||
return null;
|
|
||||||
|
|
||||||
let final = [];
|
const final = [];
|
||||||
for (let i = 0; i < categories.length; i++) {
|
categories.forEach((cat: number | null) => {
|
||||||
let cat = categories[i];
|
if (cat != null) {
|
||||||
if (cat !== null) {
|
|
||||||
final.push(
|
final.push(
|
||||||
<Chip
|
<Chip style={{marginRight: 5}} key={cat}>
|
||||||
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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,26 +116,39 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
||||||
* @param email The club contact email
|
* @param email The club contact email
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getManagersRender(managers: Array<string>, email: string | null) {
|
getManagersRender(managers: Array<string>, email: string | null): React.Node {
|
||||||
let managersListView = [];
|
const {props} = this;
|
||||||
for (let i = 0; i < managers.length; i++) {
|
const managersListView = [];
|
||||||
managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
|
managers.forEach((item: string) => {
|
||||||
}
|
managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
|
||||||
|
});
|
||||||
const hasManagers = managers.length > 0;
|
const hasManagers = managers.length > 0;
|
||||||
return (
|
return (
|
||||||
<Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
|
<Card
|
||||||
|
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={hasManagers ? i18n.t('screens.clubs.managersSubtitle') : i18n.t('screens.clubs.managersUnavailable')}
|
subtitle={
|
||||||
left={(props) => <Avatar.Icon
|
hasManagers
|
||||||
{...props}
|
? i18n.t('screens.clubs.managersSubtitle')
|
||||||
|
: i18n.t('screens.clubs.managersUnavailable')
|
||||||
|
}
|
||||||
|
left={({size}: {size: number}): React.Node => (
|
||||||
|
<Avatar.Icon
|
||||||
|
size={size}
|
||||||
style={{backgroundColor: 'transparent'}}
|
style={{backgroundColor: 'transparent'}}
|
||||||
color={hasManagers ? this.props.theme.colors.success : this.props.theme.colors.primary}
|
color={
|
||||||
icon="account-tie"/>}
|
hasManagers
|
||||||
|
? props.theme.colors.success
|
||||||
|
: props.theme.colors.primary
|
||||||
|
}
|
||||||
|
icon="account-tie"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
{managersListView}
|
{managersListView}
|
||||||
{this.getEmailButton(email, hasManagers)}
|
{ClubDisplayScreen.getEmailButton(email, hasManagers)}
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
@ -147,51 +161,45 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
||||||
* @param hasManagers True if the club has managers
|
* @param hasManagers True if the club has managers
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getEmailButton(email: string | null, hasManagers: boolean) {
|
static getEmailButton(
|
||||||
const destinationEmail = email != null && hasManagers
|
email: string | null,
|
||||||
? email
|
hasManagers: boolean,
|
||||||
: AMICALE_MAIL;
|
): React.Node {
|
||||||
const text = email != null && hasManagers
|
const destinationEmail =
|
||||||
? i18n.t("screens.clubs.clubContact")
|
email != null && hasManagers ? email : AMICALE_MAIL;
|
||||||
: i18n.t("screens.clubs.amicaleContact");
|
const text =
|
||||||
|
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={() => Linking.openURL('mailto:' + destinationEmail)}
|
onPress={() => {
|
||||||
style={{marginLeft: 'auto'}}
|
Linking.openURL(`mailto:${destinationEmail}`);
|
||||||
>
|
}}
|
||||||
|
style={{marginLeft: 'auto'}}>
|
||||||
{text}
|
{text}
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Actions>
|
</Card.Actions>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getScreen = (response: Array<ApiGenericDataType | null>): React.Node => {
|
||||||
* Updates the header title to match the given club
|
const {props} = this;
|
||||||
*
|
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[0];
|
[data] = response;
|
||||||
this.updateHeaderTitle(data);
|
this.updateHeaderTitle(data);
|
||||||
}
|
}
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
return (
|
return (
|
||||||
<CollapsibleScrollView
|
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
|
||||||
style={{paddingLeft: 5, paddingRight: 5}}
|
|
||||||
hasTab={true}
|
|
||||||
>
|
|
||||||
{this.getCategoriesRender(data.category)}
|
{this.getCategoriesRender(data.category)}
|
||||||
{data.logo !== null ?
|
{data.logo !== null ? (
|
||||||
<View style={{
|
<View
|
||||||
|
style={{
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
marginRight: 'auto',
|
marginRight: 'auto',
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
|
|
@ -199,7 +207,7 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
||||||
}}>
|
}}>
|
||||||
<ImageModal
|
<ImageModal
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
imageBackgroundColor={this.props.theme.colors.background}
|
imageBackgroundColor={props.theme.colors.background}
|
||||||
style={{
|
style={{
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
|
|
@ -209,43 +217,59 @@ class ClubDisplayScreen extends React.Component<Props, State> {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</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 <AuthenticatedScreen
|
return (
|
||||||
{...this.props}
|
<AuthenticatedScreen
|
||||||
|
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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,85 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Platform} from "react-native";
|
import {Platform} from 'react-native';
|
||||||
import {Searchbar} from 'react-native-paper';
|
import {Searchbar} from 'react-native-paper';
|
||||||
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
|
import i18n from 'i18n-js';
|
||||||
import i18n from "i18n-js";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import ClubListItem from "../../../components/Lists/Clubs/ClubListItem";
|
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
|
||||||
import {isItemInCategoryFilter, stringMatchQuery} from "../../../utils/Search";
|
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
|
||||||
import ClubListHeader from "../../../components/Lists/Clubs/ClubListHeader";
|
import {isItemInCategoryFilter, stringMatchQuery} from '../../../utils/Search';
|
||||||
import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton";
|
import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import MaterialHeaderButtons, {
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
Item,
|
||||||
import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList";
|
} from '../../../components/Overrides/CustomHeaderButton';
|
||||||
|
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
|
||||||
|
|
||||||
export type category = {
|
export type ClubCategoryType = {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type club = {
|
export type ClubType = {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
logo: string,
|
logo: string,
|
||||||
email: string | null,
|
email: string | null,
|
||||||
category: [number, number],
|
category: Array<number | null>,
|
||||||
responsibles: Array<string>,
|
responsibles: Array<string>,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
currentlySelectedCategories: Array<number>,
|
currentlySelectedCategories: Array<number>,
|
||||||
currentSearchString: string,
|
currentSearchString: string,
|
||||||
}
|
};
|
||||||
|
|
||||||
const LIST_ITEM_HEIGHT = 96;
|
const LIST_ITEM_HEIGHT = 96;
|
||||||
|
|
||||||
class ClubListScreen extends React.Component<Props, State> {
|
class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
|
categories: Array<ClubCategoryType>;
|
||||||
|
|
||||||
state = {
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
currentlySelectedCategories: [],
|
currentlySelectedCategories: [],
|
||||||
currentSearchString: '',
|
currentSearchString: '',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
categories: Array<category>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the header content
|
* Creates the header content
|
||||||
*/
|
*/
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.navigation.setOptions({
|
const {props} = this;
|
||||||
|
props.navigation.setOptions({
|
||||||
headerTitle: this.getSearchBar,
|
headerTitle: this.getSearchBar,
|
||||||
headerRight: this.getHeaderButtons,
|
headerRight: this.getHeaderButtons,
|
||||||
headerBackTitleVisible: false,
|
headerBackTitleVisible: false,
|
||||||
headerTitleContainerStyle: Platform.OS === 'ios' ?
|
headerTitleContainerStyle:
|
||||||
{marginHorizontal: 0, width: '70%'} :
|
Platform.OS === 'ios'
|
||||||
{marginHorizontal: 0, right: 50, left: 50},
|
? {marginHorizontal: 0, width: '70%'}
|
||||||
|
: {marginHorizontal: 0, right: 50, left: 50},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the header search bar
|
* Callback used when clicking an article in the list.
|
||||||
|
* It opens the modal to show detailed information about the article
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @param item The article pressed
|
||||||
*/
|
*/
|
||||||
getSearchBar = () => {
|
onListItemPress(item: ClubType) {
|
||||||
return (
|
const {props} = this;
|
||||||
<Searchbar
|
props.navigation.navigate('club-information', {
|
||||||
placeholder={i18n.t('screens.proximo.search')}
|
data: item,
|
||||||
onChangeText={this.onSearchStringChange}
|
categories: this.categories,
|
||||||
/>
|
});
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
||||||
|
|
@ -97,11 +90,46 @@ class ClubListScreen extends React.Component<Props, State> {
|
||||||
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
|
onChipSelect = (id: number) => {
|
||||||
|
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) {
|
||||||
|
|
@ -116,13 +144,69 @@ class ClubListScreen extends React.Component<Props, State> {
|
||||||
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={true}
|
removeClippedSubviews
|
||||||
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.
|
||||||
|
|
@ -134,99 +218,49 @@ class ClubListScreen extends React.Component<Props, State> {
|
||||||
* @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) {
|
||||||
let newCategoriesState = [...this.state.currentlySelectedCategories];
|
const {state} = this;
|
||||||
let newStrState = this.state.currentSearchString;
|
const newCategoriesState = [...state.currentlySelectedCategories];
|
||||||
if (filterStr !== null)
|
let newStrState = state.currentSearchString;
|
||||||
newStrState = filterStr;
|
if (filterStr !== null) newStrState = filterStr;
|
||||||
if (categoryId !== null) {
|
if (categoryId !== null) {
|
||||||
let index = newCategoriesState.indexOf(categoryId);
|
const index = newCategoriesState.indexOf(categoryId);
|
||||||
if (index === -1)
|
if (index === -1) newCategoriesState.push(categoryId);
|
||||||
newCategoriesState.push(categoryId);
|
else newCategoriesState.splice(index, 1);
|
||||||
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: club) {
|
shouldRenderItem(item: ClubType): boolean {
|
||||||
let shouldRender = this.state.currentlySelectedCategories.length === 0
|
const {state} = this;
|
||||||
|| isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category);
|
let shouldRender =
|
||||||
|
state.currentlySelectedCategories.length === 0 ||
|
||||||
|
isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
|
||||||
if (shouldRender)
|
if (shouldRender)
|
||||||
shouldRender = stringMatchQuery(item.name, this.state.currentSearchString);
|
shouldRender = stringMatchQuery(item.name, state.currentSearchString);
|
||||||
return shouldRender;
|
return shouldRender;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRenderItem = ({item}: { item: club }) => {
|
render(): React.Node {
|
||||||
const onPress = this.onListItemPress.bind(this, item);
|
const {props} = this;
|
||||||
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
|
||||||
{...this.props}
|
navigation={props.navigation}
|
||||||
requests={[
|
requests={[
|
||||||
{
|
{
|
||||||
link: 'clubs/list',
|
link: 'clubs/list',
|
||||||
params: {},
|
params: {},
|
||||||
mandatory: true,
|
mandatory: true,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
renderFunction={this.getScreen}
|
renderFunction={this.getScreen}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,79 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Button, Caption, Card, Headline, Paragraph, withTheme} from 'react-native-paper';
|
import {
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
Button,
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
Caption,
|
||||||
import type {Device} from "./EquipmentListScreen";
|
Card,
|
||||||
import {View} from "react-native";
|
Headline,
|
||||||
import i18n from "i18n-js";
|
Paragraph,
|
||||||
import {getRelativeDateString} from "../../../utils/EquipmentBooking";
|
withTheme,
|
||||||
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
|
} from 'react-native-paper';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {DeviceType} from './EquipmentListScreen';
|
||||||
|
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
|
||||||
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
|
||||||
route: {
|
route: {
|
||||||
params?: {
|
params?: {
|
||||||
item?: Device,
|
item?: DeviceType,
|
||||||
dates: [string, string]
|
dates: [string, string],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
|
class EquipmentConfirmScreen extends React.Component<PropsType> {
|
||||||
|
item: DeviceType | null;
|
||||||
|
|
||||||
class EquipmentConfirmScreen extends React.Component<Props> {
|
|
||||||
|
|
||||||
item: Device | null;
|
|
||||||
dates: [string, string] | null;
|
dates: [string, string] | null;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
if (this.props.route.params != null) {
|
if (props.route.params != null) {
|
||||||
if (this.props.route.params.item != null)
|
if (props.route.params.item != null) this.item = props.route.params.item;
|
||||||
this.item = this.props.route.params.item;
|
else this.item = null;
|
||||||
else
|
if (props.route.params.dates != null)
|
||||||
this.item = null;
|
this.dates = props.route.params.dates;
|
||||||
if (this.props.route.params.dates != null)
|
else this.dates = null;
|
||||||
this.dates = this.props.route.params.dates;
|
|
||||||
else
|
|
||||||
this.dates = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
const item = this.item;
|
const {item, dates, props} = this;
|
||||||
const dates = this.dates;
|
|
||||||
if (item != null && dates != null) {
|
if (item != null && dates != null) {
|
||||||
const start = new Date(dates[0]);
|
const start = new Date(dates[0]);
|
||||||
const end = new Date(dates[1]);
|
const end = new Date(dates[1]);
|
||||||
|
let buttonText;
|
||||||
|
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 style={{
|
<View
|
||||||
marginLeft: "auto",
|
style={{
|
||||||
marginRight: "auto",
|
marginLeft: 'auto',
|
||||||
flexDirection: "row",
|
marginRight: 'auto',
|
||||||
flexWrap: "wrap",
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
}}>
|
}}>
|
||||||
<Headline style={{textAlign: "center"}}>
|
<Headline style={{textAlign: 'center'}}>{item.name}</Headline>
|
||||||
{item.name}
|
<Caption
|
||||||
</Headline>
|
style={{
|
||||||
<Caption style={{
|
textAlign: 'center',
|
||||||
textAlign: "center",
|
|
||||||
lineHeight: 35,
|
lineHeight: 35,
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
}}>
|
}}>
|
||||||
|
|
@ -71,35 +82,21 @@ class EquipmentConfirmScreen extends React.Component<Props> {
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
icon={"check-circle-outline"}
|
icon="check-circle-outline"
|
||||||
color={this.props.theme.colors.success}
|
color={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);
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,62 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {View} from "react-native";
|
import {View} from 'react-native';
|
||||||
import {Button, withTheme} from 'react-native-paper';
|
import {Button, withTheme} from 'react-native-paper';
|
||||||
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import i18n from 'i18n-js';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
|
||||||
import i18n from "i18n-js";
|
import type {ClubType} from '../Clubs/ClubListScreen';
|
||||||
import type {club} from "../Clubs/ClubListScreen";
|
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
|
||||||
import EquipmentListItem from "../../../components/Lists/Equipment/EquipmentListItem";
|
import MascotPopup from '../../../components/Mascot/MascotPopup';
|
||||||
import MascotPopup from "../../../components/Mascot/MascotPopup";
|
import {MASCOT_STYLE} from '../../../components/Mascot/Mascot';
|
||||||
import {MASCOT_STYLE} from "../../../components/Mascot/Mascot";
|
import AsyncStorageManager from '../../../managers/AsyncStorageManager';
|
||||||
import AsyncStorageManager from "../../../managers/AsyncStorageManager";
|
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
|
||||||
import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList";
|
import type {ApiGenericDataType} from '../../../utils/WebData';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
};
|
||||||
}
|
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
mascotDialogVisible: boolean,
|
mascotDialogVisible: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Device = {
|
export type DeviceType = {
|
||||||
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 RentedDevice = {
|
export type RentedDeviceType = {
|
||||||
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<Props, State> {
|
class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
||||||
|
data: Array<DeviceType>;
|
||||||
|
|
||||||
state = {
|
userRents: Array<RentedDeviceType>;
|
||||||
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key),
|
|
||||||
}
|
|
||||||
|
|
||||||
data: Array<Device>;
|
|
||||||
userRents: Array<RentedDevice>;
|
|
||||||
|
|
||||||
authRef: {current: null | AuthenticatedScreen};
|
authRef: {current: null | AuthenticatedScreen};
|
||||||
|
|
||||||
canRefresh: boolean;
|
canRefresh: boolean;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: PropsType) {
|
||||||
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();
|
||||||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
props.navigation.addListener('focus', this.onScreenFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
onScreenFocus = () => {
|
onScreenFocus = () => {
|
||||||
|
|
@ -64,25 +65,25 @@ class EquipmentListScreen extends React.Component<Props, State> {
|
||||||
this.canRefresh = true;
|
this.canRefresh = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
getRenderItem = ({item}: { item: Device }) => {
|
getRenderItem = ({item}: {item: DeviceType}): React.Node => {
|
||||||
|
const {navigation} = this.props;
|
||||||
return (
|
return (
|
||||||
<EquipmentListItem
|
<EquipmentListItem
|
||||||
navigation={this.props.navigation}
|
navigation={navigation}
|
||||||
item={item}
|
item={item}
|
||||||
userDeviceRentDates={this.getUserDeviceRentDates(item)}
|
userDeviceRentDates={this.getUserDeviceRentDates(item)}
|
||||||
height={LIST_ITEM_HEIGHT}/>
|
height={LIST_ITEM_HEIGHT}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getUserDeviceRentDates(item: Device) {
|
getUserDeviceRentDates(item: DeviceType): [number, number] | null {
|
||||||
let dates = null;
|
let dates = null;
|
||||||
for (let i = 0; i < this.userRents.length; i++) {
|
this.userRents.forEach((device: RentedDeviceType) => {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,28 +92,29 @@ class EquipmentListScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getListHeader() {
|
getListHeader(): React.Node {
|
||||||
return (
|
return (
|
||||||
<View style={{
|
<View
|
||||||
width: "100%",
|
style={{
|
||||||
|
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: club) => item.id.toString();
|
keyExtractor = (item: ClubType): string => item.id.toString();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the main screen component with the fetched data
|
* Gets the main screen component with the fetched data
|
||||||
|
|
@ -120,16 +122,14 @@ class EquipmentListScreen extends React.Component<Props, State> {
|
||||||
* @param data The data fetched from the server
|
* @param data The data fetched from the server
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getScreen = (data: Array<{ [key: string]: any } | null>) => {
|
getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
|
||||||
if (data[0] != null) {
|
if (data[0] != null) {
|
||||||
const fetchedData = data[0];
|
const fetchedData = data[0];
|
||||||
if (fetchedData != null)
|
if (fetchedData != null) this.data = fetchedData.devices;
|
||||||
this.data = fetchedData["devices"];
|
|
||||||
}
|
}
|
||||||
if (data[1] != null) {
|
if (data[1] != null) {
|
||||||
const fetchedData = data[1];
|
const fetchedData = data[1];
|
||||||
if (fetchedData != null)
|
if (fetchedData != null) this.userRents = fetchedData.locations;
|
||||||
this.userRents = fetchedData["locations"];
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<CollapsibleFlatList
|
<CollapsibleFlatList
|
||||||
|
|
@ -138,23 +138,27 @@ class EquipmentListScreen extends React.Component<Props, State> {
|
||||||
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.PREFERENCES.equipmentShowBanner.key, false);
|
AsyncStorageManager.set(
|
||||||
this.setState({mascotDialogVisible: false})
|
AsyncStorageManager.PREFERENCES.equipmentShowBanner.key,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
this.setState({mascotDialogVisible: false});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props, state} = this;
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}}>
|
||||||
<AuthenticatedScreen
|
<AuthenticatedScreen
|
||||||
{...this.props}
|
navigation={props.navigation}
|
||||||
ref={this.authRef}
|
ref={this.authRef}
|
||||||
requests={[
|
requests={[
|
||||||
{
|
{
|
||||||
|
|
@ -166,22 +170,22 @@ class EquipmentListScreen extends React.Component<Props, State> {
|
||||||
link: 'location/my',
|
link: 'location/my',
|
||||||
params: {},
|
params: {},
|
||||||
mandatory: false,
|
mandatory: false,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
renderFunction={this.getScreen}
|
renderFunction={this.getScreen}
|
||||||
/>
|
/>
|
||||||
<MascotPopup
|
<MascotPopup
|
||||||
visible={this.state.mascotDialogVisible}
|
visible={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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,111 +1,118 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper';
|
import {
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
Button,
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
Caption,
|
||||||
import type {Device} from "./EquipmentListScreen";
|
Card,
|
||||||
import {BackHandler, View} from "react-native";
|
Headline,
|
||||||
import * as Animatable from "react-native-animatable";
|
Subheading,
|
||||||
import i18n from "i18n-js";
|
withTheme,
|
||||||
import {CalendarList} from "react-native-calendars";
|
} from 'react-native-paper';
|
||||||
import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
|
import {BackHandler, View} from 'react-native';
|
||||||
|
import * as Animatable from 'react-native-animatable';
|
||||||
|
import i18n from 'i18n-js';
|
||||||
|
import {CalendarList} from 'react-native-calendars';
|
||||||
|
import type {DeviceType} from './EquipmentListScreen';
|
||||||
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
|
||||||
|
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
|
||||||
import {
|
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 Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
route: {
|
route: {
|
||||||
params?: {
|
params?: {
|
||||||
item?: Device,
|
item?: DeviceType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
export type MarkedDatesObjectType = {
|
||||||
|
[key: string]: {startingDay: boolean, endingDay: boolean, color: string},
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateType = {
|
||||||
dialogVisible: boolean,
|
dialogVisible: boolean,
|
||||||
errorDialogVisible: boolean,
|
errorDialogVisible: boolean,
|
||||||
markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
|
markedDates: MarkedDatesObjectType,
|
||||||
currentError: number,
|
currentError: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
class EquipmentRentScreen extends React.Component<Props, State> {
|
class EquipmentRentScreen extends React.Component<PropsType, StateType> {
|
||||||
|
item: DeviceType | null;
|
||||||
|
|
||||||
state = {
|
bookedDates: Array<string>;
|
||||||
|
|
||||||
|
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 (this.props.route.params != null) {
|
if (props.route.params != null) {
|
||||||
if (this.props.route.params.item != null)
|
if (props.route.params.item != null) this.item = props.route.params.item;
|
||||||
this.item = this.props.route.params.item;
|
else this.item = null;
|
||||||
else
|
|
||||||
this.item = null;
|
|
||||||
}
|
}
|
||||||
const item = this.item;
|
const {item} = this;
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
this.lockedDates = {};
|
this.lockedDates = {};
|
||||||
for (let i = 0; i < item.booked_at.length; i++) {
|
item.booked_at.forEach((date: {begin: string, end: string}) => {
|
||||||
const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null);
|
const range = getValidRange(
|
||||||
|
new Date(date.begin),
|
||||||
|
new Date(date.end),
|
||||||
|
null,
|
||||||
|
);
|
||||||
this.lockedDates = {
|
this.lockedDates = {
|
||||||
...this.lockedDates,
|
...this.lockedDates,
|
||||||
...generateMarkedDates(
|
...generateMarkedDates(false, props.theme, range),
|
||||||
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() {
|
||||||
this.props.navigation.addListener(
|
const {navigation} = this.props;
|
||||||
'focus',
|
navigation.addListener('focus', () => {
|
||||||
() =>
|
|
||||||
BackHandler.addEventListener(
|
BackHandler.addEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
this.onBackButtonPressAndroid
|
this.onBackButtonPressAndroid,
|
||||||
)
|
|
||||||
);
|
);
|
||||||
this.props.navigation.addListener(
|
});
|
||||||
'blur',
|
navigation.addListener('blur', () => {
|
||||||
() =>
|
|
||||||
BackHandler.removeEventListener(
|
BackHandler.removeEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
this.onBackButtonPressAndroid
|
this.onBackButtonPressAndroid,
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -113,26 +120,88 @@ class EquipmentRentScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
onBackButtonPressAndroid = () => {
|
onBackButtonPressAndroid = (): boolean => {
|
||||||
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: { dateString: string, day: number, month: number, timestamp: number, year: number }) => {
|
selectNewDate = (day: {
|
||||||
|
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.hasOwnProperty(day.dateString))) {
|
if (!this.lockedDates[day.dateString] != null) {
|
||||||
if (start === null) {
|
if (start === null) {
|
||||||
this.updateSelectionRange(selected, selected);
|
this.updateSelectionRange(selected, selected);
|
||||||
this.enableBooking();
|
this.enableBooking();
|
||||||
|
|
@ -141,39 +210,21 @@ class EquipmentRentScreen extends React.Component<Props, State> {
|
||||||
} 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
|
} else this.resetSelection();
|
||||||
this.resetSelection();
|
|
||||||
this.updateMarkedSelection();
|
this.updateMarkedSelection();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
updateSelectionRange(start: Date, end: Date) {
|
showErrorDialog = (error: number) => {
|
||||||
this.bookedDates = getValidRange(start, end, this.item);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMarkedSelection() {
|
|
||||||
this.setState({
|
this.setState({
|
||||||
markedDates: generateMarkedDates(
|
errorDialogVisible: true,
|
||||||
true,
|
currentError: error,
|
||||||
this.props.theme,
|
|
||||||
this.bookedDates
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
enableBooking() {
|
showDialog = () => {
|
||||||
if (!this.canBookEquipment) {
|
this.setState({dialogVisible: true});
|
||||||
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
|
||||||
|
|
@ -193,84 +244,45 @@ class EquipmentRentScreen extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showDialog = () => {
|
enableBooking() {
|
||||||
this.setState({dialogVisible: true});
|
if (!this.canBookEquipment) {
|
||||||
|
this.showBookButton();
|
||||||
|
this.canBookEquipment = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showErrorDialog = (error: number) => {
|
resetSelection() {
|
||||||
|
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({
|
||||||
errorDialogVisible: true,
|
markedDates: generateMarkedDates(true, theme, this.bookedDates),
|
||||||
currentError: error,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onDialogDismiss = () => {
|
render(): React.Node {
|
||||||
this.setState({dialogVisible: false});
|
const {item, props, state} = this;
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
||||||
if (item != null && start != null && end != null) {
|
let subHeadingText;
|
||||||
console.log({
|
if (start == null) subHeadingText = i18n.t('screens.equipment.booking');
|
||||||
"device": item.id,
|
else if (end != null && start.getTime() !== end.getTime())
|
||||||
"begin": getISODate(start),
|
subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
|
||||||
"end": getISODate(end),
|
begin: getRelativeDateString(start),
|
||||||
})
|
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)]
|
|
||||||
});
|
});
|
||||||
resolve();
|
else
|
||||||
})
|
i18n.t('screens.equipment.bookingDay', {
|
||||||
.catch((error: number) => {
|
date: getRelativeDateString(start),
|
||||||
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);
|
||||||
|
|
@ -280,17 +292,19 @@ class EquipmentRentScreen extends React.Component<Props, State> {
|
||||||
<Card style={{margin: 5}}>
|
<Card style={{margin: 5}}>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}}>
|
||||||
<View style={{
|
<View
|
||||||
marginLeft: "auto",
|
style={{
|
||||||
marginRight: "auto",
|
marginLeft: 'auto',
|
||||||
flexDirection: "row",
|
marginRight: 'auto',
|
||||||
flexWrap: "wrap",
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
}}>
|
}}>
|
||||||
<Headline style={{textAlign: "center"}}>
|
<Headline style={{textAlign: 'center'}}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Headline>
|
</Headline>
|
||||||
<Caption style={{
|
<Caption
|
||||||
textAlign: "center",
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
lineHeight: 35,
|
lineHeight: 35,
|
||||||
marginLeft: 10,
|
marginLeft: 10,
|
||||||
}}>
|
}}>
|
||||||
|
|
@ -300,30 +314,24 @@ class EquipmentRentScreen extends React.Component<Props, State> {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
icon={isAvailable ? "check-circle-outline" : "update"}
|
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
||||||
color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
|
color={
|
||||||
mode="text"
|
isAvailable
|
||||||
>
|
? props.theme.colors.success
|
||||||
{i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)})}
|
: props.theme.colors.primary
|
||||||
</Button>
|
|
||||||
<Subheading style={{
|
|
||||||
textAlign: "center",
|
|
||||||
marginBottom: 10,
|
|
||||||
minHeight: 50
|
|
||||||
}}>
|
|
||||||
{
|
|
||||||
start == null
|
|
||||||
? i18n.t('screens.equipment.booking')
|
|
||||||
: end != null && start.getTime() !== end.getTime()
|
|
||||||
? i18n.t('screens.equipment.bookingPeriod', {
|
|
||||||
begin: getRelativeDateString(start),
|
|
||||||
end: getRelativeDateString(end)
|
|
||||||
})
|
|
||||||
: i18n.t('screens.equipment.bookingDay', {
|
|
||||||
date: getRelativeDateString(start)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
mode="text">
|
||||||
|
{i18n.t('screens.equipment.available', {
|
||||||
|
date: getRelativeDateString(firstAvailability),
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
<Subheading
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 10,
|
||||||
|
minHeight: 50,
|
||||||
|
}}>
|
||||||
|
{subHeadingText}
|
||||||
</Subheading>
|
</Subheading>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
@ -335,35 +343,34 @@ class EquipmentRentScreen extends React.Component<Props, State> {
|
||||||
// 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={true}
|
horizontal
|
||||||
// Enable paging on horizontal, default = false
|
// Enable paging on horizontal, default = false
|
||||||
pagingEnabled={true}
|
pagingEnabled
|
||||||
// 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={true}
|
disableAllTouchEventsForDisabledDays
|
||||||
// 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, ...this.state.markedDates}}
|
markedDates={{...this.lockedDates, ...state.markedDates}}
|
||||||
|
|
||||||
theme={{
|
theme={{
|
||||||
backgroundColor: this.props.theme.colors.agendaBackgroundColor,
|
backgroundColor: props.theme.colors.agendaBackgroundColor,
|
||||||
calendarBackground: this.props.theme.colors.background,
|
calendarBackground: props.theme.colors.background,
|
||||||
textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
|
textSectionTitleColor: props.theme.colors.agendaDayTextColor,
|
||||||
selectedDayBackgroundColor: this.props.theme.colors.primary,
|
selectedDayBackgroundColor: props.theme.colors.primary,
|
||||||
selectedDayTextColor: '#ffffff',
|
selectedDayTextColor: '#ffffff',
|
||||||
todayTextColor: this.props.theme.colors.text,
|
todayTextColor: props.theme.colors.text,
|
||||||
dayTextColor: this.props.theme.colors.text,
|
dayTextColor: props.theme.colors.text,
|
||||||
textDisabledColor: this.props.theme.colors.agendaDayTextColor,
|
textDisabledColor: props.theme.colors.agendaDayTextColor,
|
||||||
dotColor: this.props.theme.colors.primary,
|
dotColor: props.theme.colors.primary,
|
||||||
selectedDotColor: '#ffffff',
|
selectedDotColor: '#ffffff',
|
||||||
arrowColor: this.props.theme.colors.primary,
|
arrowColor: props.theme.colors.primary,
|
||||||
monthTextColor: this.props.theme.colors.text,
|
monthTextColor: props.theme.colors.text,
|
||||||
indicatorColor: this.props.theme.colors.primary,
|
indicatorColor: props.theme.colors.primary,
|
||||||
textDayFontFamily: 'monospace',
|
textDayFontFamily: 'monospace',
|
||||||
textMonthFontFamily: 'monospace',
|
textMonthFontFamily: 'monospace',
|
||||||
textDayHeaderFontFamily: 'monospace',
|
textDayHeaderFontFamily: 'monospace',
|
||||||
|
|
@ -379,15 +386,14 @@ class EquipmentRentScreen extends React.Component<Props, State> {
|
||||||
height: 34,
|
height: 34,
|
||||||
width: 34,
|
width: 34,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
},
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
style={{marginBottom: 50}}
|
style={{marginBottom: 50}}
|
||||||
/>
|
/>
|
||||||
</CollapsibleScrollView>
|
</CollapsibleScrollView>
|
||||||
<LoadingConfirmDialog
|
<LoadingConfirmDialog
|
||||||
visible={this.state.dialogVisible}
|
visible={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')}
|
||||||
|
|
@ -396,46 +402,40 @@ class EquipmentRentScreen extends React.Component<Props, State> {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
visible={this.state.errorDialogVisible}
|
visible={state.errorDialogVisible}
|
||||||
onDismiss={this.onErrorDialogDismiss}
|
onDismiss={this.onErrorDialogDismiss}
|
||||||
errorCode={this.state.currentError}
|
errorCode={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: [
|
transform: [{translateY: 100}],
|
||||||
{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>
|
||||||
|
);
|
||||||
)
|
}
|
||||||
} else
|
return null;
|
||||||
return <View/>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withTheme(EquipmentRentScreen);
|
export default withTheme(EquipmentRentScreen);
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,33 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {Image, KeyboardAvoidingView, StyleSheet, View} from "react-native";
|
import {Image, KeyboardAvoidingView, StyleSheet, View} from 'react-native';
|
||||||
import {Button, Card, HelperText, TextInput, withTheme} from 'react-native-paper';
|
import {
|
||||||
import ConnectionManager from "../../managers/ConnectionManager";
|
Button,
|
||||||
|
Card,
|
||||||
|
HelperText,
|
||||||
|
TextInput,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import ErrorDialog from "../../components/Dialogs/ErrorDialog";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
|
||||||
import AvailableWebsites from "../../constants/AvailableWebsites";
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||||
import MascotPopup from "../../components/Mascot/MascotPopup";
|
import AvailableWebsites from '../../constants/AvailableWebsites';
|
||||||
import LinearGradient from "react-native-linear-gradient";
|
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||||
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
|
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||||
|
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
route: {params: {nextScreen: string}},
|
route: {params: {nextScreen: string}},
|
||||||
theme: CustomTheme
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
isEmailValidated: boolean,
|
isEmailValidated: boolean,
|
||||||
|
|
@ -30,17 +36,53 @@ type State = {
|
||||||
dialogVisible: boolean,
|
dialogVisible: boolean,
|
||||||
dialogError: number,
|
dialogError: number,
|
||||||
mascotDialogVisible: boolean,
|
mascotDialogVisible: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||||
|
|
||||||
const RESET_PASSWORD_PATH = "https://www.amicale-insat.fr/password/reset";
|
const RESET_PASSWORD_PATH = 'https://www.amicale-insat.fr/password/reset';
|
||||||
|
|
||||||
const emailRegex = /^.+@.+\..+$/;
|
const emailRegex = /^.+@.+\..+$/;
|
||||||
|
|
||||||
class LoginScreen extends React.Component<Props, State> {
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
fontSize: 36,
|
||||||
|
marginBottom: 48,
|
||||||
|
},
|
||||||
|
textInput: {},
|
||||||
|
btnContainer: {
|
||||||
|
marginTop: 5,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
state = {
|
class LoginScreen extends React.Component<PropsType, StateType> {
|
||||||
|
onEmailChange: (value: string) => void;
|
||||||
|
|
||||||
|
onPasswordChange: (value: string) => void;
|
||||||
|
|
||||||
|
passwordInputRef: {current: null | TextInput};
|
||||||
|
|
||||||
|
nextScreen: string | null;
|
||||||
|
|
||||||
|
constructor(props: PropsType) {
|
||||||
|
super(props);
|
||||||
|
this.passwordInputRef = React.createRef();
|
||||||
|
this.onEmailChange = (value: string) => {
|
||||||
|
this.onInputChange(true, value);
|
||||||
|
};
|
||||||
|
this.onPasswordChange = (value: string) => {
|
||||||
|
this.onInputChange(false, value);
|
||||||
|
};
|
||||||
|
props.navigation.addListener('focus', this.onScreenFocus);
|
||||||
|
this.state = {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
isEmailValidated: false,
|
isEmailValidated: false,
|
||||||
|
|
@ -48,139 +90,27 @@ class LoginScreen extends React.Component<Props, State> {
|
||||||
loading: false,
|
loading: false,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogError: 0,
|
dialogError: 0,
|
||||||
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.loginShowBanner.key),
|
mascotDialogVisible: AsyncStorageManager.getBool(
|
||||||
|
AsyncStorageManager.PREFERENCES.loginShowBanner.key,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
onEmailChange: (value: string) => null;
|
|
||||||
onPasswordChange: (value: string) => null;
|
|
||||||
passwordInputRef: { current: null | TextInput };
|
|
||||||
|
|
||||||
nextScreen: string | null;
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.passwordInputRef = React.createRef();
|
|
||||||
this.onEmailChange = this.onInputChange.bind(this, true);
|
|
||||||
this.onPasswordChange = this.onInputChange.bind(this, false);
|
|
||||||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onScreenFocus = () => {
|
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 = () => this.props.navigation.navigate("website", {
|
onResetPasswordClick = () => {
|
||||||
|
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.
|
||||||
|
|
@ -211,7 +141,7 @@ class LoginScreen extends React.Component<Props, State> {
|
||||||
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.
|
||||||
|
|
@ -221,9 +151,11 @@ class LoginScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
onSubmit = () => {
|
onSubmit = () => {
|
||||||
|
const {email, password} = this.state;
|
||||||
if (this.shouldEnableLogin()) {
|
if (this.shouldEnableLogin()) {
|
||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
ConnectionManager.getInstance().connect(this.state.email, this.state.password)
|
ConnectionManager.getInstance()
|
||||||
|
.connect(email, password)
|
||||||
.then(this.handleSuccess)
|
.then(this.handleSuccess)
|
||||||
.catch(this.showErrorDialog)
|
.catch(this.showErrorDialog)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
|
@ -237,53 +169,48 @@ class LoginScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getFormInput() {
|
getFormInput(): React.Node {
|
||||||
|
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={this.state.email}
|
value={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
|
<HelperText type="error" visible={this.shouldShowEmailError()}>
|
||||||
type="error"
|
{i18n.t('screens.login.emailError')}
|
||||||
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={this.state.password}
|
value={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={true}
|
secureTextEntry
|
||||||
/>
|
/>
|
||||||
<HelperText
|
<HelperText type="error" visible={this.shouldShowPasswordError()}>
|
||||||
type="error"
|
{i18n.t('screens.login.passwordError')}
|
||||||
visible={this.shouldShowPasswordError()}
|
|
||||||
>
|
|
||||||
{i18n.t("screens.login.passwordError")}
|
|
||||||
</HelperText>
|
</HelperText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -293,43 +220,45 @@ class LoginScreen extends React.Component<Props, State> {
|
||||||
* Gets the card containing the input form
|
* Gets the card containing the input form
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getMainCard() {
|
getMainCard(): React.Node {
|
||||||
|
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={(props) => <Image
|
left={({size}: {size: number}): React.Node => (
|
||||||
{...props}
|
<Image
|
||||||
source={ICON_AMICALE}
|
source={ICON_AMICALE}
|
||||||
style={{
|
style={{
|
||||||
width: props.size,
|
width: size,
|
||||||
height: props.size,
|
height: 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={this.props.theme.colors.warning}
|
color={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={this.state.loading}
|
loading={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
|
||||||
|
|
@ -340,7 +269,7 @@ class LoginScreen extends React.Component<Props, State> {
|
||||||
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>
|
||||||
|
|
@ -348,45 +277,164 @@ class LoginScreen extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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%"}}>
|
<View style={{height: '100%'}}>{this.getMainCard()}</View>
|
||||||
{this.getMainCard()}
|
|
||||||
</View>
|
|
||||||
<MascotPopup
|
<MascotPopup
|
||||||
visible={this.state.mascotDialogVisible}
|
visible={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={this.state.dialogVisible}
|
visible={dialogVisible}
|
||||||
onDismiss={this.hideErrorDialog}
|
onDismiss={this.hideErrorDialog}
|
||||||
errorCode={this.state.dialogError}
|
errorCode={dialogError}
|
||||||
/>
|
/>
|
||||||
</CollapsibleScrollView>
|
</CollapsibleScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|
@ -395,23 +443,4 @@ class LoginScreen extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,47 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {FlatList, StyleSheet, View} from "react-native";
|
import {FlatList, StyleSheet, View} from 'react-native';
|
||||||
import {Avatar, Button, Card, Divider, List, Paragraph, withTheme} from 'react-native-paper';
|
import {
|
||||||
import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
|
Avatar,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
Paragraph,
|
||||||
|
withTheme,
|
||||||
|
} from 'react-native-paper';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import LogoutDialog from "../../components/Amicale/LogoutDialog";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
|
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
|
||||||
import type {cardList} from "../../components/Lists/CardList/CardList";
|
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||||
import CardList from "../../components/Lists/CardList/CardList";
|
import MaterialHeaderButtons, {
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
Item,
|
||||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
} from '../../components/Overrides/CustomHeaderButton';
|
||||||
import AvailableWebsites from "../../constants/AvailableWebsites";
|
import CardList from '../../components/Lists/CardList/CardList';
|
||||||
import Mascot, {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||||
import ServicesManager, {SERVICES_KEY} from "../../managers/ServicesManager";
|
import AvailableWebsites from '../../constants/AvailableWebsites';
|
||||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
import Mascot, {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||||
|
import ServicesManager, {SERVICES_KEY} from '../../managers/ServicesManager';
|
||||||
|
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||||
|
import type {ServiceItemType} from '../../managers/ServicesManager';
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp,
|
navigation: StackNavigationProp,
|
||||||
theme: CustomTheme,
|
theme: CustomThemeType,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
dialogVisible: boolean,
|
dialogVisible: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
type ProfileData = {
|
type ClubType = {
|
||||||
|
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,
|
||||||
|
|
@ -34,55 +50,59 @@ type ProfileData = {
|
||||||
branch: string,
|
branch: string,
|
||||||
link: string,
|
link: string,
|
||||||
validity: boolean,
|
validity: boolean,
|
||||||
clubs: Array<Club>,
|
clubs: Array<ClubType>,
|
||||||
}
|
|
||||||
type Club = {
|
|
||||||
id: number,
|
|
||||||
name: string,
|
|
||||||
is_manager: boolean,
|
|
||||||
}
|
|
||||||
|
|
||||||
class ProfileScreen extends React.Component<Props, State> {
|
|
||||||
|
|
||||||
state = {
|
|
||||||
dialogVisible: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
data: ProfileData;
|
const styles = StyleSheet.create({
|
||||||
|
card: {
|
||||||
|
margin: 10,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
editButton: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class ProfileScreen extends React.Component<PropsType, StateType> {
|
||||||
|
data: ProfileDataType;
|
||||||
|
|
||||||
flatListData: Array<{id: string}>;
|
flatListData: Array<{id: string}>;
|
||||||
amicaleDataset: cardList;
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
amicaleDataset: Array<ServiceItemType>;
|
||||||
|
|
||||||
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
this.flatListData = [
|
this.flatListData = [{id: '0'}, {id: '1'}, {id: '2'}, {id: '3'}];
|
||||||
{id: '0'},
|
|
||||||
{id: '1'},
|
|
||||||
{id: '2'},
|
|
||||||
{id: '3'},
|
|
||||||
]
|
|
||||||
const services = new ServicesManager(props.navigation);
|
const services = new ServicesManager(props.navigation);
|
||||||
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
|
this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
|
||||||
|
this.state = {
|
||||||
|
dialogVisible: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.navigation.setOptions({
|
const {navigation} = this.props;
|
||||||
|
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 = () => <MaterialHeaderButtons>
|
getHeaderButton = (): React.Node => (
|
||||||
<Item title="logout" iconName="logout" onPress={this.showDisconnectDialog}/>
|
<MaterialHeaderButtons>
|
||||||
</MaterialHeaderButtons>;
|
<Item
|
||||||
|
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
|
||||||
|
|
@ -90,10 +110,12 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
* @param data The data fetched from the server
|
* @param data The data fetched from the server
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getScreen = (data: Array<{ [key: string]: any } | null>) => {
|
getScreen = (data: Array<ProfileDataType | null>): React.Node => {
|
||||||
if (data[0] != null) {
|
const {dialogVisible} = this.state;
|
||||||
this.data = data[0];
|
const {navigation} = this.props;
|
||||||
}
|
// 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
|
||||||
|
|
@ -101,15 +123,15 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
data={this.flatListData}
|
data={this.flatListData}
|
||||||
/>
|
/>
|
||||||
<LogoutDialog
|
<LogoutDialog
|
||||||
{...this.props}
|
navigation={navigation}
|
||||||
visible={this.state.dialogVisible}
|
visible={dialogVisible}
|
||||||
onDismiss={this.hideDisconnectDialog}
|
onDismiss={this.hideDisconnectDialog}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getRenderItem = ({item}: { item: { id: string } }) => {
|
getRenderItem = ({item}: {item: {id: string}}): React.Node => {
|
||||||
switch (item.id) {
|
switch (item.id) {
|
||||||
case '0':
|
case '0':
|
||||||
return this.getWelcomeCard();
|
return this.getWelcomeCard();
|
||||||
|
|
@ -127,13 +149,8 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getServicesList() {
|
getServicesList(): React.Node {
|
||||||
return (
|
return <CardList dataset={this.amicaleDataset} isHorizontal />;
|
||||||
<CardList
|
|
||||||
dataset={this.amicaleDataset}
|
|
||||||
isHorizontal={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -141,42 +158,44 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
getWelcomeCard() {
|
getWelcomeCard(): React.Node {
|
||||||
|
const {navigation} = this.props;
|
||||||
return (
|
return (
|
||||||
<Card style={styles.card}>
|
<Card style={styles.card}>
|
||||||
<Card.Title
|
<Card.Title
|
||||||
title={i18n.t("screens.profile.welcomeTitle", {name: this.data.first_name})}
|
title={i18n.t('screens.profile.welcomeTitle', {
|
||||||
left={() =>
|
name: this.data.first_name,
|
||||||
|
})}
|
||||||
|
left={(): React.Node => (
|
||||||
<Mascot
|
<Mascot
|
||||||
style={{
|
style={{
|
||||||
width: 60
|
width: 60,
|
||||||
}}
|
}}
|
||||||
emotion={MASCOT_STYLE.COOL}
|
emotion={MASCOT_STYLE.COOL}
|
||||||
animated={true}
|
animated
|
||||||
entryAnimation={{
|
entryAnimation={{
|
||||||
animation: "bounceIn",
|
animation: 'bounceIn',
|
||||||
duration: 1000
|
duration: 1000,
|
||||||
}}
|
}}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
titleStyle={{marginLeft: 10}}
|
titleStyle={{marginLeft: 10}}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Paragraph>
|
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
|
||||||
{i18n.t("screens.profile.welcomeDescription")}
|
|
||||||
</Paragraph>
|
|
||||||
{this.getServicesList()}
|
{this.getServicesList()}
|
||||||
<Paragraph>
|
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
|
||||||
{i18n.t("screens.profile.welcomeFeedback")}
|
|
||||||
</Paragraph>
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Card.Actions>
|
<Card.Actions>
|
||||||
<Button
|
<Button
|
||||||
icon="bug"
|
icon="bug"
|
||||||
mode="contained"
|
mode="contained"
|
||||||
onPress={() => this.props.navigation.navigate('feedback')}
|
onPress={() => {
|
||||||
|
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>
|
||||||
|
|
@ -184,16 +203,6 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
||||||
|
|
@ -201,10 +210,8 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
* @param field The field to get the value from
|
* @param field The field to get the value from
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getFieldValue(field: ?string) {
|
static getFieldValue(field: ?string): string {
|
||||||
return this.isFieldAvailable(field)
|
return field != null ? field : i18n.t('screens.profile.noData');
|
||||||
? field
|
|
||||||
: i18n.t("screens.profile.noData");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -214,18 +221,21 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
* @param icon The icon to use
|
* @param icon The icon to use
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getPersonalListItem(field: ?string, icon: string) {
|
getPersonalListItem(field: ?string, icon: string): React.Node {
|
||||||
let title = this.isFieldAvailable(field) ? this.getFieldValue(field) : ':(';
|
const {theme} = this.props;
|
||||||
let subtitle = this.isFieldAvailable(field) ? '' : this.getFieldValue(field);
|
const title = field != null ? ProfileScreen.getFieldValue(field) : ':(';
|
||||||
|
const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title={title}
|
title={title}
|
||||||
description={subtitle}
|
description={subtitle}
|
||||||
left={props => <List.Icon
|
left={({size}: {size: number}): React.Node => (
|
||||||
{...props}
|
<List.Icon
|
||||||
|
size={size}
|
||||||
icon={icon}
|
icon={icon}
|
||||||
color={this.isFieldAvailable(field) ? undefined : this.props.theme.colors.textDisabled}
|
color={field != null ? null : theme.colors.textDisabled}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -235,40 +245,47 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getPersonalCard() {
|
getPersonalCard(): React.Node {
|
||||||
|
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={(props) => <Avatar.Icon
|
left={({size}: {size: number}): React.Node => (
|
||||||
{...props}
|
<Avatar.Icon
|
||||||
|
size={size}
|
||||||
icon="account"
|
icon="account"
|
||||||
color={this.props.theme.colors.primary}
|
color={theme.colors.primary}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Divider />
|
<Divider />
|
||||||
<List.Section>
|
<List.Section>
|
||||||
<List.Subheader>{i18n.t("screens.profile.personalInformation")}</List.Subheader>
|
<List.Subheader>
|
||||||
{this.getPersonalListItem(this.data.birthday, "cake-variant")}
|
{i18n.t('screens.profile.personalInformation')}
|
||||||
{this.getPersonalListItem(this.data.phone, "phone")}
|
</List.Subheader>
|
||||||
{this.getPersonalListItem(this.data.email, "email")}
|
{this.getPersonalListItem(this.data.birthday, 'cake-variant')}
|
||||||
{this.getPersonalListItem(this.data.branch, "school")}
|
{this.getPersonalListItem(this.data.phone, 'phone')}
|
||||||
|
{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={() => this.props.navigation.navigate("website", {
|
onPress={() => {
|
||||||
|
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>
|
||||||
|
|
@ -281,18 +298,21 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getClubCard() {
|
getClubCard(): React.Node {
|
||||||
|
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={(props) => <Avatar.Icon
|
left={({size}: {size: number}): React.Node => (
|
||||||
{...props}
|
<Avatar.Icon
|
||||||
|
size={size}
|
||||||
icon="account-group"
|
icon="account-group"
|
||||||
color={this.props.theme.colors.primary}
|
color={theme.colors.primary}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
@ -307,18 +327,21 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getMembershipCar() {
|
getMembershipCar(): React.Node {
|
||||||
|
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={(props) => <Avatar.Icon
|
left={({size}: {size: number}): React.Node => (
|
||||||
{...props}
|
<Avatar.Icon
|
||||||
|
size={size}
|
||||||
icon="credit-card"
|
icon="credit-card"
|
||||||
color={this.props.theme.colors.primary}
|
color={theme.colors.primary}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
<List.Section>
|
<List.Section>
|
||||||
|
|
@ -334,81 +357,106 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @return {*}
|
* @return {*}
|
||||||
*/
|
*/
|
||||||
getMembershipItem(state: boolean) {
|
getMembershipItem(state: boolean): React.Node {
|
||||||
|
const {theme} = this.props;
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
title={state ? i18n.t("screens.profile.membershipPayed") : i18n.t("screens.profile.membershipNotPayed")}
|
title={
|
||||||
left={props => <List.Icon
|
state
|
||||||
{...props}
|
? i18n.t('screens.profile.membershipPayed')
|
||||||
color={state ? this.props.theme.colors.success : this.props.theme.colors.danger}
|
: i18n.t('screens.profile.membershipNotPayed')
|
||||||
|
}
|
||||||
|
left={({size}: {size: number}): React.Node => (
|
||||||
|
<List.Icon
|
||||||
|
size={size}
|
||||||
|
color={state ? theme.colors.success : theme.colors.danger}
|
||||||
icon={state ? 'check' : 'close'}
|
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 {*}
|
||||||
*/
|
*/
|
||||||
clubListItem = ({item}: { item: Club }) => {
|
getClubListItem = ({item}: {item: ClubType}): React.Node => {
|
||||||
const onPress = () => this.openClubDetailsScreen(item.id);
|
const {theme} = this.props;
|
||||||
let description = i18n.t("screens.profile.isMember");
|
const onPress = () => {
|
||||||
let icon = (props) => <List.Icon {...props} icon="chevron-right"/>;
|
this.openClubDetailsScreen(item.id);
|
||||||
|
};
|
||||||
|
let description = i18n.t('screens.profile.isMember');
|
||||||
|
let icon = ({size, color}: {size: number, color: string}): React.Node => (
|
||||||
|
<List.Icon size={size} color={color} icon="chevron-right" />
|
||||||
|
);
|
||||||
if (item.is_manager) {
|
if (item.is_manager) {
|
||||||
description = i18n.t("screens.profile.isManager");
|
description = i18n.t('screens.profile.isManager');
|
||||||
icon = (props) => <List.Icon {...props} icon="star" color={this.props.theme.colors.primary}/>;
|
icon = ({size}: {size: number}): React.Node => (
|
||||||
|
<List.Icon size={size} icon="star" color={theme.colors.primary} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return <List.Item
|
return (
|
||||||
|
<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<Club>) {
|
getClubList(list: Array<ClubType>): React.Node {
|
||||||
list.sort(this.sortClubList);
|
list.sort(this.sortClubList);
|
||||||
return (
|
return (
|
||||||
//$FlowFixMe
|
|
||||||
<FlatList
|
<FlatList
|
||||||
renderItem={this.clubListItem}
|
renderItem={this.getClubListItem}
|
||||||
keyExtractor={this.clubKeyExtractor}
|
keyExtractor={this.clubKeyExtractor}
|
||||||
data={list}
|
data={list}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
clubKeyExtractor = (item: ClubType): string => item.name;
|
||||||
|
|
||||||
|
sortClubList = (a: ClubType): number => (a.is_manager ? -1 : 1);
|
||||||
|
|
||||||
|
showDisconnectDialog = () => {
|
||||||
|
this.setState({dialogVisible: true});
|
||||||
|
};
|
||||||
|
|
||||||
|
hideDisconnectDialog = () => {
|
||||||
|
this.setState({dialogVisible: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the club details screen for the club of given ID
|
||||||
|
* @param id The club's id to open
|
||||||
|
*/
|
||||||
|
openClubDetailsScreen(id: number) {
|
||||||
|
const {navigation} = this.props;
|
||||||
|
navigation.navigate('club-information', {clubId: id});
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): React.Node {
|
||||||
|
const {navigation} = this.props;
|
||||||
return (
|
return (
|
||||||
<AuthenticatedScreen
|
<AuthenticatedScreen
|
||||||
{...this.props}
|
navigation={navigation}
|
||||||
requests={[
|
requests={[
|
||||||
{
|
{
|
||||||
link: 'user/profile',
|
link: 'user/profile',
|
||||||
params: {},
|
params: {},
|
||||||
mandatory: true,
|
mandatory: true,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
renderFunction={this.getScreen}
|
renderFunction={this.getScreen}
|
||||||
/>
|
/>
|
||||||
|
|
@ -416,17 +464,4 @@ class ProfileScreen extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
card: {
|
|
||||||
margin: 10,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
},
|
|
||||||
editButton: {
|
|
||||||
marginLeft: 'auto'
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withTheme(ProfileScreen);
|
export default withTheme(ProfileScreen);
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,47 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {RefreshControl, View} from "react-native";
|
import {RefreshControl, View} from 'react-native';
|
||||||
import AuthenticatedScreen from "../../components/Amicale/AuthenticatedScreen";
|
import {StackNavigationProp} from '@react-navigation/stack';
|
||||||
import {getTimeOnlyString, stringToDate} from "../../utils/Planning";
|
import i18n from 'i18n-js';
|
||||||
import VoteTease from "../../components/Amicale/Vote/VoteTease";
|
import {Button} from 'react-native-paper';
|
||||||
import VoteSelect from "../../components/Amicale/Vote/VoteSelect";
|
import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
|
||||||
import VoteResults from "../../components/Amicale/Vote/VoteResults";
|
import {getTimeOnlyString, stringToDate} from '../../utils/Planning';
|
||||||
import VoteWait from "../../components/Amicale/Vote/VoteWait";
|
import VoteTease from '../../components/Amicale/Vote/VoteTease';
|
||||||
import {StackNavigationProp} from "@react-navigation/stack";
|
import VoteSelect from '../../components/Amicale/Vote/VoteSelect';
|
||||||
import i18n from "i18n-js";
|
import VoteResults from '../../components/Amicale/Vote/VoteResults';
|
||||||
import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
|
import VoteWait from '../../components/Amicale/Vote/VoteWait';
|
||||||
import MascotPopup from "../../components/Mascot/MascotPopup";
|
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
|
||||||
import AsyncStorageManager from "../../managers/AsyncStorageManager";
|
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||||
import {Button} from "react-native-paper";
|
import AsyncStorageManager from '../../managers/AsyncStorageManager';
|
||||||
import VoteNotAvailable from "../../components/Amicale/Vote/VoteNotAvailable";
|
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
|
||||||
import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
|
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
|
||||||
|
import type {ApiGenericDataType} from '../../utils/WebData';
|
||||||
|
|
||||||
export type team = {
|
export type VoteTeamType = {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
votes: number,
|
votes: number,
|
||||||
}
|
|
||||||
|
|
||||||
type teamResponse = {
|
|
||||||
has_voted: boolean,
|
|
||||||
teams: Array<team>,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type stringVoteDates = {
|
type TeamResponseType = {
|
||||||
|
has_voted: boolean,
|
||||||
|
teams: Array<VoteTeamType>,
|
||||||
|
};
|
||||||
|
|
||||||
|
type VoteDatesStringType = {
|
||||||
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 objectVoteDates = {
|
type VoteDatesObjectType = {
|
||||||
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",
|
||||||
|
|
@ -92,84 +93,48 @@ type objectVoteDates = {
|
||||||
|
|
||||||
const MIN_REFRESH_TIME = 5 * 1000;
|
const MIN_REFRESH_TIME = 5 * 1000;
|
||||||
|
|
||||||
type Props = {
|
type PropsType = {
|
||||||
navigation: StackNavigationProp
|
navigation: StackNavigationProp,
|
||||||
}
|
};
|
||||||
|
|
||||||
type State = {
|
type StateType = {
|
||||||
hasVoted: boolean,
|
hasVoted: boolean,
|
||||||
mascotDialogVisible: boolean,
|
mascotDialogVisible: boolean,
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen displaying vote information and controls
|
* Screen displaying vote information and controls
|
||||||
*/
|
*/
|
||||||
export default class VoteScreen extends React.Component<Props, State> {
|
export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
|
teams: Array<VoteTeamType>;
|
||||||
|
|
||||||
state = {
|
|
||||||
hasVoted: false,
|
|
||||||
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.voteShowBanner.key),
|
|
||||||
};
|
|
||||||
|
|
||||||
teams: Array<team>;
|
|
||||||
hasVoted: boolean;
|
hasVoted: boolean;
|
||||||
datesString: null | stringVoteDates;
|
|
||||||
dates: null | objectVoteDates;
|
datesString: null | VoteDatesStringType;
|
||||||
|
|
||||||
|
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 = [
|
this.mainFlatListData = [{key: 'main'}, {key: 'info'}];
|
||||||
{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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -185,60 +150,43 @@ export default class VoteScreen extends React.Component<Props, State> {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
isVoteRunning() {
|
getMainRenderItem = ({item}: {item: {key: string}}): React.Node => {
|
||||||
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<{ [key: string]: any } | null>) => {
|
getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
|
||||||
|
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: teamResponse | null = data[0];
|
const teams: TeamResponseType | null = data[0];
|
||||||
const dateStrings: stringVoteDates | null = data[1];
|
const dateStrings: VoteDatesStringType | null = data[1];
|
||||||
|
|
||||||
if (dateStrings != null && dateStrings.date_begin == null)
|
if (dateStrings != null && dateStrings.date_begin == null)
|
||||||
this.datesString = null;
|
this.datesString = null;
|
||||||
else
|
else this.datesString = dateStrings;
|
||||||
this.datesString = dateStrings;
|
|
||||||
|
|
||||||
if (teams != null) {
|
if (teams != null) {
|
||||||
this.teams = teams.teams;
|
this.teams = teams.teams;
|
||||||
|
|
@ -250,86 +198,173 @@ export default class VoteScreen extends React.Component<Props, State> {
|
||||||
<CollapsibleFlatList
|
<CollapsibleFlatList
|
||||||
data={this.mainFlatListData}
|
data={this.mainFlatListData}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl refreshing={false} onRefresh={this.reloadData} />
|
||||||
refreshing={false}
|
|
||||||
onRefresh={this.reloadData}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
extraData={this.state.hasVoted.toString()}
|
extraData={state.hasVoted.toString()}
|
||||||
renderItem={this.mainRenderItem}
|
renderItem={this.getMainRenderItem}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
getContent() {
|
getContent(): React.Node {
|
||||||
if (!this.isVoteStarted())
|
const {state} = this;
|
||||||
return this.getTeaseVoteCard();
|
if (!this.isVoteStarted()) return this.getTeaseVoteCard();
|
||||||
else if (this.isVoteRunning() && (!this.hasVoted && !this.state.hasVoted))
|
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted)
|
||||||
return this.getVoteCard();
|
return this.getVoteCard();
|
||||||
else if (!this.isResultStarted())
|
if (!this.isResultStarted()) return this.getWaitVoteCard();
|
||||||
return this.getWaitVoteCard();
|
if (this.isResultRunning()) return this.getVoteResultCard();
|
||||||
else if (this.isResultRunning())
|
|
||||||
return this.getVoteResultCard();
|
|
||||||
else
|
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
}
|
}
|
||||||
|
|
||||||
onVoteSuccess = () => this.setState({hasVoted: true});
|
onVoteSuccess = (): void => 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() {
|
getVoteCard(): React.Node {
|
||||||
return <VoteSelect teams={this.teams} onVoteSuccess={this.onVoteSuccess} onVoteError={this.reloadData}/>;
|
return (
|
||||||
|
<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() {
|
getVoteResultCard(): React.Node {
|
||||||
if (this.dates != null && this.datesString != null)
|
if (this.dates != null && this.datesString != null)
|
||||||
return <VoteResults
|
return (
|
||||||
|
<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() {
|
getTeaseVoteCard(): React.Node {
|
||||||
if (this.dates != null && this.datesString != null)
|
if (this.dates != null && this.datesString != null)
|
||||||
return <VoteTease
|
return (
|
||||||
startDate={this.getDateString(this.dates.date_begin, this.datesString.date_begin)}/>;
|
<VoteTease
|
||||||
else
|
startDate={this.getDateString(
|
||||||
|
this.dates.date_begin,
|
||||||
|
this.datesString.date_begin,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Votes have ended, or user has voted waiting for results
|
* Votes have ended, or user has voted waiting for results
|
||||||
*/
|
*/
|
||||||
getWaitVoteCard() {
|
getWaitVoteCard(): React.Node {
|
||||||
|
const {state} = this;
|
||||||
let startDate = null;
|
let startDate = null;
|
||||||
if (this.dates != null && this.datesString != null && this.dates.date_result_begin != null)
|
if (
|
||||||
startDate = this.getDateString(this.dates.date_result_begin, this.datesString.date_result_begin);
|
this.dates != null &&
|
||||||
return <VoteWait startDate={startDate} hasVoted={this.hasVoted || this.state.hasVoted}
|
this.datesString != null &&
|
||||||
justVoted={this.state.hasVoted}
|
this.dates.date_result_begin != null
|
||||||
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.PREFERENCES.voteShowBanner.key, false);
|
AsyncStorageManager.set(
|
||||||
this.setState({mascotDialogVisible: false})
|
AsyncStorageManager.PREFERENCES.voteShowBanner.key,
|
||||||
|
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.
|
||||||
*
|
*
|
||||||
|
|
@ -337,11 +372,12 @@ export default class VoteScreen extends React.Component<Props, State> {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
render() {
|
render(): React.Node {
|
||||||
|
const {props, state} = this;
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1}}>
|
<View style={{flex: 1}}>
|
||||||
<AuthenticatedScreen
|
<AuthenticatedScreen
|
||||||
{...this.props}
|
navigation={props.navigation}
|
||||||
ref={this.authRef}
|
ref={this.authRef}
|
||||||
requests={[
|
requests={[
|
||||||
{
|
{
|
||||||
|
|
@ -358,17 +394,17 @@ export default class VoteScreen extends React.Component<Props, State> {
|
||||||
renderFunction={this.getScreen}
|
renderFunction={this.getScreen}
|
||||||
/>
|
/>
|
||||||
<MascotPopup
|
<MascotPopup
|
||||||
visible={this.state.mascotDialogVisible}
|
visible={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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
|
||||||
export type Coordinates = {
|
export type CoordinatesType = {
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
}
|
};
|
||||||
|
|
||||||
type Shape = Array<Array<number>>;
|
export type ShapeType = Array<Array<number>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract class used to represent a BaseShape.
|
* Abstract class used to represent a BaseShape.
|
||||||
|
|
@ -15,16 +15,18 @@ type Shape = Array<Array<number>>;
|
||||||
* and in methods to implement
|
* and in methods to implement
|
||||||
*/
|
*/
|
||||||
export default class BaseShape {
|
export default class BaseShape {
|
||||||
|
#currentShape: ShapeType;
|
||||||
|
|
||||||
#currentShape: Shape;
|
|
||||||
#rotation: number;
|
#rotation: number;
|
||||||
position: Coordinates;
|
|
||||||
theme: CustomTheme;
|
position: CoordinatesType;
|
||||||
|
|
||||||
|
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: CustomTheme) {
|
constructor(theme: CustomThemeType) {
|
||||||
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;
|
||||||
|
|
@ -37,6 +39,7 @@ 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");
|
||||||
}
|
}
|
||||||
|
|
@ -47,14 +50,15 @@ export default class BaseShape {
|
||||||
*
|
*
|
||||||
* Used by tests to read private fields
|
* Used by tests to read private fields
|
||||||
*/
|
*/
|
||||||
getShapes(): Array<Shape> {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
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(): Shape {
|
getCurrentShape(): ShapeType {
|
||||||
return this.#currentShape;
|
return this.#currentShape;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,17 +67,20 @@ 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<Coordinates>} This object cells coordinates
|
* @return {Array<CoordinatesType>} This object cells coordinates
|
||||||
*/
|
*/
|
||||||
getCellsCoordinates(isAbsolute: boolean): Array<Coordinates> {
|
getCellsCoordinates(isAbsolute: boolean): Array<CoordinatesType> {
|
||||||
let coordinates = [];
|
const coordinates = [];
|
||||||
for (let row = 0; row < this.#currentShape.length; row++) {
|
for (let row = 0; row < this.#currentShape.length; row += 1) {
|
||||||
for (let col = 0; col < this.#currentShape[row].length; col++) {
|
for (let col = 0; col < this.#currentShape[row].length; col += 1) {
|
||||||
if (this.#currentShape[row][col] === 1)
|
if (this.#currentShape[row][col] === 1) {
|
||||||
if (isAbsolute)
|
if (isAbsolute) {
|
||||||
coordinates.push({x: this.position.x + col, y: this.position.y + row});
|
coordinates.push({
|
||||||
else
|
x: this.position.x + col,
|
||||||
coordinates.push({x: col, y: row});
|
y: this.position.y + row,
|
||||||
|
});
|
||||||
|
} else coordinates.push({x: col, y: row});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return coordinates;
|
return coordinates;
|
||||||
|
|
@ -85,14 +92,10 @@ export default class BaseShape {
|
||||||
* @param isForward Should we rotate clockwise?
|
* @param isForward Should we rotate clockwise?
|
||||||
*/
|
*/
|
||||||
rotate(isForward: boolean) {
|
rotate(isForward: boolean) {
|
||||||
if (isForward)
|
if (isForward) this.#rotation += 1;
|
||||||
this.#rotation++;
|
else this.#rotation -= 1;
|
||||||
else
|
if (this.#rotation > 3) this.#rotation = 0;
|
||||||
this.#rotation--;
|
else if (this.#rotation < 0) this.#rotation = 3;
|
||||||
if (this.#rotation > 3)
|
|
||||||
this.#rotation = 0;
|
|
||||||
else if (this.#rotation < 0)
|
|
||||||
this.#rotation = 3;
|
|
||||||
this.#currentShape = this.getShapes()[this.#rotation];
|
this.#currentShape = this.getShapes()[this.#rotation];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,5 +109,4 @@ export default class BaseShape {
|
||||||
this.position.x += x;
|
this.position.x += x;
|
||||||
this.position.y += y;
|
this.position.y += y;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import BaseShape from "./BaseShape";
|
import BaseShape from './BaseShape';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {ShapeType} from './BaseShape';
|
||||||
|
|
||||||
export default class ShapeI extends BaseShape {
|
export default class ShapeI extends BaseShape {
|
||||||
|
constructor(theme: CustomThemeType) {
|
||||||
constructor(theme: CustomTheme) {
|
|
||||||
super(theme);
|
super(theme);
|
||||||
this.position.x = 3;
|
this.position.x = 3;
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ export default class ShapeI extends BaseShape {
|
||||||
return this.theme.colors.tetrisI;
|
return this.theme.colors.tetrisI;
|
||||||
}
|
}
|
||||||
|
|
||||||
getShapes() {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
getShapes(): Array<ShapeType> {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
[0, 0, 0, 0],
|
[0, 0, 0, 0],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import BaseShape from "./BaseShape";
|
import BaseShape from './BaseShape';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {ShapeType} from './BaseShape';
|
||||||
|
|
||||||
export default class ShapeJ extends BaseShape {
|
export default class ShapeJ extends BaseShape {
|
||||||
|
constructor(theme: CustomThemeType) {
|
||||||
constructor(theme: CustomTheme) {
|
|
||||||
super(theme);
|
super(theme);
|
||||||
this.position.x = 3;
|
this.position.x = 3;
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ export default class ShapeJ extends BaseShape {
|
||||||
return this.theme.colors.tetrisJ;
|
return this.theme.colors.tetrisJ;
|
||||||
}
|
}
|
||||||
|
|
||||||
getShapes() {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
getShapes(): Array<ShapeType> {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
[1, 0, 0],
|
[1, 0, 0],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import BaseShape from "./BaseShape";
|
import BaseShape from './BaseShape';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {ShapeType} from './BaseShape';
|
||||||
|
|
||||||
export default class ShapeL extends BaseShape {
|
export default class ShapeL extends BaseShape {
|
||||||
|
constructor(theme: CustomThemeType) {
|
||||||
constructor(theme: CustomTheme) {
|
|
||||||
super(theme);
|
super(theme);
|
||||||
this.position.x = 3;
|
this.position.x = 3;
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ export default class ShapeL extends BaseShape {
|
||||||
return this.theme.colors.tetrisL;
|
return this.theme.colors.tetrisL;
|
||||||
}
|
}
|
||||||
|
|
||||||
getShapes() {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
getShapes(): Array<ShapeType> {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
[0, 0, 1],
|
[0, 0, 1],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import BaseShape from "./BaseShape";
|
import BaseShape from './BaseShape';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {ShapeType} from './BaseShape';
|
||||||
|
|
||||||
export default class ShapeO extends BaseShape {
|
export default class ShapeO extends BaseShape {
|
||||||
|
constructor(theme: CustomThemeType) {
|
||||||
constructor(theme: CustomTheme) {
|
|
||||||
super(theme);
|
super(theme);
|
||||||
this.position.x = 4;
|
this.position.x = 4;
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ export default class ShapeO extends BaseShape {
|
||||||
return this.theme.colors.tetrisO;
|
return this.theme.colors.tetrisO;
|
||||||
}
|
}
|
||||||
|
|
||||||
getShapes() {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
getShapes(): Array<ShapeType> {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
[1, 1],
|
[1, 1],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import BaseShape from "./BaseShape";
|
import BaseShape from './BaseShape';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {ShapeType} from './BaseShape';
|
||||||
|
|
||||||
export default class ShapeS extends BaseShape {
|
export default class ShapeS extends BaseShape {
|
||||||
|
constructor(theme: CustomThemeType) {
|
||||||
constructor(theme: CustomTheme) {
|
|
||||||
super(theme);
|
super(theme);
|
||||||
this.position.x = 3;
|
this.position.x = 3;
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ export default class ShapeS extends BaseShape {
|
||||||
return this.theme.colors.tetrisS;
|
return this.theme.colors.tetrisS;
|
||||||
}
|
}
|
||||||
|
|
||||||
getShapes() {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
getShapes(): Array<ShapeType> {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
[0, 1, 1],
|
[0, 1, 1],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import BaseShape from "./BaseShape";
|
import BaseShape from './BaseShape';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {ShapeType} from './BaseShape';
|
||||||
|
|
||||||
export default class ShapeT extends BaseShape {
|
export default class ShapeT extends BaseShape {
|
||||||
|
constructor(theme: CustomThemeType) {
|
||||||
constructor(theme: CustomTheme) {
|
|
||||||
super(theme);
|
super(theme);
|
||||||
this.position.x = 3;
|
this.position.x = 3;
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ export default class ShapeT extends BaseShape {
|
||||||
return this.theme.colors.tetrisT;
|
return this.theme.colors.tetrisT;
|
||||||
}
|
}
|
||||||
|
|
||||||
getShapes() {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
getShapes(): Array<ShapeType> {
|
||||||
return [
|
return [
|
||||||
[
|
[
|
||||||
[0, 1, 0],
|
[0, 1, 0],
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import BaseShape from "./BaseShape";
|
import BaseShape from './BaseShape';
|
||||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||||
|
import type {ShapeType} from './BaseShape';
|
||||||
|
|
||||||
export default class ShapeZ extends BaseShape {
|
export default class ShapeZ extends BaseShape {
|
||||||
|
constructor(theme: CustomThemeType) {
|
||||||
constructor(theme: CustomTheme) {
|
|
||||||
super(theme);
|
super(theme);
|
||||||
this.position.x = 3;
|
this.position.x = 3;
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,8 @@ export default class ShapeZ extends BaseShape {
|
||||||
return this.theme.colors.tetrisZ;
|
return this.theme.colors.tetrisZ;
|
||||||
}
|
}
|
||||||
|
|
||||||
getShapes() {
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
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
Loading…
Reference in a new issue