Compare commits
211 commits
3b5b0aaa1a
...
6441865b09
| Author | SHA1 | Date | |
|---|---|---|---|
| 6441865b09 | |||
| 0356736d14 | |||
| 329d6eab1d | |||
| 835656bb30 | |||
| 14e5b01341 | |||
| 6ec87821e8 | |||
| 6e7d891d5c | |||
| 88fe4de4d7 | |||
| 8d860c17c2 | |||
| e793f871c6 | |||
| c82b3c30bf | |||
| a3c691ebf0 | |||
| c2cdabe8fc | |||
| 962f629e02 | |||
| c62307af7c | |||
| 795980dc8d | |||
| 4199a8700c | |||
| 5cdf9d6318 | |||
| 19d856574e | |||
| bf09f1d14d | |||
| dfa9548a2f | |||
| 4ee7490af4 | |||
| 0e741a02e2 | |||
| 92603cbaf7 | |||
| a3ce3a76c3 | |||
| fd44a32b05 | |||
| 956d78ad7f | |||
| c522a410ab | |||
| d133ea30a5 | |||
| ee82589cae | |||
| 4c29e146bb | |||
| f0de0599dd | |||
| 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 | |||
| 05ef28f3b5 | |||
| 2aac20ccee | |||
| 26aded3684 | |||
| 1421f4f308 | |||
| e701262ef3 | |||
| 7a134c7906 | |||
| 12e57005da | |||
| 14970abeab | |||
| 5349e210cb | |||
| 6254ce1814 | |||
| 2b7e6b4541 | |||
| 7dd91574a5 | |||
| ab801ec92b | |||
| be33726e39 | |||
| 22d5f61fc5 | |||
| 560c336759 | |||
| 9f4a8c837d | |||
| 8864e686bd | |||
| ccf196abaa | |||
| ae3d9310d6 | |||
| b2ff90855f | |||
| 4f911ce32d | |||
| 0ed3122dcf | |||
| de41a57930 | |||
| 1780ab886e | |||
| 4bff6e15a8 | |||
| fdf0fffabc | |||
| 746303b35a | |||
| 3989652c29 | |||
| fe26ec0cc4 | |||
| 494b319f19 | |||
| ca107356d1 | |||
| d1fc8a9625 | |||
| 8004820c2a | |||
| 5ad1e1d3f3 | |||
| 96c64a98e0 | |||
| b405f2aa6b | |||
| 2022b738f5 | |||
| ac19b77fd3 | |||
| 1c7768d029 | |||
| 99ff524d53 | |||
| 8b201efabf | |||
| 9a379ffb3d | |||
| 57c3e7d9d7 | |||
| 5f9132a670 | |||
| ca03b70603 | |||
| 870cbfdadf | |||
| 97072be390 | |||
| ea19ca3ade | |||
| 7ef93da811 | |||
| 460d84c5f4 | |||
| f675bda780 | |||
| 088527a15f | |||
| 3c31646382 | |||
| c939d5a552 | |||
| 5396b0e733 | |||
| 04d93fcf83 | |||
| eb9bf26baa | |||
| a90dcf4460 | |||
| 63720f9101 | |||
| bc3826c59a | |||
| 8156782d10 | |||
| 2d4e118614 | |||
| 83d7aad2fe | |||
| f8d148d7ce | |||
| 434d8b6565 | |||
| 90d1437248 | |||
| eba2cebe01 | |||
| 9064b8da77 | |||
| 0d1fe124f4 | |||
| 2a9bf5bb6a | |||
| 761132732b | |||
| 976684dfce | |||
| e048035722 | |||
| 5067fd47d6 | |||
| 63b02cd83c | |||
| 3275b73708 | |||
| 9e5542359b | |||
| d7c14febb2 | |||
| 39d5e05537 | |||
| ea25d2dd67 | |||
| 01e3d96ddb | |||
| 5bfc353218 | |||
| a9caca9969 | |||
| f9efea288f | |||
| d622a2f77a | |||
| b813aa0b83 | |||
| 3f14f7bb96 | |||
| 98168b560b | |||
| b66e50eaf8 | |||
| 869a8e5ec0 | |||
| fe9efc4008 | |||
| d6ce72d195 | |||
| 401c7d85ef | |||
| 56ab0e562f | |||
| 5b225dcedb | |||
| 5c918ecb3d | |||
| bc05391708 | |||
| 32c77fab05 | |||
| b6915a1ebe | |||
| 82371e89e7 | |||
| 217e918ce8 | |||
| 88b2120c8a | |||
| aaf7084297 | |||
| 06d01e98b0 | |||
| 57f7716700 | |||
| 4cdadbc6c1 | |||
| d07b34c748 | |||
| 629f0401bc | |||
| 98359dba7d | |||
| f2acb59ea7 | |||
| fe9089881a | |||
| 72c5a91f75 | |||
| 6ac459e58a | |||
| 23bc034b34 | |||
| b1e3fe6658 | |||
| 4131b79561 | |||
| 236ee2c07c | |||
| 5690d176b7 | |||
| f321888bc3 | |||
| e43577c8cb | |||
| 584d9ca563 | |||
| 35d39a5711 | |||
| 5e36b993c5 | |||
| 09de59c178 | |||
| f644964473 | |||
| 936f2a8e9e | |||
| 3b3b07bcf2 | |||
| d9ae6ad3a5 | |||
| 86f99b49e4 | |||
| 1af4688329 | |||
| b378fde2f0 | |||
| 211e57167d | |||
| 7464ef8859 |
6
.buckconfig
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
[android]
|
||||
target = Google Inc.:Google APIs:23
|
||||
|
||||
[maven_repositories]
|
||||
central = https://repo1.maven.org/maven2
|
||||
46
.eslintrc.js
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'airbnb',
|
||||
'plugin:flowtype/recommended',
|
||||
'prettier',
|
||||
'prettier/flowtype',
|
||||
'prettier/react',
|
||||
],
|
||||
parser: 'babel-eslint',
|
||||
plugins: ['flowtype'],
|
||||
env: {
|
||||
jest: true,
|
||||
},
|
||||
rules: {
|
||||
'react/jsx-filename-extension': [1, {extensions: ['.js', '.jsx']}],
|
||||
'react/static-property-placement': [2, 'static public field'],
|
||||
'flowtype/define-flow-type': 1,
|
||||
'flowtype/no-mixed': 2,
|
||||
'flowtype/no-primitive-constructor-types': 2,
|
||||
'flowtype/no-types-missing-file-annotation': 2,
|
||||
'flowtype/no-weak-types': 2,
|
||||
'flowtype/require-parameter-type': 2,
|
||||
'flowtype/require-readonly-react-props': 0,
|
||||
'flowtype/require-return-type': [
|
||||
2,
|
||||
'always',
|
||||
{
|
||||
annotateUndefined: 'never',
|
||||
},
|
||||
],
|
||||
'flowtype/require-valid-file-annotation': 2,
|
||||
'flowtype/type-id-match': [2, '^([A-Z][a-z0-9]+)+Type$'],
|
||||
'flowtype/use-flow-type': 1,
|
||||
'flowtype/valid-syntax': 1,
|
||||
},
|
||||
settings: {
|
||||
flowtype: {
|
||||
onlyFilesWithFlowAnnotation: false,
|
||||
},
|
||||
},
|
||||
globals: {
|
||||
fetch: false,
|
||||
Headers: false,
|
||||
},
|
||||
};
|
||||
11
.flowconfig
|
|
@ -1,11 +0,0 @@
|
|||
[ignore]
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
|
||||
[lints]
|
||||
|
||||
[options]
|
||||
|
||||
[strict]
|
||||
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.pbxproj -text
|
||||
29
.gitignore
vendored
|
|
@ -1,23 +1,3 @@
|
|||
node_modules/**/*
|
||||
.expo/*
|
||||
npm-debug.*
|
||||
*.jks
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
*.orig.*
|
||||
web-build/
|
||||
web-report/
|
||||
/.expo-shared/
|
||||
/package-lock.json
|
||||
|
||||
!/.idea/
|
||||
/.idea/*
|
||||
!/.idea/runConfigurations
|
||||
|
||||
# The following contents were automatically generated by expo-cli during eject
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# OSX
|
||||
#
|
||||
.DS_Store
|
||||
|
|
@ -40,12 +20,12 @@ DerivedData
|
|||
*.hmap
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
project.xcworkspace
|
||||
|
||||
# Android/IntelliJ
|
||||
#
|
||||
build/
|
||||
.idea
|
||||
!.idea/runConfigurations
|
||||
.gradle
|
||||
local.properties
|
||||
*.iml
|
||||
|
|
@ -60,6 +40,9 @@ yarn-error.log
|
|||
buck-out/
|
||||
\.buckd/
|
||||
*.keystore
|
||||
!debug.keystore
|
||||
*.jks
|
||||
/android/keystores/release.keystore.properties
|
||||
|
||||
# fastlane
|
||||
#
|
||||
|
|
@ -77,7 +60,3 @@ buck-out/
|
|||
|
||||
# CocoaPods
|
||||
/ios/Pods/
|
||||
|
||||
# Expo
|
||||
.expo/*
|
||||
/android/gradle.properties
|
||||
|
|
|
|||
12
.idea/runConfigurations/Lint_Check.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Lint Check" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="lint" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
|
|
@ -1,14 +1,11 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Expo" type="ReactNative" factoryName="React Native">
|
||||
<configuration default="false" name="Run Android" 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" />
|
||||
<react-native value="$PROJECT_DIR$/node_modules/react-native" />
|
||||
<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>
|
||||
12
.idea/runConfigurations/Run_Android_Release.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run Android Release" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="android-release" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
13
.idea/runConfigurations/Run_iOS.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run iOS" type="ReactNative" factoryName="React Native">
|
||||
<node-interpreter value="project" />
|
||||
<react-native value="$PROJECT_DIR$/node_modules/react-native" />
|
||||
<platform value="IOS" />
|
||||
<envs />
|
||||
<only-packager />
|
||||
<browser value="98ca6316-2f89-46d9-a9e5-fa9e2b0625b3" />
|
||||
<method v="2">
|
||||
<option name="ReactNativePackager" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
6
.prettierrc.js
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
bracketSpacing: false,
|
||||
jsxBracketSameLine: true,
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
};
|
||||
370
App.js
|
|
@ -1,209 +1,213 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Platform, SafeAreaView, StatusBar, View, YellowBox} 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 {LogBox, Platform, SafeAreaView, View} from 'react-native';
|
||||
import {NavigationContainer} from '@react-navigation/native';
|
||||
import MainNavigator from './src/navigation/MainNavigator';
|
||||
import {Provider as PaperProvider} from 'react-native-paper';
|
||||
import AprilFoolsManager from "./src/managers/AprilFoolsManager";
|
||||
import Update from "./src/constants/Update";
|
||||
import ConnectionManager from "./src/managers/ConnectionManager";
|
||||
import URLHandler from "./src/utils/URLHandler";
|
||||
import {setSafeBounceHeight} from "react-navigation-collapsible";
|
||||
import SplashScreen from 'react-native-splash-screen'
|
||||
import {OverflowMenuProvider} from "react-navigation-header-buttons";
|
||||
import {setSafeBounceHeight} from 'react-navigation-collapsible';
|
||||
import SplashScreen from 'react-native-splash-screen';
|
||||
import {OverflowMenuProvider} from 'react-navigation-header-buttons';
|
||||
import LocaleManager from './src/managers/LocaleManager';
|
||||
import AsyncStorageManager from './src/managers/AsyncStorageManager';
|
||||
import CustomIntroSlider from './src/components/Overrides/CustomIntroSlider';
|
||||
import type {CustomThemeType} from './src/managers/ThemeManager';
|
||||
import ThemeManager from './src/managers/ThemeManager';
|
||||
import MainNavigator from './src/navigation/MainNavigator';
|
||||
import AprilFoolsManager from './src/managers/AprilFoolsManager';
|
||||
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
|
||||
// Crashes app when navigating away from webview on android 9+
|
||||
// enableScreens(true);
|
||||
|
||||
|
||||
YellowBox.ignoreWarnings([ // collapsible headers cause this warning, just ignore as it is not an issue
|
||||
'Non-serializable values were found in the navigation state',
|
||||
LogBox.ignoreLogs([
|
||||
// collapsible headers cause this warning, just ignore as it is not an issue
|
||||
'Non-serializable values were found in the navigation state',
|
||||
'Cannot update a component from inside the function body of a different component',
|
||||
]);
|
||||
|
||||
type Props = {};
|
||||
|
||||
type State = {
|
||||
isLoading: boolean,
|
||||
showIntro: boolean,
|
||||
showUpdate: boolean,
|
||||
showAprilFools: boolean,
|
||||
currentTheme: CustomTheme | null,
|
||||
type StateType = {
|
||||
isLoading: boolean,
|
||||
showIntro: boolean,
|
||||
showUpdate: boolean,
|
||||
showAprilFools: boolean,
|
||||
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 = {
|
||||
isLoading: true,
|
||||
showIntro: true,
|
||||
showUpdate: true,
|
||||
showAprilFools: false,
|
||||
currentTheme: null,
|
||||
defaultHomeRoute: string | null;
|
||||
|
||||
defaultHomeData: {[key: string]: string};
|
||||
|
||||
urlHandler: URLHandler;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
showIntro: true,
|
||||
showUpdate: true,
|
||||
showAprilFools: false,
|
||||
currentTheme: null,
|
||||
};
|
||||
LocaleManager.initTranslations();
|
||||
this.navigatorRef = React.createRef();
|
||||
this.defaultHomeRoute = null;
|
||||
this.defaultHomeData = {};
|
||||
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
|
||||
this.urlHandler.listen();
|
||||
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
|
||||
this.loadAssetsAsync().finally(() => {
|
||||
this.onLoadFinished();
|
||||
});
|
||||
}
|
||||
|
||||
navigatorRef: { current: null | NavigationContainer };
|
||||
/**
|
||||
* The app has been started by an url, and it has been parsed.
|
||||
* Set a new default start route based on the data parsed.
|
||||
*
|
||||
* @param parsedData The data parsed from the url
|
||||
*/
|
||||
onInitialURLParsed = (parsedData: ParsedUrlDataType) => {
|
||||
this.defaultHomeRoute = parsedData.route;
|
||||
this.defaultHomeData = parsedData.data;
|
||||
};
|
||||
|
||||
defaultHomeRoute: string | null;
|
||||
defaultHomeData: { [key: string]: any }
|
||||
|
||||
createDrawerNavigator: () => React.Node;
|
||||
|
||||
urlHandler: URLHandler;
|
||||
storageManager: AsyncStorageManager;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
LocaleManager.initTranslations();
|
||||
this.navigatorRef = React.createRef();
|
||||
this.defaultHomeRoute = null;
|
||||
this.defaultHomeData = {};
|
||||
this.storageManager = AsyncStorageManager.getInstance();
|
||||
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
|
||||
this.urlHandler.listen();
|
||||
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
|
||||
this.loadAssetsAsync().then(() => {
|
||||
this.onLoadFinished();
|
||||
});
|
||||
/**
|
||||
* An url has been opened and parsed while the app was active.
|
||||
* Redirect the user to the screen according to parsed data.
|
||||
*
|
||||
* @param parsedData The data parsed from the url
|
||||
*/
|
||||
onDetectURL = (parsedData: ParsedUrlDataType) => {
|
||||
// Navigate to nested navigator and pass data to the index screen
|
||||
const nav = this.navigatorRef.current;
|
||||
if (nav != null) {
|
||||
nav.navigate('home', {
|
||||
screen: 'index',
|
||||
params: {nextScreen: parsedData.route, data: parsedData.data},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* THe app has been started by an url, and it has been parsed.
|
||||
* Set a new default start route based on the data parsed.
|
||||
*
|
||||
* @param parsedData The data parsed from the url
|
||||
*/
|
||||
onInitialURLParsed = (parsedData: { route: string, data: { [key: string]: any } }) => {
|
||||
this.defaultHomeRoute = parsedData.route;
|
||||
this.defaultHomeData = parsedData.data;
|
||||
};
|
||||
/**
|
||||
* Updates the current theme
|
||||
*/
|
||||
onUpdateTheme = () => {
|
||||
this.setState({
|
||||
currentTheme: ThemeManager.getCurrentTheme(),
|
||||
});
|
||||
setupStatusBar();
|
||||
};
|
||||
|
||||
/**
|
||||
* An url has been opened and parsed while the app was active.
|
||||
* Redirect the user to the screen according to parsed data.
|
||||
*
|
||||
* @param parsedData The data parsed from the url
|
||||
*/
|
||||
onDetectURL = (parsedData: { route: string, data: { [key: string]: any } }) => {
|
||||
// Navigate to nested navigator and pass data to the index screen
|
||||
if (this.navigatorRef.current != null) {
|
||||
this.navigatorRef.current.navigate('home', {
|
||||
screen: 'index',
|
||||
params: {nextScreen: parsedData.route, data: parsedData.data}
|
||||
});
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Callback when user ends the intro. Save in preferences to avoid showing back the introSlides
|
||||
*/
|
||||
onIntroDone = () => {
|
||||
this.setState({
|
||||
showIntro: false,
|
||||
showUpdate: false,
|
||||
showAprilFools: false,
|
||||
});
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.showIntro.key,
|
||||
false,
|
||||
);
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.updateNumber.key,
|
||||
Update.number,
|
||||
);
|
||||
AsyncStorageManager.set(
|
||||
AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key,
|
||||
false,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the current theme
|
||||
*/
|
||||
onUpdateTheme = () => {
|
||||
this.setState({
|
||||
currentTheme: ThemeManager.getCurrentTheme()
|
||||
});
|
||||
this.setupStatusBar();
|
||||
};
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* 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);
|
||||
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
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadAssetsAsync = async () => {
|
||||
await AsyncStorageManager.getInstance().loadPreferences();
|
||||
await ConnectionManager.getInstance()
|
||||
.recoverLogin()
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the app based on loading state
|
||||
*/
|
||||
render(): React.Node {
|
||||
const {state} = this;
|
||||
if (state.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when user ends the intro. Save in preferences to avoid showing back the introSlides
|
||||
*/
|
||||
onIntroDone = () => {
|
||||
this.setState({
|
||||
showIntro: false,
|
||||
showUpdate: false,
|
||||
showAprilFools: false,
|
||||
});
|
||||
this.storageManager.savePref(this.storageManager.preferences.showIntro.key, '0');
|
||||
this.storageManager.savePref(this.storageManager.preferences.updateNumber.key, Update.number.toString());
|
||||
this.storageManager.savePref(this.storageManager.preferences.showAprilFoolsStart.key, '0');
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads every async data
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
loadAssetsAsync = async () => {
|
||||
await this.storageManager.loadPreferences();
|
||||
try {
|
||||
await ConnectionManager.getInstance().recoverLogin();
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Async loading is done, finish processing startup data
|
||||
*/
|
||||
onLoadFinished() {
|
||||
// Only show intro if this is the first time starting the app
|
||||
this.createDrawerNavigator = () => <MainNavigator
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
/>;
|
||||
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
|
||||
// Status bar goes dark if set too fast on ios
|
||||
if (Platform.OS === 'ios')
|
||||
setTimeout(this.setupStatusBar, 1000);
|
||||
else
|
||||
this.setupStatusBar();
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
currentTheme: ThemeManager.getCurrentTheme(),
|
||||
showIntro: this.storageManager.preferences.showIntro.current === '1',
|
||||
showUpdate: this.storageManager.preferences.updateNumber.current !== Update.number.toString(),
|
||||
showAprilFools: AprilFoolsManager.getInstance().isAprilFoolsEnabled() && this.storageManager.preferences.showAprilFoolsStart.current === '1',
|
||||
});
|
||||
SplashScreen.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the app based on loading state
|
||||
*/
|
||||
render() {
|
||||
if (this.state.isLoading) {
|
||||
return null;
|
||||
} else if (this.state.showIntro || this.state.showUpdate || this.state.showAprilFools) {
|
||||
return <CustomIntroSlider
|
||||
onDone={this.onIntroDone}
|
||||
isUpdate={this.state.showUpdate && !this.state.showIntro}
|
||||
isAprilFools={this.state.showAprilFools && !this.state.showIntro}
|
||||
/>;
|
||||
} else {
|
||||
return (
|
||||
<PaperProvider theme={this.state.currentTheme}>
|
||||
<OverflowMenuProvider>
|
||||
<View style={{backgroundColor: ThemeManager.getCurrentTheme().colors.background, flex: 1}}>
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<NavigationContainer theme={this.state.currentTheme} ref={this.navigatorRef}>
|
||||
<MainNavigator
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</OverflowMenuProvider>
|
||||
</PaperProvider>
|
||||
);
|
||||
}
|
||||
if (state.showIntro || state.showUpdate || state.showAprilFools) {
|
||||
return (
|
||||
<CustomIntroSlider
|
||||
onDone={this.onIntroDone}
|
||||
isUpdate={state.showUpdate && !state.showIntro}
|
||||
isAprilFools={state.showAprilFools && !state.showIntro}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PaperProvider theme={state.currentTheme}>
|
||||
<OverflowMenuProvider>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: ThemeManager.getCurrentTheme().colors.background,
|
||||
flex: 1,
|
||||
}}>
|
||||
<SafeAreaView style={{flex: 1}}>
|
||||
<NavigationContainer
|
||||
theme={state.currentTheme}
|
||||
ref={this.navigatorRef}>
|
||||
<MainNavigator
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
/>
|
||||
</NavigationContainer>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</OverflowMenuProvider>
|
||||
</PaperProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
Pensez à garder l'appli à jour pour profiter des dernières fonctionnalités !
|
||||
|
||||
- **v3.1.4** - TBA
|
||||
- Correction d'un problème de connexion sur certains appareils android
|
||||
- Amélioration des traductions
|
||||
- Amélioration des informations données par proxiwash
|
||||
- Amélioration de la visibilité des services Amicale sans compte
|
||||
- Correction de bugs
|
||||
|
||||
- **v3.0.7** - 13/06/2020
|
||||
- Correction de crash au démarrage sur certains appareils
|
||||
- Mise à jour des écrans d'intro pour mieux refléter l'appli actuelle
|
||||
|
|
|
|||
123
INSTALL.md
|
|
@ -1,123 +0,0 @@
|
|||
# Installer l'application depuis ce dépot
|
||||
|
||||
**Vous allez devoir installer git, node et npm sur votre machine, puis cloner ce dépôt.**
|
||||
|
||||
Tout est expliqué dans ce guide, si vous avez un problème ou une question, merci de me contacter par mail : app@amicale-insat.fr
|
||||
|
||||
## Table des matières
|
||||
* [Installation de Git](#installation-de-git)
|
||||
* [Installation de node](#installation-de-node)
|
||||
* [Installation de React Native](#installation-de-react-native)
|
||||
* [Configuration de NPM](#configuration-de-npm)
|
||||
* [Installation](#installation)
|
||||
* [Téléchargement du dépot](#téléchargement-du-dépot)
|
||||
* [Téléchargement des dépendances](#téléchargement-des-dépendances)
|
||||
* [Lancement de l'appli](#lancement-de-lappli)
|
||||
* [Tester sur un appareil](#tester-sur-un-appareil)
|
||||
|
||||
## Installation de Git
|
||||
|
||||
Entrez la commande suivante pour l'installer :
|
||||
```shell script
|
||||
sudo apt install git
|
||||
```
|
||||
|
||||
## Installation de node
|
||||
|
||||
Vous devez avoir une version de node > 12.0.
|
||||
Pour cela, vérifiez avec la commande :
|
||||
```shell script
|
||||
nodejs -v
|
||||
```
|
||||
|
||||
Si ce n'est pas le cas, entrez les commandes suivantes pour installer la version 12 ([plus d'informations sur ce lien](https://github.com/nodesource/distributions/blob/master/README.md#debinstall)):
|
||||
|
||||
```shell script
|
||||
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
## Installation de React Native
|
||||
|
||||
Merci de suivre les [instructions d'installation](https://reactnative.dev/docs/environment-setup) sur le site officiel.
|
||||
|
||||
## Téléchargement du dépôt
|
||||
|
||||
Clonez ce dépôt à l'aide de la commande suivante :
|
||||
````shell script
|
||||
git clone https://git.etud.insa-toulouse.fr/vergnet/application-amicale.git
|
||||
````
|
||||
|
||||
## Téléchargement des dépendances
|
||||
|
||||
Une fois le dépôt sur votre machine, ouvrez le terminal dans le dossier du dépôt cloné et tapez :
|
||||
````shell script
|
||||
npm install
|
||||
````
|
||||
Ceci installera toutes les dépendances listées dans le fichier _package.json_. Cette opération peut prendre quelques minutes et utilisera beaucoup d'espace disque (plus de 300Mo).
|
||||
|
||||
### Instructions pour iOS
|
||||
|
||||
Pour iOS, en plus de la commande précédente, il faut aussi installer les dépendances iOS. Pour cela, allez dans le dossier `ios` et installez les pods :
|
||||
```shell script
|
||||
cd ios && pod install
|
||||
```
|
||||
|
||||
## Lancement de l'appli
|
||||
|
||||
Il est conseillé d'utiliser un logiciel comme **WebStorm** (logiciel pro gratuit pour les étudiants) pour éditer l'application car ce logiciel est compatible avec les technologies utilisées.
|
||||
|
||||
Vous aurez besoin de 2 consoles :
|
||||
* Une pour lancer le *Bundler*, qui permet de mettre à jour l'application en temps réel (vous pouvez le laisser tout le temps ouvert).
|
||||
* Une autre pour installer l'application sur votre appareil/simulateur.
|
||||
|
||||
Pour lancer le *Bundler*, assurez vous d'être dans le dossier de l'application, et lancez cette commande :
|
||||
````shell script
|
||||
npx react-native start
|
||||
````
|
||||
|
||||
### Android
|
||||
Dans la deuxième console, lancez la commande suivante :
|
||||
````shell script
|
||||
npx react-native run-android
|
||||
````
|
||||
|
||||
### iOS
|
||||
Dans la deuxième console, lancez la commande suivante (valable que sur Mac) :
|
||||
````shell script
|
||||
npx react-native run-ios
|
||||
````
|
||||
|
||||
**Ne stoppez pas le Metro Bundler dans la console à chaque changement !** Toutes les modifications sont appliquées automatiquement, pas besoin de stopper et de redémarrer pour des petits changements ! Il est seulement nécessaire de redémarrer le Metro Bundler quand vous changez des librairies ou des fichiers.
|
||||
|
||||
## Tester sur un appareil
|
||||
|
||||
Assurez vous d'avoir installé et lancé le projet comme expliqué plus haut.
|
||||
|
||||
### Android
|
||||
|
||||
#### Émulateur
|
||||
|
||||
[Suivez la procédure sur ce lien pour installer un émulateur](https://docs.expo.io/versions/latest/workflow/android-studio-emulator/).
|
||||
|
||||
Une fois l'emulateur installé et démarré, lancez l'application comme expliqué plus haut.
|
||||
|
||||
#### Appareil Physique
|
||||
|
||||
Branchez votre appareil, allez dans les options développeurs et activer le *USB Debugging*. Une fois qu'il est activé et branché, lancez l'appli comme expliqué plus haut.
|
||||
|
||||
### iOS
|
||||
|
||||
#### Émulateur
|
||||
|
||||
Installez le logiciel Xcode et téléchargez l'émulateur de votre choix. Ensuite, lancez la commande suivante pour lancer l'application sur votre émulateur.
|
||||
````shell script
|
||||
npx react-native run-ios --simulator="NOM DU SIMULATEUR"
|
||||
````
|
||||
En remplaçant `NOM DU SIMULATEUR` par le simulateur que vous voulez.
|
||||
|
||||
#### Appareil Physique
|
||||
|
||||
Aucune idée je suis pauvre je n'ai pas de Mac.
|
||||
|
||||
[reference]: ##Installation de Git
|
||||
92
README.md
|
|
@ -1,38 +1,82 @@
|
|||
# CAMPUS - Application pour l'Amicale
|
||||
<img src="https://etud.insa-toulouse.fr/~amicale_app/images/promo/Banner.png" alt="banner" width="500"/>
|
||||
|
||||
Créée pendant l'été 2019, cette application compatible Android et iOS permet aux étudiants d'avoir un accès facile aux informations du campus :
|
||||
- News de l'amicale
|
||||
- État des machines à laver
|
||||
[<img src="https://etud.insa-toulouse.fr/~amicale_app/images/promo/app-store-badge.png" alt="app-store" width="150"/>](https://apps.apple.com/us/app/id1477722148)
|
||||
[<img src="https://etud.insa-toulouse.fr/~amicale_app/images/promo/google-play-badge.png" alt="google-play" width="150"/>](https://play.google.com/store/apps/details?id=fr.amicaleinsat.application)
|
||||
|
||||
Projet démarré pendant l'été 2019 par Arnaud Vergnet (alors en 3MIC), cette application compatible Android et iOS permet aux étudiants d'avoir un accès facile aux informations du campus :
|
||||
- Connexion à son compte Amicale
|
||||
- Liste des événements sur le campus
|
||||
- Stock du Proximo
|
||||
- État des machines à laver
|
||||
- Emploi du temps
|
||||
- Menu du RU
|
||||
- Disponibilité des salles libre accès
|
||||
- Réservation des Bib'Box
|
||||
|
||||
Ce dépot contient la source de cette application, sous licence GPLv3.
|
||||
|
||||
## Contribuer
|
||||
...et bien d'autres services
|
||||
|
||||
Vous voulez influencer le développement ? C'est très simple !
|
||||
Pour la source du serveur utilisé pour synchroniser les informations, merci de voir [ce dépôt](https://git.etud.insa-toulouse.fr/vergnet/application-amicale-serveur).
|
||||
|
||||
Pas besoin de connaissance, il est possible d'aider simplement en proposant des améliorations ou en rapportant des bugs par mail ([app@amicale-insat.fr](mailto:app@amicale-insat.fr)) ou sur [cette page](https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues), en vous connectant avec vos login INSA.
|
||||
# 🔎 Besoin de mainteneur
|
||||
|
||||
Si vous avez assez de connaissances et vous souhaitez proposer des modifications dans le code, [installez l'application](INSTALL.md) sur votre machine, réalisez votre modification et créez une 'pull request'. Si vous avez des problèmes ou des questions, n'hésitez pas à me contacter par mail ([app@amicale-insat.fr](mailto:app@amicale-insat.fr)).
|
||||
Ce projet a été réalisé en grande partie par **un seul étudiant**, mais cet étudiant (coucou c'est moi) ne va pas rester éternellement à l'INSA. **Il faut donc une relève !** Le projet étant stable, le minimum est de corriger les bugs rencontrés.
|
||||
|
||||
## Technologies Utilisées
|
||||
Tout le monde peut contribuer, mais **il faut tout de même au moins une personne pour gouverner le projet** : accepter les modifications, compiler et mettre en ligne sur les magasins. Pas besoin d'énormément de connaissance, seulement de la motivation.
|
||||
|
||||
Le tout, bien sûr, permet de valoriser une implication citoyenne 😉.
|
||||
|
||||
# 🚀 Contribuer
|
||||
|
||||
**Tu veux influencer le développement ? C'est très simple !**
|
||||
|
||||
#### 🙃 Aucune connaissance ?
|
||||
Pas de problème ! Tu peux aider simplement en proposant des améliorations ou en rapportant des bugs par mail ([app@amicale-insat.fr](mailto:app@amicale-insat.fr)), ou sur [cette page](https://git.etud.insa-toulouse.fr/vergnet/application-amicale/issues) en te connectant avec tes login INSA.
|
||||
|
||||
#### 🌍 Bilingue ou plus ?
|
||||
|
||||
Tu peux aider à traduire l'application ! Le projet existe en français et anglais (mais il peut y avoir des fautes !), et tout autre langue est la bienvenue.
|
||||
|
||||
Si tu es intéressé, rends-toi sur [cette page](doc/TRANSLATE.md) pour plus de détails.
|
||||
|
||||
#### 🤓 Développeur dans l'âme ?
|
||||
Peu importe ton niveau, toutes les propositions de modification sont les bienvenues ! (enfin presque)
|
||||
|
||||
Pour cela, [suis ce guide](doc/CONTRIBUTE.md).
|
||||
|
||||
#### 🤯 Motivé mais perdu ?
|
||||
Tu es quand même le bienvenu ! Tu trouveras [une liste de liens](doc/LINKS.md) pour t'aider à comprendre les technologies utilisées dans ce projet. Si tu as plus de questions, tu peux toujours me contacter par mail ([app@amicale-insat.fr](mailto:app@amicale-insat.fr)).
|
||||
|
||||
## 👨💻 Technologies Utilisées
|
||||
Cette application est faite en JavaScript avec React Native (framework Open Source créé par Facebook).
|
||||
|
||||
React Native permet de n'avoir qu'un seul code JavaScript à écrire pour Android et iOS. Pour compiler pour la plateforme souhaitée, il suffit d'effectuer une simple commande. Plus besoin de Mac pour développer une application iOS ! (Mais toujours besoin d'un pour compiler et publier sur l'App store...)
|
||||
|
||||
Cette application utilisait initialement Expo, permettant de simplifier grandement le développement et le déploiement, mais il a été abandonné à cause de ses limitations et de son impact sur les performances. Revenir sur Expo n'est pas possible sans un gros travail et une suppression de fonctionnalités non compatibles.
|
||||
Tu trouveras [une liste de liens utiles](doc/LINKS.md) pour retrouver toutes les infos !
|
||||
|
||||
## [Installer l'application depuis ce dépot](INSTALL.md)
|
||||
# 💾 [Installer l'application sur ton téléphone depuis ce dépot](doc/INSTALL.md)
|
||||
|
||||
## Liens utiles
|
||||
* [Documentation React Native](https://reactnative.dev/docs/getting-started)
|
||||
* [Documentation Expo](https://docs.expo.io/versions/latest/)
|
||||
* [Documentation React Native Paper](https://callstack.github.io/react-native-paper/)
|
||||
* [Documentation React navigation](https://reactnavigation.org/docs/getting-started)
|
||||
* [Documentation Jest](https://jestjs.io/docs/en/getting-started)
|
||||
* [Documentation Flow](https://flow.org/en/docs/react/)
|
||||
# 📔️ [Notes de changement](Changelog.md)
|
||||
|
||||
# 🗒️ [Notes sur l'état actuel du projet](doc/NOTES.md)
|
||||
|
||||
# 🔗 [Liens Utiles](doc/LINKS.md)
|
||||
|
||||
# 🤝 Contributeurs
|
||||
|
||||
| <img src="https://secure.gravatar.com/avatar/8e33a1b2cedf7168e8468a1522cc8c56?d=identicon&s=290" alt="app-store" width="150"/> | <img src="https://secure.gravatar.com/avatar/9792c3643f98cddbc2a42e05422fe66e?d=identicon&s=290" alt="app-store" width="150"/> | ❔ |
|
||||
|--------------------------------|--------------------------------|-------------------------------------------|
|
||||
| **Arnaud Vergnet** | **Yohan Simard** | **Toi ?** |
|
||||
| Créateur et mainteneur actuel | Correction de quelques bugs | [Contribue pour faire vivre le projet !](doc/CONTRIBUTE.md) |
|
||||
|
||||
# 👏 Remerciements
|
||||
|
||||
* **Béranger Quintana Y Arciosana** : Étudiant en AE (2020) et Président de l'Amicale au moment de la création et du lancement du projet. L'application, c'était son idée. Il a beaucoup aidé pour trouver des bugs, de nouvelles fonctionnalités et faire de la com.
|
||||
* **Céline Tassin** : Étudiante en GPE (2020). Sans elle, tout serait moins mignon. Elle a aidé pour écrire le texte, faire de la com, et aussi à créer la mascotte 🦊.
|
||||
* **Damien Molina** : Étudiant en IR (2020) et créateur de la dernière version du [site de l'Amicale](https://amicale-insat.fr/). Grâce à son aide, intégrer les services de l'Amicale à l'application a été très simple.
|
||||
* **Titouan Labourdette** : Étudiant en IR (2020). Il a beaucoup aidé pour trouver des bugs et proposer des nouvelles fonctionnalités.
|
||||
* **Théo Tami** : Étudiant en AE (2020). Si l'application marche sur iOS, c'est grâce à son aide lors de ses nombreux tests.
|
||||
|
||||
# 📄 Licence
|
||||
|
||||
L'application est **Open Source** sous licence **GPLv3**.
|
||||
|
||||
# 🔐 Copyright
|
||||
Apple and Apple Logo are trademarks of Apple Inc.
|
||||
|
||||
Google Play et le logo Google Play sont des marques de Google LLC.
|
||||
|
|
|
|||
|
|
@ -1,210 +1,217 @@
|
|||
jest.mock('react-native-keychain');
|
||||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import ConnectionManager from "../../src/managers/ConnectionManager";
|
||||
import {ERROR_TYPE} from "../../src/utils/WebData";
|
||||
import ConnectionManager from '../../src/managers/ConnectionManager';
|
||||
import {ERROR_TYPE} from '../../src/utils/WebData';
|
||||
|
||||
let fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||
jest.mock('react-native-keychain');
|
||||
|
||||
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
|
||||
|
||||
const c = ConnectionManager.getInstance();
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('isLoggedIn yes', () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
return expect(c.isLoggedIn()).toBe(true);
|
||||
return expect(c.isLoggedIn()).toBe(true);
|
||||
});
|
||||
|
||||
test('isLoggedIn no', () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return null;
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
return expect(c.isLoggedIn()).toBe(false);
|
||||
return expect(c.isLoggedIn()).toBe(false);
|
||||
});
|
||||
|
||||
test("isConnectionResponseValid", () => {
|
||||
let json = {
|
||||
error: 0,
|
||||
data: {token: 'token'}
|
||||
};
|
||||
expect(c.isConnectionResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 2,
|
||||
data: {}
|
||||
};
|
||||
expect(c.isConnectionResponseValid(json)).toBeTrue();
|
||||
json = {
|
||||
error: 0,
|
||||
data: {token: ''}
|
||||
};
|
||||
expect(c.isConnectionResponseValid(json)).toBeFalse();
|
||||
json = {
|
||||
error: 'prout',
|
||||
data: {token: ''}
|
||||
};
|
||||
expect(c.isConnectionResponseValid(json)).toBeFalse();
|
||||
test('connect bad credentials', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.BAD_CREDENTIALS,
|
||||
data: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.BAD_CREDENTIALS,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect bad credentials", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.BAD_CREDENTIALS,
|
||||
data: {}
|
||||
};
|
||||
},
|
||||
})
|
||||
test('connect good credentials', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'},
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.connect('email', 'password'))
|
||||
.rejects.toBe(ERROR_TYPE.BAD_CREDENTIALS);
|
||||
});
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'saveLogin')
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).resolves.toBe(undefined);
|
||||
});
|
||||
|
||||
test("connect good credentials", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'}
|
||||
};
|
||||
},
|
||||
})
|
||||
test('connect good credentials no consent', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.NO_CONSENT,
|
||||
data: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
jest.spyOn(ConnectionManager.prototype, 'saveLogin').mockImplementationOnce(() => {
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).resolves.toBeTruthy();
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.NO_CONSENT,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect good credentials no consent", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.NO_CONSENT,
|
||||
data: {}
|
||||
};
|
||||
},
|
||||
})
|
||||
test('connect good credentials, fail save token', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'},
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.connect('email', 'password'))
|
||||
.rejects.toBe(ERROR_TYPE.NO_CONSENT);
|
||||
});
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'saveLogin')
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.reject(false);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.TOKEN_SAVE,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect good credentials, fail save token", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {token: 'token'}
|
||||
};
|
||||
},
|
||||
})
|
||||
});
|
||||
jest.spyOn(ConnectionManager.prototype, 'saveLogin').mockImplementationOnce(() => {
|
||||
return Promise.reject(false);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(ERROR_TYPE.UNKNOWN);
|
||||
test('connect connection error', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.reject();
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.CONNECTION_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect connection error", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.reject();
|
||||
test('connect bogus response 1', () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
thing: true,
|
||||
wrong: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.connect('email', 'password'))
|
||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
});
|
||||
return expect(c.connect('email', 'password')).rejects.toBe(
|
||||
ERROR_TYPE.SERVER_ERROR,
|
||||
);
|
||||
});
|
||||
|
||||
test("connect bogus response 1", () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
thing: true,
|
||||
wrong: '',
|
||||
}
|
||||
},
|
||||
})
|
||||
test('authenticatedRequest success', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
return expect(c.connect('email', 'password'))
|
||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {coucou: 'toi'},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).resolves.toStrictEqual({coucou: 'toi'});
|
||||
});
|
||||
|
||||
|
||||
test("authenticatedRequest success", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
test('authenticatedRequest error wrong token', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
data: {coucou: 'toi'}
|
||||
};
|
||||
},
|
||||
})
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.BAD_TOKEN,
|
||||
data: {},
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.resolves.toStrictEqual({coucou: 'toi'});
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).rejects.toBe(ERROR_TYPE.BAD_TOKEN);
|
||||
});
|
||||
|
||||
test("authenticatedRequest error wrong token", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
test('authenticatedRequest error bogus response', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.BAD_TOKEN,
|
||||
data: {}
|
||||
};
|
||||
},
|
||||
})
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
};
|
||||
},
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.rejects.toBe(ERROR_TYPE.BAD_TOKEN);
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).rejects.toBe(ERROR_TYPE.SERVER_ERROR);
|
||||
});
|
||||
|
||||
test("authenticatedRequest error bogus response", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
test('authenticatedRequest connection error', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
});
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.resolve({
|
||||
json: () => {
|
||||
return {
|
||||
error: ERROR_TYPE.SUCCESS,
|
||||
};
|
||||
},
|
||||
})
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.reject();
|
||||
});
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
});
|
||||
|
||||
test("authenticatedRequest connection error", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return 'token';
|
||||
test('authenticatedRequest error no token', () => {
|
||||
jest
|
||||
.spyOn(ConnectionManager.prototype, 'getToken')
|
||||
.mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
|
||||
return Promise.reject()
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
|
||||
});
|
||||
|
||||
test("authenticatedRequest error no token", () => {
|
||||
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
return expect(c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'))
|
||||
.rejects.toBe(ERROR_TYPE.UNKNOWN);
|
||||
return expect(
|
||||
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
|
||||
).rejects.toBe(ERROR_TYPE.TOKEN_RETRIEVE);
|
||||
});
|
||||
|
|
|
|||
345
__tests__/utils/EquipmentBooking.test.js
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import * as EquipmentBooking from '../../src/utils/EquipmentBooking';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
test('getISODate', () => {
|
||||
let date = new Date('2020-03-05 12:00');
|
||||
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05');
|
||||
date = new Date('2020-03-05');
|
||||
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-05');
|
||||
date = new Date('2020-03-05 00:00'); // Treated as local time
|
||||
expect(EquipmentBooking.getISODate(date)).toBe('2020-03-04'); // Treated as UTC
|
||||
});
|
||||
|
||||
test('getCurrentDay', () => {
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14 14:50:35').getTime());
|
||||
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(
|
||||
new Date('2020-01-14').getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
test('isEquipmentAvailable', () => {
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
};
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-09', end: '2020-07-10'}];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-8'},
|
||||
{begin: '2020-07-10', end: '2020-07-12'},
|
||||
];
|
||||
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
|
||||
});
|
||||
|
||||
test('getFirstEquipmentAvailability', () => {
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-07-09').getTime());
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).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 = [
|
||||
{begin: '2020-07-07', end: '2020-07-09'},
|
||||
{begin: '2020-07-10', end: '2020-07-16'},
|
||||
];
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
).toBe(new Date('2020-07-17').getTime());
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-09'},
|
||||
{begin: '2020-07-10', end: '2020-07-12'},
|
||||
{begin: '2020-07-14', end: '2020-07-16'},
|
||||
];
|
||||
expect(
|
||||
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
|
||||
).toBe(new Date('2020-07-13').getTime());
|
||||
});
|
||||
|
||||
test('getRelativeDateString', () => {
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.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',
|
||||
);
|
||||
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', () => {
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
};
|
||||
let start = new Date('2020-07-11');
|
||||
let end = new Date('2020-07-15');
|
||||
let result = [
|
||||
'2020-07-11',
|
||||
'2020-07-12',
|
||||
'2020-07-13',
|
||||
'2020-07-14',
|
||||
'2020-07-15',
|
||||
];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-07', end: '2020-07-10'},
|
||||
{begin: '2020-07-13', end: '2020-07-15'},
|
||||
];
|
||||
result = ['2020-07-11', '2020-07-12'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
|
||||
result = ['2020-07-11'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-12'}];
|
||||
result = ['2020-07-13', '2020-07-14', '2020-07-15'];
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
start = new Date('2020-07-14');
|
||||
end = new Date('2020-07-14');
|
||||
result = ['2020-07-14'];
|
||||
expect(
|
||||
EquipmentBooking.getValidRange(start, start, testDevice),
|
||||
).toStrictEqual(result);
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
|
||||
start = new Date('2020-07-14');
|
||||
end = new Date('2020-07-17');
|
||||
result = ['2020-07-14', '2020-07-15', '2020-07-16', '2020-07-17'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-17', end: '2020-07-17'}];
|
||||
result = ['2020-07-14', '2020-07-15', '2020-07-16'];
|
||||
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-12', end: '2020-07-13'},
|
||||
{begin: '2020-07-15', end: '2020-07-20'},
|
||||
];
|
||||
start = new Date('2020-07-11');
|
||||
end = new Date('2020-07-23');
|
||||
result = ['2020-07-21', '2020-07-22', '2020-07-23'];
|
||||
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
|
||||
result,
|
||||
);
|
||||
});
|
||||
|
||||
test('generateMarkedDates', () => {
|
||||
let theme = {
|
||||
colors: {
|
||||
primary: 'primary',
|
||||
danger: 'primary',
|
||||
textDisabled: 'primary',
|
||||
},
|
||||
};
|
||||
let testDevice = {
|
||||
id: 1,
|
||||
name: 'Petit barbecue',
|
||||
caution: 100,
|
||||
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
|
||||
};
|
||||
let start = new Date('2020-07-11');
|
||||
let end = new Date('2020-07-13');
|
||||
let range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
let result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
'2020-07-12': {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.danger,
|
||||
},
|
||||
'2020-07-13': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
'2020-07-12': {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
'2020-07-13': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
};
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(false, theme, range),
|
||||
).toStrictEqual(result);
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
'2020-07-12': {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
'2020-07-13': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.textDisabled,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(false, theme, range),
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-13', end: '2020-07-15'}];
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
'2020-07-12': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
|
||||
testDevice.booked_at = [
|
||||
{begin: '2020-07-12', end: '2020-07-13'},
|
||||
{begin: '2020-07-15', end: '2020-07-20'},
|
||||
];
|
||||
start = new Date('2020-07-11');
|
||||
end = new Date('2020-07-23');
|
||||
result = {
|
||||
'2020-07-11': {
|
||||
startingDay: true,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(start, end, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
|
||||
result = {
|
||||
'2020-07-21': {
|
||||
startingDay: true,
|
||||
endingDay: false,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
'2020-07-22': {
|
||||
startingDay: false,
|
||||
endingDay: false,
|
||||
color: theme.colors.danger,
|
||||
},
|
||||
'2020-07-23': {
|
||||
startingDay: false,
|
||||
endingDay: true,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
};
|
||||
range = EquipmentBooking.getValidRange(end, start, testDevice);
|
||||
expect(
|
||||
EquipmentBooking.generateMarkedDates(true, theme, range),
|
||||
).toStrictEqual(result);
|
||||
});
|
||||
|
|
@ -1,210 +1,222 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import * as Planning from "../../src/utils/Planning";
|
||||
import * as Planning from '../../src/utils/Planning';
|
||||
|
||||
test('isDescriptionEmpty', () => {
|
||||
expect(Planning.isDescriptionEmpty("")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(" ")).toBeTrue();
|
||||
// noinspection CheckTagEmptyBody
|
||||
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><p><br></p>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("<p><br><br><br></p>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("<p><br>")).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(null)).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty("coucou")).toBeFalse();
|
||||
expect(Planning.isDescriptionEmpty("<p>coucou</p>")).toBeFalse();
|
||||
expect(Planning.isDescriptionEmpty('')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(' ')).toBeTrue();
|
||||
// noinspection CheckTagEmptyBody
|
||||
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><p><br></p>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('<p><br><br><br></p>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('<p><br>')).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(null)).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty(undefined)).toBeTrue();
|
||||
expect(Planning.isDescriptionEmpty('coucou')).toBeFalse();
|
||||
expect(Planning.isDescriptionEmpty('<p>coucou</p>')).toBeFalse();
|
||||
});
|
||||
|
||||
test('isEventDateStringFormatValid', () => {
|
||||
expect(Planning.isEventDateStringFormatValid("2020-03-21 09:00")).toBeTrue();
|
||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 01:16")).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:00")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("3214-64-12 1:16")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("3214-f4-12 01:16")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("sqdd 09:00")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("2020-03-21")).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid("2020-03-21 truc")).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(null)).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventDateStringFormatValid('3214-64-12 01:16:00'),
|
||||
).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('3214-64-12 1:16')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('3214-f4-12 01:16')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('sqdd 09:00')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('2020-03-21')).toBeFalse();
|
||||
expect(Planning.isEventDateStringFormatValid('2020-03-21 truc')).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(null)).toBeFalse();
|
||||
});
|
||||
|
||||
test('stringToDate', () => {
|
||||
let testDate = new Date();
|
||||
expect(Planning.stringToDate(undefined)).toBeNull();
|
||||
expect(Planning.stringToDate("")).toBeNull();
|
||||
expect(Planning.stringToDate("garbage")).toBeNull();
|
||||
expect(Planning.stringToDate("2020-03-21")).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();
|
||||
testDate.setFullYear(2020, 2, 21);
|
||||
testDate.setHours(9, 0, 0, 0);
|
||||
expect(Planning.stringToDate("2020-03-21 09:00")).toEqual(testDate);
|
||||
testDate.setFullYear(2020, 0, 31);
|
||||
testDate.setHours(18, 30, 0, 0);
|
||||
expect(Planning.stringToDate("2020-01-31 18:30")).toEqual(testDate);
|
||||
testDate.setFullYear(2020, 50, 50);
|
||||
testDate.setHours(65, 65, 0, 0);
|
||||
expect(Planning.stringToDate("2020-51-50 65:65")).toEqual(testDate);
|
||||
let testDate = new Date();
|
||||
expect(Planning.stringToDate(undefined)).toBeNull();
|
||||
expect(Planning.stringToDate('')).toBeNull();
|
||||
expect(Planning.stringToDate('garbage')).toBeNull();
|
||||
expect(Planning.stringToDate('2020-03-21')).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();
|
||||
testDate.setFullYear(2020, 2, 21);
|
||||
testDate.setHours(9, 0, 0, 0);
|
||||
expect(Planning.stringToDate('2020-03-21 09:00')).toEqual(testDate);
|
||||
testDate.setFullYear(2020, 0, 31);
|
||||
testDate.setHours(18, 30, 0, 0);
|
||||
expect(Planning.stringToDate('2020-01-31 18:30')).toEqual(testDate);
|
||||
testDate.setFullYear(2020, 50, 50);
|
||||
testDate.setHours(65, 65, 0, 0);
|
||||
expect(Planning.stringToDate('2020-51-50 65:65')).toEqual(testDate);
|
||||
});
|
||||
|
||||
test('getFormattedEventTime', () => {
|
||||
expect(Planning.getFormattedEventTime(null, null))
|
||||
.toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime(undefined, undefined))
|
||||
.toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime("20:30", "23:00"))
|
||||
.toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime("2020-03-30", "2020-03-31"))
|
||||
.toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime(null, null)).toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime(undefined, undefined)).toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime('20:30', '23:00')).toBe('/ - /');
|
||||
expect(Planning.getFormattedEventTime('2020-03-30', '2020-03-31')).toBe(
|
||||
'/ - /',
|
||||
);
|
||||
|
||||
|
||||
expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-21 09:00"))
|
||||
.toBe('09:00');
|
||||
expect(Planning.getFormattedEventTime("2020-03-21 09:00", "2020-03-22 17:00"))
|
||||
.toBe('09:00 - 23:59');
|
||||
expect(Planning.getFormattedEventTime("2020-03-30 20:30", "2020-03-30 23:00"))
|
||||
.toBe('20:30 - 23:00');
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-21 09:00'),
|
||||
).toBe('09:00');
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-21 09:00', '2020-03-22 17:00'),
|
||||
).toBe('09:00 - 23:59');
|
||||
expect(
|
||||
Planning.getFormattedEventTime('2020-03-30 20:30', '2020-03-30 23:00'),
|
||||
).toBe('20:30 - 23:00');
|
||||
});
|
||||
|
||||
test('getDateOnlyString', () => {
|
||||
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-o5 09:00")).toBeNull();
|
||||
expect(Planning.getDateOnlyString("2021-12-15 09:")).toBeNull();
|
||||
expect(Planning.getDateOnlyString("2021-12-15")).toBeNull();
|
||||
expect(Planning.getDateOnlyString("garbage")).toBeNull();
|
||||
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-o5 09:00')).toBeNull();
|
||||
expect(Planning.getDateOnlyString('2021-12-15 09:')).toBeNull();
|
||||
expect(Planning.getDateOnlyString('2021-12-15')).toBeNull();
|
||||
expect(Planning.getDateOnlyString('garbage')).toBeNull();
|
||||
});
|
||||
|
||||
test('isEventBefore', () => {
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 09:00", "2020-03-21 10:00")).toBeTrue();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:00", "2020-03-21 10:15")).toBeTrue();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:15", "2021-03-21 10:15")).toBeTrue();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:15", "2020-05-21 10:15")).toBeTrue();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:15", "2020-03-30 10:15")).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00'),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:15'),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2021-03-21 10:15'),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-05-21 10:15'),
|
||||
).toBeTrue();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-03-30 10:15'),
|
||||
).toBeTrue();
|
||||
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:00", "2020-03-21 10:00")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:00", "2020-03-21 09:00")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-21 10:15", "2020-03-21 10:00")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2021-03-21 10:15", "2020-03-21 10:15")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-05-21 10:15", "2020-03-21 10:15")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
"2020-03-30 10:15", "2020-03-21 10:15")).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 10:00'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:00', '2020-03-21 09:00'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-21 10:15', '2020-03-21 10:00'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2021-03-21 10:15', '2020-03-21 10:15'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-05-21 10:15', '2020-03-21 10:15'),
|
||||
).toBeFalse();
|
||||
expect(
|
||||
Planning.isEventBefore('2020-03-30 10:15', '2020-03-21 10:15'),
|
||||
).toBeFalse();
|
||||
|
||||
expect(Planning.isEventBefore(
|
||||
"garbage", "2020-03-21 10:15")).toBeFalse();
|
||||
expect(Planning.isEventBefore(
|
||||
undefined, undefined)).toBeFalse();
|
||||
expect(Planning.isEventBefore('garbage', '2020-03-21 10:15')).toBeFalse();
|
||||
expect(Planning.isEventBefore(undefined, undefined)).toBeFalse();
|
||||
});
|
||||
|
||||
test('dateToString', () => {
|
||||
let testDate = new Date();
|
||||
testDate.setFullYear(2020, 2, 21);
|
||||
testDate.setHours(9, 0, 0, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe("2020-03-21 09:00");
|
||||
testDate.setFullYear(2021, 0, 12);
|
||||
testDate.setHours(9, 10, 0, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe("2021-01-12 09:10");
|
||||
testDate.setFullYear(2022, 11, 31);
|
||||
testDate.setHours(9, 10, 15, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe("2022-12-31 09:10");
|
||||
let testDate = new Date();
|
||||
testDate.setFullYear(2020, 2, 21);
|
||||
testDate.setHours(9, 0, 0, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe('2020-03-21 09:00');
|
||||
testDate.setFullYear(2021, 0, 12);
|
||||
testDate.setHours(9, 10, 0, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe('2021-01-12 09:10');
|
||||
testDate.setFullYear(2022, 11, 31);
|
||||
testDate.setHours(9, 10, 15, 0);
|
||||
expect(Planning.dateToString(testDate)).toBe('2022-12-31 09:10');
|
||||
});
|
||||
|
||||
test('generateEmptyCalendar', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14T00:00:00.000Z').getTime()
|
||||
);
|
||||
let calendar = Planning.generateEmptyCalendar(1);
|
||||
expect(calendar).toHaveProperty("2020-01-14");
|
||||
expect(calendar).toHaveProperty("2020-01-20");
|
||||
expect(calendar).toHaveProperty("2020-02-10");
|
||||
expect(Object.keys(calendar).length).toBe(32);
|
||||
calendar = Planning.generateEmptyCalendar(3);
|
||||
expect(calendar).toHaveProperty("2020-01-14");
|
||||
expect(calendar).toHaveProperty("2020-01-20");
|
||||
expect(calendar).toHaveProperty("2020-02-10");
|
||||
expect(calendar).toHaveProperty("2020-02-14");
|
||||
expect(calendar).toHaveProperty("2020-03-20");
|
||||
expect(calendar).toHaveProperty("2020-04-12");
|
||||
expect(Object.keys(calendar).length).toBe(92);
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
|
||||
let calendar = Planning.generateEmptyCalendar(1);
|
||||
expect(calendar).toHaveProperty('2020-01-14');
|
||||
expect(calendar).toHaveProperty('2020-01-20');
|
||||
expect(calendar).toHaveProperty('2020-02-10');
|
||||
expect(Object.keys(calendar).length).toBe(32);
|
||||
calendar = Planning.generateEmptyCalendar(3);
|
||||
expect(calendar).toHaveProperty('2020-01-14');
|
||||
expect(calendar).toHaveProperty('2020-01-20');
|
||||
expect(calendar).toHaveProperty('2020-02-10');
|
||||
expect(calendar).toHaveProperty('2020-02-14');
|
||||
expect(calendar).toHaveProperty('2020-03-20');
|
||||
expect(calendar).toHaveProperty('2020-04-12');
|
||||
expect(Object.keys(calendar).length).toBe(92);
|
||||
});
|
||||
|
||||
test('pushEventInOrder', () => {
|
||||
let eventArray = [];
|
||||
let event1 = {date_begin: "2020-01-14 09:15"};
|
||||
Planning.pushEventInOrder(eventArray, event1);
|
||||
expect(eventArray.length).toBe(1);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
let eventArray = [];
|
||||
let event1 = {date_begin: '2020-01-14 09:15'};
|
||||
Planning.pushEventInOrder(eventArray, event1);
|
||||
expect(eventArray.length).toBe(1);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
|
||||
let event2 = {date_begin: "2020-01-14 10:15"};
|
||||
Planning.pushEventInOrder(eventArray, event2);
|
||||
expect(eventArray.length).toBe(2);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
let event2 = {date_begin: '2020-01-14 10:15'};
|
||||
Planning.pushEventInOrder(eventArray, event2);
|
||||
expect(eventArray.length).toBe(2);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
|
||||
let event3 = {date_begin: "2020-01-14 10:15", title: "garbage"};
|
||||
Planning.pushEventInOrder(eventArray, event3);
|
||||
expect(eventArray.length).toBe(3);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
expect(eventArray[2]).toBe(event3);
|
||||
let event3 = {date_begin: '2020-01-14 10:15', title: 'garbage'};
|
||||
Planning.pushEventInOrder(eventArray, event3);
|
||||
expect(eventArray.length).toBe(3);
|
||||
expect(eventArray[0]).toBe(event1);
|
||||
expect(eventArray[1]).toBe(event2);
|
||||
expect(eventArray[2]).toBe(event3);
|
||||
|
||||
let event4 = {date_begin: "2020-01-13 09:00"};
|
||||
Planning.pushEventInOrder(eventArray, event4);
|
||||
expect(eventArray.length).toBe(4);
|
||||
expect(eventArray[0]).toBe(event4);
|
||||
expect(eventArray[1]).toBe(event1);
|
||||
expect(eventArray[2]).toBe(event2);
|
||||
expect(eventArray[3]).toBe(event3);
|
||||
let event4 = {date_begin: '2020-01-13 09:00'};
|
||||
Planning.pushEventInOrder(eventArray, event4);
|
||||
expect(eventArray.length).toBe(4);
|
||||
expect(eventArray[0]).toBe(event4);
|
||||
expect(eventArray[1]).toBe(event1);
|
||||
expect(eventArray[2]).toBe(event2);
|
||||
expect(eventArray[3]).toBe(event3);
|
||||
});
|
||||
|
||||
test('generateEventAgenda', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14T00:00:00.000Z').getTime()
|
||||
);
|
||||
let eventList = [
|
||||
{date_begin: "2020-01-14 09:15"},
|
||||
{date_begin: "2020-02-01 09:15"},
|
||||
{date_begin: "2020-01-15 09:15"},
|
||||
{date_begin: "2020-02-01 09:30"},
|
||||
{date_begin: "2020-02-01 08:30"},
|
||||
];
|
||||
const calendar = Planning.generateEventAgenda(eventList, 2);
|
||||
expect(calendar["2020-01-14"].length).toBe(1);
|
||||
expect(calendar["2020-01-14"][0]).toBe(eventList[0]);
|
||||
expect(calendar["2020-01-15"].length).toBe(1);
|
||||
expect(calendar["2020-01-15"][0]).toBe(eventList[2]);
|
||||
expect(calendar["2020-02-01"].length).toBe(3);
|
||||
expect(calendar["2020-02-01"][0]).toBe(eventList[4]);
|
||||
expect(calendar["2020-02-01"][1]).toBe(eventList[1]);
|
||||
expect(calendar["2020-02-01"][2]).toBe(eventList[3]);
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T00:00:00.000Z').getTime());
|
||||
let eventList = [
|
||||
{date_begin: '2020-01-14 09:15'},
|
||||
{date_begin: '2020-02-01 09:15'},
|
||||
{date_begin: '2020-01-15 09:15'},
|
||||
{date_begin: '2020-02-01 09:30'},
|
||||
{date_begin: '2020-02-01 08:30'},
|
||||
];
|
||||
const calendar = Planning.generateEventAgenda(eventList, 2);
|
||||
expect(calendar['2020-01-14'].length).toBe(1);
|
||||
expect(calendar['2020-01-14'][0]).toBe(eventList[0]);
|
||||
expect(calendar['2020-01-15'].length).toBe(1);
|
||||
expect(calendar['2020-01-15'][0]).toBe(eventList[2]);
|
||||
expect(calendar['2020-02-01'].length).toBe(3);
|
||||
expect(calendar['2020-02-01'][0]).toBe(eventList[4]);
|
||||
expect(calendar['2020-02-01'][1]).toBe(eventList[1]);
|
||||
expect(calendar['2020-02-01'][2]).toBe(eventList[3]);
|
||||
});
|
||||
|
||||
test('getCurrentDateString', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() => {
|
||||
let date = new Date();
|
||||
date.setFullYear(2020, 0, 14);
|
||||
date.setHours(15, 30, 54, 65);
|
||||
return date.getTime();
|
||||
});
|
||||
expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30');
|
||||
jest.spyOn(Date, 'now').mockImplementation(() => {
|
||||
let date = new Date();
|
||||
date.setFullYear(2020, 0, 14);
|
||||
date.setHours(15, 30, 54, 65);
|
||||
return date.getTime();
|
||||
});
|
||||
expect(Planning.getCurrentDateString()).toBe('2020-01-14 15:30');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,142 +1,167 @@
|
|||
/* eslint-disable */
|
||||
|
||||
import React from 'react';
|
||||
import {getCleanedMachineWatched, getMachineEndDate, getMachineOfId, isMachineWatched} from "../../src/utils/Proxiwash";
|
||||
import {
|
||||
getCleanedMachineWatched,
|
||||
getMachineEndDate,
|
||||
getMachineOfId,
|
||||
isMachineWatched,
|
||||
} from '../../src/utils/Proxiwash';
|
||||
|
||||
test('getMachineEndDate', () => {
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14T15:00:00.000Z').getTime()
|
||||
);
|
||||
let expectDate = new Date('2020-01-14T15:00:00.000Z');
|
||||
expectDate.setHours(23);
|
||||
expectDate.setMinutes(10);
|
||||
expect(getMachineEndDate({endTime: "23:10"}).getTime()).toBe(expectDate.getTime());
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T15:00:00.000Z').getTime());
|
||||
let expectDate = new Date('2020-01-14T15:00:00.000Z');
|
||||
expectDate.setHours(23);
|
||||
expectDate.setMinutes(10);
|
||||
expect(getMachineEndDate({endTime: '23:10'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
);
|
||||
|
||||
expectDate.setHours(16);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: "16:30"}).getTime()).toBe(expectDate.getTime());
|
||||
expectDate.setHours(16);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: '16:30'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
);
|
||||
|
||||
expect(getMachineEndDate({endTime: "15:30"})).toBeNull();
|
||||
expect(getMachineEndDate({endTime: '15:30'})).toBeNull();
|
||||
|
||||
expect(getMachineEndDate({endTime: "13:10"})).toBeNull();
|
||||
expect(getMachineEndDate({endTime: '13:10'})).toBeNull();
|
||||
|
||||
jest.spyOn(Date, 'now')
|
||||
.mockImplementation(() =>
|
||||
new Date('2020-01-14T23:00:00.000Z').getTime()
|
||||
);
|
||||
expectDate = new Date('2020-01-14T23:00:00.000Z');
|
||||
expectDate.setHours(0);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: "00:30"}).getTime()).toBe(expectDate.getTime());
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2020-01-14T23:00:00.000Z').getTime());
|
||||
expectDate = new Date('2020-01-14T23:00:00.000Z');
|
||||
expectDate.setHours(0);
|
||||
expectDate.setMinutes(30);
|
||||
expect(getMachineEndDate({endTime: '00:30'}).getTime()).toBe(
|
||||
expectDate.getTime(),
|
||||
);
|
||||
});
|
||||
|
||||
test('isMachineWatched', () => {
|
||||
let machineList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
];
|
||||
expect(isMachineWatched({number: "0", endTime: "23:30"}, machineList)).toBeTrue();
|
||||
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();
|
||||
let machineList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
];
|
||||
expect(
|
||||
isMachineWatched({number: '0', endTime: '23:30'}, machineList),
|
||||
).toBeTrue();
|
||||
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', () => {
|
||||
let machineList = [
|
||||
{
|
||||
number: "0",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
},
|
||||
];
|
||||
expect(getMachineOfId("0", machineList)).toStrictEqual({number: "0"});
|
||||
expect(getMachineOfId("1", machineList)).toStrictEqual({number: "1"});
|
||||
expect(getMachineOfId("3", machineList)).toBeNull();
|
||||
let machineList = [
|
||||
{
|
||||
number: '0',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
},
|
||||
];
|
||||
expect(getMachineOfId('0', machineList)).toStrictEqual({number: '0'});
|
||||
expect(getMachineOfId('1', machineList)).toStrictEqual({number: '1'});
|
||||
expect(getMachineOfId('3', machineList)).toBeNull();
|
||||
});
|
||||
|
||||
test('getCleanedMachineWatched', () => {
|
||||
let machineList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
endTime: "",
|
||||
},
|
||||
];
|
||||
let watchList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
endTime: "",
|
||||
},
|
||||
];
|
||||
let cleanedList = watchList;
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
||||
let machineList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
endTime: '',
|
||||
},
|
||||
];
|
||||
let watchList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
endTime: '',
|
||||
},
|
||||
];
|
||||
let cleanedList = watchList;
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||
cleanedList,
|
||||
);
|
||||
|
||||
watchList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
{
|
||||
number: "2",
|
||||
endTime: "15:30",
|
||||
},
|
||||
];
|
||||
cleanedList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:30",
|
||||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
||||
watchList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
{
|
||||
number: '2',
|
||||
endTime: '15:30',
|
||||
},
|
||||
];
|
||||
cleanedList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:30',
|
||||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
|
||||
cleanedList,
|
||||
);
|
||||
|
||||
watchList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
{
|
||||
number: "1",
|
||||
endTime: "20:31",
|
||||
},
|
||||
{
|
||||
number: "3",
|
||||
endTime: "15:30",
|
||||
},
|
||||
];
|
||||
cleanedList = [
|
||||
{
|
||||
number: "0",
|
||||
endTime: "23:30",
|
||||
},
|
||||
];
|
||||
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(cleanedList);
|
||||
});
|
||||
watchList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
{
|
||||
number: '1',
|
||||
endTime: '20:31',
|
||||
},
|
||||
{
|
||||
number: '3',
|
||||
endTime: '15:30',
|
||||
},
|
||||
];
|
||||
cleanedList = [
|
||||
{
|
||||
number: '0',
|
||||
endTime: '23:30',
|
||||
},
|
||||
];
|
||||
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
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -18,7 +18,7 @@ import com.android.build.OutputFile
|
|||
* // the entry file for bundle generation
|
||||
* entryFile: "index.android.js",
|
||||
*
|
||||
* // https://facebook.github.io/react-native/docs/performance#enable-the-ram-format
|
||||
* // https://reactnative.dev/docs/performance#enable-the-ram-format
|
||||
* bundleCommand: "ram-bundle",
|
||||
*
|
||||
* // whether to bundle JS and assets in debug mode
|
||||
|
|
@ -124,6 +124,13 @@ def jscFlavor = 'org.webkit:android-jsc:+'
|
|||
*/
|
||||
def enableHermes = project.ext.react.get("enableHermes", false);
|
||||
|
||||
/**
|
||||
* Load release keystore
|
||||
*/
|
||||
def keystorePropertiesFile = rootProject.file("keystores/release.keystore.properties");
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
android {
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
|
|
@ -136,8 +143,8 @@ android {
|
|||
applicationId 'fr.amicaleinsat.application'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 24
|
||||
versionName "3.0.7"
|
||||
versionCode 38
|
||||
versionName "4.0.1"
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
splits {
|
||||
|
|
@ -156,12 +163,10 @@ android {
|
|||
keyPassword 'android'
|
||||
}
|
||||
release {
|
||||
if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
|
||||
storeFile file(MYAPP_UPLOAD_STORE_FILE)
|
||||
storePassword MYAPP_UPLOAD_STORE_PASSWORD
|
||||
keyAlias MYAPP_UPLOAD_KEY_ALIAS
|
||||
keyPassword MYAPP_UPLOAD_KEY_PASSWORD
|
||||
}
|
||||
storeFile file(keystoreProperties['UPLOAD_STORE_FILE'])
|
||||
storePassword keystoreProperties['UPLOAD_STORE_PASSWORD']
|
||||
keyAlias keystoreProperties['UPLOAD_KEY_ALIAS']
|
||||
keyPassword keystoreProperties['UPLOAD_KEY_PASSWORD']
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
|
|
@ -170,28 +175,13 @@ android {
|
|||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see https://facebook.github.io/react-native/docs/signed-apk-android.
|
||||
// see https://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.release
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst "lib/armeabi-v7a/libc++_shared.so"
|
||||
pickFirst "lib/arm64-v8a/libc++_shared.so"
|
||||
pickFirst "lib/x86/libc++_shared.so"
|
||||
pickFirst "lib/x86_64/libc++_shared.so"
|
||||
}
|
||||
|
||||
// Force so_loader version to fix crash on apk release
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force "com.facebook.soloader:soloader:0.8.2"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// applicationVariants are e.g. debug, release
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
|
|
@ -221,6 +211,7 @@ dependencies {
|
|||
|
||||
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
|
||||
exclude group:'com.facebook.flipper'
|
||||
exclude group:'com.squareup.okhttp3', module:'okhttp'
|
||||
}
|
||||
|
||||
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
|
||||
|
|
|
|||
BIN
android/app/debug.keystore
Normal file
3
android/app/proguard-rules.pro
vendored
|
|
@ -8,6 +8,3 @@
|
|||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
-keep class com.facebook.hermes.unicode.** { *; }
|
||||
-keep class com.facebook.jni.** { *; }
|
||||
|
|
@ -6,6 +6,8 @@
|
|||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
|
||||
|
||||
<application
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 80 KiB |
|
|
@ -7,11 +7,12 @@
|
|||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
|
||||
|
||||
<uses-permission tools:node="remove" android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.MANAGE_DOCUMENTS"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.READ_PHONE_STATE"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.USE_FINGERPRINT"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
|
@ -20,8 +21,7 @@
|
|||
<uses-permission tools:node="remove" android:name="android.permission.WRITE_CALENDAR"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.RECORD_AUDIO"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.WRITE_SETTINGS"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.WRITE_SETTINGS"/>
|
||||
<uses-permission tools:node="remove" android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "28.0.3"
|
||||
buildToolsVersion = "29.0.2"
|
||||
minSdkVersion = 21
|
||||
compileSdkVersion = 28
|
||||
targetSdkVersion = 28
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:3.5.2")
|
||||
classpath("com.android.tools.build:gradle:3.5.3")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
|
|||
27
android/gradle.properties
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Gradle settings configured through the IDE *will override*
|
||||
# any settings specified in this file.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||
# Android operating system, and which are packaged with your app's APK
|
||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||
android.useAndroidX=true
|
||||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.37.0
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
|||
29
android/gradlew
vendored
|
|
@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
|||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
|
|
@ -175,14 +175,9 @@ save () {
|
|||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
|
|
|||
5
android/gradlew.bat
vendored
|
|
@ -5,7 +5,7 @@
|
|||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem http://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
|
|
@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=.
|
|||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 7.5 KiB |
BIN
assets/mascot/mascot.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
assets/mascot/mascot_eyes_angry.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/mascot/mascot_eyes_cute.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
assets/mascot/mascot_eyes_girly.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/mascot/mascot_eyes_heart.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
assets/mascot/mascot_eyes_normal.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/mascot/mascot_eyes_wink.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/mascot/mascot_glasses.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
assets/mascot/mascot_sunglasses.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
presets: ['module:metro-react-native-babel-preset'],
|
||||
presets: ['module:metro-react-native-babel-preset', '@babel/preset-flow'],
|
||||
};
|
||||
|
|
|
|||
81
doc/CONTRIBUTE.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# Contribuer
|
||||
|
||||
Tu veux contribuer au projet ? Mais c'est magnifique ! Ce guide va te montrer comment faire pour contribuer tes modifications.
|
||||
|
||||
Si tu as des problèmes ou des questions, n'hésite pas à me contacter par mail ([app@amicale-insat.fr](mailto:app@amicale-insat.fr)).
|
||||
|
||||
|
||||
## ⚠️ Avant de commencer, merci de te familiariser avec [les bases !](LINKS.md)
|
||||
|
||||
# Table des matières
|
||||
|
||||
* [1. Prérequis](#1-prérequis)
|
||||
* [2. Fork du projet](#2-fork-du-projet)
|
||||
* [3. Création d'une nouvelle branche](#3-création-dune-nouvelle-branche)
|
||||
* [4. Réalisation d'une modification](#4-réalisation-dune-modification)
|
||||
* [5. Création d'une Pull Request](#5-création-dune-pull-request)
|
||||
|
||||
# 1. Prérequis
|
||||
|
||||
Avant toute chose, tu dois installer React Native et git sur ta machine. Pour cela, suis [ce guide](INSTALL.md) jusqu'à l'étape 3.
|
||||
|
||||
# 2. Fork du projet
|
||||
|
||||
Si tu as bien suivi les instructions plus haut, tu devrais pouvoir lancer une application vide sur un appareil. Si ce n'est pas le cas, recommence l'installation depuis le début. Si malgré tout tu n'y arrives pas, envoie-moi un petit mail : [app@amicale-insat.fr](mailto:app@amicale-insat.fr).
|
||||
|
||||
Il est maintenant temps de **Fork** le projet. Le dépôt officiel est protégé pour éviter le vandalisme. Un fork permet de copier le code du dépôt officiel et de le lier à ton compte. Sur cette nouvelle version, tu pourras faire les modifications que tu veux, et ensuite demander de fusionner ces modifications avec le dépôt officiel. Le mainteneur actuel du projet vérifiera alors tes modifications et décidera ou non de les accepter.
|
||||
|
||||
Plus d'infos sur git [ici](LINKS.md).
|
||||
|
||||
Créer un fork est très simple. Pour cela, suis ces instructions :
|
||||
|
||||
* Connecte-toi sur ce site (en haut à droite) avec tes identifiants INSA.
|
||||
* Vas sur le [dépôt officiel](https://git.etud.insa-toulouse.fr/vergnet/application-amicale) et clique sur 'Fork' en haut à droite.
|
||||
* Le site te demandera des informations sur ce fork, tu n'as rien besoin de changer et tu peux juste cliquer sur 'Fork Repository'.
|
||||
* Tu arrives ainsi sur la page du dépôt ! Il est exactement comme le dépôt officiel, à quelques détails près. Si tu regardes en haut à gauche, à la place de vergnet/application-amicale, il y a maintenant ton nom ! Tu as donc fait une copie du dépôt officiel que tu as mis sur ton compte.
|
||||
* Tu peux maintenant télécharger ce dépôt sur ta machine en utilisant la commande:
|
||||
````shell script
|
||||
git clone [LINK]
|
||||
````
|
||||
en remplaçant `[LINK]` par le lien que tu peux copier en haut à droite, au-dessus de la liste des fichiers.
|
||||
* Tu as réussi à faire un Fork, bravo !
|
||||
|
||||
# 3. Création d'une nouvelle branche
|
||||
|
||||
Comme indiqué sur [ce guide](WORKFLOW.md), chaque fonctionnalité doit être développée dans sa propre branche puis fusionnée avec le master du dépôt officiel.
|
||||
|
||||
Pour créer une nouvelle branche, utilise la commande suivante :
|
||||
````shell script
|
||||
git checkout -b <branch-name>
|
||||
````
|
||||
En remplaçant `<branch-name>` par le nom souhaité (sans espaces !). Ce nom doit décrire rapidement ce que tu veux faire grâce à tes modifications.
|
||||
|
||||
Tu es maintenant sur ta nouvelle branche et prêt à faire tes modifications.
|
||||
|
||||
# 4. Réalisation d'une modification
|
||||
|
||||
Tu peux maintenant modifier ce que tu veux pour corriger un bug ou ajoute une fonctionnalité.
|
||||
|
||||
Mais avant de faire quoi que ce sois, merci de te signaler ! Cela évitera que plusieurs personnes corrigent le même bug ou de commencer à développer une fonctionnalité non voulue.
|
||||
|
||||
Pour installer l'appli sur ton téléphone/émulateur, reviens sur le [guide d'installation](INSTALL.md), et reprends à la section 3.2.
|
||||
|
||||
Avant de passer à l'étape suivante, merci de bien vérifier et tester tes modifications.
|
||||
|
||||
# 5. Création d'une Pull Request
|
||||
|
||||
Cette étape te permet d'envoyer tes modifications sur le dépôt officiel, pour être intégrées à l'application disponible dans les magasins.
|
||||
|
||||
Tout se fait simplement sur le site en suivant ces instructions :
|
||||
|
||||
* Connecte-toi sur ce site (en haut à droite) avec tes identifiants INSA.
|
||||
* Vas sur le [dépôt officiel](https://git.etud.insa-toulouse.fr/vergnet/application-amicale) et clique sur l'onglet 'Pull Requests'.
|
||||
* Cette page t'affiche la liste de toutes les pull requests. Pour en créer une nouvelle, clique sur le bouton 'New Pull Request' en haut à droite.
|
||||
* Tu arrives maintenant sur la page de création. Choisis master comme branche de destination, et ta branche créée précédemment comme source.
|
||||
* Tu devrais voir en bas la liste de toutes tes modifications. Écris alors un titre présentant tes modifications (très court), et une description expliquant pourquoi elles sont nécessaires. Cela permettra d'expliquer au mainteneur pourquoi il devrait accepter tes modifications.
|
||||
* Quand tout est bon, clique sur 'Create Pull Request' pour l'envoyer en attente de validation.
|
||||
* Tu entreras ensuite en dialogue avec le mainteneur ! Il t'expliquera si certaines choses sont à modifier avant de fusionner dans master.
|
||||
|
||||
Et voilà tu as fait ta première pull request !
|
||||
|
||||
Si tu as des problèmes ou des questions, n'hésite pas à me contacter par mail ([app@amicale-insat.fr](mailto:app@amicale-insat.fr)).
|
||||
83
doc/INSTALL.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Installer l'application sur ta machine
|
||||
|
||||
Si tu as un problème ou une question, merci de me contacter par mail : [app@amicale-insat.fr](mailto:app@amicale-insat.fr)
|
||||
|
||||
Ce guide a été testé sur Linux (Ubuntu 18.04).
|
||||
Si tu utilises Windows, débrouilles-toi ou installe Linux.
|
||||
|
||||
## ⚠️ Avant de commencer, merci de te familiariser avec [les bases !](LINKS.md)
|
||||
|
||||
# Table des matières
|
||||
* [1. Installation de Git](#1-installation-de-git)
|
||||
* [2. Installation de React Native](#2-installation-de-react-native)
|
||||
* [3. Installation de l'application](#3-installation-de-lapplication)
|
||||
* [3.1 Téléchargement du dépot](#31-téléchargement-du-dépôt)
|
||||
* [3.2 Installation des dépendances](#32-installation-des-dépendances)
|
||||
* [4. Lancement de l'application](#4-lancement-de-lapplication)
|
||||
* [5. Compiler une version release](#5-compiler-une-version-release)
|
||||
|
||||
# 1. Installation de Git
|
||||
|
||||
Git permet de garder un historique de modification du code et de synchroniser les fichiers entre plusieurs machines. Tu trouveras un tutoriel pour te familiariser avec les bases [ici](LINKS.md).
|
||||
|
||||
Ouvre un terminal et entre la commande suivante pour l'installer :
|
||||
```shell script
|
||||
sudo apt install git
|
||||
```
|
||||
|
||||
# 2. Installation de React Native
|
||||
|
||||
Vas sur le [site officiel](https://reactnative.dev/docs/environment-setup) puis sur l'onglet `React Native CLI Quickstart`, et sélectionne ensuite ta plateforme de développement et celle de ta cible.
|
||||
|
||||
Par exemple, si tu as un PC sous linux et un téléphone Android, sélectionne donc Linux et Android.
|
||||
|
||||
⚠️ **Ne choisis pas `Expo CLI Quickstart`, suis bien les instructions pour `React Native CLI Quickstart`**
|
||||
|
||||
Suis ensuite les instructions pour bien installer React Native sur ta machine. **Va bien jusqu'à la fin**. Tu devrais pouvoir créer une application vide qui se lance sur ton téléphone/émulateur.
|
||||
|
||||
# 3. Installation de l'application
|
||||
|
||||
Si tu as bien suivi les instructions plus haut, tu devrais pouvoir lancer une application vide sur un appareil. Si ce n'est pas le cas, recommence l'installation depuis le début. Si malgré tout tu n'y arrives pas, envoie-moi un petit mail : [app@amicale-insat.fr](mailto:app@amicale-insat.fr).
|
||||
|
||||
## 3.1 Téléchargement du dépôt
|
||||
|
||||
⚠️ **La suite n'est valide que si tu veux compiler une version sans contribuer** (pour avoir les toutes dernières modifications par exemple).
|
||||
|
||||
Si tu veux contribuer des modifications, rends-toi sur [ce guide](CONTRIBUTE.md) pour comprendre comment créer un **fork**.
|
||||
|
||||
Clone ce dépôt à l'aide de la commande suivante :
|
||||
````shell script
|
||||
git clone https://git.etud.insa-toulouse.fr/vergnet/application-amicale.git
|
||||
````
|
||||
|
||||
Toute modification doit être réalisée sur une branche dédiée (pas de commit direct sur master). Cette nouvelle branche est ensuite fusionnée avec master une fois qu'elle est testée et vérifiée.
|
||||
Ainsi, en prenant la branche master a n'importe quel moment, il devrait être possible de compiler une version stable.
|
||||
|
||||
Plus d'informations sur l'organisation avec git [ici](WORKFLOW.md).
|
||||
|
||||
## 3.2 Installation des dépendances
|
||||
|
||||
Une fois le dépôt sur ta machine et git sur la branche de ton choix, ouvre un terminal dans le dossier racine et installe les dépendances avec la commande suivante :
|
||||
````shell script
|
||||
npm install
|
||||
````
|
||||
|
||||
Si tu es sur macOS, tu devras aussi lancer la commande suivante pour installer les dépendances propres à iOS :
|
||||
````shell script
|
||||
cd ios && pod install
|
||||
````
|
||||
|
||||
En cas de problème d'installation (notamment lors du changement de branche), lance la commande suivante pour réinstaller seulement les modules node utilisés :
|
||||
````shell script
|
||||
./clear-node-cache.sh
|
||||
````
|
||||
|
||||
# 4. Lancement de l'application
|
||||
|
||||
Suis les instructions sur le [site officiel](https://reactnative.dev/docs/environment-setup) pour lancer l'application. Il n'y a aucune différence avec une application classique.
|
||||
|
||||
Si tu utilises Webstorm, le projet contient des configurations de lancement pour lancer le projet d'un seul clic.
|
||||
|
||||
# 5. Compiler une version release
|
||||
|
||||
Merci de me contacter par mail pour toute information sur les release : [app@amicale-insat.fr](mailto:app@amicale-insat.fr)
|
||||
38
doc/LINKS.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Liens utiles
|
||||
|
||||
**Voici une liste de liens qui pourraient t'être utile, que ce soit pour contribuer ou tout simplement pour comprendre comment l'application fonctionne sous le capot.**
|
||||
|
||||
## 👶 Les bases
|
||||
|
||||
Le strict minimum pour pouvoir comprendre le code de l'application. Il n'est pas nécessaire d'avoir de grandes connaissances en JavaScript ou Git pour lire le code, mais une compréhension du fonctionnement et de la syntaxe de React Native est nécessaire pour pouvoir le modifier.
|
||||
|
||||
* [**Des cours d'anglais**](https://www.wikihow.com/Be-Good-at-English) : Toutes les ressources sont en anglais, le code est en anglais, tu trouveras presque rien en français, donc profite-en pour t'améliorer !
|
||||
* [**Tutoriel Git**](https://learngitbranching.js.org/) : Le système utilisé pour synchroniser le code entre plusieurs ordinateurs. Tout le projet repose sur cette technologie, une compréhension minimale de son fonctionnement est nécessaire. Si tu ne sais pas ce que veut dire commit, pull, push, merge, ou branch, alors lis ce tuto !
|
||||
* [**Tutoriel JavaScript**](https://www.w3schools.com/js) : Un minimum de connaissances en JavaScript est nécessaire pour pouvoir comprendre le code. Pas besoin de lire tout le tutoriel. Pour les bases, tu peux t'arrêter à la partie `JS Dates` ou un peu avant. Il est utile de revenir souvent vers ce guide quand tu rencontres des difficultés.
|
||||
* [**Documentation React Native**](https://reactnative.dev/docs/getting-started) : La techno de base, qui utilise JavaScript. Lire au moins les articles de la catégorie `The Basics`, tout est interactif c'est plutôt simple et rapide à comprendre.
|
||||
* [**List des librairies**](../package.json) : Tu trouveras dans ce fichier la liste de toutes les librairies utilisées dans ce projet (catégorie `dependencies`). Pour accéder à leur documentation, fais une simple recherche de leur nom dans un moteur de recherche.
|
||||
|
||||
## 🤔 Comprendre les librairies
|
||||
|
||||
Si tu as compris les bases et que tu veux te plonger un peu plus en profondeur dans le code, tu peux utiliser les liens ci-dessous pour accéder aux frameworks les plus importants.
|
||||
|
||||
* [**Documentation Flow**](https://flow.org/en/docs/react/) : Un utilitaire pour rendre JavaScript typé statique (c'est-à-dire plus robuste pour de gros projets). Flow permet de rajouter des annotations pour donner un type aux variables.
|
||||
* [**Documentation React Native Paper**](https://callstack.github.io/react-native-paper/) : Le framework utilisé pour créer l'interface utilisateur (UI). Paper met à disposition de nombreux composants respectant les normes Material Design. Comparé à d'autres frameworks, paper est léger et facile à utiliser.
|
||||
* [**Documentation React Navigation**](https://reactnavigation.org/docs/getting-started) : Le framework utilisé pour faciliter la navigation classique entre différents écrans. Permet de créer facilement une navigation par onglets/menu déroulant.
|
||||
|
||||
## 🤯 Les Plus
|
||||
|
||||
Si t'es vraiment à fond dans le projet et que tu veux faire des trucs trop ouf, tu peux lire ça. Même moi j'ai eu la flemme de tout lire.
|
||||
|
||||
* [**Tutoriel Git complet**](https://www.tutorialspoint.com/git/index.htm) : Un tutoriel expliquant de nombreux aspects de git.
|
||||
* [**Comment écrire un bon commit**](https://chris.beams.io/posts/git-commit/) : Des bonnes habitudes à prendre pour écrire des messages de commit utiles
|
||||
* [**Tutoriel JavaScript Complet**](https://www.w3schools.com/js) : Le même tuto que pour les bases, mais à lire en entier pour être un pro !
|
||||
* [**Documentation React Native Complete**](https://reactnative.dev/docs/getting-started) : Le même tuto que pour les bases, mais ya encore plein de choses à lire et apprendre !
|
||||
* [**Documentation Jest**](https://jestjs.io/docs/en/getting-started) : Framework de tests unitaires pour JavaScript, pour faire les choses proprement.
|
||||
|
||||
## 💻 Les Logiciels
|
||||
|
||||
Tu ne sais pas trop quel logiciel utiliser ? C'est normal y'a beaucoup de choix, mais tu trouveras ici une liste très réduite de logiciels qui marchent bien pour le développement.
|
||||
|
||||
* [Webstorm](https://www.jetbrains.com/webstorm/buy/#discounts?billing=yearly) : Un logiciel pas mal que j'utilise et gratuit pour les étudiants/projets open-source. C'est un IDE (environnement de développement intégré) compatible React Native, ce qui veut dire qu'il possède de très nombreuses fonctionnalités pour simplifier le développement (debugging, refactoring, auto-complétion intelligente, et autre).
|
||||
* [VSCodium](https://vscodium.com/) : Un logiciel plus simple/léger que Webstorm mais avec un peu moins de fonctionnalités. Ce n'est pas un IDE mais un éditeur de text avec des plugins. Il est donc moins puissant que Webstorm, mais plus léger e plus simple à prendre en main.
|
||||
31
doc/NOTES.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Notes de développement
|
||||
|
||||
Ce fichier permet de regrouper les différentes informations sur des décisions actuelles, comme des changements de version ou des choix de technologie, tout cela dans le but de ne pas répéter les mêmes erreurs.
|
||||
|
||||
Ces notes pouvant évoluer dans le temps, leur date d'écriture est aussi indiquée.
|
||||
|
||||
## _2020-06-23_ | Expo
|
||||
|
||||
Expo est une surcouche à react native permettant de simplifier le processus de build. Le projet à commencé en l'utilisant, mais de nombreux problèmes ont été rencontrés :
|
||||
* Augmentation importante de la taille de l'application
|
||||
* Augmentation importante du temps de démarrage
|
||||
* Impossibilité d'utiliser certaines librairies
|
||||
* Obligation d'utiliser une version de react-native spécifique
|
||||
* Impossibilité d'utiliser le moteur Hermes sur Android
|
||||
|
||||
Pour ces raisons, il a été décidé de l'abandonner pour passer à un développement en react-native pur.
|
||||
|
||||
[Site officiel](https://docs.expo.io/)
|
||||
|
||||
## _2020-06-23_ | react-native-mapbox-gl
|
||||
|
||||
Librairie utilisée pour afficher une carte en utilisant OSM. N'a pas été utilisée car augmente la taille de l'apk de quelques Mo et rend la compilation plus difficile (il est nécessaire d'augmenter la taille du java heap dans gradle.properties).
|
||||
|
||||
[Dépot](https://github.com/react-native-mapbox-gl/maps)
|
||||
|
||||
## _2020-06-23_ | react-native-screens
|
||||
|
||||
Cette librairie permet d'améliorer les performances de la navigation en utilisant les optimisations natives.
|
||||
En revanche, activer le support pour screens fait crash l'appli sur android 9+ lors de la navigation pour sortir d'un écran avec une webview.
|
||||
|
||||
[Dépot](https://github.com/software-mansion/react-native-screens) | [Référence](https://reactnavigation.org/docs/react-native-screens/)
|
||||
12
doc/TRANSLATE.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Les Traductions
|
||||
|
||||
Tu peux traduire l'application sans avoir de connaissance en programmation.
|
||||
|
||||
Pour cela, suis cette procédure :
|
||||
* Télécharge [ce fichier](../locales/fr.json). Tu y trouveras un ensemble de couples de la forme "clé": "valeur". Les clés servent à identifier les valeurs, il ne faut pas les modifier !
|
||||
* Traduis les valeurs dans ce fichier dans la langue souhaitée.
|
||||
* Envoie-moi par mail ([app@amicale-insat.fr](mailto:app@amicale-insat.fr)) ce fichier quand tu as terminé, il sera ajouté à la prochaine version de l'application.
|
||||
|
||||
Envoie-moi un mail avant de commencer pour me prévenir que tu veux travailler sur une traduction. Cela me permettra de te mettre en relation avec d'autres personnes travaillant également sur cette traduction.
|
||||
|
||||
Tu peux traduire dans la langue que tu veux, sachant que le français et l'anglais sont déjà fait.
|
||||
27
doc/WORKFLOW.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Organisation du travail
|
||||
|
||||
⚠️ **Ce projet dépend entièrement sur Git. Si tu n'es pas familier à cette technologie, rends-toi sur [cette page](LINKS.md) avant de lire la suite.**
|
||||
|
||||
La méthode ci-dessous est très fortement recommandée, car son efficacité a été testée et prouvée par de nombreux projets Open Source.
|
||||
|
||||
Ce qui suit a été inspiré des [règles de KDE](https://community.kde.org/Frameworks/Git_Workflow) et largement simplifié.
|
||||
|
||||
# Principes de base
|
||||
|
||||
## La branche Master est toujours prête
|
||||
|
||||
Cette branche est le centre du projet. Elle ne doit contenir que des fonctionnalités et améliorations achevées. **Elle doit être prête pour une release à tout moment**. Le code doit donc être testé et validé.
|
||||
|
||||
## Le développement à lieu dans les branches de 'fonctionnalités'
|
||||
|
||||
Pour des corrections de bugs ou l'implémentation de nouvelles fonctionnalités qui demandent du travail, il est nécessaire de créer une nouvelle branche depuis master. Le développeur peut manipuler cette branche comme il le souhaite, mais elle doit être testée et vérifiée avant d'être fusionnée avec master.
|
||||
|
||||
## Mainteneurs vs contributeur externe
|
||||
|
||||
Les **contributeurs externes** sont des volontaires qui veulent aider ponctuellement pour corriger des bugs/ajouter des fonctionnalités. Ils doivent suivre [la procédure pour créer un fork du projet](CONTRIBUTE.md) et faire une pull request pour intégrer leurs changements.
|
||||
|
||||
Les **mainteneurs** sont les personnes de confiance ayant un accès en écriture sur le dépôt officiel. C'est eux qui vérifient et acceptent les pull requests. Ils peuvent push et merge directement sur le dépôt officiel pour simplifier le développement.
|
||||
|
||||
#### Tu veux devenir contributeur ? Fais un tour [par ici](CONTRIBUTE.md) pour comprendre comment faire.
|
||||
|
||||
#### Tu es motivé et tu veux devenir mainteneur ? Contacte-moi par mail [app@amicale-insat.fr](mailto:app@amicale-insat.fr).
|
||||
37
eject.txt
|
|
@ -1,37 +0,0 @@
|
|||
Your git working tree is clean
|
||||
To revert the changes after this command completes, you can run the following:
|
||||
git clean --force && git reset --hard
|
||||
|
||||
✔ App configuration (app.json) updated.
|
||||
✔ Created native project directories (./ios and ./android) and updated .gitignore.
|
||||
✔ Updated package.json and added index.js entry point for iOS and Android.
|
||||
✔ Installed JavaScript dependencies.
|
||||
|
||||
⚠️ iOS configuration applied with warnings that should be fixed:
|
||||
- icon: This is the image that your app uses on your home screen, you will need to configure it manually.
|
||||
- splash: This is the image that your app uses on the loading screen, we recommend installing and using expo-splash-screen. Details. (https://github.com/expo/expo/blob/master/packages/expo-splash-screen/README.md)
|
||||
|
||||
⚠️ Android configuration applied with warnings that should be fixed:
|
||||
- splash: This is the image that your app uses on the loading screen, we recommend installing and using expo-splash-screen. Details. (https://github.com/expo/expo/blob/master/packages/expo-splash-screen/README.md)
|
||||
- icon: This is the image that your app uses on your home screen, you will need to configure it manually.
|
||||
- android.adaptiveIcon: This is the image that your app uses on your home screen, you will need to configure it manually.
|
||||
|
||||
✔ Skipped installing CocoaPods because operating system is not on macOS.
|
||||
|
||||
⚠️ Your app includes 3 packages that require additional setup in order to run:
|
||||
- expo-camera: https://github.com/expo/expo/tree/master/packages/expo-camera
|
||||
- react-native-appearance: https://github.com/expo/react-native-appearance
|
||||
- react-native-webview: https://github.com/react-native-community/react-native-webview
|
||||
|
||||
➡️ Next steps
|
||||
- 👆 Review the logs above and look for any warnings (⚠️ ) that might need follow-up.
|
||||
- 💡 You may want to run npx @react-native-community/cli doctor to help install any tools that your app may need to run your native projects.
|
||||
- 🍫 When CocoaPods is installed, initialize the project workspace: cd ios && pod install
|
||||
- 🔑 Download your Android keystore (if you're not sure if you need to, just run the command and see): expo fetch:android:keystore
|
||||
- 🚀 expo-updates (https://github.com/expo/expo/blob/master/packages/expo-updates/README.md) has been configured in your project. Before you do a release build, make sure you run expo publish. Learn more. (https://expo.fyi/release-builds-with-expo-updates)
|
||||
|
||||
☑️ When you are ready to run your project
|
||||
To compile and run your project in development, execute one of the following commands:
|
||||
- npm run ios
|
||||
- npm run android
|
||||
- npm run web
|
||||
1
index.js
|
|
@ -6,4 +6,5 @@ import {AppRegistry} from 'react-native';
|
|||
import App from './App';
|
||||
import {name as appName} from './app.json';
|
||||
|
||||
// eslint-disable-next-line flowtype/require-return-type
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@
|
|||
074F4BDC2432833400BDB9FE /* app.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 07C2E6E4243282B30028AF0A /* app.bundle */; };
|
||||
074F4BDD2432833400BDB9FE /* app.manifest in Resources */ = {isa = PBXBuildFile; fileRef = 07C2E6E3243282B30028AF0A /* app.manifest */; };
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
3DE4DAD41476765101945408 /* libPods-Campus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D43FF9D506E70904424FA7E9 /* libPods-Campus.a */; };
|
||||
931B380C24BF47D400D78120 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 931B380B24BF47D400D78120 /* Launch Screen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
|
@ -23,13 +23,13 @@
|
|||
13B07F961A680F5B00A75B9A /* application.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = application.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Campus/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Campus/AppDelegate.m; sourceTree = "<group>"; };
|
||||
13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = "<group>"; };
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Campus/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Campus/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Campus/main.m; sourceTree = "<group>"; };
|
||||
2D16E6891FA4F8E400B85C8A /* libReact.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libReact.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B47C5AFCB8BDE514B7D1AC6 /* Pods-Campus.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Campus.debug.xcconfig"; path = "Target Support Files/Pods-Campus/Pods-Campus.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
8AC623DBF3A3E2CB072F81F2 /* Pods-Campus.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Campus.release.xcconfig"; path = "Target Support Files/Pods-Campus/Pods-Campus.release.xcconfig"; sourceTree = "<group>"; };
|
||||
931B380B24BF47D400D78120 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
|
||||
D43FF9D506E70904424FA7E9 /* libPods-Campus.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Campus.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; };
|
||||
|
|
@ -55,10 +55,10 @@
|
|||
13B07FB01A68108700A75B9A /* AppDelegate.m */,
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */,
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.xib */,
|
||||
13B07FB71A68108700A75B9A /* main.m */,
|
||||
07C2E6E4243282B30028AF0A /* app.bundle */,
|
||||
07C2E6E3243282B30028AF0A /* app.manifest */,
|
||||
931B380B24BF47D400D78120 /* Launch Screen.storyboard */,
|
||||
);
|
||||
name = Campus;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
074F4BDC2432833400BDB9FE /* app.bundle in Resources */,
|
||||
074F4BDD2432833400BDB9FE /* app.manifest in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */,
|
||||
931B380C24BF47D400D78120 /* Launch Screen.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -222,6 +222,7 @@
|
|||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
|
||||
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
|
||||
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputPaths = (
|
||||
|
|
@ -241,6 +242,7 @@
|
|||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
|
||||
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
|
|
@ -302,18 +304,6 @@
|
|||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
13B07FB21A68108700A75B9A /* Base */,
|
||||
);
|
||||
name = LaunchScreen.xib;
|
||||
path = Campus;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
|
|
@ -323,12 +313,12 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Campus/application.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = 6JA7CLNUV6;
|
||||
INFOPLIST_FILE = Campus/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 3.0.5;
|
||||
MARKETING_VERSION = 4.0.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
|
@ -349,11 +339,11 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Campus/application.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 6JA7CLNUV6;
|
||||
INFOPLIST_FILE = Campus/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 3.0.5;
|
||||
MARKETING_VERSION = 4.0.1;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
#import <RNCPushNotificationIOS.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
#if DEBUG
|
||||
#ifdef FB_SONARKIT_ENABLED
|
||||
#import <FlipperKit/FlipperClient.h>
|
||||
#import <FlipperKitLayoutPlugin/FlipperKitLayoutPlugin.h>
|
||||
#import <FlipperKitUserDefaultsPlugin/FKUserDefaultsPlugin.h>
|
||||
|
|
@ -46,7 +46,7 @@ static void InitializeFlipper(UIApplication *application) {
|
|||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
#if DEBUG
|
||||
#ifdef FB_SONARKIT_ENABLED
|
||||
InitializeFlipper(application);
|
||||
#endif
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina3_5" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="460"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchScreen" translatesAutoresizingMaskIntoConstraints="NO" id="MEu-9j-Yk9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="440"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.74509803921568629" green="0.082352941176470587" blue="0.13333333333333333" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="MEu-9j-Yk9" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="topMargin" id="JBv-Ns-A6x"/>
|
||||
<constraint firstItem="MEu-9j-Yk9" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="JNO-FD-uRI"/>
|
||||
<constraint firstItem="MEu-9j-Yk9" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="KdP-HF-t4U"/>
|
||||
<constraint firstItem="MEu-9j-Yk9" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="LNb-Oe-Px1"/>
|
||||
</constraints>
|
||||
<nil key="simulatedStatusBarMetrics"/>
|
||||
<modalPageSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="546.37681159420299" y="453.26086956521743"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="LaunchScreen" width="682.66668701171875" height="200"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "splash.png",
|
||||
"filename" : "launch_screen.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
|
|
|
|||
BIN
ios/Campus/Images.xcassets/LaunchScreen.imageset/launch_screen.png
vendored
Normal file
|
After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 147 KiB |
|
|
@ -53,13 +53,13 @@
|
|||
</dict>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Allow Campus to use the camera</string>
|
||||
<string>Allow Campus to use the camera to scan QRCodes</string>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>MaterialCommunityIcons.ttf</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<string>Launch Screen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
|
|
|
|||
43
ios/Launch Screen.storyboard
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" image="LaunchScreen" translatesAutoresizingMaskIntoConstraints="NO" id="pl8-ut-yN1">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.74509803919999995" green="0.08235294118" blue="0.1333333333" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="bottom" secondItem="pl8-ut-yN1" secondAttribute="bottom" id="BeQ-Vq-15R"/>
|
||||
<constraint firstItem="Bcu-3y-fUS" firstAttribute="trailing" secondItem="pl8-ut-yN1" secondAttribute="trailing" id="F1S-I7-kPb"/>
|
||||
<constraint firstItem="pl8-ut-yN1" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="ach-ld-mbT"/>
|
||||
<constraint firstItem="pl8-ut-yN1" firstAttribute="leading" secondItem="Bcu-3y-fUS" secondAttribute="leading" id="bsE-gq-0f4"/>
|
||||
<constraint firstItem="pl8-ut-yN1" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="lyr-KB-P3m"/>
|
||||
<constraint firstItem="pl8-ut-yN1" firstAttribute="top" secondItem="Bcu-3y-fUS" secondAttribute="top" id="zkD-PB-Piv"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchScreen" width="360" height="640"/>
|
||||
</resources>
|
||||
</document>
|
||||
88
ios/Podfile
|
|
@ -1,83 +1,13 @@
|
|||
platform :ios, '9.0'
|
||||
require_relative '../node_modules/react-native/scripts/react_native_pods'
|
||||
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
|
||||
|
||||
def add_flipper_pods!(versions = {})
|
||||
versions['Flipper'] ||= '~> 0.33.1'
|
||||
versions['DoubleConversion'] ||= '1.1.7'
|
||||
versions['Flipper-Folly'] ||= '~> 2.1'
|
||||
versions['Flipper-Glog'] ||= '0.3.6'
|
||||
versions['Flipper-PeerTalk'] ||= '~> 0.0.4'
|
||||
versions['Flipper-RSocket'] ||= '~> 1.0'
|
||||
pod 'FlipperKit', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FlipperKitLayoutPlugin', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/SKIOSNetworkPlugin', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FlipperKitUserDefaultsPlugin', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FlipperKitReactPlugin', versions['Flipper'], :configuration => 'Debug'
|
||||
# List all transitive dependencies for FlipperKit pods
|
||||
# to avoid them being linked in Release builds
|
||||
pod 'Flipper', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'Flipper-DoubleConversion', versions['DoubleConversion'], :configuration => 'Debug'
|
||||
pod 'Flipper-Folly', versions['Flipper-Folly'], :configuration => 'Debug'
|
||||
pod 'Flipper-Glog', versions['Flipper-Glog'], :configuration => 'Debug'
|
||||
pod 'Flipper-PeerTalk', versions['Flipper-PeerTalk'], :configuration => 'Debug'
|
||||
pod 'Flipper-RSocket', versions['Flipper-RSocket'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/Core', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/CppBridge', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FBCxxFollyDynamicConvert', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FBDefines', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FKPortForwarding', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FlipperKitHighlightOverlay', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FlipperKitLayoutTextSearchable', versions['Flipper'], :configuration => 'Debug'
|
||||
pod 'FlipperKit/FlipperKitNetworkPlugin', versions['Flipper'], :configuration => 'Debug'
|
||||
end
|
||||
|
||||
# Post Install processing for Flipper
|
||||
def flipper_post_install(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
if target.name == 'YogaKit'
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['SWIFT_VERSION'] = '4.1'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
platform :ios, '10.0'
|
||||
|
||||
target 'Campus' do
|
||||
# Pods for Campus
|
||||
rnPrefix = "../node_modules/react-native"
|
||||
pod 'FBLazyVector', :path => "#{rnPrefix}/Libraries/FBLazyVector"
|
||||
pod 'FBReactNativeSpec', :path => "#{rnPrefix}/Libraries/FBReactNativeSpec"
|
||||
pod 'RCTRequired', :path => "#{rnPrefix}/Libraries/RCTRequired"
|
||||
pod 'RCTTypeSafety', :path => "#{rnPrefix}/Libraries/TypeSafety"
|
||||
pod 'React', :path => "#{rnPrefix}/"
|
||||
pod 'React-Core', :path => "#{rnPrefix}/"
|
||||
pod 'React-CoreModules', :path => "#{rnPrefix}/React/CoreModules"
|
||||
pod 'React-RCTActionSheet', :path => "#{rnPrefix}/Libraries/ActionSheetIOS"
|
||||
pod 'React-RCTAnimation', :path => "#{rnPrefix}/Libraries/NativeAnimation"
|
||||
pod 'React-RCTBlob', :path => "#{rnPrefix}/Libraries/Blob"
|
||||
pod 'React-RCTImage', :path => "#{rnPrefix}/Libraries/Image"
|
||||
pod 'React-RCTLinking', :path => "#{rnPrefix}/Libraries/LinkingIOS"
|
||||
pod 'React-RCTNetwork', :path => "#{rnPrefix}/Libraries/Network"
|
||||
pod 'React-RCTSettings', :path => "#{rnPrefix}/Libraries/Settings"
|
||||
pod 'React-RCTText', :path => "#{rnPrefix}/Libraries/Text"
|
||||
pod 'React-RCTVibration', :path => "#{rnPrefix}/Libraries/Vibration"
|
||||
pod 'React-Core/RCTWebSocket', :path => "#{rnPrefix}/"
|
||||
config = use_native_modules!
|
||||
|
||||
use_react_native!(:path => config["reactNativePath"])
|
||||
|
||||
pod 'React-cxxreact', :path => "#{rnPrefix}/ReactCommon/cxxreact"
|
||||
pod 'React-jsi', :path => "#{rnPrefix}/ReactCommon/jsi"
|
||||
pod 'React-jsiexecutor', :path => "#{rnPrefix}/ReactCommon/jsiexecutor"
|
||||
pod 'React-jsinspector', :path => "#{rnPrefix}/ReactCommon/jsinspector"
|
||||
pod 'ReactCommon/callinvoker', :path => "#{rnPrefix}/ReactCommon"
|
||||
pod 'ReactCommon/turbomodule/core', :path => "#{rnPrefix}/ReactCommon"
|
||||
pod 'Yoga', :path => "#{rnPrefix}/ReactCommon/yoga", :modular_headers => true
|
||||
|
||||
pod 'DoubleConversion', :podspec => "#{rnPrefix}/third-party-podspecs/DoubleConversion.podspec"
|
||||
pod 'glog', :podspec => "#{rnPrefix}/third-party-podspecs/glog.podspec"
|
||||
pod 'Folly', :podspec => "#{rnPrefix}/third-party-podspecs/Folly.podspec"
|
||||
|
||||
# react-native-cli autolinking
|
||||
use_native_modules!
|
||||
|
||||
# Permissions
|
||||
permissions_path = '../node_modules/react-native-permissions/ios'
|
||||
|
||||
|
|
@ -88,9 +18,9 @@ target 'Campus' do
|
|||
#
|
||||
# Note that if you have use_frameworks! enabled, Flipper will not work and
|
||||
# you should disable these next few lines.
|
||||
#add_flipper_pods!
|
||||
#post_install do |installer|
|
||||
# flipper_post_install(installer)
|
||||
#end
|
||||
# use_flipper!
|
||||
# post_install do |installer|
|
||||
# flipper_post_install(installer)
|
||||
# end
|
||||
|
||||
end
|
||||
|
|
|
|||
497
locales/en.json
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
{
|
||||
"screens": {
|
||||
"services": {
|
||||
"title": "Services",
|
||||
"more": "Click to see more",
|
||||
"categories": {
|
||||
"amicale": "The Amicale",
|
||||
"students": "Student services",
|
||||
"insa": "INSA services",
|
||||
"special": "Proxiwash"
|
||||
},
|
||||
"descriptions": {
|
||||
"clubs": "See info about your favorite club and discover new ones",
|
||||
"profile": "See your personal information",
|
||||
"amicaleWebsite": "See more information on the website",
|
||||
"vote": "Vote for the upcoming elections",
|
||||
"proximo": "Check the store's stock",
|
||||
"wiketud": "Read useful info about classes and campus life",
|
||||
"elusEtudiants": "The students in contact with the administration",
|
||||
"tutorInsa": "Give and take part in tutorials by students",
|
||||
"self": "Check the RU menu",
|
||||
"availableRooms": "See how many rooms are free",
|
||||
"bib": "Book a Bib'Box for project work",
|
||||
"mails": "Check your INSA mails",
|
||||
"ent": "See your grades",
|
||||
"insaAccount": "See your information and change your password",
|
||||
"equipment": "Book a BBQ or other equipment",
|
||||
"washers": "Number of available washers",
|
||||
"dryers": "Number of available dryers"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "A bit lost?",
|
||||
"message": "Here is a mix of handy services! Between INSA and students services, I am sure you will find something for you.\n\nAnd if you have an Amicale account, you will have even more choices!",
|
||||
"button": "Thx buddy"
|
||||
}
|
||||
},
|
||||
"proxiwash": {
|
||||
"title": "Proxiwash",
|
||||
"dryer": "Dryer",
|
||||
"dryers": "Dryers",
|
||||
"washer": "Washer",
|
||||
"washers": "Washers",
|
||||
"min": "min",
|
||||
"description": "This is the washing service operated by Promologis for INSA's residences (We don't mind if you do not live on the campus and you do your laundry here). The room is right next to the R2, with 3 dryers and 9 washers, is open 7d/7 24h/24 ! Here you can check their availability ! You can bring your own detergent, use the one given on site or buy it at the Proximo (cheaper than the one given by the machines ). You can pay by credit card or cash.",
|
||||
"informationTab": "Information",
|
||||
"paymentTab": "Payment",
|
||||
"tariffs": "Tariffs",
|
||||
"washersTariff": "3€ the washer + 0.80€ with detergent.",
|
||||
"dryersTariff": "0.35€ for 5min of dryer usage.",
|
||||
"paymentMethods": "Payment Methods",
|
||||
"paymentMethodsDescription": "Cash up until 10€.\nCredit Card also accepted.",
|
||||
"washerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the command central, then press the START button on the machine.\n\nWhen the program is finished, the screen indicates 'Programme terminé', press the yellow button to open the lid and retrieve your laundry.",
|
||||
"washerTips": "Program 'blanc/couleur': 6kg of dry laundry (cotton linen, linen, underwear, sheets, jeans, towels).\n\nProgram 'non repassable': 3,5 kg of dry laundry (synthetic fibre linen, cotton and polyester mixed).\n\nProgram 'fin 30°C': 2,5 kg of dry laundry (delicate linen in synthetic fibres).\n\nProgram 'laine 30°C': 2,5 kg of dry laundry (wool textiles).",
|
||||
"dryerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the command central, then press the START button on the machine.",
|
||||
"dryerTips": "The advised dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.",
|
||||
"procedure": "Procedure",
|
||||
"tips": "Tips",
|
||||
"numAvailable": "available",
|
||||
"numAvailablePlural": "available",
|
||||
"modal": {
|
||||
"enableNotifications": "Notify me",
|
||||
"disableNotifications": "Stop notifications",
|
||||
"ok": "OK",
|
||||
"cancel": "Cancel",
|
||||
"finished": "This machine is finished. If you started it, you can get back your laundry.",
|
||||
"ready": "This machine is empty and ready to use.",
|
||||
"running": "This machine has been started at %{start} and will end at %{end}.\n\nRemaining time: %{remaining} min.\nProgram: %{program}",
|
||||
"runningNotStarted": "This machine is ready but not started. Please make sure you pressed the start button.",
|
||||
"broken": "This machine is out of order and cannot be used. Thank you for your comprehension.",
|
||||
"error": "There has been an error and we are unable to get information from this machine. Sorry for the inconvenience.",
|
||||
"unknown": "This machine is in an unknown state. Sorry for the inconvenience.",
|
||||
"notificationErrorTitle": "Error",
|
||||
"notificationErrorDescription": "Impossible to create notifications. Please make sure you enabled notifications then restart the app."
|
||||
},
|
||||
"states": {
|
||||
"finished": "FINISHED",
|
||||
"ready": "READY",
|
||||
"running": "RUNNING",
|
||||
"runningNotStarted": "NOT STARTED",
|
||||
"broken": "OUT OF ORDER",
|
||||
"error": "ERROR",
|
||||
"unknown": "UNKNOWN"
|
||||
},
|
||||
"notifications": {
|
||||
"machineFinishedTitle": "Laundry Ready",
|
||||
"machineFinishedBody": "The machine n°{{number}} is finished and your laundry is ready to pickup",
|
||||
"machineRunningTitle": "Laundry running: {{time}} minutes left",
|
||||
"machineRunningBody": "The machine n°{{number}} is still running"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Small tips",
|
||||
"message": "No need for queues anymore, you will be notified when machines are ready !\n\nIf you have your head in the clouds, you can turn on notifications for your machine by clicking on it.",
|
||||
"ok": "Got it!"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "Campus",
|
||||
"feedTitle": "Campus News",
|
||||
"feed": "Details",
|
||||
"feedLoading": "Loading News",
|
||||
"feedError": "Failed to load news",
|
||||
"dashboard": {
|
||||
"seeMore": "Click to see more",
|
||||
"todayEventsTitle": "Today's events",
|
||||
"todayEventsSubtitleNA": "No events today",
|
||||
"todayEventsSubtitle": " event coming today",
|
||||
"todayEventsSubtitlePlural": " events coming today"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Welcome, you!",
|
||||
"message": "Login to your Amicale account to get access to more services!\n\nYou will still be able to login later.",
|
||||
"login": "Login",
|
||||
"later": "Later"
|
||||
}
|
||||
},
|
||||
"planning": {
|
||||
"title": "Events",
|
||||
"eventDetails": "Event details",
|
||||
"invalidEvent": "Could not find the event. Please make sure the event you are trying to access is valid.",
|
||||
"mascotDialog": {
|
||||
"title": "Let's party!",
|
||||
"message": "And even more! Here you will find every event on the campus.\n\nFrom pancake sales, to the Gala, you will never miss anything!",
|
||||
"button": "Let's go!"
|
||||
}
|
||||
},
|
||||
"planex": {
|
||||
"title": "Planex",
|
||||
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button bellow.",
|
||||
"favorites": "Favorites",
|
||||
"mascotDialog": {
|
||||
"title": "Skipping classes is bad",
|
||||
"message": "Here is Planex! You can set your class and your crush's to favorites in order to find them back easily!\n\nIf you mainly use Campus for Planex, go to the settings to make the app directly start on it!",
|
||||
"ok": "Settings",
|
||||
"cancel": "Later"
|
||||
}
|
||||
},
|
||||
|
||||
"amicaleAbout": {
|
||||
"title": "A question ?",
|
||||
"subtitle": "Ask the Amicale",
|
||||
"message": "You want to revive a club?\nYou want to start a new project?\nHere are al the contacts you need! Do not hesitate to write a mail or send a message to the Amicale's Facebook page!",
|
||||
"roles": {
|
||||
"interSchools": "Inter Schools",
|
||||
"culture": "Culture",
|
||||
"animation": "Animation",
|
||||
"clubs": "Clubs",
|
||||
"event": "Events",
|
||||
"tech": "Technique",
|
||||
"communication": "Communication",
|
||||
"intraSchools": "Alumni / IAT",
|
||||
"publicRelations": "Public Relations"
|
||||
}
|
||||
},
|
||||
"proximo": {
|
||||
"title": "Proximo",
|
||||
"articleList": "Articles",
|
||||
"emptyList": "Empty List",
|
||||
"article": "Article",
|
||||
"articles": "Articles",
|
||||
"sortOrder": "Sort by",
|
||||
"sortName": "Name",
|
||||
"sortNameReverse": "Name (reverse)",
|
||||
"sortPrice": "Price",
|
||||
"sortPriceReverse": "Price (reverse)",
|
||||
"inStock": "in stock",
|
||||
"description": "The Proximo is your small grocery store maintained by students directly on the campus. Open every day from 18h30 to 19h30, we welcome you when you are short on pastas or sodas ! Different products for different problems, everything at cost price. You can pay by Lydia or cash.",
|
||||
"openingHours": "Openning Hours",
|
||||
"paymentMethods": "Payment Methods",
|
||||
"paymentMethodsDescription": "Cash or Lydia",
|
||||
"search": "Search",
|
||||
"all": "All"
|
||||
},
|
||||
"insaAccount": {
|
||||
"title": "INSA Account"
|
||||
},
|
||||
"menu": {
|
||||
"title": "RU Menu"
|
||||
},
|
||||
"websites": {
|
||||
"amicale": "Amicale's website",
|
||||
"rooms": "Available rooms",
|
||||
"bib": "Bib'Box",
|
||||
"mails": "INSA Mails",
|
||||
"ent": "INSA ENT"
|
||||
},
|
||||
|
||||
"login": {
|
||||
"title": "Login",
|
||||
"subtitle": "Please enter your credentials",
|
||||
"email": "Email",
|
||||
"emailError": "Please enter a valid email",
|
||||
"password": "Password",
|
||||
"passwordError": "Please enter a password",
|
||||
"resetPassword": "Forgot Password",
|
||||
"mascotDialog": {
|
||||
"title": "An account?",
|
||||
"message": "An Amicale account allows you to take part in several activities around campus. You can join a club, or even create your own!\n\nLogging into your Amicale account on the app will allow you to see all available clubs on the campus, vote for the upcoming elections, and more to come!\n\nNo Account? Go to the Amicale's building during open hours to create one.",
|
||||
"button": "OK"
|
||||
}
|
||||
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile",
|
||||
"personalInformation": "Personal information",
|
||||
"noData": "No data",
|
||||
"editInformation": "Edit Information",
|
||||
"clubs": "Your clubs",
|
||||
"clubsSubtitle": "Click on a club to show its information",
|
||||
"isMember": "Member",
|
||||
"isManager": "Manager",
|
||||
"membership": "Membership Fee",
|
||||
"membershipSubtitle": "Allows you to take part in various activities",
|
||||
"membershipPayed": "Payed",
|
||||
"membershipNotPayed": "Not payed",
|
||||
"welcomeTitle": "Welcome %{name}!",
|
||||
"welcomeDescription": "This is your Amicale INSA Toulouse personal space. Bellow are the current services you can access thanks to your account. Feels empty? You're right and we plan on fixing that, so stay tuned!",
|
||||
"welcomeFeedback": "We plan on doing more! If you have any suggestions or found bugs, please tell us by clicking the button bellow."
|
||||
},
|
||||
"clubs": {
|
||||
"title": "Clubs",
|
||||
"details": "Club details",
|
||||
"managers": "Managers",
|
||||
"managersSubtitle": "These people make the club live",
|
||||
"managersUnavailable": "This club has no one :(",
|
||||
"categories": "Categories",
|
||||
"categoriesFilterMessage": "Click on a category to filter the list",
|
||||
"clubContact": "Contact the club",
|
||||
"amicaleContact": "Contact the Amicale",
|
||||
"invalidClub": "Could not find the club. Please make sure the club you are trying to access is valid.",
|
||||
"about": {
|
||||
"text": "The clubs, making the campus live, with more than sixty clubs offering various activities! From the philosophy club to the PABI (Production Artisanale de Bière Insaienne), without forgetting the multiple music and dance clubs, you will surely find an activity that suits you!",
|
||||
"title": "A question ?",
|
||||
"subtitle": "Ask the Amicale",
|
||||
"message": "You have a question concerning the clubs?\nYou want to revive or create a club?\nContact the Amicale at the following address:"
|
||||
}
|
||||
},
|
||||
"vote": {
|
||||
"title": "Elections",
|
||||
"noVote": "No vote available",
|
||||
"select": {
|
||||
"title": "Elections open",
|
||||
"subtitle": "Vote now!",
|
||||
"sendButton": "Send Vote",
|
||||
"dialogTitle": "Send Vote?",
|
||||
"dialogTitleLoading": "Sending vote...",
|
||||
"dialogMessage": "Are you sure you want to send your vote? You will not be able to change it."
|
||||
},
|
||||
"tease": {
|
||||
"title": "Elections incoming",
|
||||
"subtitle": "Be ready to vote!",
|
||||
"message": "Vote start:"
|
||||
},
|
||||
"wait": {
|
||||
"titleSubmitted": "Vote submitted!",
|
||||
"titleEnded": "Votes closed",
|
||||
"subtitle": "Waiting for results...",
|
||||
"messageSubmitted": "Vote submitted successfully.",
|
||||
"messageVoted": "Thank you for your participation.",
|
||||
"messageDate": "Results available:",
|
||||
"messageDateUndefined": "Results will be available shortly"
|
||||
},
|
||||
"results": {
|
||||
"title": "Results",
|
||||
"subtitle": "Available until:",
|
||||
"totalVotes": "Total votes:",
|
||||
"votes": "votes"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Why vote?",
|
||||
"message": "The Amicale's elections is the right moment for you to choose the next team, which will handle different projects on the campus, help organizing your favorite events, animate the campus life during the whole year, and relay your ideas to the administration, so that your campus life is the most enjoyable possible!\nYour turn to make a change!\uD83D\uDE09\n\nNote: If there is only one list, it is still important to vote to show your support, so that the administration knows the current list is supported by students. It is always a plus when taking difficult decisions! \uD83D\uDE09",
|
||||
"button": "Ok"
|
||||
}
|
||||
},
|
||||
"equipment": {
|
||||
"title": "Equipment Booking",
|
||||
"book": "Book",
|
||||
"confirm": "Confirmation",
|
||||
"bail": "Bail: %{cost}€",
|
||||
"available": "Available %{date}",
|
||||
"today": "today",
|
||||
"tomorrow": "tomorrow",
|
||||
"thisMonth": "the %{date}",
|
||||
"otherMonth": "the %{date} of %{month}",
|
||||
"otherYear": "the %{date} of %{month} %{year}",
|
||||
"bookingDay": "Booked for %{date}",
|
||||
"bookingPeriod": "Booked from %{begin} to %{end}",
|
||||
"booking": "Click on the calendar to set the start and end dates",
|
||||
"bookButton": "Book selected dates",
|
||||
"dialogTitle": "Confirm booking?",
|
||||
"dialogTitleLoading": "Sending your booking...",
|
||||
"dialogMessage": "Are you sure you want to confirm your booking?\n\nYou will then be able to claim the selected equipment at the Amicale for the duration of your booking in exchange of a bail.",
|
||||
"bookingConfirmedMessage": "Do not forget to come by the Amicale to give your bail in exchange of the equipment.",
|
||||
"mascotDialog": {
|
||||
"title": "How does it work ?",
|
||||
"message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, click the equipment of your choice in the list bellow, enter your lend dates, then come around the Amicale to claim it and give your bail.",
|
||||
"button": "Ok"
|
||||
}
|
||||
},
|
||||
|
||||
"scanner": {
|
||||
"title": "Scanotron 3000",
|
||||
"permissions": {
|
||||
"error": "Scanotron 3000 needs access to the camera in order to scan QR codes.\nThe camera will never be used for any other purpose.",
|
||||
"button": "Grant camera access"
|
||||
},
|
||||
"error": {
|
||||
"title": "QR code invalid",
|
||||
"message": "The QR code scanned could not be recognised, please make sure it is valid."
|
||||
},
|
||||
"help": {
|
||||
"button": "What can I scan?"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Scano...what?",
|
||||
"message": "Scanotron 3000 allows you to scan Campus QR codes, created by clubs or event managers, to get more detailed info!\n\nThe camera will never be used for any other purposes.",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"generalCard": "General",
|
||||
"nightMode": "Night Mode",
|
||||
"nightModeSubOn": "Your eyes are at peace",
|
||||
"nightModeSubOff": "Your eyes are burning",
|
||||
"nightModeAuto": "Follow system dark mode",
|
||||
"nightModeAutoSub": "Follows the mode chosen by your system",
|
||||
"startScreen": "Start Screen",
|
||||
"startScreenSub": "Select which screen to start the app on",
|
||||
"dashboard": "Dashboard",
|
||||
"dashboardSub": "Edit what services to display on the dashboard",
|
||||
"proxiwashNotifReminder": "Machine running reminder",
|
||||
"proxiwashNotifReminderSub": "How many minutes before",
|
||||
"information": "Information",
|
||||
"dashboardEdit": {
|
||||
"title": "Edit dashboard",
|
||||
"message": "The five items above represent your dashboard.\nYou can replace one of its services by selecting it, and then by clicking on the desired new service in the list bellow.",
|
||||
"undo": "Undo changes"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"buttonDesc": "Information about the app and its creator",
|
||||
"appstore": "See on the Appstore",
|
||||
"playstore": "See on the Playstore",
|
||||
"changelog": "Changelog",
|
||||
"license": "License",
|
||||
"debug": "Debug",
|
||||
"team": "Team",
|
||||
"author": "Author and maintainer",
|
||||
"authorMail": "Send an email",
|
||||
"additionalDev": "Thanks",
|
||||
"technologies": "Technologies",
|
||||
"reactNative": "Made with React Native",
|
||||
"libs": "Libraries used"
|
||||
},
|
||||
"feedback": {
|
||||
"title": "Feedback",
|
||||
"bugs": "Report Bugs",
|
||||
"bugsSubtitle": "Did you find a bug? Let us know!",
|
||||
"bugsDescription": "Reporting bugs helps us make the app better. To do so, use one of the buttons below and be as precise as possible when describing your problem!",
|
||||
"feedbackSubtitle": "Let us know what you think!",
|
||||
"feedbackDescription": "Do you have a feature you want to be added/changed/removed, want to give your opinion on the app or simply chat with the dev? Use one of the links below!",
|
||||
"contactMeans": "Using Gitea is recommended, to use it simply login with your INSA account.",
|
||||
"homeButtonTitle": "Feedback/Bug report",
|
||||
"homeButtonSubtitle": "Contact the devs"
|
||||
},
|
||||
"game": {
|
||||
"title": "So Awesome Game",
|
||||
"welcomeTitle": "Welcome !",
|
||||
"welcomeMessage": "Stuck on the toilet? The teacher is late?\nThis game is for you!\n\nTry to get the best score and beat your friends.",
|
||||
"play": "Play!",
|
||||
"score": "Score: %{score}",
|
||||
"highScore": "High score: %{score}",
|
||||
"newHighScore": "New High Score!",
|
||||
"time": "Time:",
|
||||
"level": "Level:",
|
||||
"pause": "Game Paused",
|
||||
"pauseMessage": "You paused, loser",
|
||||
"resume": "Resume",
|
||||
"gameOver": "Game Over",
|
||||
"restart": {
|
||||
"text": "Restart",
|
||||
"confirm": "Are you sure you want to restart?",
|
||||
"confirmMessage": "You will lose you progress, continue?",
|
||||
"confirmYes": "Yes",
|
||||
"confirmNo": "No"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "A secret!",
|
||||
"message": "You found the secret game, awesome ! If you have time to lose, this game is for you.",
|
||||
"button": "Yay!"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"title": "Debug"
|
||||
}
|
||||
},
|
||||
"intro": {
|
||||
"slideMain": {
|
||||
"title": "Welcome to CAMPUS!",
|
||||
"text": "The students app of the INSA Toulouse! Read along to see everything you can do."
|
||||
},
|
||||
"slidePlanex": {
|
||||
"title": "Prettier Planex",
|
||||
"text": "Lookup your and your friends timetable with a mobile friendly Planex!"
|
||||
},
|
||||
"slideEvents": {
|
||||
"title": "Events",
|
||||
"text": "Be aware of any event occurring on the campus, from pancake sales to Enfoiros concerts!"
|
||||
},
|
||||
"slideServices": {
|
||||
"title": "And even more!",
|
||||
"text": "You can do much more with CAMPUS, but I can't explain everything here. Explore the app to find out!"
|
||||
},
|
||||
"slideDone": {
|
||||
"title": "Your feedback is valuable!",
|
||||
"text": "This app is the work of one student (with some help here and there), so your feedback is much appreciated!"
|
||||
},
|
||||
"updateSlide0": {
|
||||
"title": "New in this update!",
|
||||
"text": "Faster than ever and easier to use!\nThis update includes lots of changes to improve your experience.\nUse the brand new feedback button on the home screen to talk to the developer!"
|
||||
},
|
||||
"updateSlide1": {
|
||||
"title": "Improved Planex!",
|
||||
"text": "You now have access to new controls, improved display, and you can also mark groups as favorites."
|
||||
},
|
||||
"updateSlide2": {
|
||||
"title": "Scanotron 3000!",
|
||||
"text": "Say hello to Scanotron 3000!\nAvailable from the Qr-Code button on the home screen, it will help you get information about clubs and events around the campus.\n(Useless right now but we have hope for next year)"
|
||||
},
|
||||
"updateSlide3": {
|
||||
"title": "Amicale Account!",
|
||||
"text": "You can now connect to your Amicale INSAT account from within the app! See all available clubs and more to come!\nClick on the login button from the home screen."
|
||||
},
|
||||
"aprilFoolsSlide": {
|
||||
"title": "New in this update!",
|
||||
"text": "We heard you, you don't like the new design and colors, so we changed them!\nLove."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"title": "Error!",
|
||||
"badCredentials": "Email or password invalid.",
|
||||
"badToken": "You are not logged in. Please login and try again.",
|
||||
"noConsent": "You did not give your consent for data processing to the Amicale.",
|
||||
"tokenSave": "Could not save session token. Please contact support.",
|
||||
"badInput": "Invalid input. Please try again.",
|
||||
"forbidden": "You do not have access to this data.",
|
||||
"connectionError": "Network error. Please check your internet connection.",
|
||||
"serverError": "Server error. Please contact support.",
|
||||
"unknown": "Unknown error. Please contact support."
|
||||
},
|
||||
"dialog": {
|
||||
"ok": "OK",
|
||||
"yes": "Yes",
|
||||
"cancel": "Cancel",
|
||||
"disconnect": {
|
||||
"title": "Disconnect",
|
||||
"titleLoading": "Disconnecting...",
|
||||
"message": "Are you sure you want to disconnect from your Amicale account?"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"loading": "Loading...",
|
||||
"retry": "Retry",
|
||||
"networkError": "Unable to contact servers. Make sure you are connected to Internet.",
|
||||
"goBack": "Go Back",
|
||||
"goForward": "Go Forward",
|
||||
"openInBrowser": "Open in Browser",
|
||||
"notAvailable": "Not available",
|
||||
"listUpdateFail": "Error while updating list"
|
||||
},
|
||||
"date": {
|
||||
"daysOfWeek": {
|
||||
"monday": "Monday",
|
||||
"tuesday": "Tuesday",
|
||||
"wednesday": "Wednesday",
|
||||
"thursday": "Thursday",
|
||||
"friday": "Friday",
|
||||
"saturday": "Saturday",
|
||||
"sunday": "Sunday"
|
||||
},
|
||||
"monthsOfYear": {
|
||||
"january": "January",
|
||||
"february": "February",
|
||||
"march": "March",
|
||||
"april": "April",
|
||||
"may": "May",
|
||||
"june": "June",
|
||||
"july": "July",
|
||||
"august": "August",
|
||||
"september": "September",
|
||||
"october": "October",
|
||||
"november": "November",
|
||||
"december": "December"
|
||||
}
|
||||
}
|
||||
}
|
||||
496
locales/fr.json
Normal file
|
|
@ -0,0 +1,496 @@
|
|||
{
|
||||
"screens": {
|
||||
"services": {
|
||||
"title": "Services",
|
||||
"more": "Clique pour voir plus",
|
||||
"categories": {
|
||||
"amicale": "L' Amicale",
|
||||
"students": "Services étudiants",
|
||||
"insa": "Services de l'INSA",
|
||||
"special": "Proxiwash"
|
||||
},
|
||||
"descriptions": {
|
||||
"clubs": "Tous les clubs et leurs infos",
|
||||
"profile": "Ton profil Amicaliste et tes infos renseignées",
|
||||
"amicaleWebsite": "Voir ce site pour avoir plus d'infos",
|
||||
"vote": "Vote pour les prochaines élections",
|
||||
"proximo": "Regarde le stock du Proximo",
|
||||
"wiketud": "Trouve des infos utiles sur les cours et la vie du campus",
|
||||
"elusEtudiants": "Le site des étudiants en contact avec l'administration",
|
||||
"tutorInsa": "Donne et bénéficie de tutorats par d'autres étudiants",
|
||||
"self": "Regarde le menu du RU",
|
||||
"availableRooms": "Vérifie les salles disponibles",
|
||||
"bib": "Réserve une Bib'Box pour les travaux de groupe",
|
||||
"mails": "Vérifie tes mails INSA",
|
||||
"ent": "Retrouve tes notes",
|
||||
"insaAccount": "Accède à tes infos INSA et modifie ton mot de passe",
|
||||
"equipment": "Réserve un BBQ ou autre matériel",
|
||||
"washers": "Nombre de lave-Linges disponibles",
|
||||
"dryers": "Nombre de sèche-Linges disponibles"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Un peu perdu ?",
|
||||
"message": "Ici c'est le méli-mélo de services trop utiles ! Entre les services des étudiants, et ceux de l'INSA, tu trouveras forcément quelque chose pour toi.\n\nSi en plus tu as un compte Amicale, tu auras encore plus de choix !",
|
||||
"button": "Capish"
|
||||
}
|
||||
},
|
||||
"proxiwash": {
|
||||
"title": "Proxiwash",
|
||||
"dryer": "Sèche-Linge",
|
||||
"dryers": "Sèche-Linges",
|
||||
"washer": "Lave-Linge",
|
||||
"washers": "Lave-Linges",
|
||||
"min": "min",
|
||||
"description": "C'est le service de laverie proposé par Promologis pour les résidences INSA (On t'en voudra pas si tu loges pas sur le campus et que tu fais ta machine ici). Le local situé au pied du R2 avec ses 3 sèche-linges et 9 machines est ouvert 7J/7 24h/24 ! Ici tu peux vérifier leur disponibilité ! Tu peux amener ta lessive, la prendre sur place ou encore mieux l'acheter au Proximo (moins chère qu'à la laverie directement). Tu peux payer par CB ou espèces.",
|
||||
"informationTab": "Informations",
|
||||
"paymentTab": "Paiement",
|
||||
"tariffs": "Tarifs",
|
||||
"washersTariff": "3€ la machine + 0.80€ avec la lessive.",
|
||||
"dryersTariff": "0.35€ pour 5min de sèche linge.",
|
||||
"paymentMethods": "Moyens de Paiement",
|
||||
"paymentMethodsDescription": "Toute monnaie jusqu'à 10€.\nCarte bancaire acceptée.",
|
||||
"washerProcedure": "Déposer le linge dans le tambour sans le tasser et en respectant les charges.\n\nFermer la porte de l'appareil.\n\nSélectionner un programme avec l'une des quatre touches de programme favori standard.\n\nAprès avoir payé à la centrale de commande, appuyer sur le bouton marqué START du lave-linge.\n\nDès que le programme est terminé, l’afficheur indique 'Programme terminé', appuyer sur le bouton jaune d’ouverture du hublot pour récupérer le linge.",
|
||||
"washerTips": "Programme blanc/couleur : 6kg de linge sec (textiles en coton, lin, linge de corps, draps, jeans,serviettes de toilettes).\n\nProgramme nonrepassable : 3,5 kg de linge sec (textiles en fibres synthétiques, cotonet polyester mélangés).\n\nProgramme fin 30°C : 2,5 kg de linge sec (textiles délicats en fibres synthétiques, rayonne).\n\nProgramme laine 30°C : 2,5 kg de linge sec (textiles en laine et lainages lavables).",
|
||||
"dryerProcedure": "Déposer le linge dans le tambour sans le tasser et en respectant les charges.\n\nFermer la porte de l'appareil.\n\nSélectionner un programme avec l'une des quatre touches de programme favori standard.\n\nAprès avoir payé à la centrale de commande, appuyer sur le bouton marqué START du lave-linge.",
|
||||
"dryerTips": "La durée conseillée est de 35 minutes pour 14kg de linge. Vous pouvez choisir une durée plus courte si le sèche-linge n'est pas chargé.",
|
||||
"procedure": "Procédure",
|
||||
"tips": "Conseils",
|
||||
"numAvailable": "disponible",
|
||||
"numAvailablePlural": "disponibles",
|
||||
"modal": {
|
||||
"enableNotifications": "Me Notifier",
|
||||
"disableNotifications": "Désactiver les notifications",
|
||||
"ok": "OK",
|
||||
"cancel": "Annuler",
|
||||
"finished": "Cette machine est terminée. Si tu l'as démarrée, tu peux récupérer ton linge.",
|
||||
"ready": "Cette machine est vide et prête à être utilisée.",
|
||||
"running": "Cette machine a démarré à %{start} et terminera à %{end}.\n\nTemps restant : %{remaining} min.\nProgramme: %{program}",
|
||||
"runningNotStarted": "Cette machine est prête mais n'est pas démarrée. Si c'est la tienne, assure toi de bien avoir appuyé sur le bouton start.",
|
||||
"broken": "Cette machine est hors service. Merci pour ta compréhension.",
|
||||
"error": "Il y a eu une erreur et il est impossible de récupérer les informations de cette machine. Merci de nous excuser pour le gène occasionnée.",
|
||||
"unknown": "Cette machine est dans un état inconnu. Merci de nous excuser pour ce problème.",
|
||||
"notificationErrorTitle": "Erreur",
|
||||
"notificationErrorDescription": "Impossible de créer les notifications. Merci de vérifier que tu as activé les notifications puis redémarre l'appli."
|
||||
},
|
||||
"states": {
|
||||
"finished": "TERMINÉ",
|
||||
"ready": "DISPONIBLE",
|
||||
"running": "EN COURS",
|
||||
"runningNotStarted": "NON DÉMARRÉE",
|
||||
"broken": "HORS SERVICE",
|
||||
"error": "ERREUR",
|
||||
"unknown": "INCONNU"
|
||||
},
|
||||
"notifications": {
|
||||
"machineFinishedTitle": "Linge prêt",
|
||||
"machineFinishedBody": "La machine n°{{number}} est terminée et ton linge est prêt à être récupéré",
|
||||
"machineRunningTitle": "Machine en cours: {{time}} minutes restantes",
|
||||
"machineRunningBody": "La machine n°{{number}} n'est pas encore terminée"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Pour info",
|
||||
"message": "Plus besoin de faire la queue, tu seras informé des machines disponibles !\n\nSi tu es tête en l'air, tu peux activer les notifications pour ta machine en cliquant dessus.",
|
||||
"ok": "Mercé"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "Campus",
|
||||
"feedTitle": "News du Campus",
|
||||
"feed": "Détails",
|
||||
"feedLoading": "Chargement des news",
|
||||
"feedError": "Erreur de chargement des news",
|
||||
"dashboard": {
|
||||
"seeMore": "Clique pour plus d'infos",
|
||||
"todayEventsTitle": "Événements aujourd'hui",
|
||||
"todayEventsSubtitleNA": "Pas d'événement",
|
||||
"todayEventsSubtitle": " événement aujourd'hui",
|
||||
"todayEventsSubtitlePlural": " événements aujourd'hui"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Coucou toi !",
|
||||
"message": "Connecte toi à ton compte Amicale pour profiter de plus de services !\n\nSi tu n'as pas le temps, tu pourras toujours t'y connecter plus tard.",
|
||||
"login": "Se Connecter",
|
||||
"later": "Plus Tard"
|
||||
}
|
||||
},
|
||||
"planning": {
|
||||
"title": "Événements",
|
||||
"eventDetails": "Détails",
|
||||
"invalidEvent": "Impossible de trouver l'événement. Merci de vérifier que l'événement que tu veux voir est valide.",
|
||||
"mascotDialog": {
|
||||
"title": "Yay des soirées !",
|
||||
"message": "Et pas que ! Ici tu pourras voir tous les évents du campus.\n\nDe la vente de crêpes à la soirée du Gala, tu ne manqueras rien !",
|
||||
"button": "Zé parti !"
|
||||
}
|
||||
},
|
||||
"planex": {
|
||||
"title": "Planex",
|
||||
"noGroupSelected": "Pas de groupe sélectionné. Choisis un groupe avec le beau bouton rouge ci-dessous.",
|
||||
"favorites": "Favoris",
|
||||
"mascotDialog": {
|
||||
"title": "Sécher c'est mal",
|
||||
"message": "Ici c'est Planex ! Tu peux mettre en favoris ta classe et celle de ton crush pour l'espio... les retrouver facilement !\n\nSi tu utilises Campus surtout pour Planex, vas dans les paramètres pour faire démarrer l'appli direct dessus !",
|
||||
"ok": "Paramètres",
|
||||
"cancel": "Plus tard"
|
||||
}
|
||||
},
|
||||
|
||||
"amicaleAbout": {
|
||||
"title": "Une Question ?",
|
||||
"subtitle": "Pose tes questions à l'Amicale",
|
||||
"message": "Tu veux reprendre un club ?\nTu veux te lancer dans un projet ?\n\nVoici tous les contacts de l'amicale ! N'hésite pas à nous écrire par mail ou sur la page facebook de l'Amicale !",
|
||||
"roles": {
|
||||
"interSchools": "Inter Écoles",
|
||||
"culture": "Culture",
|
||||
"animation": "Animation",
|
||||
"clubs": "Clubs",
|
||||
"event": "Événements",
|
||||
"tech": "Technique",
|
||||
"communication": "Communication",
|
||||
"intraSchools": "Alumni / IAT",
|
||||
"publicRelations": "Relations Publiques"
|
||||
}
|
||||
},
|
||||
"proximo": {
|
||||
"title": "Proximo",
|
||||
"articleList": "Articles",
|
||||
"emptyList": "Liste Vide",
|
||||
"article": "Article",
|
||||
"articles": "Articles",
|
||||
"sortOrder": "Trier par :",
|
||||
"sortName": "Nom",
|
||||
"sortNameReverse": "Nom (inversé)",
|
||||
"sortPrice": "Prix",
|
||||
"sortPriceReverse": "Prix (inversé)",
|
||||
"inStock": "en stock",
|
||||
"description": "Le Proximo c’est ta petite épicerie étudiante tenue par les étudiants directement sur le campus. Ouverte tous les jours de 18h30 à 19h30, nous t’accueillons et te sauvons quand tu n’as plus de pâtes ou de diluant ! Différents produits pour différentes galères, le tout à prix coûtant. Tu peux payer par Lydia ou par espèce.",
|
||||
"openingHours": "Horaires d'ouverture",
|
||||
"paymentMethods": "Moyens de Paiement",
|
||||
"paymentMethodsDescription": "Espèce ou Lydia",
|
||||
"search": "Rechercher",
|
||||
"all": "Tout"
|
||||
},
|
||||
"insaAccount": {
|
||||
"title": "Compte INSA"
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu du RU"
|
||||
},
|
||||
"websites": {
|
||||
"amicale": "Site de l'Amicale",
|
||||
"rooms": "Salles disponibles",
|
||||
"bib": "Bib'Box",
|
||||
"mails": "Mails INSA",
|
||||
"ent": "ENT INSA"
|
||||
},
|
||||
|
||||
"login": {
|
||||
"title": "Connexion",
|
||||
"subtitle": "Entre tes identifiants",
|
||||
"email": "Email",
|
||||
"emailError": "Merci d'entrer un email valide",
|
||||
"password": "Mot de passe",
|
||||
"passwordError": "Merci d'entrer un mot de passe",
|
||||
"resetPassword": "Mdp oublié",
|
||||
"mascotDialog": {
|
||||
"title": "Un compte ?",
|
||||
"message": "Un compte Amicale te donne la possibilité de participer à diverses activités sur le campus. tu peux rejoindre des clubs ou même créer le tiens !\n\nTe connecter à ton compte Amicale sur l'appli te permettra de voir tous les clubs en activité, de réserver du matériel, de voter pour les prochaines élections, et plus à venir !\n\nPas de compte ? Passe à l'Amicale pendant une perm pour en créer un.",
|
||||
"button": "Dac"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profil",
|
||||
"personalInformation": "Informations Personnelles",
|
||||
"noData": "Pas de données",
|
||||
"editInformation": "Modifier les informations",
|
||||
"clubs": "Tes clubs",
|
||||
"clubsSubtitle": "Clique sur un club pour afficher ses informations",
|
||||
"isMember": "Membre",
|
||||
"isManager": "Responsable",
|
||||
"membership": "Cotisation",
|
||||
"membershipSubtitle": "Permet de participer à diverses activités",
|
||||
"membershipPayed": "Payée",
|
||||
"membershipNotPayed": "Non payée :(",
|
||||
"welcomeTitle": "Bonjour %{name} !",
|
||||
"welcomeDescription": "Ceci est ton espace personnel Amicale INSA Toulouse. Tu trouveras ci-dessous les services disponibles avec ton compte. Un peu vide ? Tu as raison et on va essayer de corriger ça, donc reste à jour !",
|
||||
"welcomeFeedback": "Nous allons essayer de proposer plus de services ! Si tu as des suggestions, ou as trouvé des bugs, merci de nous contacter avec le bouton ci-dessous."
|
||||
},
|
||||
"clubs": {
|
||||
"title": "Liste des Clubs",
|
||||
"details": "Détails",
|
||||
"managers": "Responsables",
|
||||
"managersSubtitle": "Ces personnes font vivre le club",
|
||||
"managersUnavailable": "Ce club est tout seul :(",
|
||||
"categories": "Catégories",
|
||||
"categoriesFilterMessage": "Clique sur une catégorie pour filtrer la liste",
|
||||
"clubContact": "Contacter le club",
|
||||
"amicaleContact": "Contacter l'Amicale",
|
||||
"invalidClub": "Impossible de trouver le club. Merci de vérifier que le club que tu veux voir est valide.",
|
||||
"about": {
|
||||
"text": "Les clubs, c'est ce qui fait vivre le campus au quotidien, plus d'une soixantaine de clubs qui proposent des activités diverses et variées ! Du club Philosophie au PABI (Production Artisanale de Bière Insaienne), en passant par les multiples clubs de musique et de danse, tu trouveras forcément une activité qui te permettras de t'épanouir sur le campus !",
|
||||
"title": "Une question ?",
|
||||
"subtitle": "Pose tes questions à l'Amicale",
|
||||
"message": "Tu as des question concernant les clubs ?\nTu veux reprendre ou créer un club ?\n\nContacte les responsables au mail ci-dessous :"
|
||||
}
|
||||
},
|
||||
"vote": {
|
||||
"title": "Élections",
|
||||
"noVote": "Pas de vote en cours",
|
||||
"select": {
|
||||
"title": "Élections ouvertes",
|
||||
"subtitle": "Vote maintenant !",
|
||||
"sendButton": "Envoyer ton vote",
|
||||
"dialogTitle": "Envoyer ton vote ?",
|
||||
"dialogTitleLoading": "Envoi du vote...",
|
||||
"dialogMessage": "Est-tu sûr de vouloir envoyer ton vote ? Tu ne pourras plus le changer."
|
||||
},
|
||||
"tease": {
|
||||
"title": "Les élections arrivent",
|
||||
"subtitle": "Prépare toi à voter !",
|
||||
"message": "Début des votes :"
|
||||
},
|
||||
"wait": {
|
||||
"titleSubmitted": "Vote envoyé !",
|
||||
"titleEnded": "Votes fermés",
|
||||
"subtitle": "Attente des résultats...",
|
||||
"messageSubmitted": "Ton vote a bien été envoyé.",
|
||||
"messageVoted": "Merci pour ta participation.",
|
||||
"messageDate": "Disponibilité des résultats :",
|
||||
"messageDateUndefined": "les résultats seront disponibles sous peu."
|
||||
},
|
||||
"results": {
|
||||
"title": "Résultats",
|
||||
"subtitle": "Disponibles jusqu'à :",
|
||||
"totalVotes": "Nombre total de votes :",
|
||||
"votes": "votes"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Pourquoi voter ?",
|
||||
"message": "Les élections de l'amicale, c'est le moment pour toi de choisir la prochaine équipe qui portera les différents projets du campus, qui soutiendra les organisations de tes événements favoris, qui te proposera des animations tout au long de l'année, et qui poussera tes idées à l’administration pour que la vie de campus soit des plus riches !\nAlors à toi de jouer ! \uD83D\uDE09\n\nNB : Si par cas il n'y a qu'une liste qui se présente, il est important que tout le monde vote, afin qui la liste puisse montrer à l’administration que les INSAiens la soutiennent ! Ça compte toujours pour les décisions difficiles ! \uD83D\uDE09",
|
||||
"button": "Oké"
|
||||
}
|
||||
},
|
||||
"equipment": {
|
||||
"title": "Réservation de Matériel",
|
||||
"book": "Réserver",
|
||||
"confirm": "Confirmation",
|
||||
"bail": "Caution : %{cost}€",
|
||||
"available": "Disponible %{date}",
|
||||
"today": "aujourd'hui",
|
||||
"tomorrow": "demain",
|
||||
"thisMonth": "le %{date}",
|
||||
"otherMonth": "le %{date} %{month}",
|
||||
"otherYear": "le %{date} %{month} %{year}",
|
||||
"bookingDay": "Réservé pour %{date}",
|
||||
"bookingPeriod": "Début : %{begin}\nFin : %{end}",
|
||||
"booking": "Clique sur le calendrier pour choisir les dates de début et de fin du prêt",
|
||||
"bookButton": "Réserver ces dates",
|
||||
"dialogTitle": "Confirmer la réservation ?",
|
||||
"dialogTitleLoading": "Envoi de votre réservation...",
|
||||
"dialogMessage": "Est-tu sûr de confirmer ta réservation ?\n\nTu pourras ensuite passer à l'Amicale récupérer le matériel pour la durée de la réservation en échange d'une caution.",
|
||||
"bookingConfirmedMessage": "N'oublie pas de passer à L'Amicale pour donner la caution en échange du matériel.",
|
||||
"mascotDialog": {
|
||||
"title": "Comme ça marche ?",
|
||||
"message": "L'Amicale met à disposition des étudiants du matériel comme des BBQ, des appareils à raclette et autres. Pour réserver l'un de ces formidables appareils, clique sur celui de ton choix dans la liste, indique les dates du prêt, puis passe à l'Amicale pour le récupérer et donner la caution.",
|
||||
"button": "Merci !"
|
||||
}
|
||||
},
|
||||
|
||||
"scanner": {
|
||||
"title": "Scanotron 3000",
|
||||
"permissions": {
|
||||
"error": "Scanotron 3000 a besoin d'accéder à la caméra pour scanner des QR codes.\n\nLa caméra ne sera jamais utilisée autrement.",
|
||||
"button": "Autoriser l'accès à la caméra"
|
||||
},
|
||||
"error": {
|
||||
"title": "QR code invalide",
|
||||
"message": "Le QR code scannée n'a pas été reconnu. Merci de vérifier sa validité."
|
||||
},
|
||||
"help": {
|
||||
"button": "Quoi scanner ?"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Scano...quoi ?",
|
||||
"message": "Scanotron 3000 te permet de scanner des QR codes Campus, affichés par des clubs ou des respo d'évenements, pour avoir plus d'infos !\n\nL'appareil photo ne sera jamais utilisé pour d'autres raisons.",
|
||||
"button": "Oké"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"generalCard": "Général",
|
||||
"nightMode": "Mode Nuit",
|
||||
"nightModeSubOn": "Tes yeux te remercient",
|
||||
"nightModeSubOff": "Tes yeux brulent",
|
||||
"nightModeAuto": "Mode nuit système",
|
||||
"nightModeAutoSub": "Suit le mode sélectionné par le système",
|
||||
"startScreen": "Écran de démarrage",
|
||||
"startScreenSub": "Choisis l'écran sur lequel démarre Campus",
|
||||
"dashboard": "Dashboard",
|
||||
"dashboardSub": "Choisis les services à afficher sur la dashboard",
|
||||
"proxiwashNotifReminder": "Rappel de machine en cours",
|
||||
"proxiwashNotifReminderSub": "Combien de minutes avant",
|
||||
"information": "Informations",
|
||||
"dashboardEdit": {
|
||||
"title": "Modifier la dashboard",
|
||||
"message": "Les 5 icones ci-dessus représentent ta dashboard.\nTu peux remplacer un de ses services en cliquant dessus, puis en sélectionnant le nouveau service de ton choix dans la liste ci-dessous.",
|
||||
"undo": "Annuler les changements"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "À Propos",
|
||||
"buttonDesc": "Informations sur l'appli et son créateur",
|
||||
"appstore": "Voir sur l'Appstore",
|
||||
"playstore": "Voir sur le Playstore",
|
||||
"changelog": "Changelog",
|
||||
"license": "Licence",
|
||||
"debug": "Debug",
|
||||
"team": "Équipe",
|
||||
"author": "Auteur et mainteneur",
|
||||
"authorMail": "Envoyer un mail",
|
||||
"additionalDev": "Remerciements",
|
||||
"technologies": "Technologies",
|
||||
"reactNative": "Créé avec React Native",
|
||||
"libs": "Librairies utilisées"
|
||||
},
|
||||
"feedback": {
|
||||
"title": "Feedback",
|
||||
"bugs": "Rapporter des Bugs",
|
||||
"bugsSubtitle": "Tu as trouvé un bug ?",
|
||||
"bugsDescription": "Rapporter les bugs m'aide à améliorer l'appli. Pour cela, merci d'utiliser un des boutons ci-dessous et de décrire ton problème le plus précisément possible !",
|
||||
"feedbackSubtitle": "Dis moi ce que tu penses !",
|
||||
"feedbackDescription": "Tu veux voir une fonctionnalité ajoutée/modifiée/supprimée ? Tu veux donner ton opinion sur l'appli ou simplement discuter avec le développeur (c'est moi coucou) ? Utilise un des liens ci-dessous !",
|
||||
"contactMeans": "L'utilisation de Gitea est recommandée, pour l'utiliser, connecte toi avec tes identifiants INSA.",
|
||||
"homeButtonTitle": "Feedback/Bugs",
|
||||
"homeButtonSubtitle": "Contacte le développeur de l'appli"
|
||||
},
|
||||
"game": {
|
||||
"title": "Jeu trop ouf",
|
||||
"welcomeTitle": "Bienvenue !",
|
||||
"welcomeMessage": "Coincé sur les WC ? Le prof est pas là ?\nCe jeu est fait pour toi !\n\nEssaie d'avoir le meilleur score et de battre tes amis.",
|
||||
"play": "Jouer !",
|
||||
"score": "Score : %{score}",
|
||||
"highScore": "Meilleur score : %{score}",
|
||||
"newHighScore": "Meilleur score !",
|
||||
"time": "Temps :",
|
||||
"level": "Niveau :",
|
||||
"pause": "Pause",
|
||||
"pauseMessage": "T'as fait pause, t'es nul",
|
||||
"resume": "Continuer",
|
||||
"gameOver": "Game Over",
|
||||
"restart": {
|
||||
"text": "Redémarrer",
|
||||
"confirm": "T'es sûr de vouloir redémarrer ?",
|
||||
"confirmMessage": "Tout ton progrès sera perdu, continuer ?",
|
||||
"confirmYes": "Oui",
|
||||
"confirmNo": "Oula non"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Un secret !",
|
||||
"message": "Tu as découvert le jeu secret, bravo !\nSi jamais tu as du temps à perdre, ce jeu est là pour toi.",
|
||||
"button": "Youpi !"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"title": "Debug"
|
||||
}
|
||||
},
|
||||
"intro": {
|
||||
"slideMain": {
|
||||
"title": "Bienvenue sur CAMPUS !",
|
||||
"text": "L'appli du campus de l'INSA Toulouse ! Laisse toi guider pour comprendre tout ce que tu peux faire."
|
||||
},
|
||||
"slidePlanex": {
|
||||
"title": "Planex tout beau",
|
||||
"text": "Regarde ton emploi du temps et celui de tes amis avec un Planex adapté mobile !"
|
||||
},
|
||||
"slideEvents": {
|
||||
"title": "Les Events",
|
||||
"text": "Sois au courant de tout ce qui se passe sur le campus, de la vente de crêpes jusqu'aux concerts Enfoiros !"
|
||||
},
|
||||
"slideServices": {
|
||||
"title": "Et plus encore !",
|
||||
"text": "Tu peux faire bien plus avec CAMPUS, mais je n'ai pas le temps de tout dire ici. Balade toi sur l'appli pour tout découvrir !"
|
||||
},
|
||||
"slideDone": {
|
||||
"title": "Ton avis compte !",
|
||||
"text": "Cette appli à été réalisée par un seul étudiant (avec un peu d'aide par-ci par-là), donc tes retours sont les bienvenus !"
|
||||
},
|
||||
"updateSlide0": {
|
||||
"title": "Nouveau dans cette mise à jour !",
|
||||
"text": "Plus rapide que jamais et plus simple à utiliser !\nCette mise à jour contient de nombreux changements pour améliorer votre expérience.\nUtilisez le tout nouveau bouton de Feedback pour parler directement au développeur!"
|
||||
},
|
||||
"updateSlide1": {
|
||||
"title": "Planex tout beau !",
|
||||
"text": "Vous avez maintenant accès à de nouveaux contrôles, un affichage amélioré, et vous pouvez marquer des groupes en favoris."
|
||||
},
|
||||
"updateSlide2": {
|
||||
"title": "Scanotron 3000 !",
|
||||
"text": "Dites bonjour à Scanotron 3000 !\nDisponible depuis le bouton Qr-Code sur le menu principal, il vous aidera à avoir des informations sur les clubs et les événements du campus.\n(Inutile tout de suite mais on verra l'année pro)"
|
||||
},
|
||||
"updateSlide3": {
|
||||
"title": "Compte Amicale !",
|
||||
"text": "Vous pouvez maintenant vous connecter à votre compte Amicale depuis l'appli ! Accédez à la liste des clubs et plus à venir !\nCliquez sur le bouton Se Connecter dans le menu principal."
|
||||
},
|
||||
"aprilFoolsSlide": {
|
||||
"title": "Nouveau dans cette mise à jour !",
|
||||
"text": "Nous vous avons entendu, vous n'aimez pas le nouveau design et couleurs, alors on les as changés !\nLa bise."
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"title": "Erreur !",
|
||||
"badCredentials": "Email ou mot de passe invalide.",
|
||||
"badToken": "Tu n'est pas connecté. Merci de te connecter puis réessayes.",
|
||||
"noConsent": "Tu n'as pas donné ton consentement pour l'utilisation de tes données personnelles.",
|
||||
"tokenSave": "Impossible de sauvegarder le token de session. Merci de contacter le support.",
|
||||
"badInput": "Entrée invalide. Merci de réessayer.",
|
||||
"forbidden": "Tu n'as pas accès à cette information.",
|
||||
"connectionError": "Erreur de réseau. Merci de vérifier ta connexion Internet.",
|
||||
"serverError": "Erreur de serveur. Merci de contacter le support.",
|
||||
"unknown": "Erreur inconnue. Merci de contacter le support."
|
||||
},
|
||||
"dialog": {
|
||||
"ok": "OK",
|
||||
"yes": "Oui",
|
||||
"cancel": "Annuler",
|
||||
"disconnect": {
|
||||
"title": "Déconnexion",
|
||||
"titleLoading": "Déconnexion...",
|
||||
"message": "Veux-tu vraiment te déconnecter de ton compte Amicale ?"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"loading": "Chargement...",
|
||||
"retry": "Réessayer",
|
||||
"networkError": "Impossible de contacter les serveurs. Assure-toi d'être connecté à Internet.",
|
||||
"goBack": "Suivant",
|
||||
"goForward": "Précédent",
|
||||
"openInBrowser": "Ouvrir dans le navigateur",
|
||||
"notAvailable": "Non disponible",
|
||||
"listUpdateFail": "Erreur lors de la mise à jour de la liste"
|
||||
},
|
||||
"date": {
|
||||
"daysOfWeek": {
|
||||
"monday": "Lundi",
|
||||
"tuesday": "Mardi",
|
||||
"wednesday": "Mercredi",
|
||||
"thursday": "Jeudi",
|
||||
"friday": "Vendredi",
|
||||
"saturday": "Samedi",
|
||||
"sunday": "Dimanche"
|
||||
},
|
||||
"monthsOfYear": {
|
||||
"january": "Janvier",
|
||||
"february": "Février",
|
||||
"march": "Mars",
|
||||
"april": "Avril",
|
||||
"may": "Mai",
|
||||
"june": "Juin",
|
||||
"july": "Juillet",
|
||||
"august": "Août",
|
||||
"september": "Septembre",
|
||||
"october": "Octobre",
|
||||
"november": "Novembre",
|
||||
"december": "Décembre"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,12 +6,13 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
transformer: {
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
transformer: {
|
||||
// eslint-disable-next-line flowtype/require-return-type
|
||||
getTransformOptions: async () => ({
|
||||
transform: {
|
||||
experimentalImportSupport: false,
|
||||
inlineRequires: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
12418
package-lock.json
generated
Normal file
88
package.json
|
|
@ -1,10 +1,11 @@
|
|||
{
|
||||
"name": "campus",
|
||||
"version": "3.0.7",
|
||||
"version": "4.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "react-native start",
|
||||
"android": "react-native run-android",
|
||||
"android-release": "react-native run-android --variant=release",
|
||||
"ios": "react-native run-ios",
|
||||
"test": "jest",
|
||||
"lint": "eslint ."
|
||||
|
|
@ -19,53 +20,62 @@
|
|||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@nartc/react-native-barcode-mask": "^1.1.9",
|
||||
"@react-native-community/async-storage": "^1.9.0",
|
||||
"@nartc/react-native-barcode-mask": "^1.2.0",
|
||||
"@react-native-community/async-storage": "^1.11.0",
|
||||
"@react-native-community/masked-view": "^0.1.10",
|
||||
"@react-native-community/push-notification-ios": "^1.1.1",
|
||||
"@react-native-community/slider": "^3.0.0",
|
||||
"@react-navigation/bottom-tabs": "^5.3.2",
|
||||
"@react-navigation/native": "^5.2.2",
|
||||
"@react-navigation/stack": "^5.2.17",
|
||||
"i18n-js": "^3.3.0",
|
||||
"react": "16.11.0",
|
||||
"react-native": "0.62.2",
|
||||
"@react-native-community/push-notification-ios": "^1.4.0",
|
||||
"@react-native-community/slider": "^3.0.3",
|
||||
"@react-navigation/bottom-tabs": "5.7.3",
|
||||
"@react-navigation/native": "5.7.2",
|
||||
"@react-navigation/stack": "5.8.0",
|
||||
"i18n-js": "^3.7.1",
|
||||
"react": "16.13.1",
|
||||
"react-native": "0.63.2",
|
||||
"react-native-animatable": "^1.3.3",
|
||||
"react-native-app-intro-slider": "^4.0.0",
|
||||
"react-native-appearance": "~0.3.3",
|
||||
"react-native-app-intro-slider": "^4.0.4",
|
||||
"react-native-appearance": "^0.3.4",
|
||||
"react-native-autolink": "^3.0.0",
|
||||
"react-native-calendars": "^1.260.0",
|
||||
"react-native-camera": "^3.23.1",
|
||||
"react-native-collapsible": "^1.5.2",
|
||||
"react-native-gesture-handler": "~1.6.0",
|
||||
"react-native-image-modal": "^1.0.6",
|
||||
"react-native-keychain": "^6.0.0",
|
||||
"react-native-calendars": "^1.308.0",
|
||||
"react-native-camera": "^3.35.0",
|
||||
"react-native-collapsible": "^1.5.3",
|
||||
"react-native-gesture-handler": "^1.7.0",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
"react-native-keychain": "^6.1.1",
|
||||
"react-native-linear-gradient": "^2.5.6",
|
||||
"react-native-localize": "^1.4.0",
|
||||
"react-native-modalize": "^2.0.4",
|
||||
"react-native-paper": "^3.10.1",
|
||||
"react-native-permissions": "^2.1.4",
|
||||
"react-native-push-notification": "^3.3.1",
|
||||
"react-native-reanimated": "^1.8.0",
|
||||
"react-native-render-html": "^4.1.2",
|
||||
"react-native-localize": "^1.4.1",
|
||||
"react-native-modalize": "^2.0.5",
|
||||
"react-native-paper": "^4.0.1",
|
||||
"react-native-permissions": "^2.1.5",
|
||||
"react-native-push-notification": "^5.0.1",
|
||||
"react-native-reanimated": "^1.10.2",
|
||||
"react-native-render-html": "^4.2.2",
|
||||
"react-native-safe-area-context": "0.7.3",
|
||||
"react-native-screens": "^2.7.0",
|
||||
"react-native-screens": "^2.10.1",
|
||||
"react-native-splash-screen": "^3.2.0",
|
||||
"react-native-vector-icons": "^6.6.0",
|
||||
"react-native-webview": "^9.4.0",
|
||||
"react-navigation-collapsible": "^5.6.0",
|
||||
"react-navigation-header-buttons": "^4.0.2"
|
||||
"react-native-vector-icons": "^7.0.0",
|
||||
"react-native-webview": "^10.4.0",
|
||||
"react-navigation-collapsible": "^5.6.4",
|
||||
"react-navigation-header-buttons": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.6.2",
|
||||
"@babel/runtime": "^7.6.2",
|
||||
"@react-native-community/eslint-config": "^0.0.5",
|
||||
"babel-jest": "^25.5.1",
|
||||
"eslint": "^6.5.1",
|
||||
"@babel/core": "^7.11.0",
|
||||
"@babel/preset-flow": "^7.10.4",
|
||||
"@babel/runtime": "^7.11.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^25.1.0",
|
||||
"eslint": "^7.2.0",
|
||||
"eslint-config-airbnb": "^18.2.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-flowtype": "^5.2.0",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-react": "^7.20.5",
|
||||
"eslint-plugin-react-hooks": "^4.0.0",
|
||||
"flow-bin": "^0.123.0",
|
||||
"jest": "^25.5.3",
|
||||
"jest": "^25.1.0",
|
||||
"jest-extended": "^0.11.5",
|
||||
"metro-react-native-babel-preset": "^0.58.0",
|
||||
"react-test-renderer": "16.11.0"
|
||||
"metro-react-native-babel-preset": "^0.59.0",
|
||||
"prettier": "2.0.5",
|
||||
"react-test-renderer": "16.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,219 +1,205 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import ConnectionManager from "../../managers/ConnectionManager";
|
||||
import {ERROR_TYPE} from "../../utils/WebData";
|
||||
import ErrorView from "../Screens/ErrorView";
|
||||
import BasicLoadingScreen from "../Screens/BasicLoadingScreen";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import type {ApiGenericDataType} from '../../utils/WebData';
|
||||
import {ERROR_TYPE} from '../../utils/WebData';
|
||||
import ErrorView from '../Screens/ErrorView';
|
||||
import BasicLoadingScreen from '../Screens/BasicLoadingScreen';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
requests: Array<{
|
||||
link: string,
|
||||
params: Object,
|
||||
mandatory: boolean
|
||||
}>,
|
||||
renderFunction: (Array<{ [key: string]: any } | null>) => React.Node,
|
||||
errorViewOverride?: Array<{
|
||||
errorCode: number,
|
||||
message: string,
|
||||
icon: string,
|
||||
showRetryButton: boolean
|
||||
}>,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
requests: Array<{
|
||||
link: string,
|
||||
params: {...},
|
||||
mandatory: boolean,
|
||||
}>,
|
||||
renderFunction: (Array<ApiGenericDataType | null>) => React.Node,
|
||||
errorViewOverride?: Array<{
|
||||
errorCode: number,
|
||||
message: string,
|
||||
icon: string,
|
||||
showRetryButton: boolean,
|
||||
}> | null,
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean,
|
||||
}
|
||||
type StateType = {
|
||||
loading: boolean,
|
||||
};
|
||||
|
||||
class AuthenticatedScreen extends React.Component<Props, State> {
|
||||
class AuthenticatedScreen extends React.Component<PropsType, StateType> {
|
||||
static defaultProps = {
|
||||
errorViewOverride: null,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
currentUserToken: string | null;
|
||||
|
||||
connectionManager: ConnectionManager;
|
||||
|
||||
errors: Array<number>;
|
||||
|
||||
fetchedData: Array<ApiGenericDataType | null>;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
};
|
||||
this.connectionManager = ConnectionManager.getInstance();
|
||||
props.navigation.addListener('focus', this.onScreenFocus);
|
||||
this.fetchedData = new Array(props.requests.length);
|
||||
this.errors = new Array(props.requests.length);
|
||||
}
|
||||
|
||||
currentUserToken: string | null;
|
||||
connectionManager: ConnectionManager;
|
||||
errors: Array<number>;
|
||||
fetchedData: Array<{ [key: string]: any } | null>;
|
||||
/**
|
||||
* Refreshes screen if user changed
|
||||
*/
|
||||
onScreenFocus = () => {
|
||||
if (this.currentUserToken !== this.connectionManager.getToken()) {
|
||||
this.currentUserToken = this.connectionManager.getToken();
|
||||
this.fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props: Object) {
|
||||
super(props);
|
||||
this.connectionManager = ConnectionManager.getInstance();
|
||||
this.props.navigation.addListener('focus', this.onScreenFocus);
|
||||
this.fetchedData = new Array(this.props.requests.length);
|
||||
this.errors = new Array(this.props.requests.length);
|
||||
/**
|
||||
* Callback used when a request finishes, successfully or not.
|
||||
* Saves data and error code.
|
||||
* If the token is invalid, logout the user and open the login screen.
|
||||
* If the last request was received, stop the loading screen.
|
||||
*
|
||||
* @param data The data fetched from the server
|
||||
* @param index The index for the data
|
||||
* @param error The error code received
|
||||
*/
|
||||
onRequestFinished(
|
||||
data: ApiGenericDataType | null,
|
||||
index: number,
|
||||
error?: number,
|
||||
) {
|
||||
const {props} = this;
|
||||
if (index >= 0 && index < props.requests.length) {
|
||||
this.fetchedData[index] = data;
|
||||
this.errors[index] = error != null ? error : ERROR_TYPE.SUCCESS;
|
||||
}
|
||||
// Token expired, logout user
|
||||
if (error === ERROR_TYPE.BAD_TOKEN) this.connectionManager.disconnect();
|
||||
|
||||
if (this.allRequestsFinished()) this.setState({loading: false});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error to render.
|
||||
* Non-mandatory requests are ignored.
|
||||
*
|
||||
*
|
||||
* @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
|
||||
*/
|
||||
getError(): number {
|
||||
const {props} = this;
|
||||
for (let i = 0; i < this.errors.length; i += 1) {
|
||||
if (
|
||||
this.errors[i] !== ERROR_TYPE.SUCCESS &&
|
||||
props.requests[i].mandatory
|
||||
) {
|
||||
return this.errors[i];
|
||||
}
|
||||
}
|
||||
return ERROR_TYPE.SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error view to display in case of error
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getErrorRender(): React.Node {
|
||||
const {props} = this;
|
||||
const errorCode = this.getError();
|
||||
let shouldOverride = false;
|
||||
let override = null;
|
||||
const overrideList = props.errorViewOverride;
|
||||
if (overrideList != null) {
|
||||
for (let i = 0; i < overrideList.length; i += 1) {
|
||||
if (overrideList[i].errorCode === errorCode) {
|
||||
shouldOverride = true;
|
||||
override = overrideList[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes screen if user changed
|
||||
*/
|
||||
onScreenFocus = () => {
|
||||
if (this.currentUserToken !== this.connectionManager.getToken()) {
|
||||
this.currentUserToken = this.connectionManager.getToken();
|
||||
this.fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the data from the server.
|
||||
*
|
||||
* If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
|
||||
*
|
||||
* If the user is logged in, send all requests.
|
||||
*/
|
||||
fetchData = () => {
|
||||
if (!this.state.loading)
|
||||
this.setState({loading: true});
|
||||
if (this.connectionManager.isLoggedIn()) {
|
||||
for (let i = 0; i < this.props.requests.length; i++) {
|
||||
this.connectionManager.authenticatedRequest(
|
||||
this.props.requests[i].link,
|
||||
this.props.requests[i].params)
|
||||
.then((data) => {
|
||||
this.onRequestFinished(data, i, -1);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.onRequestFinished(null, i, error);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < this.props.requests.length; i++) {
|
||||
this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when a request finishes, successfully or not.
|
||||
* Saves data and error code.
|
||||
* If the token is invalid, logout the user and open the login screen.
|
||||
* If the last request was received, stop the loading screen.
|
||||
*
|
||||
* @param data The data fetched from the server
|
||||
* @param index The index for the data
|
||||
* @param error The error code received
|
||||
*/
|
||||
onRequestFinished(data: { [key: string]: any } | null, index: number, error: number) {
|
||||
if (index >= 0 && index < this.props.requests.length) {
|
||||
this.fetchedData[index] = data;
|
||||
this.errors[index] = error;
|
||||
}
|
||||
|
||||
if (error === ERROR_TYPE.BAD_TOKEN) // Token expired, logout user
|
||||
this.connectionManager.disconnect();
|
||||
|
||||
if (this.allRequestsFinished())
|
||||
this.setState({loading: false});
|
||||
if (shouldOverride && override != null) {
|
||||
return (
|
||||
<ErrorView
|
||||
icon={override.icon}
|
||||
message={override.message}
|
||||
showRetryButton={override.showRetryButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
* 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 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;
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error to render.
|
||||
* Non-mandatory requests are ignored.
|
||||
*
|
||||
*
|
||||
* @return {number} The error code or ERROR_TYPE.SUCCESS if no error was found
|
||||
*/
|
||||
getError() {
|
||||
for (let i = 0; i < this.errors.length; i++) {
|
||||
if (this.errors[i] !== 0 && this.props.requests[i].mandatory) {
|
||||
return this.errors[i];
|
||||
}
|
||||
}
|
||||
return ERROR_TYPE.SUCCESS;
|
||||
}
|
||||
/**
|
||||
* Reloads the data, to be called using ref by parent components
|
||||
*/
|
||||
reload() {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the error view to display in case of error
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getErrorRender() {
|
||||
const errorCode = this.getError();
|
||||
let shouldOverride = false;
|
||||
let override = null;
|
||||
const overrideList = this.props.errorViewOverride;
|
||||
if (overrideList != null) {
|
||||
for (let i = 0; i < overrideList.length; i++) {
|
||||
if (overrideList[i].errorCode === errorCode) {
|
||||
shouldOverride = true;
|
||||
override = overrideList[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldOverride && override != null) {
|
||||
return (
|
||||
<ErrorView
|
||||
{...this.props}
|
||||
icon={override.icon}
|
||||
message={override.message}
|
||||
showRetryButton={override.showRetryButton}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ErrorView
|
||||
{...this.props}
|
||||
errorCode={errorCode}
|
||||
onRefresh={this.fetchData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the data, to be called using ref by parent components
|
||||
*/
|
||||
reload() {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
this.state.loading
|
||||
? <BasicLoadingScreen/>
|
||||
: (this.allRequestsValid()
|
||||
? this.props.renderFunction(this.fetchedData)
|
||||
: this.getErrorRender())
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {state, props} = this;
|
||||
if (state.loading) return <BasicLoadingScreen />;
|
||||
if (this.getError() === ERROR_TYPE.SUCCESS)
|
||||
return props.renderFunction(this.fetchedData);
|
||||
return this.getErrorRender();
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthenticatedScreen;
|
||||
|
|
|
|||
|
|
@ -2,45 +2,46 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import i18n from 'i18n-js';
|
||||
import LoadingConfirmDialog from "../Dialogs/LoadingConfirmDialog";
|
||||
import ConnectionManager from "../../managers/ConnectionManager";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
visible: boolean,
|
||||
onDismiss: () => void,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
visible: boolean,
|
||||
onDismiss: () => void,
|
||||
};
|
||||
|
||||
class LogoutDialog extends React.PureComponent<Props> {
|
||||
|
||||
onClickAccept = async () => {
|
||||
return new Promise((resolve) => {
|
||||
ConnectionManager.getInstance().disconnect()
|
||||
.then(() => {
|
||||
this.props.navigation.reset({
|
||||
index: 0,
|
||||
routes: [{name: 'main'}],
|
||||
});
|
||||
this.props.onDismiss();
|
||||
resolve();
|
||||
});
|
||||
class LogoutDialog extends React.PureComponent<PropsType> {
|
||||
onClickAccept = async (): Promise<void> => {
|
||||
const {props} = this;
|
||||
return new Promise((resolve: () => void) => {
|
||||
ConnectionManager.getInstance()
|
||||
.disconnect()
|
||||
.then(() => {
|
||||
props.navigation.reset({
|
||||
index: 0,
|
||||
routes: [{name: 'main'}],
|
||||
});
|
||||
props.onDismiss();
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LoadingConfirmDialog
|
||||
{...this.props}
|
||||
visible={this.props.visible}
|
||||
onDismiss={this.props.onDismiss}
|
||||
onAccept={this.onClickAccept}
|
||||
title={i18n.t("dialog.disconnect.title")}
|
||||
titleLoading={i18n.t("dialog.disconnect.titleLoading")}
|
||||
message={i18n.t("dialog.disconnect.message")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<LoadingConfirmDialog
|
||||
visible={props.visible}
|
||||
onDismiss={props.onDismiss}
|
||||
onAccept={this.onClickAccept}
|
||||
title={i18n.t('dialog.disconnect.title')}
|
||||
titleLoading={i18n.t('dialog.disconnect.titleLoading')}
|
||||
message={i18n.t('dialog.disconnect.message')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LogoutDialog;
|
||||
|
|
|
|||
39
src/components/Amicale/Vote/VoteNotAvailable.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from 'react-native';
|
||||
import {Headline, withTheme} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class VoteNotAvailable extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
}}>
|
||||
<Headline
|
||||
style={{
|
||||
color: props.theme.colors.textDisabled,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{i18n.t('screens.vote.noVote')}
|
||||
</Headline>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(VoteNotAvailable);
|
||||
|
|
@ -1,116 +1,138 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Card, List, ProgressBar, Subheading, withTheme} from "react-native-paper";
|
||||
import {FlatList, StyleSheet} from "react-native";
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
List,
|
||||
ProgressBar,
|
||||
Subheading,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {FlatList, StyleSheet} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {team} from "../../../screens/Amicale/VoteScreen";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {
|
||||
CardTitleIconPropsType,
|
||||
ListIconPropsType,
|
||||
} from '../../../constants/PaperStyles';
|
||||
|
||||
|
||||
type Props = {
|
||||
teams: Array<team>,
|
||||
dateEnd: string,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
|
||||
class VoteResults extends React.Component<Props> {
|
||||
|
||||
totalVotes: number;
|
||||
winnerIds: Array<number>;
|
||||
|
||||
constructor(props) {
|
||||
super();
|
||||
props.teams.sort(this.sortByVotes);
|
||||
this.getTotalVotes(props.teams);
|
||||
this.getWinnerIds(props.teams);
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
sortByVotes = (a: team, b: team) => b.votes - a.votes;
|
||||
|
||||
getTotalVotes(teams: Array<team>) {
|
||||
this.totalVotes = 0;
|
||||
for (let i = 0; i < teams.length; i++) {
|
||||
this.totalVotes += teams[i].votes;
|
||||
}
|
||||
}
|
||||
|
||||
getWinnerIds(teams: Array<team>) {
|
||||
let max = teams[0].votes;
|
||||
this.winnerIds = [];
|
||||
for (let i = 0; i < teams.length; i++) {
|
||||
if (teams[i].votes === max)
|
||||
this.winnerIds.push(teams[i].id);
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
voteKeyExtractor = (item: team) => item.id.toString();
|
||||
|
||||
resultRenderItem = ({item}: { item: team }) => {
|
||||
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
|
||||
const isDraw = this.winnerIds.length > 1;
|
||||
const colors = this.props.theme.colors;
|
||||
return (
|
||||
<Card style={{
|
||||
marginTop: 10,
|
||||
elevation: isWinner ? 5 : 3,
|
||||
}}>
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={item.votes + ' ' + i18n.t('voteScreen.results.votes')}
|
||||
left={props => isWinner
|
||||
? <List.Icon {...props} icon={isDraw ? "trophy-outline" : "trophy"} color={colors.primary}/>
|
||||
: null}
|
||||
titleStyle={{
|
||||
color: isWinner
|
||||
? colors.primary
|
||||
: colors.text
|
||||
}}
|
||||
style={{padding: 0}}
|
||||
/>
|
||||
<ProgressBar progress={item.votes / this.totalVotes} color={colors.primary}/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('voteScreen.results.title')}
|
||||
subtitle={i18n.t('voteScreen.results.subtitle') + ' ' + this.props.dateEnd}
|
||||
left={(props) => <Avatar.Icon
|
||||
{...props}
|
||||
icon={"podium-gold"}
|
||||
/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Subheading>{i18n.t('voteScreen.results.totalVotes') + ' ' + this.totalVotes}</Subheading>
|
||||
{/*$FlowFixMe*/}
|
||||
<FlatList
|
||||
data={this.props.teams}
|
||||
keyExtractor={this.voteKeyExtractor}
|
||||
renderItem={this.resultRenderItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
type PropsType = {
|
||||
teams: Array<VoteTeamType>,
|
||||
dateEnd: string,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
class VoteResults extends React.Component<PropsType> {
|
||||
totalVotes: number;
|
||||
|
||||
winnerIds: Array<number>;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super();
|
||||
props.teams.sort(this.sortByVotes);
|
||||
this.getTotalVotes(props.teams);
|
||||
this.getWinnerIds(props.teams);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
getTotalVotes(teams: Array<VoteTeamType>) {
|
||||
this.totalVotes = 0;
|
||||
for (let i = 0; i < teams.length; i += 1) {
|
||||
this.totalVotes += teams[i].votes;
|
||||
}
|
||||
}
|
||||
|
||||
getWinnerIds(teams: Array<VoteTeamType>) {
|
||||
const max = teams[0].votes;
|
||||
this.winnerIds = [];
|
||||
for (let i = 0; i < teams.length; i += 1) {
|
||||
if (teams[i].votes === max) this.winnerIds.push(teams[i].id);
|
||||
else break;
|
||||
}
|
||||
}
|
||||
|
||||
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
|
||||
|
||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||
|
||||
resultRenderItem = ({item}: {item: VoteTeamType}): React.Node => {
|
||||
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
|
||||
const isDraw = this.winnerIds.length > 1;
|
||||
const {props} = this;
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
marginTop: 10,
|
||||
elevation: isWinner ? 5 : 3,
|
||||
}}>
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
|
||||
left={(iconProps: ListIconPropsType): React.Node =>
|
||||
isWinner ? (
|
||||
<List.Icon
|
||||
style={iconProps.style}
|
||||
icon={isDraw ? 'trophy-outline' : 'trophy'}
|
||||
color={props.theme.colors.primary}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
titleStyle={{
|
||||
color: isWinner
|
||||
? props.theme.colors.primary
|
||||
: props.theme.colors.text,
|
||||
}}
|
||||
style={{padding: 0}}
|
||||
/>
|
||||
<ProgressBar
|
||||
progress={item.votes / this.totalVotes}
|
||||
color={props.theme.colors.primary}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.vote.results.title')}
|
||||
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
|
||||
props.dateEnd
|
||||
}`}
|
||||
left={(iconProps: CardTitleIconPropsType): React.Node => (
|
||||
<Avatar.Icon size={iconProps.size} icon="podium-gold" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Subheading>{`${i18n.t('screens.vote.results.totalVotes')} ${
|
||||
this.totalVotes
|
||||
}`}</Subheading>
|
||||
{/* $FlowFixMe */}
|
||||
<FlatList
|
||||
data={props.teams}
|
||||
keyExtractor={this.voteKeyExtractor}
|
||||
renderItem={this.resultRenderItem}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(VoteResults);
|
||||
|
|
|
|||
|
|
@ -1,137 +1,147 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Button, Card, RadioButton} from "react-native-paper";
|
||||
import {FlatList, StyleSheet, View} from "react-native";
|
||||
import ConnectionManager from "../../../managers/ConnectionManager";
|
||||
import LoadingConfirmDialog from "../../Dialogs/LoadingConfirmDialog";
|
||||
import ErrorDialog from "../../Dialogs/ErrorDialog";
|
||||
import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
|
||||
import {FlatList, StyleSheet, View} from 'react-native';
|
||||
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';
|
||||
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
|
||||
|
||||
type Props = {
|
||||
teams: Array<team>,
|
||||
onVoteSuccess: () => void,
|
||||
onVoteError: () => void,
|
||||
}
|
||||
type PropsType = {
|
||||
teams: Array<VoteTeamType>,
|
||||
onVoteSuccess: () => void,
|
||||
onVoteError: () => void,
|
||||
};
|
||||
|
||||
type State = {
|
||||
selectedTeam: string,
|
||||
voteDialogVisible: boolean,
|
||||
errorDialogVisible: boolean,
|
||||
currentError: number,
|
||||
}
|
||||
|
||||
|
||||
export default class VoteSelect extends React.PureComponent<Props, State> {
|
||||
|
||||
state = {
|
||||
selectedTeam: "none",
|
||||
voteDialogVisible: false,
|
||||
errorDialogVisible: false,
|
||||
currentError: 0,
|
||||
};
|
||||
|
||||
onVoteSelectionChange = (team: string) => this.setState({selectedTeam: team});
|
||||
|
||||
voteKeyExtractor = (item: team) => item.id.toString();
|
||||
|
||||
voteRenderItem = ({item}: { item: team }) => <RadioButton.Item label={item.name} value={item.id.toString()}/>;
|
||||
|
||||
showVoteDialog = () => this.setState({voteDialogVisible: true});
|
||||
|
||||
onVoteDialogDismiss = () => this.setState({voteDialogVisible: false,});
|
||||
|
||||
onVoteDialogAccept = async () => {
|
||||
return new Promise((resolve) => {
|
||||
ConnectionManager.getInstance().authenticatedRequest(
|
||||
"elections/vote",
|
||||
{"team": parseInt(this.state.selectedTeam)})
|
||||
.then(() => {
|
||||
this.onVoteDialogDismiss();
|
||||
this.props.onVoteSuccess();
|
||||
resolve();
|
||||
})
|
||||
.catch((error: number) => {
|
||||
this.onVoteDialogDismiss();
|
||||
this.showErrorDialog(error);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showErrorDialog = (error: number) => this.setState({
|
||||
errorDialogVisible: true,
|
||||
currentError: error,
|
||||
});
|
||||
|
||||
onErrorDialogDismiss = () => {
|
||||
this.setState({errorDialogVisible: false});
|
||||
this.props.onVoteError();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('voteScreen.select.title')}
|
||||
subtitle={i18n.t('voteScreen.select.subtitle')}
|
||||
left={(props) =>
|
||||
<Avatar.Icon
|
||||
{...props}
|
||||
icon={"alert-decagram"}
|
||||
/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<RadioButton.Group
|
||||
onValueChange={this.onVoteSelectionChange}
|
||||
value={this.state.selectedTeam}
|
||||
>
|
||||
{/*$FlowFixMe*/}
|
||||
<FlatList
|
||||
data={this.props.teams}
|
||||
keyExtractor={this.voteKeyExtractor}
|
||||
extraData={this.state.selectedTeam}
|
||||
renderItem={this.voteRenderItem}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
onPress={this.showVoteDialog}
|
||||
style={{marginLeft: 'auto'}}
|
||||
disabled={this.state.selectedTeam === "none"}
|
||||
>
|
||||
{i18n.t('voteScreen.select.sendButton')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
<LoadingConfirmDialog
|
||||
visible={this.state.voteDialogVisible}
|
||||
onDismiss={this.onVoteDialogDismiss}
|
||||
onAccept={this.onVoteDialogAccept}
|
||||
title={i18n.t('voteScreen.select.dialogTitle')}
|
||||
titleLoading={i18n.t('voteScreen.select.dialogTitleLoading')}
|
||||
message={i18n.t('voteScreen.select.dialogMessage')}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={this.state.errorDialogVisible}
|
||||
onDismiss={this.onErrorDialogDismiss}
|
||||
errorCode={this.state.currentError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
type StateType = {
|
||||
selectedTeam: string,
|
||||
voteDialogVisible: boolean,
|
||||
errorDialogVisible: boolean,
|
||||
currentError: number,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default class VoteSelect extends React.PureComponent<
|
||||
PropsType,
|
||||
StateType,
|
||||
> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
selectedTeam: 'none',
|
||||
voteDialogVisible: false,
|
||||
errorDialogVisible: false,
|
||||
currentError: 0,
|
||||
};
|
||||
}
|
||||
|
||||
onVoteSelectionChange = (teamName: string): void =>
|
||||
this.setState({selectedTeam: teamName});
|
||||
|
||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||
|
||||
voteRenderItem = ({item}: {item: VoteTeamType}): React.Node => (
|
||||
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
||||
);
|
||||
|
||||
showVoteDialog = (): void => this.setState({voteDialogVisible: true});
|
||||
|
||||
onVoteDialogDismiss = (): void => this.setState({voteDialogVisible: false});
|
||||
|
||||
onVoteDialogAccept = async (): Promise<void> => {
|
||||
return new Promise((resolve: () => void) => {
|
||||
const {state} = this;
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest('elections/vote', {
|
||||
team: parseInt(state.selectedTeam, 10),
|
||||
})
|
||||
.then(() => {
|
||||
this.onVoteDialogDismiss();
|
||||
const {props} = this;
|
||||
props.onVoteSuccess();
|
||||
resolve();
|
||||
})
|
||||
.catch((error: number) => {
|
||||
this.onVoteDialogDismiss();
|
||||
this.showErrorDialog(error);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showErrorDialog = (error: number): void =>
|
||||
this.setState({
|
||||
errorDialogVisible: true,
|
||||
currentError: error,
|
||||
});
|
||||
|
||||
onErrorDialogDismiss = () => {
|
||||
this.setState({errorDialogVisible: false});
|
||||
const {props} = this;
|
||||
props.onVoteError();
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {state, props} = this;
|
||||
return (
|
||||
<View>
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.vote.select.title')}
|
||||
subtitle={i18n.t('screens.vote.select.subtitle')}
|
||||
left={(iconProps: CardTitleIconPropsType): React.Node => (
|
||||
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<RadioButton.Group
|
||||
onValueChange={this.onVoteSelectionChange}
|
||||
value={state.selectedTeam}>
|
||||
{/* $FlowFixMe */}
|
||||
<FlatList
|
||||
data={props.teams}
|
||||
keyExtractor={this.voteKeyExtractor}
|
||||
extraData={state.selectedTeam}
|
||||
renderItem={this.voteRenderItem}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
onPress={this.showVoteDialog}
|
||||
style={{marginLeft: 'auto'}}
|
||||
disabled={state.selectedTeam === 'none'}>
|
||||
{i18n.t('screens.vote.select.sendButton')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
<LoadingConfirmDialog
|
||||
visible={state.voteDialogVisible}
|
||||
onDismiss={this.onVoteDialogDismiss}
|
||||
onAccept={this.onVoteDialogAccept}
|
||||
title={i18n.t('screens.vote.select.dialogTitle')}
|
||||
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
||||
message={i18n.t('screens.vote.select.dialogMessage')}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={state.errorDialogVisible}
|
||||
onDismiss={this.onErrorDialogDismiss}
|
||||
errorCode={state.currentError}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,45 +1,46 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Card, Paragraph} from "react-native-paper";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Avatar, Card, Paragraph} from 'react-native-paper';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
|
||||
|
||||
type Props = {
|
||||
startDate: string,
|
||||
}
|
||||
|
||||
export default class VoteTease extends React.Component<Props> {
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('voteScreen.tease.title')}
|
||||
subtitle={i18n.t('voteScreen.tease.subtitle')}
|
||||
left={props => <Avatar.Icon
|
||||
{...props}
|
||||
icon="vote"/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>
|
||||
{i18n.t('voteScreen.tease.message') + ' ' + this.props.startDate}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
type PropsType = {
|
||||
startDate: string,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
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={(iconProps: CardTitleIconPropsType): React.Node => (
|
||||
<Avatar.Icon size={iconProps.size} icon="vote" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>
|
||||
{`${i18n.t('screens.vote.tease.message')} ${props.startDate}`}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Card, Paragraph} from "react-native-paper";
|
||||
import {StyleSheet} from "react-native";
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
const ICON_AMICALE = require('../../../../assets/amicale.png');
|
||||
|
||||
export default class VoteTitle extends React.Component<{}> {
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('voteScreen.title.title')}
|
||||
subtitle={i18n.t('voteScreen.title.subtitle')}
|
||||
left={(props) => <Avatar.Image
|
||||
{...props}
|
||||
source={ICON_AMICALE}
|
||||
style={styles.icon}
|
||||
/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>
|
||||
{i18n.t('voteScreen.title.paragraph1')}
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
{i18n.t('voteScreen.title.paragraph2')}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
});
|
||||
|
|
@ -1,72 +1,74 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {ActivityIndicator, Card, Paragraph, withTheme} from "react-native-paper";
|
||||
import {StyleSheet} from "react-native";
|
||||
import {Avatar, Card, Paragraph, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
|
||||
|
||||
type Props = {
|
||||
startDate: string | null,
|
||||
justVoted: boolean,
|
||||
hasVoted: boolean,
|
||||
isVoteRunning: boolean,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
|
||||
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('voteScreen.wait.titleSubmitted')
|
||||
: i18n.t('voteScreen.wait.titleEnded')}
|
||||
subtitle={i18n.t('voteScreen.wait.subtitle')}
|
||||
left={(props) => <ActivityIndicator {...props}/>}
|
||||
/>
|
||||
<Card.Content>
|
||||
{
|
||||
this.props.justVoted
|
||||
? <Paragraph style={{color: colors.success}}>
|
||||
{i18n.t('voteScreen.wait.messageSubmitted')}
|
||||
</Paragraph>
|
||||
: null
|
||||
}
|
||||
{
|
||||
this.props.hasVoted
|
||||
? <Paragraph style={{color: colors.success}}>
|
||||
{i18n.t('voteScreen.wait.messageVoted')}
|
||||
</Paragraph>
|
||||
: null
|
||||
}
|
||||
{
|
||||
startDate != null
|
||||
? <Paragraph>
|
||||
{i18n.t('voteScreen.wait.messageDate') + ' ' + startDate}
|
||||
</Paragraph>
|
||||
: <Paragraph>{i18n.t('voteScreen.wait.messageDateUndefined')}</Paragraph>
|
||||
}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
type PropsType = {
|
||||
startDate: string | null,
|
||||
justVoted: boolean,
|
||||
hasVoted: boolean,
|
||||
isVoteRunning: boolean,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
class VoteWait extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {startDate} = props;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={
|
||||
props.isVoteRunning
|
||||
? i18n.t('screens.vote.wait.titleSubmitted')
|
||||
: i18n.t('screens.vote.wait.titleEnded')
|
||||
}
|
||||
subtitle={i18n.t('screens.vote.wait.subtitle')}
|
||||
left={(iconProps: CardTitleIconPropsType): React.Node => (
|
||||
<Avatar.Icon size={iconProps.size} icon="progress-check" />
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
{props.justVoted ? (
|
||||
<Paragraph style={{color: props.theme.colors.success}}>
|
||||
{i18n.t('screens.vote.wait.messageSubmitted')}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{props.hasVoted ? (
|
||||
<Paragraph style={{color: props.theme.colors.success}}>
|
||||
{i18n.t('screens.vote.wait.messageVoted')}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{startDate != null ? (
|
||||
<Paragraph>
|
||||
{`${i18n.t('screens.vote.wait.messageDate')} ${startDate}`}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Paragraph>
|
||||
{i18n.t('screens.vote.wait.messageDateUndefined')}
|
||||
</Paragraph>
|
||||
)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(VoteWait);
|
||||
|
|
|
|||
|
|
@ -1,101 +1,117 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {View} from "react-native";
|
||||
import {View} from 'react-native';
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import Collapsible from "react-native-collapsible";
|
||||
import * as Animatable from "react-native-animatable";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import Collapsible from 'react-native-collapsible';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {ListIconPropsType} from '../../constants/PaperStyles';
|
||||
|
||||
type Props = {
|
||||
theme: CustomTheme,
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
left?: (props: { [keys: string]: any }) => React.Node,
|
||||
opened?: boolean,
|
||||
unmountWhenCollapsed: boolean,
|
||||
children?: React.Node,
|
||||
}
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
title: string,
|
||||
subtitle?: string,
|
||||
left?: () => React.Node,
|
||||
opened?: boolean,
|
||||
unmountWhenCollapsed?: boolean,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
type State = {
|
||||
expanded: boolean,
|
||||
}
|
||||
type StateType = {
|
||||
expanded: boolean,
|
||||
};
|
||||
|
||||
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
|
||||
|
||||
class AnimatedAccordion extends React.Component<Props, State> {
|
||||
class AnimatedAccordion extends React.Component<PropsType, StateType> {
|
||||
static defaultProps = {
|
||||
subtitle: '',
|
||||
left: null,
|
||||
opened: null,
|
||||
unmountWhenCollapsed: false,
|
||||
children: null,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
unmountWhenCollapsed: false,
|
||||
}
|
||||
chevronRef: { current: null | AnimatedListIcon };
|
||||
chevronIcon: string;
|
||||
animStart: string;
|
||||
animEnd: string;
|
||||
chevronRef: {current: null | AnimatedListIcon};
|
||||
|
||||
state = {
|
||||
expanded: this.props.opened != null ? this.props.opened : false,
|
||||
}
|
||||
chevronIcon: string;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.chevronRef = React.createRef();
|
||||
this.setupChevron();
|
||||
}
|
||||
animStart: string;
|
||||
|
||||
setupChevron() {
|
||||
if (this.state.expanded) {
|
||||
this.chevronIcon = "chevron-up";
|
||||
this.animStart = "180deg";
|
||||
this.animEnd = "0deg";
|
||||
} else {
|
||||
this.chevronIcon = "chevron-down";
|
||||
this.animStart = "0deg";
|
||||
this.animEnd = "180deg";
|
||||
}
|
||||
}
|
||||
animEnd: string;
|
||||
|
||||
toggleAccordion = () => {
|
||||
if (this.chevronRef.current != null) {
|
||||
this.chevronRef.current.transitionTo({rotate: this.state.expanded ? this.animStart : this.animEnd});
|
||||
this.setState({expanded: !this.state.expanded})
|
||||
}
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
expanded: props.opened != null ? props.opened : false,
|
||||
};
|
||||
this.chevronRef = React.createRef();
|
||||
this.setupChevron();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
|
||||
if (nextProps.opened != null && nextProps.opened !== this.props.opened)
|
||||
this.state.expanded = nextProps.opened;
|
||||
return true;
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {state, props} = this;
|
||||
if (nextProps.opened != null && nextProps.opened !== props.opened)
|
||||
state.expanded = nextProps.opened;
|
||||
return true;
|
||||
}
|
||||
|
||||
setupChevron() {
|
||||
const {expanded} = this.state;
|
||||
if (expanded) {
|
||||
this.chevronIcon = 'chevron-up';
|
||||
this.animStart = '180deg';
|
||||
this.animEnd = '0deg';
|
||||
} else {
|
||||
this.chevronIcon = 'chevron-down';
|
||||
this.animStart = '0deg';
|
||||
this.animEnd = '180deg';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const colors = this.props.theme.colors;
|
||||
return (
|
||||
<View>
|
||||
<List.Item
|
||||
{...this.props}
|
||||
title={this.props.title}
|
||||
subtitle={this.props.subtitle}
|
||||
titleStyle={this.state.expanded ? {color: colors.primary} : undefined}
|
||||
onPress={this.toggleAccordion}
|
||||
right={(props) => <AnimatedListIcon
|
||||
ref={this.chevronRef}
|
||||
{...props}
|
||||
icon={this.chevronIcon}
|
||||
color={this.state.expanded ? colors.primary : undefined}
|
||||
useNativeDriver
|
||||
/>}
|
||||
left={this.props.left}
|
||||
/>
|
||||
<Collapsible collapsed={!this.state.expanded}>
|
||||
{!this.props.unmountWhenCollapsed || (this.props.unmountWhenCollapsed && this.state.expanded)
|
||||
? this.props.children
|
||||
: null}
|
||||
</Collapsible>
|
||||
</View>
|
||||
);
|
||||
toggleAccordion = () => {
|
||||
const {expanded} = this.state;
|
||||
if (this.chevronRef.current != null) {
|
||||
this.chevronRef.current.transitionTo({
|
||||
rotate: expanded ? this.animStart : this.animEnd,
|
||||
});
|
||||
this.setState((prevState: StateType): {expanded: boolean} => ({
|
||||
expanded: !prevState.expanded,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {props, state} = this;
|
||||
const {colors} = props.theme;
|
||||
return (
|
||||
<View>
|
||||
<List.Item
|
||||
title={props.title}
|
||||
subtitle={props.subtitle}
|
||||
titleStyle={state.expanded ? {color: colors.primary} : null}
|
||||
onPress={this.toggleAccordion}
|
||||
right={(iconProps: ListIconPropsType): React.Node => (
|
||||
<AnimatedListIcon
|
||||
ref={this.chevronRef}
|
||||
style={iconProps.style}
|
||||
icon={this.chevronIcon}
|
||||
color={state.expanded ? colors.primary : iconProps.color}
|
||||
useNativeDriver
|
||||
/>
|
||||
)}
|
||||
left={props.left}
|
||||
/>
|
||||
<Collapsible collapsed={!state.expanded}>
|
||||
{!props.unmountWhenCollapsed ||
|
||||
(props.unmountWhenCollapsed && state.expanded)
|
||||
? props.children
|
||||
: null}
|
||||
</Collapsible>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(AnimatedAccordion);
|
||||
export default withTheme(AnimatedAccordion);
|
||||
|
|
|
|||
|
|
@ -1,170 +1,179 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import {FAB, IconButton, Surface, withTheme} from "react-native-paper";
|
||||
import AutoHideHandler from "../../utils/AutoHideHandler";
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import {FAB, IconButton, Surface, withTheme} from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import CustomTabBar from "../Tabbar/CustomTabBar";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {OnScrollType} from '../../utils/AutoHideHandler';
|
||||
|
||||
const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
onPress: (action: string, data: any) => void,
|
||||
seekAttention: boolean,
|
||||
}
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
onPress: (action: string, data?: string) => void,
|
||||
seekAttention: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
currentMode: string,
|
||||
}
|
||||
type StateType = {
|
||||
currentMode: string,
|
||||
};
|
||||
|
||||
const DISPLAY_MODES = {
|
||||
DAY: "agendaDay",
|
||||
WEEK: "agendaWeek",
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
DAY: 'agendaDay',
|
||||
WEEK: 'agendaWeek',
|
||||
MONTH: 'month',
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
left: '5%',
|
||||
width: '90%',
|
||||
},
|
||||
surface: {
|
||||
position: 'relative',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderRadius: 50,
|
||||
elevation: 2,
|
||||
},
|
||||
fabContainer: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: "center",
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
top: '-25%',
|
||||
}
|
||||
container: {
|
||||
position: 'absolute',
|
||||
left: '5%',
|
||||
width: '90%',
|
||||
},
|
||||
surface: {
|
||||
position: 'relative',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderRadius: 50,
|
||||
elevation: 2,
|
||||
},
|
||||
fabContainer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
alignSelf: 'center',
|
||||
top: '-25%',
|
||||
},
|
||||
});
|
||||
|
||||
class AnimatedBottomBar extends React.Component<PropsType, StateType> {
|
||||
ref: {current: null | Animatable.View};
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,66 +1,63 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet} from "react-native";
|
||||
import {FAB} from "react-native-paper";
|
||||
import AutoHideHandler from "../../utils/AutoHideHandler";
|
||||
import {StyleSheet} from 'react-native';
|
||||
import {FAB} from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import CustomTabBar from "../Tabbar/CustomTabBar";
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import AutoHideHandler from '../../utils/AutoHideHandler';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
icon: string,
|
||||
onPress: () => void,
|
||||
}
|
||||
type PropsType = {
|
||||
icon: string,
|
||||
onPress: () => void,
|
||||
};
|
||||
|
||||
const AnimatedFab = Animatable.createAnimatableComponent(FAB);
|
||||
|
||||
export default class AnimatedFAB extends React.Component<Props> {
|
||||
|
||||
ref: { current: null | Animatable.View };
|
||||
hideHandler: AutoHideHandler;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ref = React.createRef();
|
||||
this.hideHandler = new AutoHideHandler(false);
|
||||
this.hideHandler.addListener(this.onHideChange);
|
||||
}
|
||||
|
||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||
this.hideHandler.onScroll(event);
|
||||
};
|
||||
|
||||
onHideChange = (shouldHide: boolean) => {
|
||||
if (this.ref.current != null) {
|
||||
if (shouldHide)
|
||||
this.ref.current.bounceOutDown(1000);
|
||||
else
|
||||
this.ref.current.bounceInUp(1000);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AnimatedFab
|
||||
ref={this.ref}
|
||||
useNativeDriver
|
||||
icon={this.props.icon}
|
||||
onPress={this.props.onPress}
|
||||
style={{
|
||||
...styles.fab,
|
||||
bottom: CustomTabBar.TAB_BAR_HEIGHT
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
margin: 16,
|
||||
right: 0,
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
margin: 16,
|
||||
right: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default class AnimatedFAB extends React.Component<PropsType> {
|
||||
ref: {current: null | Animatable.View};
|
||||
|
||||
hideHandler: AutoHideHandler;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ref = React.createRef();
|
||||
this.hideHandler = new AutoHideHandler(false);
|
||||
this.hideHandler.addListener(this.onHideChange);
|
||||
}
|
||||
|
||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||
this.hideHandler.onScroll(event);
|
||||
};
|
||||
|
||||
onHideChange = (shouldHide: boolean) => {
|
||||
if (this.ref.current != null) {
|
||||
if (shouldHide) this.ref.current.bounceOutDown(1000);
|
||||
else this.ref.current.bounceInUp(1000);
|
||||
}
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<AnimatedFab
|
||||
ref={this.ref}
|
||||
useNativeDriver
|
||||
icon={props.icon}
|
||||
onPress={props.onPress}
|
||||
style={{
|
||||
...styles.fab,
|
||||
bottom: CustomTabBar.TAB_BAR_HEIGHT,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
59
src/components/Collapsible/CollapsibleComponent.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Collapsible} from 'react-navigation-collapsible';
|
||||
import withCollapsible from '../../utils/withCollapsible';
|
||||
import CustomTabBar from '../Tabbar/CustomTabBar';
|
||||
|
||||
export type CollapsibleComponentPropsType = {
|
||||
children?: React.Node,
|
||||
hasTab?: boolean,
|
||||
onScroll?: (event: SyntheticEvent<EventTarget>) => void,
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
...CollapsibleComponentPropsType,
|
||||
collapsibleStack: Collapsible,
|
||||
// eslint-disable-next-line flowtype/no-weak-types
|
||||
component: any,
|
||||
};
|
||||
|
||||
class CollapsibleComponent extends React.Component<PropsType> {
|
||||
static defaultProps = {
|
||||
children: null,
|
||||
hasTab: false,
|
||||
onScroll: null,
|
||||
};
|
||||
|
||||
onScroll = (event: SyntheticEvent<EventTarget>) => {
|
||||
const {props} = this;
|
||||
if (props.onScroll) props.onScroll(event);
|
||||
};
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const Comp = props.component;
|
||||
const {
|
||||
containerPaddingTop,
|
||||
scrollIndicatorInsetTop,
|
||||
onScrollWithListener,
|
||||
} = props.collapsibleStack;
|
||||
|
||||
return (
|
||||
<Comp
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
onScroll={onScrollWithListener(this.onScroll)}
|
||||
contentContainerStyle={{
|
||||
paddingTop: containerPaddingTop,
|
||||
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
|
||||
minHeight: '100%',
|
||||
}}
|
||||
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
|
||||
{props.children}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withCollapsible(CollapsibleComponent);
|
||||
26
src/components/Collapsible/CollapsibleFlatList.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated} from 'react-native';
|
||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||
import CollapsibleComponent from './CollapsibleComponent';
|
||||
|
||||
type PropsType = {
|
||||
...CollapsibleComponentPropsType,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class CollapsibleFlatList extends React.Component<PropsType> {
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
component={Animated.FlatList}>
|
||||
{props.children}
|
||||
</CollapsibleComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CollapsibleFlatList;
|
||||
26
src/components/Collapsible/CollapsibleScrollView.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated} from 'react-native';
|
||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||
import CollapsibleComponent from './CollapsibleComponent';
|
||||
|
||||
type PropsType = {
|
||||
...CollapsibleComponentPropsType,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class CollapsibleScrollView extends React.Component<PropsType> {
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
component={Animated.ScrollView}>
|
||||
{props.children}
|
||||
</CollapsibleComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CollapsibleScrollView;
|
||||
26
src/components/Collapsible/CollapsibleSectionList.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated} from 'react-native';
|
||||
import type {CollapsibleComponentPropsType} from './CollapsibleComponent';
|
||||
import CollapsibleComponent from './CollapsibleComponent';
|
||||
|
||||
type PropsType = {
|
||||
...CollapsibleComponentPropsType,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class CollapsibleSectionList extends React.Component<PropsType> {
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
component={Animated.SectionList}>
|
||||
{props.children}
|
||||
</CollapsibleComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CollapsibleSectionList;
|
||||
|
|
@ -2,33 +2,32 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
type Props = {
|
||||
visible: boolean,
|
||||
onDismiss: () => void,
|
||||
title: string,
|
||||
message: string,
|
||||
}
|
||||
type PropsType = {
|
||||
visible: boolean,
|
||||
onDismiss: () => void,
|
||||
title: string,
|
||||
message: string,
|
||||
};
|
||||
|
||||
class AlertDialog extends React.PureComponent<Props> {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={this.props.visible}
|
||||
onDismiss={this.props.onDismiss}>
|
||||
<Dialog.Title>{this.props.title}</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Paragraph>{this.props.message}</Paragraph>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions>
|
||||
<Button onPress={this.props.onDismiss}>OK</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
class AlertDialog extends React.PureComponent<PropsType> {
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
|
||||
<Dialog.Title>{props.title}</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Paragraph>{props.message}</Paragraph>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions>
|
||||
<Button onPress={props.onDismiss}>{i18n.t('dialog.ok')}</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AlertDialog;
|
||||
|
|
|
|||
|
|
@ -1,57 +1,71 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import i18n from "i18n-js";
|
||||
import {ERROR_TYPE} from "../../utils/WebData";
|
||||
import AlertDialog from "./AlertDialog";
|
||||
import i18n from 'i18n-js';
|
||||
import {ERROR_TYPE} from '../../utils/WebData';
|
||||
import AlertDialog from './AlertDialog';
|
||||
|
||||
type Props = {
|
||||
visible: boolean,
|
||||
onDismiss: () => void,
|
||||
errorCode: number,
|
||||
}
|
||||
type PropsType = {
|
||||
visible: boolean,
|
||||
onDismiss: () => void,
|
||||
errorCode: number,
|
||||
};
|
||||
|
||||
class ErrorDialog extends React.PureComponent<Props> {
|
||||
class ErrorDialog extends React.PureComponent<PropsType> {
|
||||
title: string;
|
||||
|
||||
title: string;
|
||||
message: string;
|
||||
message: string;
|
||||
|
||||
generateMessage() {
|
||||
this.title = i18n.t("errors.title");
|
||||
switch (this.props.errorCode) {
|
||||
case ERROR_TYPE.BAD_CREDENTIALS:
|
||||
this.message = i18n.t("errors.badCredentials");
|
||||
break;
|
||||
case ERROR_TYPE.BAD_TOKEN:
|
||||
this.message = i18n.t("errors.badToken");
|
||||
break;
|
||||
case ERROR_TYPE.NO_CONSENT:
|
||||
this.message = i18n.t("errors.noConsent");
|
||||
break;
|
||||
case ERROR_TYPE.BAD_INPUT:
|
||||
this.message = i18n.t("errors.badInput");
|
||||
break;
|
||||
case ERROR_TYPE.FORBIDDEN:
|
||||
this.message = i18n.t("errors.forbidden");
|
||||
break;
|
||||
case ERROR_TYPE.CONNECTION_ERROR:
|
||||
this.message = i18n.t("errors.connectionError");
|
||||
break;
|
||||
case ERROR_TYPE.SERVER_ERROR:
|
||||
this.message = i18n.t("errors.serverError");
|
||||
break;
|
||||
default:
|
||||
this.message = i18n.t("errors.unknown");
|
||||
break;
|
||||
}
|
||||
generateMessage() {
|
||||
const {props} = this;
|
||||
this.title = i18n.t('errors.title');
|
||||
switch (props.errorCode) {
|
||||
case ERROR_TYPE.BAD_CREDENTIALS:
|
||||
this.message = i18n.t('errors.badCredentials');
|
||||
break;
|
||||
case ERROR_TYPE.BAD_TOKEN:
|
||||
this.message = i18n.t('errors.badToken');
|
||||
break;
|
||||
case ERROR_TYPE.NO_CONSENT:
|
||||
this.message = i18n.t('errors.noConsent');
|
||||
break;
|
||||
case ERROR_TYPE.TOKEN_SAVE:
|
||||
this.message = i18n.t('errors.tokenSave');
|
||||
break;
|
||||
case ERROR_TYPE.TOKEN_RETRIEVE:
|
||||
this.message = i18n.t('errors.unknown');
|
||||
break;
|
||||
case ERROR_TYPE.BAD_INPUT:
|
||||
this.message = i18n.t('errors.badInput');
|
||||
break;
|
||||
case ERROR_TYPE.FORBIDDEN:
|
||||
this.message = i18n.t('errors.forbidden');
|
||||
break;
|
||||
case ERROR_TYPE.CONNECTION_ERROR:
|
||||
this.message = i18n.t('errors.connectionError');
|
||||
break;
|
||||
case ERROR_TYPE.SERVER_ERROR:
|
||||
this.message = i18n.t('errors.serverError');
|
||||
break;
|
||||
default:
|
||||
this.message = i18n.t('errors.unknown');
|
||||
break;
|
||||
}
|
||||
this.message += `\n\nCode ${props.errorCode}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
this.generateMessage();
|
||||
return (
|
||||
<AlertDialog {...this.props} title={this.title} message={this.message}/>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
this.generateMessage();
|
||||
const {props} = this;
|
||||
return (
|
||||
<AlertDialog
|
||||
visible={props.visible}
|
||||
onDismiss={props.onDismiss}
|
||||
title={this.title}
|
||||
message={this.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorDialog;
|
||||
|
|
|
|||
|
|
@ -1,92 +1,106 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {ActivityIndicator, Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
||||
import i18n from "i18n-js";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Button,
|
||||
Dialog,
|
||||
Paragraph,
|
||||
Portal,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
|
||||
type Props = {
|
||||
visible: boolean,
|
||||
onDismiss: () => void,
|
||||
onAccept: () => Promise<void>, // async function to be executed
|
||||
title: string,
|
||||
titleLoading: string,
|
||||
message: string,
|
||||
startLoading: boolean,
|
||||
}
|
||||
type PropsType = {
|
||||
visible: boolean,
|
||||
onDismiss?: () => void,
|
||||
onAccept?: () => Promise<void>, // async function to be executed
|
||||
title?: string,
|
||||
titleLoading?: string,
|
||||
message?: string,
|
||||
startLoading?: boolean,
|
||||
};
|
||||
|
||||
type State = {
|
||||
loading: boolean,
|
||||
}
|
||||
type StateType = {
|
||||
loading: boolean,
|
||||
};
|
||||
|
||||
class LoadingConfirmDialog extends React.PureComponent<Props, State> {
|
||||
class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
|
||||
static defaultProps = {
|
||||
onDismiss: () => {},
|
||||
onAccept: (): Promise<void> => {
|
||||
return Promise.resolve();
|
||||
},
|
||||
title: '',
|
||||
titleLoading: '',
|
||||
message: '',
|
||||
startLoading: false,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
title: '',
|
||||
message: '',
|
||||
onDismiss: () => {},
|
||||
onAccept: () => {return Promise.resolve()},
|
||||
startLoading: false,
|
||||
}
|
||||
|
||||
state = {
|
||||
loading: this.props.startLoading,
|
||||
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
|
||||
*/
|
||||
onClickAccept = () => {
|
||||
this.setState({loading: true});
|
||||
this.props.onAccept().then(this.hideLoading);
|
||||
};
|
||||
/**
|
||||
* Set the dialog into loading state and closes it when operation finishes
|
||||
*/
|
||||
onClickAccept = () => {
|
||||
const {props} = this;
|
||||
this.setState({loading: true});
|
||||
if (props.onAccept != null) props.onAccept().then(this.hideLoading);
|
||||
};
|
||||
|
||||
/**
|
||||
* Waits for fade out animations to finish before hiding loading
|
||||
* @returns {TimeoutID}
|
||||
*/
|
||||
hideLoading = () => setTimeout(() => {
|
||||
this.setState({loading: false})
|
||||
/**
|
||||
* Waits for fade out animations to finish before hiding loading
|
||||
* @returns {TimeoutID}
|
||||
*/
|
||||
hideLoading = (): TimeoutID =>
|
||||
setTimeout(() => {
|
||||
this.setState({loading: false});
|
||||
}, 200);
|
||||
|
||||
/**
|
||||
* Hide the dialog if it is not loading
|
||||
*/
|
||||
onDismiss = () => {
|
||||
if (!this.state.loading)
|
||||
this.props.onDismiss();
|
||||
};
|
||||
/**
|
||||
* Hide the dialog if it is not loading
|
||||
*/
|
||||
onDismiss = () => {
|
||||
const {state, props} = this;
|
||||
if (!state.loading && props.onDismiss != null) props.onDismiss();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={this.props.visible}
|
||||
onDismiss={this.onDismiss}>
|
||||
<Dialog.Title>
|
||||
{this.state.loading
|
||||
? this.props.titleLoading
|
||||
: this.props.title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
{this.state.loading
|
||||
? <ActivityIndicator
|
||||
animating={true}
|
||||
size={'large'}/>
|
||||
: <Paragraph>{this.props.message}</Paragraph>
|
||||
}
|
||||
</Dialog.Content>
|
||||
{this.state.loading
|
||||
? null
|
||||
: <Dialog.Actions>
|
||||
<Button onPress={this.onDismiss}
|
||||
style={{marginRight: 10}}>{i18n.t("dialog.cancel")}</Button>
|
||||
<Button onPress={this.onClickAccept}>{i18n.t("dialog.yes")}</Button>
|
||||
</Dialog.Actions>
|
||||
}
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {state, props} = this;
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog visible={props.visible} onDismiss={this.onDismiss}>
|
||||
<Dialog.Title>
|
||||
{state.loading ? props.titleLoading : props.title}
|
||||
</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
{state.loading ? (
|
||||
<ActivityIndicator animating size="large" />
|
||||
) : (
|
||||
<Paragraph>{props.message}</Paragraph>
|
||||
)}
|
||||
</Dialog.Content>
|
||||
{state.loading ? null : (
|
||||
<Dialog.Actions>
|
||||
<Button onPress={this.onDismiss} style={{marginRight: 10}}>
|
||||
{i18n.t('dialog.cancel')}
|
||||
</Button>
|
||||
<Button onPress={this.onClickAccept}>
|
||||
{i18n.t('dialog.yes')}
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
)}
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default LoadingConfirmDialog;
|
||||
|
|
|
|||
51
src/components/Dialogs/OptionsDialog.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
|
||||
import {FlatList} from 'react-native';
|
||||
|
||||
export type OptionsDialogButtonType = {
|
||||
title: string,
|
||||
onPress: () => void,
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
visible: boolean,
|
||||
title: string,
|
||||
message: string,
|
||||
buttons: Array<OptionsDialogButtonType>,
|
||||
onDismiss: () => void,
|
||||
};
|
||||
|
||||
class OptionsDialog extends React.PureComponent<PropsType> {
|
||||
getButtonRender = ({item}: {item: OptionsDialogButtonType}): React.Node => {
|
||||
return <Button onPress={item.onPress}>{item.title}</Button>;
|
||||
};
|
||||
|
||||
keyExtractor = (item: OptionsDialogButtonType): string => item.title;
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog visible={props.visible} onDismiss={props.onDismiss}>
|
||||
<Dialog.Title>{props.title}</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Paragraph>{props.message}</Paragraph>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions>
|
||||
<FlatList
|
||||
data={props.buttons}
|
||||
renderItem={this.getButtonRender}
|
||||
keyExtractor={this.keyExtractor}
|
||||
horizontal
|
||||
inverted
|
||||
/>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default OptionsDialog;
|
||||
|
|
@ -1,86 +1,56 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Card, List, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet, View} from "react-native";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import {View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {ListIconPropsType} from '../../constants/PaperStyles';
|
||||
|
||||
const ICON_AMICALE = require("../../../assets/amicale.png");
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
isLoggedIn: boolean,
|
||||
class ActionsDashBoardItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return nextProps.theme.dark !== props.theme.dark;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {navigation} = this.props;
|
||||
return (
|
||||
<View>
|
||||
<List.Item
|
||||
title={i18n.t('screens.feedback.homeButtonTitle')}
|
||||
description={i18n.t('screens.feedback.homeButtonSubtitle')}
|
||||
left={(props: ListIconPropsType): React.Node => (
|
||||
<List.Icon
|
||||
color={props.color}
|
||||
style={props.style}
|
||||
icon="comment-quote"
|
||||
/>
|
||||
)}
|
||||
right={(props: ListIconPropsType): React.Node => (
|
||||
<List.Icon
|
||||
color={props.color}
|
||||
style={props.style}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
)}
|
||||
onPress={(): void => navigation.navigate('feedback')}
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionsDashBoardItem extends React.Component<Props> {
|
||||
|
||||
shouldComponentUpdate(nextProps: Props): boolean {
|
||||
return (nextProps.theme.dark !== this.props.theme.dark)
|
||||
|| (nextProps.isLoggedIn !== this.props.isLoggedIn);
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLoggedIn = this.props.isLoggedIn;
|
||||
return (
|
||||
<View>
|
||||
<Card style={{
|
||||
...styles.card,
|
||||
borderColor: this.props.theme.colors.primary,
|
||||
}}>
|
||||
<List.Item
|
||||
title={i18n.t("homeScreen.dashboard.amicaleTitle")}
|
||||
description={isLoggedIn
|
||||
? i18n.t("homeScreen.dashboard.amicaleConnected")
|
||||
: i18n.t("homeScreen.dashboard.amicaleConnect")}
|
||||
left={props => <Avatar.Image
|
||||
{...props}
|
||||
size={40}
|
||||
source={ICON_AMICALE}
|
||||
style={styles.avatar}/>}
|
||||
right={props => <List.Icon {...props} icon={isLoggedIn
|
||||
? "chevron-right"
|
||||
: "login"}/>}
|
||||
onPress={isLoggedIn
|
||||
? () => this.props.navigation.navigate("services", {
|
||||
screen: 'index'
|
||||
})
|
||||
: () => this.props.navigation.navigate("login")}
|
||||
style={styles.list}
|
||||
/>
|
||||
</Card>
|
||||
<List.Item
|
||||
title={i18n.t("feedbackScreen.homeButtonTitle")}
|
||||
description={i18n.t("feedbackScreen.homeButtonSubtitle")}
|
||||
left={props => <List.Icon {...props} icon={"bug"}/>}
|
||||
right={props => <List.Icon {...props} icon={"chevron-right"}/>}
|
||||
onPress={() => this.props.navigation.navigate("feedback")}
|
||||
style={{...styles.list, marginLeft: 10, marginRight: 10}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: 'auto',
|
||||
margin: 10,
|
||||
borderWidth: 1,
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
list: {
|
||||
// height: 50,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
}
|
||||
});
|
||||
|
||||
export default withTheme(ActionsDashBoardItem);
|
||||
|
|
|
|||
|
|
@ -1,87 +1,97 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Card, Text, withTheme} from 'react-native-paper';
|
||||
import {StyleSheet} from "react-native";
|
||||
import i18n from "i18n-js";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import {
|
||||
Avatar,
|
||||
Card,
|
||||
Text,
|
||||
TouchableRipple,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
import type {CardTitleIconPropsType} from '../../constants/PaperStyles';
|
||||
|
||||
type Props = {
|
||||
eventNumber: number;
|
||||
clickAction: () => void,
|
||||
theme: CustomTheme,
|
||||
children?: React.Node
|
||||
}
|
||||
type PropsType = {
|
||||
eventNumber: number,
|
||||
clickAction: () => void,
|
||||
theme: CustomThemeType,
|
||||
children?: React.Node,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: 'auto',
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
marginTop: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display a dashboard item containing a preview event
|
||||
*/
|
||||
class EventDashBoardItem extends React.Component<Props> {
|
||||
class EventDashBoardItem extends React.Component<PropsType> {
|
||||
static defaultProps = {
|
||||
children: null,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
return (nextProps.theme.dark !== this.props.theme.dark)
|
||||
|| (nextProps.eventNumber !== this.props.eventNumber);
|
||||
}
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.eventNumber !== props.eventNumber
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const colors = props.theme.colors;
|
||||
const isAvailable = props.eventNumber > 0;
|
||||
const iconColor = isAvailable ?
|
||||
colors.planningColor :
|
||||
colors.textDisabled;
|
||||
const textColor = isAvailable ?
|
||||
colors.text :
|
||||
colors.textDisabled;
|
||||
let subtitle;
|
||||
if (isAvailable) {
|
||||
subtitle =
|
||||
<Text>
|
||||
<Text style={{fontWeight: "bold"}}>{props.eventNumber}</Text>
|
||||
<Text>
|
||||
{props.eventNumber > 1
|
||||
? i18n.t('homeScreen.dashboard.todayEventsSubtitlePlural')
|
||||
: i18n.t('homeScreen.dashboard.todayEventsSubtitle')}
|
||||
</Text>
|
||||
</Text>;
|
||||
} else
|
||||
subtitle = i18n.t('homeScreen.dashboard.todayEventsSubtitleNA');
|
||||
return (
|
||||
<Card
|
||||
style={styles.card}
|
||||
onPress={props.clickAction}>
|
||||
<Card.Title
|
||||
title={i18n.t('homeScreen.dashboard.todayEventsTitle')}
|
||||
titleStyle={{color: textColor}}
|
||||
subtitle={subtitle}
|
||||
subtitleStyle={{color: textColor}}
|
||||
left={() =>
|
||||
<Avatar.Icon
|
||||
icon={'calendar-range'}
|
||||
color={iconColor}
|
||||
size={60}
|
||||
style={styles.avatar}/>}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {colors} = props.theme;
|
||||
const isAvailable = props.eventNumber > 0;
|
||||
const iconColor = isAvailable ? colors.planningColor : colors.textDisabled;
|
||||
const textColor = isAvailable ? colors.text : colors.textDisabled;
|
||||
let subtitle;
|
||||
if (isAvailable) {
|
||||
subtitle = (
|
||||
<Text>
|
||||
<Text style={{fontWeight: 'bold'}}>{props.eventNumber}</Text>
|
||||
<Text>
|
||||
{props.eventNumber > 1
|
||||
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
|
||||
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
|
||||
</Text>
|
||||
</Text>
|
||||
);
|
||||
} else subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
|
||||
titleStyle={{color: textColor}}
|
||||
subtitle={subtitle}
|
||||
subtitleStyle={{color: textColor}}
|
||||
left={(iconProps: CardTitleIconPropsType): React.Node => (
|
||||
<Avatar.Icon
|
||||
icon="calendar-range"
|
||||
color={iconColor}
|
||||
size={iconProps.size}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<Card.Content>
|
||||
{props.children}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
)}
|
||||
/>
|
||||
<Card.Content>{props.children}</Card.Content>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: 'auto',
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
marginTop: 10,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
});
|
||||
|
||||
export default withTheme(EventDashBoardItem);
|
||||
|
|
|
|||
|
|
@ -1,118 +1,120 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Button, Card, Text} from 'react-native-paper';
|
||||
import {View} from "react-native";
|
||||
import Autolink from "react-native-autolink";
|
||||
import i18n from "i18n-js";
|
||||
import ImageModal from 'react-native-image-modal';
|
||||
import {StackNavigationProp} from "@react-navigation/stack";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import type {feedItem} from "../../screens/Home/HomeScreen";
|
||||
|
||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp,
|
||||
theme: CustomTheme,
|
||||
item: feedItem,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
height: number,
|
||||
}
|
||||
import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
|
||||
import {Image, View} from 'react-native';
|
||||
import Autolink from 'react-native-autolink';
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {FeedItemType} from '../../screens/Home/HomeScreen';
|
||||
import NewsSourcesConstants from '../../constants/NewsSourcesConstants';
|
||||
import type {NewsSourceType} from '../../constants/NewsSourcesConstants';
|
||||
import ImageGalleryButton from '../Media/ImageGalleryButton';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
item: FeedItemType,
|
||||
height: number,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component used to display a feed item
|
||||
*/
|
||||
class FeedItem extends React.Component<Props> {
|
||||
class FeedItem extends React.Component<PropsType> {
|
||||
/**
|
||||
* Converts a dateString using Unix Timestamp to a formatted date
|
||||
*
|
||||
* @param dateString {string} The Unix Timestamp representation of a date
|
||||
* @return {string} The formatted output date
|
||||
*/
|
||||
static getFormattedDate(dateString: number): string {
|
||||
const date = new Date(dateString * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the amicale INSAT logo
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getAvatar() {
|
||||
return (
|
||||
<Avatar.Image
|
||||
size={48} source={ICON_AMICALE}
|
||||
style={{backgroundColor: 'transparent'}}/>
|
||||
);
|
||||
}
|
||||
onPress = () => {
|
||||
const {item, navigation} = this.props;
|
||||
navigation.navigate('feed-information', {
|
||||
data: item,
|
||||
date: FeedItem.getFormattedDate(item.time),
|
||||
});
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
this.props.navigation.navigate(
|
||||
'feed-information',
|
||||
{
|
||||
data: this.props.item,
|
||||
date: this.props.subtitle
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const item = this.props.item;
|
||||
const hasImage = item.full_picture !== '' && item.full_picture !== undefined;
|
||||
|
||||
const cardMargin = 10;
|
||||
const cardHeight = this.props.height - 2 * cardMargin;
|
||||
const imageSize = 250;
|
||||
const titleHeight = 80;
|
||||
const actionsHeight = 60;
|
||||
const textHeight = hasImage
|
||||
? cardHeight - titleHeight - actionsHeight - imageSize
|
||||
: cardHeight - titleHeight - actionsHeight;
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
margin: cardMargin,
|
||||
height: cardHeight,
|
||||
}}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<Card.Title
|
||||
title={this.props.title}
|
||||
subtitle={this.props.subtitle}
|
||||
left={this.getAvatar}
|
||||
style={{height: titleHeight}}
|
||||
render(): React.Node {
|
||||
const {item, height, navigation} = this.props;
|
||||
const image = item.image !== '' && item.image != null ? item.image : null;
|
||||
const pageSource: NewsSourceType = NewsSourcesConstants[item.page_id];
|
||||
const cardMargin = 10;
|
||||
const cardHeight = height - 2 * cardMargin;
|
||||
const imageSize = 250;
|
||||
const titleHeight = 80;
|
||||
const actionsHeight = 60;
|
||||
const textHeight =
|
||||
image != null
|
||||
? cardHeight - titleHeight - actionsHeight - imageSize
|
||||
: cardHeight - titleHeight - actionsHeight;
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
margin: cardMargin,
|
||||
height: cardHeight,
|
||||
}}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={this.onPress}>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={pageSource.name}
|
||||
subtitle={FeedItem.getFormattedDate(item.time)}
|
||||
left={(): React.Node => (
|
||||
<Image
|
||||
size={48}
|
||||
source={pageSource.icon}
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
}}
|
||||
/>
|
||||
{hasImage ?
|
||||
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<ImageModal
|
||||
resizeMode="contain"
|
||||
imageBackgroundColor={"#000"}
|
||||
style={{
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
}}
|
||||
source={{
|
||||
uri: item.full_picture,
|
||||
}}
|
||||
/></View> : null}
|
||||
<Card.Content>
|
||||
{item.message !== undefined ?
|
||||
<Autolink
|
||||
text={item.message}
|
||||
hashtag="facebook"
|
||||
component={Text}
|
||||
style={{height: textHeight}}
|
||||
/> : null
|
||||
}
|
||||
</Card.Content>
|
||||
<Card.Actions style={{height: actionsHeight}}>
|
||||
<Button
|
||||
onPress={this.onPress}
|
||||
icon={'plus'}
|
||||
style={{marginLeft: 'auto'}}>
|
||||
{i18n.t('homeScreen.dashboard.seeMore')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
)}
|
||||
style={{height: titleHeight}}
|
||||
/>
|
||||
{image != null ? (
|
||||
<ImageGalleryButton
|
||||
navigation={navigation}
|
||||
images={[{url: image}]}
|
||||
style={{
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Card.Content>
|
||||
{item.message !== undefined ? (
|
||||
<Autolink
|
||||
text={item.message}
|
||||
hashtag="facebook"
|
||||
component={Text}
|
||||
style={{height: textHeight}}
|
||||
/>
|
||||
) : null}
|
||||
</Card.Content>
|
||||
<Card.Actions style={{height: actionsHeight}}>
|
||||
<Button
|
||||
onPress={this.onPress}
|
||||
icon="plus"
|
||||
style={{marginLeft: 'auto'}}>
|
||||
{i18n.t('screens.home.dashboard.seeMore')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FeedItem;
|
||||
|
|
|
|||
|
|
@ -1,89 +1,100 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {StyleSheet} from "react-native";
|
||||
import i18n from "i18n-js";
|
||||
import {Avatar, Button, Card} from 'react-native-paper';
|
||||
import {getFormattedEventTime, isDescriptionEmpty} from "../../utils/Planning";
|
||||
import CustomHTML from "../Overrides/CustomHTML";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import type {event} from "../../screens/Home/HomeScreen";
|
||||
import {StyleSheet, View} from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
|
||||
import {getFormattedEventTime, isDescriptionEmpty} from '../../utils/Planning';
|
||||
import CustomHTML from '../Overrides/CustomHTML';
|
||||
import type {EventType} from '../../screens/Home/HomeScreen';
|
||||
|
||||
type Props = {
|
||||
event?: event,
|
||||
clickAction: () => void,
|
||||
theme?: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
event?: EventType | null,
|
||||
clickAction: () => void,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
content: {
|
||||
maxHeight: 150,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
actions: {
|
||||
marginLeft: 'auto',
|
||||
marginTop: 'auto',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Component used to display an event preview if an event is available
|
||||
*/
|
||||
class PreviewEventDashboardItem extends React.Component<Props> {
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
class PreviewEventDashboardItem extends React.Component<PropsType> {
|
||||
static defaultProps = {
|
||||
event: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const isEmpty = props.event == null
|
||||
? true
|
||||
: isDescriptionEmpty(props.event.description);
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {event} = props;
|
||||
const isEmpty =
|
||||
event == null ? true : isDescriptionEmpty(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}
|
||||
onPress={props.clickAction}
|
||||
elevation={3}
|
||||
>
|
||||
{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}
|
||||
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("homeScreen.dashboard.seeMore")}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
);
|
||||
} else
|
||||
return null;
|
||||
<Card.Actions style={styles.actions}>
|
||||
<Button icon="chevron-right">
|
||||
{i18n.t('screens.home.dashboard.seeMore')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginBottom: 10
|
||||
},
|
||||
content: {
|
||||
maxHeight: 150,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
actions: {
|
||||
marginLeft: 'auto',
|
||||
marginTop: 'auto',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
avatar: {
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
});
|
||||
|
||||
export default PreviewEventDashboardItem;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Badge, IconButton, withTheme} from 'react-native-paper';
|
||||
import {View} from "react-native";
|
||||
import type {CustomTheme} from "../../managers/ThemeManager";
|
||||
import * as Animatable from "react-native-animatable";
|
||||
import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import type {CustomThemeType} from '../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
color: string,
|
||||
icon: string,
|
||||
clickAction: () => void,
|
||||
isAvailable: boolean,
|
||||
badgeNumber: number,
|
||||
theme: CustomTheme,
|
||||
type PropsType = {
|
||||
image: string | null,
|
||||
onPress: () => void | null,
|
||||
badgeCount: number | null,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
|
||||
|
|
@ -20,47 +18,68 @@ const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
|
|||
/**
|
||||
* Component used to render a small dashboard item
|
||||
*/
|
||||
class SmallDashboardItem extends React.Component<Props> {
|
||||
class SmallDashboardItem extends React.Component<PropsType> {
|
||||
itemSize: number;
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
return (nextProps.theme.dark !== this.props.theme.dark)
|
||||
|| (nextProps.isAvailable !== this.props.isAvailable)
|
||||
|| (nextProps.badgeNumber !== this.props.badgeNumber);
|
||||
}
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.itemSize = Dimensions.get('window').width / 8;
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const colors = props.theme.colors;
|
||||
return (
|
||||
<View>
|
||||
<IconButton
|
||||
icon={props.icon}
|
||||
color={
|
||||
props.isAvailable
|
||||
? props.color
|
||||
: colors.textDisabled
|
||||
}
|
||||
size={35}
|
||||
onPress={props.clickAction}
|
||||
/>
|
||||
{
|
||||
props.badgeNumber > 0 ?
|
||||
<AnimatableBadge
|
||||
animation={"zoomIn"}
|
||||
duration={300}
|
||||
useNativeDriver
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
right: 5
|
||||
}}>
|
||||
{props.badgeNumber}
|
||||
</AnimatableBadge> : null
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.theme.dark !== props.theme.dark ||
|
||||
nextProps.badgeCount !== props.badgeCount
|
||||
);
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
borderless
|
||||
style={{
|
||||
marginLeft: this.itemSize / 6,
|
||||
marginRight: this.itemSize / 6,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
width: this.itemSize,
|
||||
height: this.itemSize,
|
||||
}}>
|
||||
<Image
|
||||
source={{uri: props.image}}
|
||||
style={{
|
||||
width: '80%',
|
||||
height: '80%',
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}
|
||||
/>
|
||||
{props.badgeCount != null && props.badgeCount > 0 ? (
|
||||
<AnimatableBadge
|
||||
animation="zoomIn"
|
||||
duration={300}
|
||||
useNativeDriver
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
backgroundColor: props.theme.colors.primary,
|
||||
borderColor: props.theme.colors.background,
|
||||
borderWidth: 2,
|
||||
}}>
|
||||
{props.badgeCount}
|
||||
</AnimatableBadge>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(SmallDashboardItem);
|
||||
|
|
|
|||
|
|
@ -1,63 +1,73 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Animated} from "react-native";
|
||||
import ImageListItem from "./ImageListItem";
|
||||
import CardListItem from "./CardListItem";
|
||||
import {Animated, Dimensions} from 'react-native';
|
||||
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||
import ImageListItem from './ImageListItem';
|
||||
import CardListItem from './CardListItem';
|
||||
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||
|
||||
type Props = {
|
||||
dataset: Array<cardItem>,
|
||||
isHorizontal: boolean,
|
||||
}
|
||||
|
||||
export type cardItem = {
|
||||
title: string,
|
||||
subtitle: string,
|
||||
image: string | number,
|
||||
onPress: () => void,
|
||||
type PropsType = {
|
||||
dataset: Array<ServiceItemType>,
|
||||
isHorizontal?: boolean,
|
||||
contentContainerStyle?: ViewStyle | null,
|
||||
};
|
||||
|
||||
export type cardList = Array<cardItem>;
|
||||
export default class CardList extends React.Component<PropsType> {
|
||||
static defaultProps = {
|
||||
isHorizontal: false,
|
||||
contentContainerStyle: null,
|
||||
};
|
||||
|
||||
windowWidth: number;
|
||||
|
||||
export default class CardList extends React.Component<Props> {
|
||||
horizontalItemSize: number;
|
||||
|
||||
static defaultProps = {
|
||||
isHorizontal: false,
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.windowWidth = Dimensions.get('window').width;
|
||||
this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll
|
||||
}
|
||||
|
||||
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => {
|
||||
const {props} = this;
|
||||
if (props.isHorizontal)
|
||||
return (
|
||||
<ImageListItem
|
||||
item={item}
|
||||
key={item.title}
|
||||
width={this.horizontalItemSize}
|
||||
/>
|
||||
);
|
||||
return <CardListItem item={item} key={item.title} />;
|
||||
};
|
||||
|
||||
keyExtractor = (item: ServiceItemType): string => item.key;
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
let containerStyle = {};
|
||||
if (props.isHorizontal) {
|
||||
containerStyle = {
|
||||
height: this.horizontalItemSize + 50,
|
||||
justifyContent: 'space-around',
|
||||
};
|
||||
}
|
||||
|
||||
renderItem = ({item}: { item: cardItem }) => {
|
||||
if (this.props.isHorizontal)
|
||||
return <ImageListItem item={item} key={item.title}/>;
|
||||
else
|
||||
return <CardListItem item={item} key={item.title}/>;
|
||||
};
|
||||
|
||||
keyExtractor = (item: cardItem) => item.title;
|
||||
|
||||
render() {
|
||||
let containerStyle;
|
||||
if (this.props.isHorizontal) {
|
||||
containerStyle = {
|
||||
...this.props.contentContainerStyle,
|
||||
height: 150,
|
||||
justifyContent: 'space-around',
|
||||
};
|
||||
} else {
|
||||
containerStyle = {
|
||||
...this.props.contentContainerStyle,
|
||||
}
|
||||
return (
|
||||
<Animated.FlatList
|
||||
data={props.dataset}
|
||||
renderItem={this.getRenderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
numColumns={props.isHorizontal ? undefined : 2}
|
||||
horizontal={props.isHorizontal}
|
||||
contentContainerStyle={
|
||||
props.isHorizontal ? containerStyle : props.contentContainerStyle
|
||||
}
|
||||
return (
|
||||
<Animated.FlatList
|
||||
{...this.props}
|
||||
data={this.props.dataset}
|
||||
renderItem={this.renderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
numColumns={this.props.isHorizontal ? undefined : 2}
|
||||
horizontal={this.props.isHorizontal}
|
||||
contentContainerStyle={containerStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
pagingEnabled={props.isHorizontal}
|
||||
snapToInterval={
|
||||
props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,42 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Caption, Card, Paragraph} from 'react-native-paper';
|
||||
import type {cardItem} from "./CardList";
|
||||
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
|
||||
import {View} from 'react-native';
|
||||
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||
|
||||
type Props = {
|
||||
item: cardItem,
|
||||
type PropsType = {
|
||||
item: ServiceItemType,
|
||||
};
|
||||
|
||||
export default class CardListItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {item} = props;
|
||||
const source =
|
||||
typeof item.image === 'number' ? item.image : {uri: item.image};
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
width: '40%',
|
||||
margin: 5,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}>
|
||||
<TouchableRipple style={{flex: 1}} onPress={item.onPress}>
|
||||
<View>
|
||||
<Card.Cover style={{height: 80}} source={source} />
|
||||
<Card.Content>
|
||||
<Paragraph>{item.title}</Paragraph>
|
||||
<Caption>{item.subtitle}</Caption>
|
||||
</Card.Content>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class CardListItem extends React.Component<Props> {
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const item = props.item;
|
||||
const source = typeof item.image === "number"
|
||||
? item.image
|
||||
: {uri: item.image};
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
width: '40%',
|
||||
margin: 5,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
>
|
||||
<Card.Cover
|
||||
style={{height: 80}}
|
||||
source={source}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Paragraph>{item.title}</Paragraph>
|
||||
<Caption>{item.subtitle}</Caption>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,51 +3,52 @@
|
|||
import * as React from 'react';
|
||||
import {Text, TouchableRipple} from 'react-native-paper';
|
||||
import {Image, View} from 'react-native';
|
||||
import type {cardItem} from "./CardList";
|
||||
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||
|
||||
type Props = {
|
||||
item: cardItem,
|
||||
type PropsType = {
|
||||
item: ServiceItemType,
|
||||
width: number,
|
||||
};
|
||||
|
||||
export default class ImageListItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {item} = props;
|
||||
const source =
|
||||
typeof item.image === 'number' ? item.image : {uri: item.image};
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={{
|
||||
width: props.width,
|
||||
height: props.width + 40,
|
||||
margin: 5,
|
||||
}}
|
||||
onPress={item.onPress}>
|
||||
<View>
|
||||
<Image
|
||||
style={{
|
||||
width: props.width - 20,
|
||||
height: props.width - 20,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
source={source}
|
||||
/>
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 5,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class ImageListItem extends React.Component<Props> {
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = this.props;
|
||||
const item = props.item;
|
||||
const source = typeof item.image === "number"
|
||||
? item.image
|
||||
: {uri: item.image};
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={{
|
||||
width: 100,
|
||||
height: 150,
|
||||
margin: 5,
|
||||
}}
|
||||
onPress={item.onPress}
|
||||
>
|
||||
<View>
|
||||
<Image
|
||||
style={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
source={source}
|
||||
/>
|
||||
<Text style={{
|
||||
marginTop: 5,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
textAlign: 'center'
|
||||
}}>{item.title}</Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,82 +2,91 @@
|
|||
|
||||
import * as React from 'react';
|
||||
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 AnimatedAccordion from "../../Animations/AnimatedAccordion";
|
||||
import {isItemInCategoryFilter} from "../../../utils/Search";
|
||||
import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen";
|
||||
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||
import {isItemInCategoryFilter} from '../../../utils/Search';
|
||||
import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||
import type {ListIconPropsType} from '../../../constants/PaperStyles';
|
||||
|
||||
type Props = {
|
||||
categories: Array<category>,
|
||||
onChipSelect: (id: number) => void,
|
||||
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, 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("clubs.categories")}
|
||||
left={props => <List.Icon {...props} icon="star"/>}
|
||||
opened={true}
|
||||
>
|
||||
<Text style={styles.text}>{i18n.t("clubs.categoriesFilterMessage")}</Text>
|
||||
<View style={styles.chipContainer}>
|
||||
{this.getCategoriesRender()}
|
||||
</View>
|
||||
</AnimatedAccordion>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
type PropsType = {
|
||||
categories: Array<ClubCategoryType>,
|
||||
onChipSelect: (id: number) => void,
|
||||
selectedCategories: Array<number>,
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 5
|
||||
},
|
||||
text: {
|
||||
paddingLeft: 0,
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
chipContainer: {
|
||||
justifyContent: 'space-around',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingLeft: 0,
|
||||
marginBottom: 5,
|
||||
},
|
||||
card: {
|
||||
margin: 5,
|
||||
},
|
||||
text: {
|
||||
paddingLeft: 0,
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
chipContainer: {
|
||||
justifyContent: 'space-around',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingLeft: 0,
|
||||
marginBottom: 5,
|
||||
},
|
||||
});
|
||||
|
||||
class ClubListHeader extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.selectedCategories.length !== props.selectedCategories.length
|
||||
);
|
||||
}
|
||||
|
||||
getChipRender = (category: ClubCategoryType, key: string): React.Node => {
|
||||
const {props} = this;
|
||||
const onPress = (): void => props.onChipSelect(category.id);
|
||||
return (
|
||||
<Chip
|
||||
selected={isItemInCategoryFilter(props.selectedCategories, [
|
||||
category.id,
|
||||
null,
|
||||
])}
|
||||
mode="outlined"
|
||||
onPress={onPress}
|
||||
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
|
||||
key={key}>
|
||||
{category.name}
|
||||
</Chip>
|
||||
);
|
||||
};
|
||||
|
||||
getCategoriesRender(): React.Node {
|
||||
const {props} = this;
|
||||
const final = [];
|
||||
props.categories.forEach((cat: ClubCategoryType) => {
|
||||
final.push(this.getChipRender(cat, cat.id.toString()));
|
||||
});
|
||||
return final;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<AnimatedAccordion
|
||||
title={i18n.t('screens.clubs.categories')}
|
||||
left={(props: ListIconPropsType): React.Node => (
|
||||
<List.Icon color={props.color} style={props.style} icon="star" />
|
||||
)}
|
||||
opened>
|
||||
<Text style={styles.text}>
|
||||
{i18n.t('screens.clubs.categoriesFilterMessage')}
|
||||
</Text>
|
||||
<View style={styles.chipContainer}>{this.getCategoriesRender()}</View>
|
||||
</AnimatedAccordion>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ClubListHeader;
|
||||
|
|
|
|||
|
|
@ -2,80 +2,93 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
|
||||
import {View} from "react-native";
|
||||
import type {category, club} from "../../../screens/Amicale/Clubs/ClubListScreen";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import {View} from 'react-native';
|
||||
import type {
|
||||
ClubCategoryType,
|
||||
ClubType,
|
||||
} from '../../../screens/Amicale/Clubs/ClubListScreen';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type Props = {
|
||||
onPress: () => void,
|
||||
categoryTranslator: (id: number) => category,
|
||||
item: club,
|
||||
height: number,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
onPress: () => void,
|
||||
categoryTranslator: (id: number) => ClubCategoryType,
|
||||
item: ClubType,
|
||||
height: number,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class ClubListItem extends React.Component<Props> {
|
||||
class ClubListItem extends React.Component<PropsType> {
|
||||
hasManagers: boolean;
|
||||
|
||||
hasManagers: boolean;
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.hasManagers = props.item.responsibles.length > 0;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.hasManagers = props.item.responsibles.length > 0;
|
||||
}
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
getCategoriesRender(categories: Array<number | null>) {
|
||||
let final = [];
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
if (categories[i] !== null) {
|
||||
const category: category = this.props.categoryTranslator(categories[i]);
|
||||
final.push(
|
||||
<Chip
|
||||
style={{marginRight: 5, marginBottom: 5}}
|
||||
key={this.props.item.id + ':' + category.id}
|
||||
>
|
||||
{category.name}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <View style={{flexDirection: 'row'}}>{final}</View>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const categoriesRender = this.getCategoriesRender.bind(this, this.props.item.category);
|
||||
const colors = this.props.theme.colors;
|
||||
return (
|
||||
<List.Item
|
||||
title={this.props.item.name}
|
||||
description={categoriesRender}
|
||||
onPress={this.props.onPress}
|
||||
left={(props) => <Avatar.Image
|
||||
{...props}
|
||||
style={{backgroundColor: 'transparent'}}
|
||||
size={64}
|
||||
source={{uri: this.props.item.logo}}/>}
|
||||
right={(props) => <Avatar.Icon
|
||||
{...props}
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
size={48}
|
||||
icon={this.hasManagers ? "check-circle-outline" : "alert-circle-outline"}
|
||||
color={this.hasManagers ? colors.success : colors.primary}
|
||||
/>}
|
||||
style={{
|
||||
height: this.props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
getCategoriesRender(categories: Array<number | null>): React.Node {
|
||||
const {props} = this;
|
||||
const final = [];
|
||||
categories.forEach((cat: number | null) => {
|
||||
if (cat != null) {
|
||||
const category: ClubCategoryType = props.categoryTranslator(cat);
|
||||
final.push(
|
||||
<Chip
|
||||
style={{marginRight: 5, marginBottom: 5}}
|
||||
key={`${props.item.id}:${category.id}`}>
|
||||
{category.name}
|
||||
</Chip>,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
return <View style={{flexDirection: 'row'}}>{final}</View>;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const categoriesRender = (): React.Node =>
|
||||
this.getCategoriesRender(props.item.category);
|
||||
const {colors} = props.theme;
|
||||
return (
|
||||
<List.Item
|
||||
title={props.item.name}
|
||||
description={categoriesRender}
|
||||
onPress={props.onPress}
|
||||
left={(): React.Node => (
|
||||
<Avatar.Image
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
}}
|
||||
size={64}
|
||||
source={{uri: props.item.logo}}
|
||||
/>
|
||||
)}
|
||||
right={(): React.Node => (
|
||||
<Avatar.Icon
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
size={48}
|
||||
icon={
|
||||
this.hasManagers ? 'check-circle-outline' : 'alert-circle-outline'
|
||||
}
|
||||
color={this.hasManagers ? colors.success : colors.primary}
|
||||
/>
|
||||
)}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ClubListItem);
|
||||
|
|
|
|||
89
src/components/Lists/DashboardEdit/DashboardEditAccordion.js
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {withTheme} from 'react-native-paper';
|
||||
import {FlatList, Image, View} from 'react-native';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import DashboardEditItem from './DashboardEditItem';
|
||||
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||
import type {
|
||||
ServiceCategoryType,
|
||||
ServiceItemType,
|
||||
} from '../../../managers/ServicesManager';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
item: ServiceCategoryType,
|
||||
activeDashboard: Array<string>,
|
||||
onPress: (service: ServiceItemType) => void,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
|
||||
class DashboardEditAccordion extends React.Component<PropsType> {
|
||||
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => {
|
||||
const {props} = this;
|
||||
return (
|
||||
<DashboardEditItem
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
item={item}
|
||||
isActive={props.activeDashboard.includes(item.key)}
|
||||
onPress={() => {
|
||||
props.onPress(item);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getItemLayout = (
|
||||
data: ?Array<ServiceItemType>,
|
||||
index: number,
|
||||
): {length: number, offset: number, index: number} => ({
|
||||
length: LIST_ITEM_HEIGHT,
|
||||
offset: LIST_ITEM_HEIGHT * index,
|
||||
index,
|
||||
});
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {item} = props;
|
||||
return (
|
||||
<View>
|
||||
<AnimatedAccordion
|
||||
title={item.title}
|
||||
left={(): React.Node =>
|
||||
typeof item.image === 'number' ? (
|
||||
<Image
|
||||
source={item.image}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<MaterialCommunityIcons
|
||||
// $FlowFixMe
|
||||
name={item.image}
|
||||
color={props.theme.colors.primary}
|
||||
size={40}
|
||||
/>
|
||||
)
|
||||
}>
|
||||
{/* $FlowFixMe */}
|
||||
<FlatList
|
||||
data={item.content}
|
||||
extraData={props.activeDashboard.toString()}
|
||||
renderItem={this.getRenderItem}
|
||||
listKey={item.key}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
getItemLayout={this.getItemLayout}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
</AnimatedAccordion>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(DashboardEditAccordion);
|
||||
62
src/components/Lists/DashboardEdit/DashboardEditItem.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Image} from 'react-native';
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ServiceItemType} from '../../../managers/ServicesManager';
|
||||
import type {ListIconPropsType} from '../../../constants/PaperStyles';
|
||||
|
||||
type PropsType = {
|
||||
item: ServiceItemType,
|
||||
isActive: boolean,
|
||||
height: number,
|
||||
onPress: () => void,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class DashboardEditItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {isActive} = this.props;
|
||||
return nextProps.isActive !== isActive;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {item, onPress, height, isActive, theme} = this.props;
|
||||
return (
|
||||
<List.Item
|
||||
title={item.title}
|
||||
description={item.subtitle}
|
||||
onPress={isActive ? null : onPress}
|
||||
left={(): React.Node => (
|
||||
<Image
|
||||
source={{uri: item.image}}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
right={(props: ListIconPropsType): React.Node =>
|
||||
isActive ? (
|
||||
<List.Icon
|
||||
style={props.style}
|
||||
icon="check"
|
||||
color={theme.colors.success}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
style={{
|
||||
height,
|
||||
justifyContent: 'center',
|
||||
paddingLeft: 30,
|
||||
backgroundColor: isActive
|
||||
? theme.colors.proxiwashFinishedColor
|
||||
: 'transparent',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(DashboardEditItem);
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import {Dimensions, Image, View} from 'react-native';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
|
||||
type PropsType = {
|
||||
image: string,
|
||||
isActive: boolean,
|
||||
onPress: () => void,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component used to render a small dashboard item
|
||||
*/
|
||||
class DashboardEditPreviewItem extends React.Component<PropsType> {
|
||||
itemSize: number;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.itemSize = Dimensions.get('window').width / 8;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={props.onPress}
|
||||
borderless
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
backgroundColor: props.isActive
|
||||
? props.theme.colors.textDisabled
|
||||
: 'transparent',
|
||||
borderRadius: 5,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
width: this.itemSize,
|
||||
height: this.itemSize,
|
||||
}}>
|
||||
<Image
|
||||
source={{uri: props.image}}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(DashboardEditPreviewItem);
|
||||
113
src/components/Lists/Equipment/EquipmentListItem.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {Avatar, List, withTheme} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import {StackNavigationProp} from '@react-navigation/stack';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
|
||||
import {
|
||||
getFirstEquipmentAvailability,
|
||||
getRelativeDateString,
|
||||
isEquipmentAvailable,
|
||||
} from '../../../utils/EquipmentBooking';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp,
|
||||
userDeviceRentDates: [string, string],
|
||||
item: DeviceType,
|
||||
height: number,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
class EquipmentListItem extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {userDeviceRentDates} = this.props;
|
||||
return nextProps.userDeviceRentDates !== userDeviceRentDates;
|
||||
}
|
||||
|
||||
render(): React.Node {
|
||||
const {item, userDeviceRentDates, navigation, height, theme} = this.props;
|
||||
const isRented = userDeviceRentDates != null;
|
||||
const isAvailable = isEquipmentAvailable(item);
|
||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||
|
||||
let onPress;
|
||||
if (isRented)
|
||||
onPress = () => {
|
||||
navigation.navigate('equipment-confirm', {
|
||||
item,
|
||||
dates: userDeviceRentDates,
|
||||
});
|
||||
};
|
||||
else
|
||||
onPress = () => {
|
||||
navigation.navigate('equipment-rent', {item});
|
||||
};
|
||||
|
||||
let description;
|
||||
if (isRented) {
|
||||
const start = new Date(userDeviceRentDates[0]);
|
||||
const end = new Date(userDeviceRentDates[1]);
|
||||
if (start.getTime() !== end.getTime())
|
||||
description = i18n.t('screens.equipment.bookingPeriod', {
|
||||
begin: getRelativeDateString(start),
|
||||
end: getRelativeDateString(end),
|
||||
});
|
||||
else
|
||||
description = i18n.t('screens.equipment.bookingDay', {
|
||||
date: getRelativeDateString(start),
|
||||
});
|
||||
} else if (isAvailable)
|
||||
description = i18n.t('screens.equipment.bail', {cost: item.caution});
|
||||
else
|
||||
description = i18n.t('screens.equipment.available', {
|
||||
date: getRelativeDateString(firstAvailability),
|
||||
});
|
||||
|
||||
let icon;
|
||||
if (isRented) icon = 'bookmark-check';
|
||||
else if (isAvailable) icon = 'check-circle-outline';
|
||||
else icon = 'update';
|
||||
|
||||
let color;
|
||||
if (isRented) color = theme.colors.warning;
|
||||
else if (isAvailable) color = theme.colors.success;
|
||||
else color = theme.colors.primary;
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={description}
|
||||
onPress={onPress}
|
||||
left={({size}: {size: number}): React.Node => (
|
||||
<Avatar.Icon
|
||||
size={size}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
icon={icon}
|
||||
color={color}
|
||||
/>
|
||||
)}
|
||||
right={(): React.Node => (
|
||||
<Avatar.Icon
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
size={48}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
)}
|
||||
style={{
|
||||
height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(EquipmentListItem);
|
||||
|
|
@ -2,96 +2,117 @@
|
|||
|
||||
import * as React from 'react';
|
||||
import {List, withTheme} from 'react-native-paper';
|
||||
import {FlatList, View} from "react-native";
|
||||
import {stringMatchQuery} from "../../../utils/Search";
|
||||
import GroupListItem from "./GroupListItem";
|
||||
import AnimatedAccordion from "../../Animations/AnimatedAccordion";
|
||||
import type {group, groupCategory} from "../../../screens/Planex/GroupSelectionScreen";
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import {FlatList, View} from 'react-native';
|
||||
import {stringMatchQuery} from '../../../utils/Search';
|
||||
import GroupListItem from './GroupListItem';
|
||||
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
|
||||
import type {
|
||||
PlanexGroupType,
|
||||
PlanexGroupCategoryType,
|
||||
} from '../../../screens/Planex/GroupSelectionScreen';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {ListIconPropsType} from '../../../constants/PaperStyles';
|
||||
|
||||
type Props = {
|
||||
item: groupCategory,
|
||||
onGroupPress: (group) => void,
|
||||
onFavoritePress: (group) => void,
|
||||
currentSearchString: string,
|
||||
favoriteNumber: number,
|
||||
height: number,
|
||||
theme: CustomTheme,
|
||||
}
|
||||
type PropsType = {
|
||||
item: PlanexGroupCategoryType,
|
||||
favorites: Array<PlanexGroupType>,
|
||||
onGroupPress: (PlanexGroupType) => void,
|
||||
onFavoritePress: (PlanexGroupType) => void,
|
||||
currentSearchString: string,
|
||||
height: number,
|
||||
theme: CustomThemeType,
|
||||
};
|
||||
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
const REPLACE_REGEX = /_/g;
|
||||
|
||||
class GroupListAccordion extends React.Component<Props> {
|
||||
class GroupListAccordion extends React.Component<PropsType> {
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {props} = this;
|
||||
return (
|
||||
nextProps.currentSearchString !== props.currentSearchString ||
|
||||
nextProps.favorites.length !== props.favorites.length ||
|
||||
nextProps.item.content.length !== props.item.content.length
|
||||
);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
return (nextProps.currentSearchString !== this.props.currentSearchString)
|
||||
|| (nextProps.favoriteNumber !== this.props.favoriteNumber)
|
||||
|| (nextProps.item.content.length !== this.props.item.content.length);
|
||||
}
|
||||
getRenderItem = ({item}: {item: PlanexGroupType}): React.Node => {
|
||||
const {props} = this;
|
||||
const onPress = () => {
|
||||
props.onGroupPress(item);
|
||||
};
|
||||
const onStarPress = () => {
|
||||
props.onFavoritePress(item);
|
||||
};
|
||||
return (
|
||||
<GroupListItem
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
item={item}
|
||||
favorites={props.favorites}
|
||||
onPress={onPress}
|
||||
onStarPress={onStarPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
keyExtractor = (item: group) => item.id.toString();
|
||||
getData(): Array<PlanexGroupType> {
|
||||
const {props} = this;
|
||||
const originalData = props.item.content;
|
||||
const displayData = [];
|
||||
originalData.forEach((data: PlanexGroupType) => {
|
||||
if (stringMatchQuery(data.name, props.currentSearchString))
|
||||
displayData.push(data);
|
||||
});
|
||||
return displayData;
|
||||
}
|
||||
|
||||
renderItem = ({item}: { item: group }) => {
|
||||
const onPress = () => this.props.onGroupPress(item);
|
||||
const onStarPress = () => this.props.onFavoritePress(item);
|
||||
return (
|
||||
<GroupListItem
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
item={item}
|
||||
onPress={onPress}
|
||||
onStarPress={onStarPress}/>
|
||||
);
|
||||
}
|
||||
itemLayout = (
|
||||
data: ?Array<PlanexGroupType>,
|
||||
index: number,
|
||||
): {length: number, offset: number, index: number} => ({
|
||||
length: LIST_ITEM_HEIGHT,
|
||||
offset: LIST_ITEM_HEIGHT * index,
|
||||
index,
|
||||
});
|
||||
|
||||
getData() {
|
||||
const originalData = this.props.item.content;
|
||||
let displayData = [];
|
||||
for (let i = 0; i < originalData.length; i++) {
|
||||
if (stringMatchQuery(originalData[i].name, this.props.currentSearchString))
|
||||
displayData.push(originalData[i]);
|
||||
}
|
||||
return displayData;
|
||||
}
|
||||
keyExtractor = (item: PlanexGroupType): string => item.id.toString();
|
||||
|
||||
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
|
||||
|
||||
|
||||
render() {
|
||||
const item = this.props.item;
|
||||
return (
|
||||
<View>
|
||||
<AnimatedAccordion
|
||||
title={item.name}
|
||||
style={{
|
||||
height: this.props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
left={props =>
|
||||
item.id === 0
|
||||
? <List.Icon
|
||||
{...props}
|
||||
icon={"star"}
|
||||
color={this.props.theme.colors.tetrisScore}
|
||||
/>
|
||||
: null}
|
||||
unmountWhenCollapsed={true}// Only render list if expanded for increased performance
|
||||
opened={this.props.item.id === 0 || this.props.currentSearchString.length > 0}
|
||||
>
|
||||
{/*$FlowFixMe*/}
|
||||
<FlatList
|
||||
data={this.getData()}
|
||||
extraData={this.props.currentSearchString}
|
||||
renderItem={this.renderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
listKey={item.id.toString()}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
getItemLayout={this.itemLayout}
|
||||
removeClippedSubviews={true}
|
||||
/>
|
||||
</AnimatedAccordion>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {item} = this.props;
|
||||
return (
|
||||
<View>
|
||||
<AnimatedAccordion
|
||||
title={item.name.replace(REPLACE_REGEX, ' ')}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
left={(iconProps: ListIconPropsType): React.Node =>
|
||||
item.id === 0 ? (
|
||||
<List.Icon
|
||||
style={iconProps.style}
|
||||
icon="star"
|
||||
color={props.theme.colors.tetrisScore}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
unmountWhenCollapsed={item.id !== 0} // Only render list if expanded for increased performance
|
||||
opened={props.currentSearchString.length > 0}>
|
||||
<FlatList
|
||||
data={this.getData()}
|
||||
extraData={props.currentSearchString + props.favorites.length}
|
||||
renderItem={this.getRenderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
listKey={item.id.toString()}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
getItemLayout={this.itemLayout}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
</AnimatedAccordion>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(GroupListAccordion)
|
||||
export default withTheme(GroupListAccordion);
|
||||
|
|
|
|||
|
|
@ -1,66 +1,106 @@
|
|||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import {IconButton, List, withTheme} from 'react-native-paper';
|
||||
import type {CustomTheme} from "../../../managers/ThemeManager";
|
||||
import type {group} from "../../../screens/Planex/GroupSelectionScreen";
|
||||
import {List, TouchableRipple, withTheme} from 'react-native-paper';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
||||
import type {PlanexGroupType} from '../../../screens/Planex/GroupSelectionScreen';
|
||||
import type {ListIconPropsType} from '../../../constants/PaperStyles';
|
||||
|
||||
type Props = {
|
||||
theme: CustomTheme,
|
||||
onPress: () => void,
|
||||
onStarPress: () => void,
|
||||
item: group,
|
||||
height: number,
|
||||
}
|
||||
type PropsType = {
|
||||
theme: CustomThemeType,
|
||||
onPress: () => void,
|
||||
onStarPress: () => void,
|
||||
item: PlanexGroupType,
|
||||
favorites: Array<PlanexGroupType>,
|
||||
height: number,
|
||||
};
|
||||
|
||||
type State = {
|
||||
isFav: boolean,
|
||||
}
|
||||
const REPLACE_REGEX = /_/g;
|
||||
|
||||
class GroupListItem extends React.Component<Props, State> {
|
||||
class GroupListItem extends React.Component<PropsType> {
|
||||
isFav: boolean;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isFav: (props.item.isFav !== undefined && props.item.isFav),
|
||||
}
|
||||
starRef: null | Animatable.View;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.isFav = this.isGroupInFavorites(props.favorites);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: PropsType): boolean {
|
||||
const {favorites} = this.props;
|
||||
const favChanged = favorites.length !== nextProps.favorites.length;
|
||||
let newFavState = this.isFav;
|
||||
if (favChanged) newFavState = this.isGroupInFavorites(nextProps.favorites);
|
||||
const shouldUpdate = this.isFav !== newFavState;
|
||||
this.isFav = newFavState;
|
||||
return shouldUpdate;
|
||||
}
|
||||
|
||||
onStarPress = () => {
|
||||
const {props} = this;
|
||||
const ref = this.starRef;
|
||||
if (ref != null) {
|
||||
if (this.isFav) ref.rubberBand();
|
||||
else ref.swing();
|
||||
}
|
||||
props.onStarPress();
|
||||
};
|
||||
|
||||
shouldComponentUpdate(prevProps: Props, prevState: State) {
|
||||
return (prevState.isFav !== this.state.isFav);
|
||||
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 = () => {
|
||||
this.setState({isFav: !this.state.isFav});
|
||||
this.props.onStarPress();
|
||||
}
|
||||
|
||||
render() {
|
||||
const colors = this.props.theme.colors;
|
||||
return (
|
||||
<List.Item
|
||||
title={this.props.item.name}
|
||||
onPress={this.props.onPress}
|
||||
left={props =>
|
||||
<List.Icon
|
||||
{...props}
|
||||
icon={"chevron-right"}/>}
|
||||
right={props =>
|
||||
<IconButton
|
||||
{...props}
|
||||
icon={"star"}
|
||||
onPress={this.onStarPress}
|
||||
color={this.state.isFav
|
||||
? colors.tetrisScore
|
||||
: props.color}
|
||||
/>}
|
||||
style={{
|
||||
height: this.props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
render(): React.Node {
|
||||
const {props} = this;
|
||||
const {colors} = props.theme;
|
||||
return (
|
||||
<List.Item
|
||||
title={props.item.name.replace(REPLACE_REGEX, ' ')}
|
||||
onPress={props.onPress}
|
||||
left={(iconProps: ListIconPropsType): React.Node => (
|
||||
<List.Icon
|
||||
color={iconProps.color}
|
||||
style={iconProps.style}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
)}
|
||||
right={(iconProps: ListIconPropsType): React.Node => (
|
||||
<Animatable.View
|
||||
ref={(ref: Animatable.View) => {
|
||||
this.starRef = ref;
|
||||
}}
|
||||
useNativeDriver>
|
||||
<TouchableRipple
|
||||
onPress={this.onStarPress}
|
||||
style={{
|
||||
marginRight: 10,
|
||||
marginLeft: 'auto',
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
}}>
|
||||
<MaterialCommunityIcons
|
||||
size={30}
|
||||
style={{padding: 10}}
|
||||
name="star"
|
||||
color={this.isFav ? colors.tetrisScore : iconProps.color}
|
||||
/>
|
||||
</TouchableRipple>
|
||||
</Animatable.View>
|
||||
)}
|
||||
style={{
|
||||
height: props.height,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(GroupListItem);
|
||||
|
|
|
|||