Compare commits

..

211 commits

Author SHA1 Message Date
6441865b09 Fix eslint errors 2020-08-10 12:46:26 +02:00
0356736d14 bump android version code 2020-08-09 21:36:31 +02:00
329d6eab1d Remove unused libs from lock 2020-08-09 20:57:14 +02:00
835656bb30 Use Image instead of avatar
This removes round corners
2020-08-09 20:47:41 +02:00
14e5b01341 Fix feed item screen page source 2020-08-09 20:44:25 +02:00
6ec87821e8 Replace image modal by image gallery screen
This improves performance and allows multi-images view
2020-08-09 20:41:49 +02:00
6e7d891d5c Update libs 2020-08-09 17:22:51 +02:00
88fe4de4d7 Fix missing links 2020-08-09 16:04:14 +02:00
8d860c17c2 Fix incorrect link 2020-08-09 16:00:46 +02:00
e793f871c6 Fix table links 2020-08-09 15:59:31 +02:00
c82b3c30bf Try fix link 2020-08-09 15:57:18 +02:00
a3c691ebf0 Fix spelling 2020-08-09 15:52:30 +02:00
c2cdabe8fc Improve readme and doc 2020-08-09 15:49:45 +02:00
962f629e02 Improve readme 2020-08-08 19:59:50 +02:00
c62307af7c Fix eslint error 2020-08-08 16:39:26 +02:00
795980dc8d Add support for new server news api 2020-08-08 16:38:20 +02:00
4199a8700c Update install instructions 2020-08-07 18:36:00 +02:00
5cdf9d6318 Merge remote-tracking branch 'origin/dev' into dev 2020-08-07 18:28:46 +02:00
19d856574e Move keystore properties in own file
This allows to sync gradle.properties with git
2020-08-07 18:19:41 +02:00
bf09f1d14d bump ios version 2020-08-07 15:31:55 +02:00
dfa9548a2f Bump version number 2020-08-07 14:42:08 +02:00
4ee7490af4 Added new run webstorm run configurations 2020-08-07 14:41:55 +02:00
0e741a02e2 Make mascot heart eyes red 2020-08-07 14:06:51 +02:00
92603cbaf7 Use transparent header for game 2020-08-07 14:00:21 +02:00
a3ce3a76c3 Use native driver for more animations 2020-08-07 13:49:57 +02:00
fd44a32b05 Improve intro slides style 2020-08-07 13:45:13 +02:00
956d78ad7f Fix promise rejection warning 2020-08-07 13:38:37 +02:00
c522a410ab Improve favorite buttons 2020-08-07 13:34:39 +02:00
d133ea30a5 Improve list icon handling 2020-08-07 13:19:38 +02:00
ee82589cae Fix single date selection text not showing 2020-08-07 11:52:04 +02:00
4c29e146bb Fix crash on equipment screen enter 2020-08-07 11:49:01 +02:00
f0de0599dd Add android release npm script 2020-08-06 18:03:38 +02:00
b378473591 Add star press animation 2020-08-06 17:48:35 +02:00
327488a470 Improve planex group favorite handling 2020-08-06 17:34:53 +02:00
eef6f75414 Change default android modal transition 2020-08-06 14:16:22 +02:00
5166e0b879 Fix crash on collapsible click 2020-08-06 14:14:01 +02:00
98770611ff Fix crash on planex group screen open 2020-08-06 14:01:28 +02:00
ee9e225dae Fix game navigation issue
Fixed start game screen being replaced by end screen when exiting a runnnning game
2020-08-06 13:54:09 +02:00
05f769fe79 Fix crash on app start
Used animated from react native instead of react native reanimated in the tab bar
2020-08-06 13:46:48 +02:00
1be913c5aa Update tests to match new implementations 2020-08-06 12:09:17 +02:00
c86281cbd2 Change name 2020-08-05 21:09:37 +02:00
4cc9c61d72 Disable lint for test files 2020-08-05 21:09:04 +02:00
1e81b2cd7b Improve remaining files to match linter 2020-08-05 20:58:28 +02:00
cbe3777957 Improve Game files to match linter 2020-08-05 20:24:08 +02:00
569e659779 Improve utils files to match linter 2020-08-05 18:52:18 +02:00
fcbc70956b Improve Services screen components to match linter 2020-08-05 18:39:44 +02:00
3ce23726c2 Improve Planning screen components to match linter 2020-08-05 15:04:41 +02:00
a3299c19f7 Improve Settings screen components to match linter 2020-08-05 13:51:14 +02:00
0a64f5fcd7 Improve Amicale screen components to match linter 2020-08-05 11:54:13 +02:00
483970c9a8 Improve about components to match linter 2020-08-05 00:37:51 +02:00
3e4f2f4ac1 Improve navigators to match linter 2020-08-05 00:16:05 +02:00
7107a8eadf Improve constants to match linter 2020-08-05 00:06:05 +02:00
7ac62b99f4 Improve constants to match linter 2020-08-04 23:51:32 +02:00
aa992d20b2 Improve tab components to match linter 2020-08-04 23:49:18 +02:00
0117b25cd8 Improve basic screen components to match linter 2020-08-04 21:49:19 +02:00
4db4516296 Improve override components to match linter 2020-08-04 21:24:43 +02:00
7b94afadcc Improve Mascot components to match linter 2020-08-04 19:26:25 +02:00
1cc0802c12 Improve Proxiwash components to match linter 2020-08-04 18:53:10 +02:00
547af66977 Improve Proximo components to match linter 2020-08-04 18:00:45 +02:00
ab86c1c85c Improve planex components to match linter 2020-08-04 14:06:09 +02:00
11b5f2ac71 Improve equipment booking components to match linter 2020-08-04 10:57:19 +02:00
70365136ac Improve Dashboard edit components to match linter 2020-08-04 09:31:27 +02:00
93d12b27f8 Improve Clubs components to match linter 2020-08-03 21:53:53 +02:00
33d98b024b Improve Services components to match linter 2020-08-03 21:06:39 +02:00
6b12b4cde2 Improve Home components to match linter 2020-08-03 18:36:52 +02:00
34ccf9c4c9 Improve collapsible components to match linter 2020-08-03 17:18:50 +02:00
9d92a88627 Improve animated components to match linter 2020-08-03 16:45:10 +02:00
925bded69b Improve dialog components to match linter 2020-08-03 16:28:03 +02:00
3629c5730a Improve connection manager to match linter 2020-08-02 19:53:05 +02:00
3d9bfdea4c Improve vote screen to match linter 2020-08-02 19:52:19 +02:00
142b861ccb Improve vote screens to match linter 2020-08-02 19:51:19 +02:00
0a9e0eb0ca Improve requests handlers to match linter 2020-08-02 19:45:19 +02:00
9fc02baf6d Remove Expo launch config
Removed unused config
2020-08-02 19:44:06 +02:00
22eabf28d5 Update eslint rules
Changed rules to fix conflicts with flow
2020-08-02 19:43:06 +02:00
c0777511a6 Add flow babel preset 2020-08-01 21:00:28 +02:00
be1f61b671 Fix eslint errors
First files rewritten to match the new eslint config
2020-08-01 20:59:59 +02:00
b596f68abe Remove annoying eslint warning
This warning would have made me rename all my .js files to .jsx, which is pointless
2020-08-01 20:59:04 +02:00
05ef28f3b5 Revert flow config
This prevented flow from starting so I had to reset it.
2020-08-01 20:41:47 +02:00
2aac20ccee Added support for eslint and prettier using airbnb config 2020-08-01 19:01:51 +02:00
26aded3684 Updated project config to match default react native projects 2020-08-01 16:48:10 +02:00
1421f4f308 Updated libs 2020-08-01 15:53:37 +02:00
e701262ef3 Updated launch screen 2020-08-01 15:53:29 +02:00
7a134c7906 Improve the english locale 2020-07-24 15:17:15 +02:00
12e57005da Improve the english locale 2020-07-24 15:17:01 +02:00
14970abeab Added collapsible headers to more screens 2020-07-23 15:19:04 +02:00
5349e210cb Allow mascot popup to be controlled directly with a pref key 2020-07-23 14:52:56 +02:00
6254ce1814 Improved async storage usage 2020-07-23 12:03:51 +02:00
2b7e6b4541 Updated translations 2020-07-23 09:09:42 +02:00
7dd91574a5 Updated react native to latest 2020-07-23 00:44:08 +02:00
ab801ec92b Updated libs 2020-07-23 00:18:52 +02:00
be33726e39 Added collapsible header to more screens and added an abstraction layer around collapsible behaviour 2020-07-22 23:54:05 +02:00
22d5f61fc5 Improved home screen handling 2020-07-22 22:15:24 +02:00
560c336759 Improved home screen layout 2020-07-21 22:55:09 +02:00
9f4a8c837d Fixed game tick interval continuing after game exit 2020-07-21 22:45:45 +02:00
8864e686bd Improved dashboard item size 2020-07-21 22:40:24 +02:00
ccf196abaa Fixed duplicate list child key warning 2020-07-21 22:35:38 +02:00
ae3d9310d6 Improved game start screen gradient 2020-07-21 22:21:37 +02:00
b2ff90855f Added game score save support 2020-07-21 20:43:03 +02:00
4f911ce32d Improved game start screen 2020-07-20 18:26:27 +02:00
0ed3122dcf Improved mascot style management 2020-07-20 16:36:16 +02:00
de41a57930 Small game UI improvements 2020-07-19 10:46:30 +02:00
1780ab886e Adapt preview size to shape size 2020-07-19 10:43:46 +02:00
4bff6e15a8 Improved game UI 2020-07-18 22:38:17 +02:00
fdf0fffabc Replaced game alert by paper dialog 2020-07-18 21:28:15 +02:00
746303b35a Added mascot dialog on game first start 2020-07-18 19:51:07 +02:00
3989652c29 Changed game project organization and added basic start screen 2020-07-18 19:45:24 +02:00
fe26ec0cc4 Improved game flow typing 2020-07-18 19:25:51 +02:00
494b319f19 Renamed game to a more general name 2020-07-18 15:52:41 +02:00
ca107356d1 Improved dashboard badge borders 2020-07-17 17:31:02 +02:00
d1fc8a9625 Updated react native paper to latest major version and fixed error on card press 2020-07-17 17:28:39 +02:00
8004820c2a Updated react native and libraries to new minor version 2020-07-17 15:17:24 +02:00
5ad1e1d3f3 Added mascot on home header 2020-07-16 23:31:04 +02:00
96c64a98e0 Improved UI and layout 2020-07-16 23:12:03 +02:00
b405f2aa6b Added ability to set a custom dashboard via settings 2020-07-16 22:53:48 +02:00
2022b738f5 Dashboard can now display any service from ServicesManager.js 2020-07-16 18:25:54 +02:00
ac19b77fd3 Grouped all services in a single class for easier manipulation 2020-07-15 19:11:24 +02:00
1c7768d029 Improved french translation 2020-07-15 18:20:16 +02:00
99ff524d53 Added mascot to profile screen 2020-07-15 18:12:55 +02:00
8b201efabf Added mascot to equipment screen 2020-07-15 18:05:56 +02:00
9a379ffb3d Added mascot to vote screen 2020-07-15 17:53:31 +02:00
57c3e7d9d7 Added new launch screen for iOS 2020-07-15 17:15:33 +02:00
5f9132a670 Improved intro slides 2020-07-14 23:44:03 +02:00
ca03b70603 Fixed invalid icons 2020-07-14 22:43:43 +02:00
870cbfdadf Improved services list display and enabled paging for easier navigation 2020-07-14 22:28:23 +02:00
97072be390 Changed android splash screen and removed unused assets 2020-07-14 21:50:44 +02:00
ea19ca3ade Use square image for amicale logo across the app 2020-07-14 21:50:22 +02:00
7ef93da811 Ignored warning thrown by collapsible headers lib 2020-07-14 18:57:33 +02:00
460d84c5f4 Improved mascot popup buttons layout 2020-07-14 18:51:00 +02:00
f675bda780 Merge branch 'dev' of https://git.etud.insa-toulouse.fr/vergnet/application-amicale into dev 2020-07-14 18:13:38 +02:00
088527a15f Updated ios project 2020-07-14 18:13:28 +02:00
3c31646382 Improved install script for ios 2020-07-14 18:11:44 +02:00
c939d5a552 Fixed Podfile 2020-07-14 17:59:05 +02:00
5396b0e733 Improved install instructions 2020-07-14 17:33:28 +02:00
04d93fcf83 Fixed install script errors 2020-07-14 17:22:24 +02:00
eb9bf26baa Improved install script and instructions 2020-07-14 17:20:58 +02:00
a90dcf4460 Added missing assets 2020-07-14 16:59:11 +02:00
63720f9101 Improved install script 2020-07-14 11:39:32 +02:00
bc3826c59a Updated react-native-keychain to latest as project uses sdk 29 by default 2020-07-14 11:32:39 +02:00
8156782d10 Updated project to React Native 0.63.0 2020-07-14 11:19:02 +02:00
2d4e118614 Updated remaining libraries 2020-07-14 09:59:36 +02:00
83d7aad2fe Updated some libraries 2020-07-14 00:24:54 +02:00
f8d148d7ce Improved login screen 2020-07-13 20:57:23 +02:00
434d8b6565 Updated intro slides to make them shorter and include the mascot 2020-07-13 20:09:28 +02:00
90d1437248 Fixed typo 2020-07-12 22:42:26 +02:00
eba2cebe01 Improved french translation and made it more familiar 2020-07-12 22:41:32 +02:00
9064b8da77 Improved french mascot translation 2020-07-12 18:09:45 +02:00
0d1fe124f4 Improved locale files structure 2020-07-12 16:40:37 +02:00
2a9bf5bb6a Added mascot to every main screen and allow cancel with back button 2020-07-12 11:37:11 +02:00
761132732b Added a new mascot dialog to replace banners 2020-07-12 00:04:33 +02:00
976684dfce Added booking confirmation screen 2020-07-10 17:04:29 +02:00
e048035722 Improved equipment rent screens to match new api version 2020-07-10 15:04:35 +02:00
5067fd47d6 Added basic equipment booking functionality 2020-07-09 14:40:01 +02:00
63b02cd83c Improved planex to match new website version 2020-07-07 23:46:20 +02:00
3275b73708 Fixed amicale category image 2020-07-07 18:49:40 +02:00
9e5542359b Use custom icon for amicale website 2020-07-07 18:46:32 +02:00
d7c14febb2 Updated Changelog.md 2020-07-02 15:22:00 +02:00
39d5e05537 updated project version 2020-07-02 13:18:45 +02:00
ea25d2dd67 Updated version number 2020-07-02 12:21:33 +02:00
01e3d96ddb Updated home screen to use v2 dashboard 2020-07-02 12:13:57 +02:00
5bfc353218 Improved install instructions 2020-07-02 11:20:29 +02:00
a9caca9969 Improved tab bar hiding logic 2020-07-02 00:17:53 +02:00
f9efea288f Removed unused function 2020-07-01 23:38:08 +02:00
d622a2f77a Improved website handling 2020-07-01 20:01:14 +02:00
b813aa0b83 Improved doc and typing, improved API connection handling 2020-07-01 13:14:17 +02:00
3f14f7bb96 Added fingerprint permission to fix connection problem on some android devices 2020-06-29 19:07:38 +02:00
98168b560b Fixed crash on login input change 2020-06-29 16:02:25 +02:00
b66e50eaf8 Improved doc and typing and removed unused file 2020-06-29 15:09:33 +02:00
869a8e5ec0 Improved doc and typing 2020-06-29 12:12:14 +02:00
fe9efc4008 Updated notes links 2020-06-28 12:51:35 +02:00
d6ce72d195 Added notes about react native screens 2020-06-28 12:48:31 +02:00
401c7d85ef Improved doc 2020-06-28 12:39:13 +02:00
56ab0e562f Updated gradlew.bat to match 62.2 release 2020-06-28 09:45:24 +02:00
5b225dcedb Fixed french title translation too long for some devices 2020-06-27 23:39:29 +02:00
5c918ecb3d Added storage write permission to allow webview file download 2020-06-27 23:26:49 +02:00
bc05391708 Increased version number and moved back to keychain 6.0.0 and sdk 18 as it did not fix the known issue 2020-06-27 22:49:46 +02:00
32c77fab05 Added a little surprise 2020-06-27 19:37:34 +02:00
b6915a1ebe Improved HTML parsing 2020-06-26 18:22:00 +02:00
82371e89e7 Improved layout margins 2020-06-26 18:07:04 +02:00
217e918ce8 Allow navigating to amicale services section without being logged in 2020-06-26 17:54:28 +02:00
88b2120c8a Open login screen when user tries to access an amicale service instead of showing an error 2020-06-26 17:50:42 +02:00
aaf7084297 Do not show banner if user has logged in 2020-06-26 17:44:15 +02:00
06d01e98b0 Improved banners and added one on the home screen 2020-06-26 15:53:49 +02:00
57f7716700 Use built in webview to display password reset page 2020-06-26 13:25:28 +02:00
4cdadbc6c1 Use built in webview to display password reset page 2020-06-26 13:25:20 +02:00
d07b34c748 Fixed information edit link not working 2020-06-26 13:18:38 +02:00
629f0401bc Fixed post login screen when clicking on home header button login 2020-06-26 13:09:32 +02:00
98359dba7d Added a welcome box on profile screen 2020-06-26 13:01:27 +02:00
f2acb59ea7 Improved amicale home button and post login behavior 2020-06-26 12:28:18 +02:00
fe9089881a Show amicale services even when not logged in 2020-06-26 12:11:19 +02:00
72c5a91f75 Improved error display 2020-06-26 12:03:08 +02:00
6ac459e58a Improved translation 2020-06-25 15:40:00 +02:00
23bc034b34 Updated proxiwash to match v2 API 2020-06-24 19:22:27 +02:00
b1e3fe6658 Tried to upgrade keychain and sdk version in an effort to fix a keychain error on some devices 2020-06-24 18:34:09 +02:00
4131b79561 Fixed crash on self menu screen 2020-06-23 22:27:57 +02:00
236ee2c07c Added development notes 2020-06-23 21:03:26 +02:00
5690d176b7 Updated webview to v10 2020-06-23 20:44:54 +02:00
f321888bc3 Removed map screen 2020-06-23 20:26:45 +02:00
e43577c8cb Lowered keychain version to prevent upgrading sdk and removed mapbox as it makes building release harder and increased apk size 2020-06-23 20:13:52 +02:00
584d9ca563 Updated dependencies and changed sdk version to 29 (required for keychain 6.1.1) 2020-06-23 17:40:41 +02:00
35d39a5711 Updated services descriptions 2020-06-22 20:43:40 +02:00
5e36b993c5 Merge branch 'map' into dev 2020-06-22 20:08:01 +02:00
09de59c178 Added map icon 2020-06-22 20:07:26 +02:00
f644964473 Updated translations 2020-06-22 20:06:34 +02:00
936f2a8e9e Use public token 2020-06-22 20:02:34 +02:00
3b3b07bcf2 Removed unused file 2020-06-22 16:20:38 +02:00
d9ae6ad3a5 Merge remote-tracking branch 'origin/dev' into dev 2020-06-22 16:11:51 +02:00
86f99b49e4 Updated installation instructions 2020-06-22 16:11:35 +02:00
1af4688329 Tried implementing basic map markers 2020-06-22 16:04:08 +02:00
b378fde2f0 updated ios camera usage description 2020-06-17 14:48:32 +02:00
211e57167d Added basic map 2020-06-17 14:34:41 +02:00
7464ef8859 Fixed typo 2020-06-16 17:49:27 +02:00
234 changed files with 33364 additions and 14543 deletions

6
.buckconfig Normal file
View 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
View 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,
},
};

View file

@ -1,11 +0,0 @@
[ignore]
[include]
[libs]
[lints]
[options]
[strict]

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
*.pbxproj -text

29
.gitignore vendored
View file

@ -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

View 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>

View file

@ -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>

View 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>

View 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
View file

@ -0,0 +1,6 @@
module.exports = {
bracketSpacing: false,
jsxBracketSameLine: true,
singleQuote: true,
trailingComma: 'all',
};

370
App.js
View file

@ -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>
);
}
}

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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);
});

View 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);
});

View file

@ -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');
});

View file

@ -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,
);
});

View file

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

View file

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

View file

@ -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

Binary file not shown.

View file

@ -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.** { *; }

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View file

@ -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>

View file

@ -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
View 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

View file

@ -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
View file

@ -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
View file

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

BIN
assets/mascot/mascot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

View file

@ -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
View 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
View 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
View 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
View 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
View 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
View 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).

View file

@ -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

View file

@ -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);

View file

@ -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",

View file

@ -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

View file

@ -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>

View file

@ -9,7 +9,7 @@
"scale" : "2x"
},
{
"filename" : "splash.png",
"filename" : "launch_screen.png",
"idiom" : "universal",
"scale" : "3x"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

View file

@ -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>

View 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>

View file

@ -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
View 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
View 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é, lafficheur indique 'Programme terminé', appuyer sur le bouton jaune douverture 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 cest ta petite épicerie étudiante tenue par les étudiants directement sur le campus. Ouverte tous les jours de 18h30 à 19h30, nous taccueillons et te sauvons quand tu nas 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 à ladministration 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 à ladministration 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"
}
}
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

View file

@ -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;

View file

@ -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;

View 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);

View file

@ -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);

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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'
},
});

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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,
}}
/>
);
}
}

View 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);

View 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;

View 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;

View 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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View 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;

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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
}
/>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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>
);
}
}

View file

@ -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;

View file

@ -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);

View 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);

View 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);

View file

@ -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);

View 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);

View file

@ -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);

View file

@ -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);

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