Compare commits

...

191 commits

Author SHA1 Message Date
Arnaud Vergnet
aaf72d9122 feat: render planex in incognito mode
Should hopefully fix the old planning being rendered issue
2021-09-18 11:52:50 +02:00
Arnaud Vergnet
1a696f0628 fix: infinite refresh if no internet 2021-09-18 11:45:29 +02:00
Arnaud Vergnet
9acfbf00df feat: change game piece colors 2021-09-18 11:30:24 +02:00
Arnaud Vergnet
9efd40e48c Upgrade to 5.0.0-3 2021-09-12 23:41:37 +02:00
Arnaud Vergnet
de8820eada feat: save login token 2021-09-12 23:39:23 +02:00
Arnaud Vergnet
dc944060e1 feat: update html render 2021-09-12 23:31:17 +02:00
Arnaud Vergnet
2c11addf40 fix: make search fields take whole header 2021-09-12 22:38:52 +02:00
Arnaud Vergnet
8bacddc7b5 chore: remove comment 2021-09-12 22:27:40 +02:00
Arnaud Vergnet
53ec2bb578 chore: ignore lib warning 2021-09-12 18:38:09 +02:00
Arnaud Vergnet
764296708c feat: fix typescript and eslint errors 2021-09-12 18:33:18 +02:00
Arnaud Vergnet
d3e94ac9b3 feat: update iOS project to use hermes 2021-09-12 17:31:22 +02:00
Arnaud Vergnet
7c38ec0bdb Upgrade to 5.0.0-2 2021-09-10 17:43:03 +02:00
Arnaud Vergnet
cb3af52483 build: increase java heap space
Useful for android release builds
2021-09-10 17:42:21 +02:00
Arnaud Vergnet
d5c6aa6b48 feat: updated libs 2021-09-10 16:44:26 +02:00
Arnaud Vergnet
6104b88815 build: update to react native 0.65.1 2021-09-10 12:21:27 +02:00
Arnaud Vergnet
26f6518270 Fix proxiwash notifications 2021-07-17 09:57:26 +02:00
Arnaud Vergnet
3b2776542a Show proxiwash message in header if available 2021-07-16 15:30:55 +02:00
Arnaud Vergnet
76f13f04d5 fix: move back text in comment 2021-07-14 16:43:53 +02:00
Arnaud Vergnet
67b5a5fb4f feat: update libs 2021-07-14 15:52:00 +02:00
Arnaud Vergnet
c75b90d254 Change proxiwash website icon to open-in-new 2021-07-06 19:11:15 +02:00
Arnaud Vergnet
b9c99bf269 Merge branch 'master' of https://git.etud.insa-toulouse.fr/vergnet/application-amicale 2021-07-06 19:01:23 +02:00
7f763dcbcb Merge branch 'btn-proxiwash' of leban/application-amicale into master 2021-07-06 19:01:11 +02:00
Arnaud Vergnet
1f930223c4 Improve english locale 2021-07-06 18:59:39 +02:00
Gérald LEBAN
ba62e5d3ec Add a button to open the proxiwash website in the default browser 2021-07-05 13:19:02 +02:00
Gérald LEBAN
b127cca068 Add a button to open the proxiwash website in the default browser 2021-07-05 13:09:22 +02:00
Gérald LEBAN
06dc9966ec Add urls leading to proxiwash webpage (washinsa and tripodeB) 2021-07-05 12:58:29 +02:00
Gérald LEBAN
53b3f00005 Add urls leading to proxiwash webpage (washinsa and tripodeB) 2021-07-05 12:58:16 +02:00
Arnaud Vergnet
20aed5cc80 Update prettier config 2021-05-23 23:08:22 +02:00
Arnaud Vergnet
0be3a53747 Upgrade to 5.0.0-1 2021-05-23 16:15:44 +02:00
Arnaud Vergnet
bdffd01df4 Fix mascot dialog not showing 2021-05-23 16:12:42 +02:00
Arnaud Vergnet
c500ae05e6 Redirect to login screen if not logged in 2021-05-23 15:43:50 +02:00
Arnaud Vergnet
b289a85b8a Do not show retry button on token error 2021-05-23 15:07:37 +02:00
Arnaud Vergnet
ffa4cfa376 Fix state errors 2021-05-23 15:04:19 +02:00
Arnaud Vergnet
541c002558 convert connection manager to context 2021-05-23 14:14:20 +02:00
Arnaud Vergnet
44aa52b3aa Upgrade to 5.0.0-0 2021-05-22 19:25:38 +02:00
Arnaud Vergnet
b15b200846 Hide loading screen only after JS loaded 2021-05-22 19:14:03 +02:00
Arnaud Vergnet
fe96d9f8a1 fix proxiwash change not auto refreshing list 2021-05-22 18:38:10 +02:00
Arnaud Vergnet
44d35090ac Add token retrieve error 2021-05-22 18:23:29 +02:00
Arnaud Vergnet
245e6c5cc8 Remove log 2021-05-22 15:45:56 +02:00
Arnaud Vergnet
8f9c02ff75 Fix game reset and highscore updates 2021-05-22 15:45:24 +02:00
Arnaud Vergnet
9ae585bdf8 Convert game into functional component 2021-05-22 15:39:35 +02:00
Arnaud Vergnet
14365a92a4 fix login screen header color 2021-05-22 14:06:27 +02:00
Arnaud Vergnet
19f6dd3cf0 fix header darkmode not changing 2021-05-22 11:18:48 +02:00
Arnaud Vergnet
acbbd2d27d Move preferences in separate contextes
This improves performance when updating preferences
2021-05-22 11:11:53 +02:00
Arnaud Vergnet
20d5e790d0 fix proxiwash update loop 2021-05-19 13:00:39 +02:00
Arnaud Vergnet
9e6fee467f Fix tab navigator render loop 2021-05-19 09:47:36 +02:00
Arnaud Vergnet
94a0ca33a4 Remove log 2021-05-18 18:45:56 +02:00
Arnaud Vergnet
c3304c6f06 Fix initial preferences loading 2021-05-18 18:43:14 +02:00
Arnaud Vergnet
7d0df0e7ce Fix startup crash 2021-05-18 18:31:18 +02:00
Arnaud Vergnet
00f9428972 Use context to handle preferences
This is not tested, expect crashes
2021-05-18 11:36:15 +02:00
Arnaud Vergnet
b5d4ad83c3 Move context files into own folder 2021-05-15 11:41:17 +02:00
Arnaud Vergnet
1d2ec83619 Add context async storage logic
This isn't implemented yet, but the necessary files are here
2021-05-15 11:38:12 +02:00
Arnaud Vergnet
d55c692bd3 Improve request error handling 2021-05-13 19:10:28 +02:00
Arnaud Vergnet
a1cfb0385a Move accordion children in prop 2021-05-13 18:20:47 +02:00
Arnaud Vergnet
52651ecf85 Convert planex group components to functional 2021-05-13 17:19:43 +02:00
Arnaud Vergnet
9675d329cc Remove unused import 2021-05-13 16:33:00 +02:00
Arnaud Vergnet
5795fca035 Convert mascot popup to functional
THis fixes TS issues
2021-05-13 16:32:41 +02:00
Arnaud Vergnet
ae1e2fcdc0 fix typescript errors 2021-05-13 13:28:34 +02:00
Arnaud Vergnet
742643b9e2 Replace Authenticated screen by RequestScreen 2021-05-13 13:19:28 +02:00
Arnaud Vergnet
9b4caade00 Update api error codes 2021-05-13 10:58:47 +02:00
Arnaud Vergnet
50c62dd676 fix state update 2021-05-13 10:32:44 +02:00
Arnaud Vergnet
6516cf918d remove log 2021-05-13 10:27:25 +02:00
Arnaud Vergnet
e7cffde198 Remove annoying snackbar 2021-05-13 10:27:01 +02:00
Arnaud Vergnet
c1dd69d0ed move last refresh date in request screen 2021-05-13 09:59:38 +02:00
Arnaud Vergnet
02135d64ff Allow laundromat swwitch from header 2021-05-12 23:56:44 +02:00
Arnaud Vergnet
360023aea6 show proxiwash last updated date 2021-05-12 23:49:41 +02:00
Arnaud Vergnet
27199b85e5 Update proxiwash screen and notifications 2021-05-12 23:01:41 +02:00
Arnaud Vergnet
e08fdc7c37 Improve planex group search performance 2021-05-11 16:43:43 +02:00
Arnaud Vergnet
ed4bb216a0 Improve favorites handling 2021-05-11 16:31:15 +02:00
Arnaud Vergnet
c2fdda5588 fix no group selected screen 2021-05-11 15:56:04 +02:00
Arnaud Vergnet
8506d3d81f Sync planex bottom bar animation with scroll 2021-05-11 15:49:55 +02:00
Arnaud Vergnet
46944a4487 show event color in popup 2021-05-11 09:08:05 +02:00
Arnaud Vergnet
115534f1c6 fix price decimals 2021-05-11 08:57:23 +02:00
Arnaud Vergnet
92eedda98b fix planex initial group 2021-05-11 08:55:20 +02:00
Arnaud Vergnet
35a4b377f8 simplify web section list 2021-05-11 08:47:54 +02:00
Arnaud Vergnet
3cb6ddd7f9 remove unecessary rule 2021-05-10 23:38:52 +02:00
Arnaud Vergnet
27f7a079b4 refactor planex groups with functionnal components 2021-05-10 23:38:04 +02:00
Arnaud Vergnet
aac598a94a Fix animation error 2021-05-10 21:34:15 +02:00
Arnaud Vergnet
d3a48d95c3 Fix group selection 2021-05-10 21:34:07 +02:00
Arnaud Vergnet
f6f1a5519e improve proximo list item 2021-05-10 21:27:27 +02:00
Arnaud Vergnet
aefeb8373a refactor planex screen to functionnal component 2021-05-10 21:21:15 +02:00
Arnaud Vergnet
a8dde29654 fix proximo images 2021-05-10 20:55:00 +02:00
Arnaud Vergnet
63722c2417 fix some ts errors 2021-05-10 18:00:57 +02:00
Arnaud Vergnet
2bbb3f60ce fix lint errors 2021-05-10 17:55:47 +02:00
Arnaud Vergnet
0a28cf16e3 change sorting function name 2021-05-10 17:42:58 +02:00
Arnaud Vergnet
0182d6118f show article count 2021-05-10 17:38:39 +02:00
Arnaud Vergnet
2f1c64e6f9 add session cache for proximo data 2021-05-10 15:14:55 +02:00
Arnaud Vergnet
a94006d18a Improve websectionlist and update proximo api 2021-05-10 14:55:21 +02:00
Arnaud Vergnet
aed58f8749 Re-add link to game 2021-05-09 15:11:01 +02:00
Arnaud Vergnet
7a58ce6b70 Improve webview state 2021-05-09 15:06:45 +02:00
Arnaud Vergnet
128af0b813 Centralized urls 2021-05-09 15:00:39 +02:00
Arnaud Vergnet
8f06843ba6 update react native paper 2021-05-08 23:06:37 +02:00
Arnaud Vergnet
286c1e6411 update react native collapsible 2021-05-08 22:27:05 +02:00
Arnaud Vergnet
0b4f115a14 Update some libs 2021-05-07 16:48:18 +02:00
Arnaud Vergnet
95a35038eb update react native 2021-05-07 15:41:22 +02:00
Arnaud Vergnet
02f9241d28 Update prettier and eslint config 2021-05-07 15:10:36 +02:00
Arnaud Vergnet
18f7a6abbd Update changelog 2020-10-11 12:36:01 +02:00
b3bd429afa Bump iOS version 2020-10-10 22:16:27 +02:00
Arnaud Vergnet
9c65aadfbd Bump android version code 2020-10-10 20:26:29 +02:00
Arnaud Vergnet
e33320da10 Update changelog to include v4.1.0 patch notes 2020-10-10 20:14:53 +02:00
Arnaud Vergnet
b5dcb00fce Fix selling errors 2020-10-10 20:00:14 +02:00
Arnaud Vergnet
d42c719cf1 Make planex title prettier 2020-10-09 10:02:52 +02:00
Arnaud Vergnet
5d65d72418 Remove useless button in proxiwash modal 2020-10-09 09:56:14 +02:00
Arnaud Vergnet
b692b6e7f6 Bump version to 4.1.0 2020-10-07 11:04:13 +02:00
Arnaud Vergnet
8dae8adfbe Downgrade react-native-keychain to improve startup speed
Due to a bug, token retrieving was taking several seconds on recent Android versions.
Reference: https://github.com/oblador/react-native-keychain/issues/337
2020-10-07 11:03:32 +02:00
Arnaud Vergnet
00ed963503 Use generic password instead of internet credentials 2020-10-07 10:53:31 +02:00
Arnaud Vergnet
7672dd109d load all initial data at once 2020-10-07 10:42:08 +02:00
Arnaud Vergnet
f1318c6aed Add new contributor and improve contributors generation 2020-10-07 10:08:07 +02:00
Arnaud Vergnet
25a12dad94 Fix qr code scanner not working 2020-10-07 09:39:02 +02:00
Arnaud Vergnet
6e7b3d02cd Fix gallery screen controls invisible in light mode 2020-10-05 19:49:36 +02:00
Arnaud Vergnet
5d692c6840 Update docs to replace flow references 2020-09-24 13:55:53 +02:00
Arnaud Vergnet
09ed0058ae Remove useless console.log 2020-09-23 19:08:29 +02:00
Arnaud Vergnet
1639544810 Open current webview url instead of start url 2020-09-23 19:08:09 +02:00
Arnaud Vergnet
4492debad9 Fix french translation for go back/next 2020-09-23 19:03:25 +02:00
Arnaud Vergnet
faac5688f8 Improve equipment confirm screen dates display 2020-09-23 18:58:13 +02:00
Arnaud Vergnet
346b00defd Fix group accordions expanding incorrectly 2020-09-23 18:47:00 +02:00
Arnaud Vergnet
11609f8277 Fix props equality checking functions 2020-09-23 18:40:22 +02:00
Arnaud Vergnet
74f757ce66 Fix animated fab position 2020-09-23 09:22:36 +02:00
Arnaud Vergnet
df8f1cab24 Fix tab bar not animating in certain cases 2020-09-23 09:20:06 +02:00
Arnaud Vergnet
eaf1c52af5 Really fix translations not loading 2020-09-23 09:14:54 +02:00
Arnaud Vergnet
da55abed8f Fix proxiwash translations not loading 2020-09-23 09:08:47 +02:00
Arnaud Vergnet
f5233c53f8 Fix crash on app start 2020-09-23 09:03:48 +02:00
Arnaud Vergnet
c5287611c4 Fix errors induced by refactoring 2020-09-22 23:20:16 +02:00
Arnaud Vergnet
fde9a12ef9 Update game to use TypeScript 2020-09-22 23:13:09 +02:00
Arnaud Vergnet
c198a40148 Update home screens to use TypeScript 2020-09-22 22:52:35 +02:00
Arnaud Vergnet
38afbf02a3 Update services screens to use TypeScript 2020-09-22 22:40:38 +02:00
Arnaud Vergnet
2eb4a3e0c1 Update Proximo screens to use TypeScript 2020-09-22 22:35:24 +02:00
Arnaud Vergnet
9f4dcda7d9 Update proxiwash screens to use TypeScript 2020-09-22 22:29:39 +02:00
Arnaud Vergnet
b78357968a Update planning screens to use TypeScript 2020-09-22 22:18:05 +02:00
Arnaud Vergnet
742cb1802d Update Planex screens to use TypeScript 2020-09-22 22:04:39 +02:00
Arnaud Vergnet
4d0df7a5b7 Update misc screens to use TypeScript 2020-09-22 21:58:09 +02:00
Arnaud Vergnet
b8e7272d2c Update navigation components to use TypeScript 2020-09-22 21:49:39 +02:00
Arnaud Vergnet
300558ac56 Update image gallery screen to use TypeScript 2020-09-22 19:55:41 +02:00
Arnaud Vergnet
d70f22bdae Update remaining Amicale screens to use TypeScript 2020-09-22 19:50:31 +02:00
Arnaud Vergnet
67cb96dd03 Update equipment screens to use TypeScript 2020-09-22 19:36:48 +02:00
Arnaud Vergnet
5977ce257b Update clubs screens to use TypeScript 2020-09-22 19:08:44 +02:00
Arnaud Vergnet
f7e767748a Update about screens to use TypeScript 2020-09-22 18:58:56 +02:00
Arnaud Vergnet
172b7e8187 Update mascot components to use TypeScript 2020-09-22 18:25:19 +02:00
Arnaud Vergnet
e4530ded18 Update Home base components to use TypeScript 2020-09-22 18:06:08 +02:00
Arnaud Vergnet
140bcf3675 Update animated components to use TypeScript 2020-09-22 17:43:40 +02:00
Arnaud Vergnet
f43dc55735 Update basic screen components to use TypeScript 2020-09-22 17:23:17 +02:00
Arnaud Vergnet
8ac19f36de Update constants to use TypeScript 2020-09-22 15:19:54 +02:00
Arnaud Vergnet
5261e85254 Update custom tab bar to use TypeScript 2020-09-22 15:16:25 +02:00
Arnaud Vergnet
e4adcd0057 Update list components to use TypeScript 2020-09-22 15:06:38 +02:00
Arnaud Vergnet
acc4f8cdcc Update component overrides and intro slides to use TypeScript 2020-09-22 14:26:44 +02:00
Arnaud Vergnet
18f8c64302 Update image gallery button to use TypeScript 2020-09-22 11:52:17 +02:00
Arnaud Vergnet
98518c46b6 Update dialogs to use TypeScript 2020-09-22 11:49:31 +02:00
Arnaud Vergnet
f95635136e Update Amicale and related components to use TypeScript 2020-09-22 11:34:18 +02:00
Arnaud Vergnet
18ec6e0a59 Update utility files to use TypeScript 2020-09-21 22:40:58 +02:00
Arnaud Vergnet
375fc8b971 Update App.tsx and related files to use TypeScript 2020-09-21 21:44:07 +02:00
Arnaud Vergnet
54486d1deb Update project config files to use TypeScript 2020-09-21 21:14:42 +02:00
Arnaud Vergnet
64e643f5c6 Allow clicking links on Planex title 2020-09-21 19:15:44 +02:00
Arnaud Vergnet
8892095f8a Warn about Amicale vs INSA account 2020-09-21 19:09:53 +02:00
Arnaud Vergnet
89c65d7070 Improve android splash screen style 2020-09-21 19:01:32 +02:00
Arnaud Vergnet
97d6e1dffc Make tab touch feedback rounded 2020-09-21 18:57:12 +02:00
Arnaud Vergnet
85b0b9de29 Remove unused imports 2020-09-21 18:53:46 +02:00
Arnaud Vergnet
5c1052fbd4 Improve tab home button display 2020-09-21 18:53:25 +02:00
Arnaud Vergnet
5b13eacc12 Update modules 2020-09-21 18:50:17 +02:00
Arnaud Vergnet
cc03aa6912 Add licence headers 2020-09-21 18:12:58 +02:00
Arnaud Vergnet
b9e9e03d0a Fix users descriptions not loading 2020-09-21 17:47:55 +02:00
Arnaud Vergnet
2c3f89816b Add button to change laundromat 2020-09-21 17:39:36 +02:00
Arnaud Vergnet
538d4d2187 Wait a few milliseconds before refreshing list 2020-09-21 17:34:51 +02:00
Arnaud Vergnet
b654a928a2 Reflect laundromat change when opening proxiwash screen 2020-09-21 17:32:15 +02:00
Arnaud Vergnet
b27864858b Move constant data in constants file 2020-09-21 17:15:58 +02:00
Arnaud Vergnet
61647ce6ec Fix flow errors and remove unused functions 2020-09-21 17:14:03 +02:00
docjyJ
d5e1e6c7eb Update Proxiwash Mascot popup 2020-09-13 16:12:07 +02:00
docjyJ
bc14d4cd8d Add proxiwashChangeWash lang key for the setting 2020-09-13 16:03:49 +02:00
docjyJ
82f7af58bd Translate subtitle ans description key
(en.screens.proxiwash.tripodeB.*)
2020-09-12 12:06:58 +02:00
docjyJ
c5b6e128ee Bug Correction
- bad language display
- update 'maxWeight ' to maxWeight
2020-09-12 11:59:48 +02:00
docjyJ
5b6176a361 Update ui proxiwash settings go to general Settings
Remove proxiwash settings screen
move setings in setings screen
2020-09-11 09:22:31 +02:00
docjyJ
02e30f87cf Update ui to go to proxiwash Settings
Remove the header of proxiwash screen
Add a button on proxiwash screen tool bar
2020-09-11 09:22:31 +02:00
docjyJ
83a3354d1e Update tariff language key for washinsa and tripode B 2020-09-11 09:22:31 +02:00
docjyJ
ce1227901c Update Docs 2020-09-11 09:22:31 +02:00
docjyJ
5a92b5096a Update Proxiwash About Screen 2020-09-11 09:22:31 +02:00
docjyJ
115c90b712 Add new Screen to select wash 2020-09-11 09:22:31 +02:00
docjyJ
41a17a9a91 Update Language File 2020-09-11 09:22:31 +02:00
docjyJ
7867e12a49 Update Settigns Screen 2020-09-11 09:22:31 +02:00
docjyJ
f8f5749478 add settings selection 2020-09-11 09:22:31 +02:00
Arnaud Vergnet
3de49732b9 Improve local management
This allows fast refresh when working on locales
2020-09-10 18:51:34 +02:00
Arnaud Vergnet
c3d324549d Update contributors translations 2020-09-09 17:26:14 +02:00
Arnaud Vergnet
182360aabd Improve structure and flow types 2020-09-09 17:12:23 +02:00
docjyJ
3eabd84b58 Remove Useless var 2020-09-03 13:00:58 +02:00
docjyJ
e819e94395 Update About Screen 2020-09-03 12:45:40 +02:00
docjyJ
70dcb5fe97 Update Language file 2020-09-03 12:45:06 +02:00
docjyJ
3ff5a40f8d Add Thanks key 2020-09-03 12:01:05 +02:00
docjyJ
955eb11b84 Update Screen 2020-09-03 12:01:05 +02:00
docjyJ
47e749b528 Update Lang 2020-09-03 12:01:05 +02:00
docjyJ
2f94be64e5 Update Options Dialog to support Icon 2020-09-03 12:01:05 +02:00
docjyJ
a9a2b9150b Update About Screen 2020-09-03 12:01:05 +02:00
docjyJ
a8b9fd49b7 Add Thanks key 2020-09-03 12:00:59 +02:00
docjyJ
2ed4fbd780 Add Thaks card 2020-09-03 12:00:59 +02:00
316 changed files with 50076 additions and 22487 deletions

View file

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

3
.gitattributes vendored
View file

@ -1 +1,4 @@
*.pbxproj -text
# Windows files should use crlf line endings
# https://help.github.com/articles/dealing-with-line-endings/
*.bat text eol=crlf

View file

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

4
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"i18n-ally.localesPaths": "locales",
"i18n-ally.keystyle": "nested"
}

213
App.js
View file

@ -1,213 +0,0 @@
// @flow
import * as React from 'react';
import {LogBox, Platform, SafeAreaView, View} from 'react-native';
import {NavigationContainer} from '@react-navigation/native';
import {Provider as PaperProvider} from 'react-native-paper';
import {setSafeBounceHeight} from 'react-navigation-collapsible';
import SplashScreen from 'react-native-splash-screen';
import {OverflowMenuProvider} from 'react-navigation-header-buttons';
import LocaleManager from './src/managers/LocaleManager';
import 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);
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 StateType = {
isLoading: boolean,
showIntro: boolean,
showUpdate: boolean,
showAprilFools: boolean,
currentTheme: CustomThemeType | null,
};
export default class App extends React.Component<null, StateType> {
navigatorRef: {current: null | NavigationContainer};
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();
});
}
/**
* 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;
};
/**
* 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},
});
}
};
/**
* Updates the current theme
*/
onUpdateTheme = () => {
this.setState({
currentTheme: ThemeManager.getCurrentTheme(),
});
setupStatusBar();
};
/**
* 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,
);
};
/**
* Async loading is done, finish processing startup data
*/
onLoadFinished() {
// Only show intro if this is the first time starting the app
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
// Status bar goes dark if set too fast on ios
if (Platform.OS === 'ios') setTimeout(setupStatusBar, 1000);
else setupStatusBar();
this.setState({
isLoading: false,
currentTheme: ThemeManager.getCurrentTheme(),
showIntro: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.showIntro.key,
),
showUpdate:
AsyncStorageManager.getNumber(
AsyncStorageManager.PREFERENCES.updateNumber.key,
) !== Update.number,
showAprilFools:
AprilFoolsManager.getInstance().isAprilFoolsEnabled() &&
AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.showAprilFoolsStart.key,
),
});
SplashScreen.hide();
}
/**
* Loads every async data
*
* @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;
}
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>
);
}
}

217
App.tsx Normal file
View file

@ -0,0 +1,217 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import React from 'react';
import { LogBox, Platform } from 'react-native';
import { setSafeBounceHeight } from 'react-navigation-collapsible';
import SplashScreen from 'react-native-splash-screen';
import type { ParsedUrlDataType } from './src/utils/URLHandler';
import URLHandler from './src/utils/URLHandler';
import initLocales from './src/utils/Locales';
import { NavigationContainerRef } from '@react-navigation/core';
import {
defaultMascotPreferences,
defaultPlanexPreferences,
defaultPreferences,
defaultProxiwashPreferences,
GeneralPreferenceKeys,
GeneralPreferencesType,
MascotPreferenceKeys,
MascotPreferencesType,
PlanexPreferenceKeys,
PlanexPreferencesType,
ProxiwashPreferenceKeys,
ProxiwashPreferencesType,
retrievePreferences,
} from './src/utils/asyncStorage';
import {
GeneralPreferencesProvider,
MascotPreferencesProvider,
PlanexPreferencesProvider,
ProxiwashPreferencesProvider,
} from './src/components/providers/PreferencesProvider';
import MainApp from './src/screens/MainApp';
import LoginProvider from './src/components/providers/LoginProvider';
import { retrieveLoginToken } from './src/utils/loginToken';
import { setupNotifications } from './src/utils/Notifications';
import { TabRoutes } from './src/navigation/TabNavigator';
initLocales();
setupNotifications();
LogBox.ignoreLogs([
'Cannot update a component from inside the function body of a different component',
'`new NativeEventEmitter()` was called with a non-null argument',
]);
type StateType = {
isLoading: boolean;
initialPreferences: {
general: GeneralPreferencesType;
planex: PlanexPreferencesType;
proxiwash: ProxiwashPreferencesType;
mascot: MascotPreferencesType;
};
loginToken?: string;
};
export default class App extends React.Component<{}, StateType> {
navigatorRef: { current: null | NavigationContainerRef<any> };
defaultData?: ParsedUrlDataType;
urlHandler: URLHandler;
constructor(props: {}) {
super(props);
this.state = {
isLoading: true,
initialPreferences: {
general: defaultPreferences,
planex: defaultPlanexPreferences,
proxiwash: defaultProxiwashPreferences,
mascot: defaultMascotPreferences,
},
loginToken: undefined,
};
this.navigatorRef = React.createRef();
this.defaultData = undefined;
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
this.urlHandler.listen();
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
this.loadAssetsAsync();
}
/**
* 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.defaultData = parsedData;
};
/**
* 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(TabRoutes.Home, {
nextScreen: parsedData.route,
data: parsedData.data,
});
}
};
/**
* Async loading is done, finish processing startup data
*/
onLoadFinished = (
values: Array<
| GeneralPreferencesType
| PlanexPreferencesType
| ProxiwashPreferencesType
| MascotPreferencesType
| string
| undefined
>
) => {
const [general, planex, proxiwash, mascot, token] = values;
this.setState({
isLoading: false,
initialPreferences: {
general: general as GeneralPreferencesType,
planex: planex as PlanexPreferencesType,
proxiwash: proxiwash as ProxiwashPreferencesType,
mascot: mascot as MascotPreferencesType,
},
loginToken: token as string | undefined,
});
SplashScreen.hide();
};
/**
* Loads every async data
*
* @returns {Promise<void>}
*/
loadAssetsAsync() {
Promise.all([
retrievePreferences(
Object.values(GeneralPreferenceKeys),
defaultPreferences
),
retrievePreferences(
Object.values(PlanexPreferenceKeys),
defaultPlanexPreferences
),
retrievePreferences(
Object.values(ProxiwashPreferenceKeys),
defaultProxiwashPreferences
),
retrievePreferences(
Object.values(MascotPreferenceKeys),
defaultMascotPreferences
),
retrieveLoginToken(),
])
.then(this.onLoadFinished)
.catch(this.onLoadFinished);
}
/**
* Renders the app based on loading state
*/
render() {
const { state } = this;
if (state.isLoading) {
return null;
}
return (
<GeneralPreferencesProvider
initialPreferences={this.state.initialPreferences.general}
>
<PlanexPreferencesProvider
initialPreferences={this.state.initialPreferences.planex}
>
<ProxiwashPreferencesProvider
initialPreferences={this.state.initialPreferences.proxiwash}
>
<MascotPreferencesProvider
initialPreferences={this.state.initialPreferences.mascot}
>
<LoginProvider initialToken={this.state.loginToken}>
<MainApp
ref={this.navigatorRef}
defaultData={this.defaultData}
/>
</LoginProvider>
</MascotPreferencesProvider>
</ProxiwashPreferencesProvider>
</PlanexPreferencesProvider>
</GeneralPreferencesProvider>
);
}
}

View file

@ -1,21 +1,24 @@
# Version actuelle - v3.0.7 - 13/06/2020
# Version actuelle - v4.1.0 - 11/10/2020
## 🎉 Nouveautés
- Mise à jour des écrans d'intro pour mieux refléter l'appli actuelle
- Déplacement du bouton *À propos* dans les paramètres
- Mode sombre par défaut parce que voilà
- Possibilité de sélectionner la laverie des Tripodes à la place de celle de l'INSA
- Possibilité d'ouvrir les liens zoom depuis planex !
- Ajout d'une icône adaptive pour Android 9+
- Ajout des remerciements dans la page À propos
- Amélioration des animations au clic de la barre d'onglets
## 🐛 Corrections de bugs
- Correction de crash au démarrage sur certains appareils
- Correction de l'affichage de certains sites web
- Correction du démarrage très lent sur certains appareils Android
- Correction du comportement inconsistant de la liste des groupes pour Planex
## 🖥️ Notes de développement
- Force soloader 0.8.2
- Migration de Flow vers TypeScript
- Blocage de react-native-keychain à la version 4.0.5 en raison d'un bug dans la librairie
# Prochainement - **v4.0.1**
# Versions précédentes
<details><summary>**v4.0.1**</summary>
<details><summary>**v4.0.1** - 30/09/2020</summary>
## 🎉 Nouveautés
- Ajout d'une mascotte !
@ -41,7 +44,21 @@
</details>
# Versions précédentes
<details><summary>**v3.0.7** - 13/06/2020</summary>
## 🎉 Nouveautés
- Mise à jour des écrans d'intro pour mieux refléter l'appli actuelle
- Déplacement du bouton *À propos* dans les paramètres
- Mode sombre par défaut parce que voilà
## 🐛 Corrections de bugs
- Correction de crash au démarrage sur certains appareils
- Correction de l'affichage de certains sites web
## 🖥️ Notes de développement
- Force soloader 0.8.2
</details>
<details><summary>**v3.0.5** - 28/05/2020</summary>

View file

@ -1,7 +1,7 @@
const keychainMock = {
SECURITY_LEVEL_ANY: "MOCK_SECURITY_LEVEL_ANY",
SECURITY_LEVEL_SECURE_SOFTWARE: "MOCK_SECURITY_LEVEL_SECURE_SOFTWARE",
SECURITY_LEVEL_SECURE_HARDWARE: "MOCK_SECURITY_LEVEL_SECURE_HARDWARE",
}
SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY',
SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE',
SECURITY_LEVEL_SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE',
};
export default keychainMock;
export default keychainMock;

View file

@ -1,11 +1,9 @@
/* eslint-disable */
import React from 'react';
import ConnectionManager from '../../src/managers/ConnectionManager';
import {ERROR_TYPE} from '../../src/utils/WebData';
import { ERROR_TYPE } from '../../src/utils/WebData';
jest.mock('react-native-keychain');
// eslint-disable-next-line no-unused-vars
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
const c = ConnectionManager.getInstance();
@ -44,7 +42,7 @@ test('connect bad credentials', () => {
});
});
return expect(c.connect('email', 'password')).rejects.toBe(
ERROR_TYPE.BAD_CREDENTIALS,
ERROR_TYPE.BAD_CREDENTIALS
);
});
@ -54,7 +52,7 @@ test('connect good credentials', () => {
json: () => {
return {
error: ERROR_TYPE.SUCCESS,
data: {token: 'token'},
data: { token: 'token' },
};
},
});
@ -79,7 +77,7 @@ test('connect good credentials no consent', () => {
});
});
return expect(c.connect('email', 'password')).rejects.toBe(
ERROR_TYPE.NO_CONSENT,
ERROR_TYPE.NO_CONSENT
);
});
@ -89,7 +87,7 @@ test('connect good credentials, fail save token', () => {
json: () => {
return {
error: ERROR_TYPE.SUCCESS,
data: {token: 'token'},
data: { token: 'token' },
};
},
});
@ -100,7 +98,7 @@ test('connect good credentials, fail save token', () => {
return Promise.reject(false);
});
return expect(c.connect('email', 'password')).rejects.toBe(
ERROR_TYPE.TOKEN_SAVE,
ERROR_TYPE.TOKEN_SAVE
);
});
@ -109,7 +107,7 @@ test('connect connection error', () => {
return Promise.reject();
});
return expect(c.connect('email', 'password')).rejects.toBe(
ERROR_TYPE.CONNECTION_ERROR,
ERROR_TYPE.CONNECTION_ERROR
);
});
@ -125,7 +123,7 @@ test('connect bogus response 1', () => {
});
});
return expect(c.connect('email', 'password')).rejects.toBe(
ERROR_TYPE.SERVER_ERROR,
ERROR_TYPE.SERVER_ERROR
);
});
@ -140,14 +138,14 @@ test('authenticatedRequest success', () => {
json: () => {
return {
error: ERROR_TYPE.SUCCESS,
data: {coucou: 'toi'},
data: { coucou: 'toi' },
};
},
});
});
return expect(
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
).resolves.toStrictEqual({coucou: 'toi'});
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
).resolves.toStrictEqual({ coucou: 'toi' });
});
test('authenticatedRequest error wrong token', () => {
@ -167,7 +165,7 @@ test('authenticatedRequest error wrong token', () => {
});
});
return expect(
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
).rejects.toBe(ERROR_TYPE.BAD_TOKEN);
});
@ -187,7 +185,7 @@ test('authenticatedRequest error bogus response', () => {
});
});
return expect(
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
).rejects.toBe(ERROR_TYPE.SERVER_ERROR);
});
@ -201,7 +199,7 @@ test('authenticatedRequest connection error', () => {
return Promise.reject();
});
return expect(
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
).rejects.toBe(ERROR_TYPE.CONNECTION_ERROR);
});
@ -212,6 +210,6 @@ test('authenticatedRequest error no token', () => {
return null;
});
return expect(
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check'),
c.authenticatedRequest('https://www.amicale-insat.fr/api/token/check')
).rejects.toBe(ERROR_TYPE.TOKEN_RETRIEVE);
});

View file

@ -1,6 +1,3 @@
/* eslint-disable */
import React from 'react';
import * as EquipmentBooking from '../../src/utils/EquipmentBooking';
import i18n from 'i18n-js';
@ -18,7 +15,7 @@ test('getCurrentDay', () => {
.spyOn(Date, 'now')
.mockImplementation(() => new Date('2020-01-14 14:50:35').getTime());
expect(EquipmentBooking.getCurrentDay().getTime()).toBe(
new Date('2020-01-14').getTime(),
new Date('2020-01-14').getTime()
);
});
@ -30,19 +27,19 @@ test('isEquipmentAvailable', () => {
id: 1,
name: 'Petit barbecue',
caution: 100,
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
booked_at: [{ begin: '2020-07-07', end: '2020-07-10' }],
};
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
testDevice.booked_at = [{ begin: '2020-07-07', end: '2020-07-09' }];
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
testDevice.booked_at = [{begin: '2020-07-09', end: '2020-07-10'}];
testDevice.booked_at = [{ begin: '2020-07-09', end: '2020-07-10' }];
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
testDevice.booked_at = [
{begin: '2020-07-07', end: '2020-07-8'},
{begin: '2020-07-10', end: '2020-07-12'},
{ begin: '2020-07-07', end: '2020-07-8' },
{ begin: '2020-07-10', end: '2020-07-12' },
];
expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
});
@ -55,29 +52,29 @@ test('getFirstEquipmentAvailability', () => {
id: 1,
name: 'Petit barbecue',
caution: 100,
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
booked_at: [{ begin: '2020-07-07', end: '2020-07-10' }],
};
expect(
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()
).toBe(new Date('2020-07-11').getTime());
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-09'}];
testDevice.booked_at = [{ begin: '2020-07-07', end: '2020-07-09' }];
expect(
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
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'},
{ begin: '2020-07-07', end: '2020-07-09' },
{ begin: '2020-07-10', end: '2020-07-16' },
];
expect(
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime(),
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'},
{ 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(),
EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()
).toBe(new Date('2020-07-13').getTime());
});
@ -85,7 +82,7 @@ test('getRelativeDateString', () => {
jest
.spyOn(Date, 'now')
.mockImplementation(() => new Date('2020-07-09').getTime());
jest.spyOn(i18n, 't').mockImplementation((translationString: string) => {
jest.spyOn(i18n, 't').mockImplementation((translationString) => {
const prefix = 'screens.equipment.';
if (translationString === prefix + 'otherYear') return '0';
else if (translationString === prefix + 'otherMonth') return '1';
@ -95,25 +92,25 @@ test('getRelativeDateString', () => {
else return null;
});
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-09'))).toBe(
'4',
'4'
);
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-10'))).toBe(
'3',
'3'
);
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-11'))).toBe(
'2',
'2'
);
expect(EquipmentBooking.getRelativeDateString(new Date('2020-07-30'))).toBe(
'2',
'2'
);
expect(EquipmentBooking.getRelativeDateString(new Date('2020-08-30'))).toBe(
'1',
'1'
);
expect(EquipmentBooking.getRelativeDateString(new Date('2020-11-10'))).toBe(
'1',
'1'
);
expect(EquipmentBooking.getRelativeDateString(new Date('2021-11-10'))).toBe(
'0',
'0'
);
});
@ -122,7 +119,7 @@ test('getValidRange', () => {
id: 1,
name: 'Petit barbecue',
caution: 100,
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
booked_at: [{ begin: '2020-07-07', end: '2020-07-10' }],
};
let start = new Date('2020-07-11');
let end = new Date('2020-07-15');
@ -134,62 +131,62 @@ test('getValidRange', () => {
'2020-07-15',
];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
result,
result
);
testDevice.booked_at = [
{begin: '2020-07-07', end: '2020-07-10'},
{begin: '2020-07-13', end: '2020-07-15'},
{ 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,
result
);
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
testDevice.booked_at = [{ begin: '2020-07-12', end: '2020-07-13' }];
result = ['2020-07-11'];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
result,
result
);
testDevice.booked_at = [{begin: '2020-07-07', end: '2020-07-12'}];
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,
result
);
start = new Date('2020-07-14');
end = new Date('2020-07-14');
result = ['2020-07-14'];
expect(
EquipmentBooking.getValidRange(start, start, testDevice),
EquipmentBooking.getValidRange(start, start, testDevice)
).toStrictEqual(result);
expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(
result,
result
);
expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(
result,
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,
result
);
testDevice.booked_at = [{begin: '2020-07-17', end: '2020-07-17'}];
testDevice.booked_at = [{ begin: '2020-07-17', end: '2020-07-17' }];
result = ['2020-07-14', '2020-07-15', '2020-07-16'];
expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(
result,
result
);
testDevice.booked_at = [
{begin: '2020-07-12', end: '2020-07-13'},
{begin: '2020-07-15', end: '2020-07-20'},
{ 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,
result
);
});
@ -205,7 +202,7 @@ test('generateMarkedDates', () => {
id: 1,
name: 'Petit barbecue',
caution: 100,
booked_at: [{begin: '2020-07-07', end: '2020-07-10'}],
booked_at: [{ begin: '2020-07-07', end: '2020-07-10' }],
};
let start = new Date('2020-07-11');
let end = new Date('2020-07-13');
@ -228,7 +225,7 @@ test('generateMarkedDates', () => {
},
};
expect(
EquipmentBooking.generateMarkedDates(true, theme, range),
EquipmentBooking.generateMarkedDates(true, theme, range)
).toStrictEqual(result);
result = {
'2020-07-11': {
@ -248,7 +245,7 @@ test('generateMarkedDates', () => {
},
};
expect(
EquipmentBooking.generateMarkedDates(false, theme, range),
EquipmentBooking.generateMarkedDates(false, theme, range)
).toStrictEqual(result);
result = {
'2020-07-11': {
@ -269,10 +266,10 @@ test('generateMarkedDates', () => {
};
range = EquipmentBooking.getValidRange(end, start, testDevice);
expect(
EquipmentBooking.generateMarkedDates(false, theme, range),
EquipmentBooking.generateMarkedDates(false, theme, range)
).toStrictEqual(result);
testDevice.booked_at = [{begin: '2020-07-13', end: '2020-07-15'}];
testDevice.booked_at = [{ begin: '2020-07-13', end: '2020-07-15' }];
result = {
'2020-07-11': {
startingDay: true,
@ -287,10 +284,10 @@ test('generateMarkedDates', () => {
};
range = EquipmentBooking.getValidRange(start, end, testDevice);
expect(
EquipmentBooking.generateMarkedDates(true, theme, range),
EquipmentBooking.generateMarkedDates(true, theme, range)
).toStrictEqual(result);
testDevice.booked_at = [{begin: '2020-07-12', end: '2020-07-13'}];
testDevice.booked_at = [{ begin: '2020-07-12', end: '2020-07-13' }];
result = {
'2020-07-11': {
startingDay: true,
@ -300,12 +297,12 @@ test('generateMarkedDates', () => {
};
range = EquipmentBooking.getValidRange(start, end, testDevice);
expect(
EquipmentBooking.generateMarkedDates(true, theme, range),
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'},
{ 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');
@ -318,7 +315,7 @@ test('generateMarkedDates', () => {
};
range = EquipmentBooking.getValidRange(start, end, testDevice);
expect(
EquipmentBooking.generateMarkedDates(true, theme, range),
EquipmentBooking.generateMarkedDates(true, theme, range)
).toStrictEqual(result);
result = {
@ -340,6 +337,6 @@ test('generateMarkedDates', () => {
};
range = EquipmentBooking.getValidRange(end, start, testDevice);
expect(
EquipmentBooking.generateMarkedDates(true, theme, range),
EquipmentBooking.generateMarkedDates(true, theme, range)
).toStrictEqual(result);
});

View file

@ -1,6 +1,3 @@
/* eslint-disable */
import React from 'react';
import * as Planning from '../../src/utils/Planning';
test('isDescriptionEmpty', () => {
@ -24,7 +21,7 @@ test('isEventDateStringFormatValid', () => {
expect(Planning.isEventDateStringFormatValid('3214-64-12 01:16')).toBeTrue();
expect(
Planning.isEventDateStringFormatValid('3214-64-12 01:16:00'),
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();
@ -32,7 +29,7 @@ test('isEventDateStringFormatValid', () => {
expect(Planning.isEventDateStringFormatValid('2020-03-21')).toBeFalse();
expect(Planning.isEventDateStringFormatValid('2020-03-21 truc')).toBeFalse();
expect(
Planning.isEventDateStringFormatValid('3214-64-12 1:16:65'),
Planning.isEventDateStringFormatValid('3214-64-12 1:16:65')
).toBeFalse();
expect(Planning.isEventDateStringFormatValid('garbage')).toBeFalse();
expect(Planning.isEventDateStringFormatValid('')).toBeFalse();
@ -65,17 +62,17 @@ test('getFormattedEventTime', () => {
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'),
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'),
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'),
Planning.getFormattedEventTime('2020-03-30 20:30', '2020-03-30 23:00')
).toBe('20:30 - 23:00');
});
@ -90,38 +87,38 @@ test('getDateOnlyString', () => {
test('isEventBefore', () => {
expect(
Planning.isEventBefore('2020-03-21 09:00', '2020-03-21 10:00'),
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'),
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'),
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'),
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'),
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'),
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'),
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'),
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'),
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'),
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'),
Planning.isEventBefore('2020-03-30 10:15', '2020-03-21 10:15')
).toBeFalse();
expect(Planning.isEventBefore('garbage', '2020-03-21 10:15')).toBeFalse();
@ -162,25 +159,25 @@ test('generateEmptyCalendar', () => {
test('pushEventInOrder', () => {
let eventArray = [];
let event1 = {date_begin: '2020-01-14 09:15'};
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'};
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'};
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'};
let event4 = { date_begin: '2020-01-13 09:00' };
Planning.pushEventInOrder(eventArray, event4);
expect(eventArray.length).toBe(4);
expect(eventArray[0]).toBe(event4);
@ -194,11 +191,11 @@ test('generateEventAgenda', () => {
.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'},
{ 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);

View file

@ -1,6 +1,3 @@
/* eslint-disable */
import React from 'react';
import {
getCleanedMachineWatched,
getMachineEndDate,
@ -15,19 +12,19 @@ test('getMachineEndDate', () => {
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(),
expect(getMachineEndDate({ endTime: '23:10' }).getTime()).toBe(
expectDate.getTime()
);
expectDate.setHours(16);
expectDate.setMinutes(30);
expect(getMachineEndDate({endTime: '16:30'}).getTime()).toBe(
expectDate.getTime(),
expect(getMachineEndDate({ endTime: '16:30' }).getTime()).toBe(
expectDate.getTime()
);
expect(getMachineEndDate({endTime: '15:30'})).toBeNull();
expect(getMachineEndDate({ endTime: '15:30' })).toBeNull();
expect(getMachineEndDate({endTime: '13:10'})).toBeNull();
expect(getMachineEndDate({ endTime: '13:10' })).toBeNull();
jest
.spyOn(Date, 'now')
@ -35,8 +32,8 @@ test('getMachineEndDate', () => {
expectDate = new Date('2020-01-14T23:00:00.000Z');
expectDate.setHours(0);
expectDate.setMinutes(30);
expect(getMachineEndDate({endTime: '00:30'}).getTime()).toBe(
expectDate.getTime(),
expect(getMachineEndDate({ endTime: '00:30' }).getTime()).toBe(
expectDate.getTime()
);
});
@ -52,16 +49,16 @@ test('isMachineWatched', () => {
},
];
expect(
isMachineWatched({number: '0', endTime: '23:30'}, machineList),
isMachineWatched({ number: '0', endTime: '23:30' }, machineList)
).toBeTrue();
expect(
isMachineWatched({number: '1', endTime: '20:30'}, machineList),
isMachineWatched({ number: '1', endTime: '20:30' }, machineList)
).toBeTrue();
expect(
isMachineWatched({number: '3', endTime: '20:30'}, machineList),
isMachineWatched({ number: '3', endTime: '20:30' }, machineList)
).toBeFalse();
expect(
isMachineWatched({number: '1', endTime: '23:30'}, machineList),
isMachineWatched({ number: '1', endTime: '23:30' }, machineList)
).toBeFalse();
});
@ -74,8 +71,8 @@ test('getMachineOfId', () => {
number: '1',
},
];
expect(getMachineOfId('0', machineList)).toStrictEqual({number: '0'});
expect(getMachineOfId('1', machineList)).toStrictEqual({number: '1'});
expect(getMachineOfId('0', machineList)).toStrictEqual({ number: '0' });
expect(getMachineOfId('1', machineList)).toStrictEqual({ number: '1' });
expect(getMachineOfId('3', machineList)).toBeNull();
});
@ -110,7 +107,7 @@ test('getCleanedMachineWatched', () => {
];
let cleanedList = watchList;
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
cleanedList,
cleanedList
);
watchList = [
@ -138,7 +135,7 @@ test('getCleanedMachineWatched', () => {
},
];
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
cleanedList,
cleanedList
);
watchList = [
@ -162,6 +159,6 @@ test('getCleanedMachineWatched', () => {
},
];
expect(getCleanedMachineWatched(watchList, machineList)).toStrictEqual(
cleanedList,
cleanedList
);
});

View file

@ -1,8 +1,6 @@
/* eslint-disable */
import React from 'react';
import {isApiResponseValid} from '../../src/utils/WebData';
import { isApiResponseValid } from '../../src/utils/WebData';
// eslint-disable-next-line no-unused-vars
const fetch = require('isomorphic-fetch'); // fetch is not implemented in nodeJS but in react-native
test('isRequestResponseValid', () => {
@ -23,7 +21,7 @@ test('isRequestResponseValid', () => {
expect(isApiResponseValid(json)).toBeTrue();
json = {
error: 50,
data: {truc: 'machin'},
data: { truc: 'machin' },
};
expect(isApiResponseValid(json)).toBeTrue();
json = {
@ -32,7 +30,7 @@ test('isRequestResponseValid', () => {
expect(isApiResponseValid(json)).toBeFalse();
json = {
error: 'coucou',
data: {truc: 'machin'},
data: { truc: 'machin' },
};
expect(isApiResponseValid(json)).toBeFalse();
json = {

View file

@ -137,19 +137,16 @@ if (keystorePropertiesFile.exists() && !keystorePropertiesFile.isDirectory()) {
}
android {
compileSdkVersion rootProject.ext.compileSdkVersion
ndkVersion rootProject.ext.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId 'fr.amicaleinsat.application'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 42
versionName "4.0.1"
versionCode 49
versionName "5.0.0-3"
missingDimensionStrategy 'react-native-camera', 'general'
}
splits {
@ -192,11 +189,12 @@ android {
variant.outputs.each { output ->
// For each separate APK per architecture, set a unique version code as described here:
// https://developer.android.com/studio/build/configure-apk-splits.html
// Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc.
def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
def abi = output.getFilter(OutputFile.ABI)
if (abi != null) { // null for the universal-debug, universal-release variants
output.versionCodeOverride =
versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
defaultConfig.versionCode * 1000 + versionCodes.get(abi)
}
}
@ -235,7 +233,7 @@ dependencies {
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile
from configurations.implementation
into 'libs'
}

View file

@ -4,5 +4,10 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" />
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />
</application>
</manifest>

View file

@ -8,7 +8,6 @@
<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
android:name=".MainApplication"
@ -19,31 +18,33 @@
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
>
<!-- NOTIFICATIONS -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_name"
android:value="reminders"/>
<meta-data android:name="com.dieam.reactnativepushnotification.notification_channel_description"
android:value="reminders"/>
<!-- Change the resource name to your App's accent color - or any other color you want -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/colorPrimary"/> <!-- or @android:color/{name} to use a standard color -->
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher"/>
<!-- START NOTIFICATIONS -->
<!-- Change the value to true to enable pop-up for in foreground on receiving remote notifications (for prevent duplicating while showing local notifications set this to false) -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="false"/>
<!-- Change the resource name to your App's accent color - or any other color you want -->
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/colorPrimary"/>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationBootEventReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<service
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false">
android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- END NOTIFICATIONS-->
<!-- END NOTIFICATIONS -->
<meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false"/>
@ -67,6 +68,5 @@
<data android:scheme="campus-insat"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
</application>
</manifest>

View file

@ -5,25 +5,14 @@ import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate;
import com.facebook.react.ReactRootView;
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
import android.content.Intent;
import android.content.res.Configuration;
import org.devio.rn.splashscreen.SplashScreen;
public class MainActivity extends ReactActivity {
// Added automatically by Expo Config
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Intent intent = new Intent("onConfigurationChanged");
intent.putExtra("newConfig", newConfig);
sendBroadcast(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
SplashScreen.show(this);
SplashScreen.show(this, R.style.SplashScreenTheme);
super.onCreate(savedInstanceState);
}

View file

@ -1,10 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="android:textColor">#000000</item>
<item name="android:windowBackground">@color/activityBackground</item>
<item name="android:navigationBarColor">@color/navigationBarColor</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorPrimary">@color/colorPrimary</item>
</style>
</resources>
<style name="SplashScreenTheme" parent="SplashScreen_SplashTheme">
<item name="android:navigationBarColor">@color/activityBackground</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
</style>
</resources>

View file

@ -21,7 +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_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,18 @@
buildscript {
ext {
buildToolsVersion = "29.0.2"
minSdkVersion = 21
compileSdkVersion = 29
targetSdkVersion = 29
buildToolsVersion = "30.0.2"
minSdkVersion = 23
compileSdkVersion = 30
targetSdkVersion = 30
ndkVersion = "20.1.5948944"
}
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:3.5.3")
classpath("com.android.tools.build:gradle:4.2.1")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -21,6 +22,7 @@ buildscript {
allprojects {
repositories {
mavenCentral()
mavenLocal()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
@ -35,7 +37,6 @@ allprojects {
url "$rootDir/../node_modules/expo-camera/android/maven"
}
google()
jcenter()
maven { url 'https://www.jitpack.io' }
}
}

View file

@ -24,4 +24,8 @@ 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
FLIPPER_VERSION=0.93.0
# Increase Java heap size for compilation
org.gradle.jvmargs=-Xmx2048M

View file

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

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

View file

@ -1,18 +0,0 @@
#!/bin/bash
echo "Removing node_modules..."
rm -rf node_modules/
echo -e "Done\n"
echo "Removing locks..."
rm -f package-lock.json && rm -f yarn.lock
echo -e "Done\n"
#echo "Verifying npm cache..."
#npm cache verify
#echo -e "Done\n"
echo "Installing dependencies..."
npm install
echo -e "Done\n"

View file

@ -8,14 +8,15 @@ Le strict minimum pour pouvoir comprendre le code de l'application. Il n'est pas
* [**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.
* [**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.
* [**Tutoriel TypeScript**](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) : Un tuto rapide de cette surcouche à JavaScript, permettant de le rendre typé statique.
* [**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.
## 🤔 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.
* [**TypeScript Handbook**](https://www.typescriptlang.org/docs/handbook/intro.html) : Un tuto TypeScript complet permettant de bien maitriser cette technologie.
* [**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.
* [**Liste 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.

View file

@ -4,6 +4,22 @@ Ce fichier permet de regrouper les différentes informations sur des décisions
Ces notes pouvant évoluer dans le temps, leur date d'écriture est aussi indiquée.
## _2020-10-07_ | react-native-keychain
Bloquée en 4.0.5 à cause d'un problème de performances. Au dessus de cette version, la récupération du token prend plusieurs secondes, ce qui n'est pas acceptable.
[Référence](https://github.com/oblador/react-native-keychain/issues/337)
## _2020-09-24_ | Flow
Flow est un système d'annotation permettant de rendre JavaScript typé statique. Développée par Facebook, cette technologie à initialement été adoptée. En revanche, de nombreux problèmes sont apparus :
* Système très complexe donnant de nombreuses erreurs inconnues, rendant la contribution complexe pour les non-initiés
* Manque de compatibilité avec les librairies existantes (la majorité utilisant TypeScript)
* Utilisation excessive du système lors du développement
* Plantage régulier du service Flow, nécessitant un redémarrage manuel
Ainsi, il a été décidé de migrer le projet vers Typescript.
## _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 :

View file

@ -1,10 +1,28 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* @format
*/
import {AppRegistry} from 'react-native';
import { AppRegistry } from 'react-native';
import App from './App';
import {name as appName} from './app.json';
import { name as appName } from './app.json';
// eslint-disable-next-line flowtype/require-return-type
AppRegistry.registerComponent(appName, () => App);

View file

@ -126,6 +126,7 @@
13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle Expo Assets */,
58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */,
2C1F7D7FCACF5494D140CFB7 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@ -199,6 +200,24 @@
shellPath = /bin/sh;
shellScript = "../node_modules/react-native/scripts/react-native-xcode.sh\n";
};
2C1F7D7FCACF5494D140CFB7 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Campus/Pods-Campus-frameworks.sh",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Campus/Pods-Campus-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -313,12 +332,12 @@
CODE_SIGN_ENTITLEMENTS = Campus/application.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 2;
DEAD_CODE_STRIPPING = NO;
DEVELOPMENT_TEAM = 6JA7CLNUV6;
INFOPLIST_FILE = Campus/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 4.0.1;
MARKETING_VERSION = 4.1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -339,11 +358,11 @@
CODE_SIGN_ENTITLEMENTS = Campus/application.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = 6JA7CLNUV6;
INFOPLIST_FILE = Campus/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 4.0.1;
MARKETING_VERSION = 4.1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@ -388,6 +407,7 @@
COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@ -403,7 +423,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application;
@ -444,6 +464,7 @@
COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -452,7 +473,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application;
PRODUCT_NAME = application;

View file

@ -52,7 +52,11 @@ static void InitializeFlipper(UIApplication *application) {
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"Campus" initialProperties:nil];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
if (@available(iOS 13.0, *)) {
rootView.backgroundColor = [UIColor systemBackgroundColor];
} else {
rootView.backgroundColor = [UIColor whiteColor];
}
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<string>5.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
@ -30,25 +30,25 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<string>4</string>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<false/>
<false />
<key>FacebookAutoInitEnabled</key>
<false/>
<false />
<key>FacebookAutoLogAppEventsEnabled</key>
<false/>
<false />
<key>LSRequiresIPhoneOS</key>
<true/>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<true />
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<true />
</dict>
</dict>
</dict>
@ -65,7 +65,7 @@
<string>armv7</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<true />
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@ -74,6 +74,6 @@
<key>UIUserInterfaceStyle</key>
<string>Automatic</string>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<false />
</dict>
</plist>

View file

@ -1,26 +1,31 @@
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
platform :ios, '10.0'
platform :ios, '11.0'
target 'Campus' do
config = use_native_modules!
use_react_native!(:path => config["reactNativePath"])
use_react_native!(
:path => config[:reactNativePath],
# to enable hermes on iOS, change `false` to `true` and then install pods
:hermes_enabled => true
)
# Permissions
permissions_path = '../node_modules/react-native-permissions/ios'
pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications.podspec"
pod 'Permission-Camera', :path => "#{permissions_path}/Camera.podspec"
pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications"
pod 'Permission-Camera', :path => "#{permissions_path}/Camera"
# Enables Flipper.
#
# Note that if you have use_frameworks! enabled, Flipper will not work and
# you should disable these next few lines.
# use_flipper!
# post_install do |installer|
# flipper_post_install(installer)
# end
# you should disable the next line.
# use_flipper!()
post_install do |installer|
react_native_post_install(installer)
end
end

View file

@ -40,30 +40,45 @@
"dryers": "Dryers",
"washer": "Washer",
"washers": "Washers",
"updated": "Updated ",
"switch": "Switch laundromat",
"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.",
"washerProcedure": "Put your laundry in the tumble without tamping it and by respecting weight limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the central command, 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.",
"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 central command , then press the START button on the machine.",
"dryerTips": "The recommended 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",
"errors": {
"title": "Proxiwash message",
"button": "More info"
},
"washinsa": {
"title": "INSA laundromat",
"subtitle": "Your favorite laundromat!!",
"description": "This is the washing service for INSA's residences (We don't mind if you do not live on the campus and do your laundry here). The room is right next to the R2, with 3 dryers and 9 washers. It is open 7d/7 24h/24! 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).",
"tariff": "Washers 6kg: 3€ per run + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.",
"paymentMethods": "Cash up to 10€.\nCredit Cards also accepted."
},
"tripodeB": {
"title": "Tripode B laundromat",
"subtitle": "For those who live near the metro.",
"description": "This is the washing service for Tripode B and C residences, as well as Thalès and Pythagore. The room is at the foot of Tripod B in front of the Pythagore residence, with 2 dryers and 6 washers. It is open 7d/7 from 7am to 11pm. In addition to the 6kg washers there is one 10kg washer.",
"tariff": "Washers 6kg: 2.60€ per run + 0.90€ with detergent.\nWashers 10kg: 4.90€ per run + 1.50€ with detergent.\nDryers 14kg: 0.40€ for 5min of dryer usage.",
"paymentMethods": "Credit Cards accepted."
},
"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.",
"finished": "This machine is finished. If you started it, you can pick up your laundry.",
"ready": "This machine is empty and ready for 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.",
@ -82,15 +97,20 @@
"unknown": "UNKNOWN"
},
"notifications": {
"channel": {
"title": "Laundry reminders",
"description": "Get reminders for watched washers/dryers"
},
"machineFinishedTitle": "Laundry Ready",
"machineFinishedBody": "The machine n°{{number}} is finished and your laundry is ready to pickup",
"machineFinishedBody": "Machine n°{{number}} is finished and your laundry is ready for pickup",
"machineRunningTitle": "Laundry running: {{time}} minutes left",
"machineRunningBody": "The machine n°{{number}} is still running"
"machineRunningBody": "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!"
"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.\n\nIf you live off campus we have another available laundromat, check the settings !!!!",
"ok": "Settings",
"cancel": "Later"
}
},
"home": {
@ -125,8 +145,14 @@
},
"planex": {
"title": "Planex",
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button bellow.",
"favorites": "Favorites",
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button below.",
"favorites": {
"title": "Favorites",
"empty": {
"title": "No favorites",
"subtitle": "Click on the star next to a group to add it to the favorites"
}
},
"mascotDialog": {
"title": "Don't skip class",
"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!",
@ -138,7 +164,7 @@
"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!",
"message": "Want to revive a club?\nWant to start a new project?\nHere are all the contacts you need! Don't hesitate to write a mail or send a message to the Amicale's Facebook page!",
"roles": {
"interSchools": "Inter Schools",
"culture": "Culture",
@ -163,8 +189,8 @@
"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",
"description": "The Proximo is your small grocery store held by students directly on campus. Open every day from 18h30 to 19h30, we welcome you when you are short on pasta or soda ! Different products for different problems, everything is sold at cost. You can pay with Lydia or cash.",
"openingHours": "Opening Hours",
"paymentMethods": "Payment Methods",
"paymentMethodsDescription": "Cash or Lydia",
"search": "Search",
@ -186,7 +212,7 @@
"login": {
"title": "Login",
"subtitle": "Please enter your credentials",
"subtitle": "Please enter your AMICALE credentials",
"email": "Email",
"emailError": "Please enter a valid email",
"password": "Password",
@ -194,10 +220,9 @@
"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.",
"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 opening hours to create one.",
"button": "OK"
}
},
"profile": {
"title": "Profile",
@ -213,8 +238,8 @@
"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."
"welcomeDescription": "This is your Amicale INSA Toulouse personal space. Below are the services you can currently 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 below."
},
"clubs": {
"title": "Clubs",
@ -228,10 +253,10 @@
"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!",
"text": "The clubs keep the campus alive, with more than sixty clubs offering various activities! From the philosophy club to the PABI (Production Artisanale de Bière Insalienne), 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:"
"message": "Do you have a question regarding clubs?\nWant to revive or create a club?\nContact the Amicale at the following address:"
}
},
"vote": {
@ -240,14 +265,14 @@
"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."
"sendButton": "Cast Vote",
"dialogTitle": "Cast Vote?",
"dialogTitleLoading": "Casting vote...",
"dialogMessage": "Are you sure you want to cast your vote? You will not be able to change it."
},
"tease": {
"title": "Elections incoming",
"subtitle": "Be ready to vote!",
"subtitle": "Get ready to vote!",
"message": "Vote start:"
},
"wait": {
@ -267,7 +292,7 @@
},
"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",
"message": "The Amicale's elections are 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"
}
},
@ -292,7 +317,7 @@
"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.",
"message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, select the equipment of your choice in the list below, enter your lend dates, then come around the Amicale to claim it and give your bail.",
"button": "Ok"
}
},
@ -312,7 +337,7 @@
},
"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.",
"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 purpose.",
"button": "OK"
}
},
@ -323,17 +348,19 @@
"nightModeSubOn": "Your eyes are at peace",
"nightModeSubOff": "Your eyes are burning",
"nightModeAuto": "Follow system dark mode",
"nightModeAutoSub": "Follows the mode chosen by your system",
"nightModeAutoSub": "Follows the mode set 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",
"dashboardSub": "Edit which services to display on the dashboard",
"proxiwashNotifReminder": "Machine running reminder",
"proxiwashNotifReminderSub": "How many minutes before",
"proxiwashChangeWash": "Laundromat selection",
"proxiwashChangeWashSub": "Which laundromat to display",
"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.",
"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 below.",
"undo": "Undo changes"
}
},
@ -346,21 +373,30 @@
"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"
"libs": "Libraries used",
"thanks": "Thanks",
"user": {
"you": "You ?",
"arnaud": "Student in 4IR (2020). He is the creator of this app you use everyday.",
"docjyj": "Student in 2MIC FAS (2020). He added some new features and fixed some bugs.",
"yohan": "Student in 4IR (2020). He helped to fix bugs and gave some ideas.",
"beranger": "Student in 4AE (2020) and president of the Amicale when the app was created. The app was his idea. He helped a lot to find bugs, new features and communication.",
"celine": "Student in 4GPE (2020). Without her, everything wouldn't be as cute. She helped to write the text, for communication, and also to create the mascot 🦊.",
"damien": "Student in 4IR (2020) and creator of the 2020 version of the Amicale's website. Thanks to his help, integrating Amicale's services into the app was child's play.",
"titouan": "Student in 4IR (2020). He helped a lot in finding bugs and new features.",
"theo": "Student in 4AE (2020). If the app works on iOS, this is all thanks to his help during his numerous tests."
}
},
"feedback": {
"title": "Contribute",
"feedback": "Contact the dev",
"feedbackSubtitle": "A student like you!",
"feedbackDescription": "Feedback or bugs, you are always welcome.\nChoose your preferred way from the buttons bellow.",
"feedbackDescription": "Feedback or bugs, you are always welcome.\nChoose your preferred way from the buttons below.",
"contribute": "Contribute to the project",
"contributeSubtitle": "With a possible \"implication citoyenne\"!",
"contributeDescription": "Everyone can help: communication, design or coding! You are free to contribute as you like.\nYou can find bellow a link to Trello for project organization, and a link to the source code on GitEtud.",
"contributeDescription": "Everyone can help: communication, design or coding! You are free to contribute as you like.\nYou can find below a link to Trello for project organization, and a link to the source code on GitEtud.",
"homeButtonTitle": "Contribute to the project",
"homeButtonSubtitle": "Your help is important"
},
@ -398,11 +434,11 @@
"intro": {
"slideMain": {
"title": "Welcome to CAMPUS!",
"text": "The students app of the INSA Toulouse! Read along to see everything you can do."
"text": "INSA Toulouse's student app! Read along to see everything you can do."
},
"slidePlanex": {
"title": "Prettier Planex",
"text": "Lookup your and your friends timetable with a mobile friendly Planex!"
"text": "Lookup your friends' and your own timetables with a mobile friendly Planex!"
},
"slideEvents": {
"title": "Events",
@ -410,7 +446,7 @@
},
"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!"
"text": "You can do much more with CAMPUS, but I can't explain everything here. Explore the app to find out for yourself!"
},
"slideDone": {
"title": "Contribute to the project!",
@ -431,10 +467,11 @@
},
"errors": {
"title": "Error!",
"badCredentials": "Email or password invalid.",
"badCredentials": "Email or password invalid.\n\nMake sure you are using your AMICALE credentials, and not INSA.",
"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.",
"tokenRetrieve": "Could not retrieve 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.",

View file

@ -40,15 +40,13 @@
"dryers": "Sèche-Linges",
"washer": "Lave-Linge",
"washers": "Lave-Linges",
"updated": "Mise à jour ",
"switch": "Changer de laverie",
"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 non repassable : 3,5 kg de linge sec (textiles en fibres synthétiques, coton et 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.",
@ -57,10 +55,27 @@
"tips": "Conseils",
"numAvailable": "disponible",
"numAvailablePlural": "disponibles",
"errors": {
"title": "Message laverie",
"button": "En savoir plus"
},
"washinsa": {
"title": "Laverie INSA",
"subtitle": "Ta laverie préférée !!",
"description": "C'est le service de laverie 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 ! Tu peux amener ta lessive, la prendre sur place ou encore mieux l'acheter au Proximo (moins chère qu'à la laverie directement).",
"tariff": "Lave-Linges 6kg: 3€ la machine + 0.80€ avec la lessive.\nSèche-Linges 14kg: 0.35€ pour 5min de sèche linge.",
"paymentMethods": "Toute monnaie jusqu'à 10€.\nCarte bancaire acceptée."
},
"tripodeB": {
"title": "Laverie Tripode B",
"subtitle": "Pour ceux qui habitent proche du métro.",
"description": "C'est le service de laverie pour les résidences Tripode B et C ainsi que Thalès et Pythagore. Le local situé au pied du Tripode B, en face de de la résidence Pythagore, avec ses 2 sèche-linges et 6 machines est ouvert 7J/7 de 7h à 23h. En plus des machine 6kg il y a une machine de 10kg.",
"tariff": "Lave-Linges 6kg: 2.60€ la machine + 0.90€ avec la lessive.\nLave-Linges 10kg: 4.90€ la machine + 1.50€ avec la lessive.\nSèche-Linges 14kg: 0.40€ pour 5min de sèche linge.",
"paymentMethods": "Carte bancaire acceptée."
},
"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.",
@ -82,6 +97,10 @@
"unknown": "INCONNU"
},
"notifications": {
"channel": {
"title": "Rappels laverie",
"description": "Recevoir des rappels pour les machines demandées"
},
"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",
@ -89,8 +108,9 @@
},
"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é"
"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.\n\nSi tu habites hors du campus on a d'autre laverie disponible, vas voir dans les paramètres !!!!",
"ok": "Paramètres",
"cancel": "Plus tard"
}
},
"home": {
@ -126,7 +146,13 @@
"planex": {
"title": "Planex",
"noGroupSelected": "Pas de groupe sélectionné. Choisis un groupe avec le beau bouton rouge ci-dessous.",
"favorites": "Favoris",
"favorites": {
"title": "Favoris",
"empty": {
"title": "Aucun favoris",
"subtitle": "Cliquez sur l'étoile à côté d'un groupe pour l'ajouter aux 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 !",
@ -186,7 +212,7 @@
"login": {
"title": "Connexion",
"subtitle": "Entre tes identifiants",
"subtitle": "Entre tes identifiants AMICALE",
"email": "Email",
"emailError": "Merci d'entrer un email valide",
"password": "Mot de passe",
@ -311,7 +337,7 @@
},
"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.",
"message": "Scanotron 3000 te permet de scanner des QR codes Campus, affichés par des clubs ou des respo d'événements, pour avoir plus d'infos !\n\nL'appareil photo ne sera jamais utilisé pour d'autres raisons.",
"button": "Oké"
}
},
@ -329,10 +355,12 @@
"dashboardSub": "Choisis les services à afficher sur la dashboard",
"proxiwashNotifReminder": "Rappel de machine en cours",
"proxiwashNotifReminderSub": "Combien de minutes avant",
"proxiwashChangeWash": "Sélection de la laverie",
"proxiwashChangeWashSub": "Quelle laverie afficher",
"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.",
"message": "Les 5 icônes 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"
}
},
@ -345,12 +373,21 @@
"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"
"libs": "Librairies utilisées",
"thanks": "Remerciements",
"user": {
"you": "Toi ?",
"arnaud": "Étudiant en 4IR (2020). C'est le créateur de cette application que t' utilises tous les jours.",
"docjyj": "Étudiant en 2MIC FAS (2020). Il a ajouté quelques nouvelles fonctionnalités et corrigé des bugs.",
"yohan": "Étudiant en 4IR (2020). Il a aidé à corriger des bug et a proposé quelques idées.",
"beranger": "Étudiant en 4AE (2020) et Président de lAmicale au moment de la création et du lancement du projet. Lapplication, cétait son idée. Il a beaucoup aidé pour trouver des bugs, de nouvelles fonctionnalités et faire de la com.",
"celine": "Étudiante en 4GPE (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": "Étudiant en 4IR (2020) et créateur de la dernière version du site de lAmicale. Grâce à son aide, intégrer les services de lAmicale à lapplication a été très simple.",
"titouan": "Étudiant en 4IR (2020). Il a beaucoup aidé pour trouver des bugs et proposer des nouvelles fonctionnalités.",
"theo": "Étudiant en 4AE (2020). Si lapplication marche sur iOS, cest grâce à son aide lors de ses nombreux tests."
}
},
"feedback": {
"title": "Participer",
@ -430,10 +467,11 @@
},
"errors": {
"title": "Erreur !",
"badCredentials": "Email ou mot de passe invalide.",
"badCredentials": "Email ou mot de passe invalide.\n\nVérifie que tu utilises bien tes identifiants AMICALE et non pas INSA.",
"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.",
"tokenRetrieve": "Impossible de récupérer 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.",
@ -454,8 +492,8 @@
"loading": "Chargement...",
"retry": "Réessayer",
"networkError": "Impossible de contacter les serveurs. Assure-toi d'être connecté à Internet.",
"goBack": "Suivant",
"goForward": "Précédent",
"goBack": "Précédent",
"goForward": "Suivant",
"openInBrowser": "Ouvrir dans le navigateur",
"notAvailable": "Non disponible",
"listUpdateFail": "Erreur lors de la mise à jour de la liste"

View file

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

35301
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,81 +1,141 @@
{
"name": "campus",
"version": "4.0.1",
"version": "5.0.0-3",
"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",
"start": "react-native start",
"start-no-cache": "react-native start --reset-cache",
"test": "jest",
"lint": "eslint ."
"typescript": "tsc --noEmit",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"full-check": "npm run typescript && npm run lint && npm run test",
"pod": "cd ios && pod install && cd ..",
"bundle": "cd android && ./gradlew bundleRelease",
"clean": "react-native-clean-project",
"postversion": "react-native-version"
},
"dependencies": {
"@nartc/react-native-barcode-mask": "1.2.0",
"@react-native-async-storage/async-storage": "1.15.7",
"@react-native-community/masked-view": "0.1.11",
"@react-native-community/push-notification-ios": "1.10.1",
"@react-native-community/slider": "4.1.6",
"@react-navigation/bottom-tabs": "6.0.5",
"@react-navigation/native": "6.0.2",
"@react-navigation/stack": "6.0.7",
"i18n-js": "3.8.0",
"moment": "2.29.1",
"react": "17.0.2",
"react-native": "0.65.1",
"react-native-animatable": "1.3.3",
"react-native-app-intro-slider": "4.0.4",
"react-native-appearance": "0.3.4",
"react-native-autolink": "4.0.0",
"react-native-calendars": "1.1266.0",
"react-native-camera": "4.1.1",
"react-native-collapsible": "1.6.0",
"react-native-gesture-handler": "1.10.3",
"react-native-image-zoom-viewer": "3.0.1",
"react-native-keychain": "4.0.5",
"react-native-linear-gradient": "2.5.6",
"react-native-localize": "2.1.4",
"react-native-modalize": "2.0.8",
"react-native-paper": "4.9.2",
"react-native-permissions": "3.0.5",
"react-native-push-notification": "8.1.0",
"react-native-reanimated": "1.13.2",
"react-native-render-html": "6.1.0",
"react-native-safe-area-context": "3.3.2",
"react-native-screens": "3.7.0",
"react-native-splash-screen": "3.2.0",
"react-native-timeago": "0.5.0",
"react-native-vector-icons": "8.1.0",
"react-native-webview": "11.13.0",
"react-navigation-collapsible": "6.0.0",
"react-navigation-header-buttons": "9.0.0"
},
"devDependencies": {
"@babel/core": "7.12.9",
"@babel/runtime": "7.12.5",
"@react-native-community/eslint-config": "3.0.1",
"@types/i18n-js": "3.8.2",
"@types/jest": "26.0.24",
"@types/react": "17.0.3",
"@types/react-native": "0.65.0",
"@types/react-native-calendars": "1.1264.2",
"@types/react-native-push-notification": "7.3.2",
"@types/react-native-vector-icons": "6.4.8",
"@types/react-test-renderer": "17.0.1",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"babel-jest": "26.6.3",
"eslint": "7.32.0",
"eslint-config-prettier": "8.3.0",
"jest": "26.6.3",
"jest-extended": "0.11.5",
"jest-fetch-mock": "3.0.3",
"metro-react-native-babel-preset": "0.66.0",
"prettier": "2.4.0",
"react-native-clean-project": "3.6.7",
"react-native-codegen": "0.0.7",
"react-native-version": "4.0.0",
"react-test-renderer": "17.0.2",
"typescript": "4.4.2"
},
"eslintConfig": {
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"@react-native-community",
"prettier"
],
"rules": {
"no-undef": 0,
"no-shadow": "off",
"@typescript-eslint/no-shadow": [
"error"
],
"prettier/prettier": [
"error",
{
"quoteProps": "consistent",
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
}
]
}
},
"eslintIgnore": [
"node_modules/"
],
"prettier": {
"quoteProps": "consistent",
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
},
"jest": {
"preset": "react-native",
"transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base)"
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"setupFilesAfterEnv": [
"jest-extended"
]
},
"dependencies": {
"@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.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.4",
"react-native-appearance": "^0.3.4",
"react-native-autolink": "^3.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.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.10.1",
"react-native-splash-screen": "^3.2.0",
"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.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.1.0",
"jest-extended": "^0.11.5",
"metro-react-native-babel-preset": "^0.59.0",
"prettier": "2.0.5",
"react-test-renderer": "16.13.1"
}
}

View file

@ -1,205 +0,0 @@
// @flow
import * as React from 'react';
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 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 StateType = {
loading: boolean,
};
class AuthenticatedScreen extends React.Component<PropsType, StateType> {
static defaultProps = {
errorViewOverride: null,
};
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);
}
/**
* Refreshes screen if user changed
*/
onScreenFocus = () => {
if (this.currentUserToken !== this.connectionManager.getToken()) {
this.currentUserToken = this.connectionManager.getToken();
this.fetchData();
}
};
/**
* 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;
}
}
}
if (shouldOverride && override != null) {
return (
<ErrorView
icon={override.icon}
message={override.message}
showRetryButton={override.showRetryButton}
/>
);
}
return <ErrorView errorCode={errorCode} onRefresh={this.fetchData} />;
}
/**
* Fetches the data from the server.
*
* If the user is not logged in errorCode is set to BAD_TOKEN and all requests fail.
*
* If the user is logged in, send all requests.
*/
fetchData = () => {
const {state, props} = this;
if (!state.loading) this.setState({loading: true});
if (this.connectionManager.isLoggedIn()) {
for (let i = 0; i < props.requests.length; i += 1) {
this.connectionManager
.authenticatedRequest(
props.requests[i].link,
props.requests[i].params,
)
.then((response: ApiGenericDataType): void =>
this.onRequestFinished(response, i),
)
.catch((error: number): void =>
this.onRequestFinished(null, i, error),
);
}
} else {
for (let i = 0; i < props.requests.length; i += 1) {
this.onRequestFinished(null, i, ERROR_TYPE.BAD_TOKEN);
}
}
};
/**
* Checks if all requests finished processing
*
* @return {boolean} True if all finished
*/
allRequestsFinished(): boolean {
let finished = true;
this.errors.forEach((error: number | null) => {
if (error == null) finished = false;
});
return finished;
}
/**
* Reloads the data, to be called using ref by parent components
*/
reload() {
this.fetchData();
}
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

@ -0,0 +1,231 @@
import React, { useRef, useState } from 'react';
import {
Image,
StyleSheet,
View,
TextInput as RNTextInput,
} from 'react-native';
import {
Button,
Card,
HelperText,
TextInput,
useTheme,
} from 'react-native-paper';
import i18n from 'i18n-js';
import GENERAL_STYLES from '../../../constants/Styles';
type Props = {
loading: boolean;
onSubmit: (email: string, password: string) => void;
onHelpPress: () => void;
onResetPasswordPress: () => void;
};
const ICON_AMICALE = require('../../../../assets/amicale.png');
const styles = StyleSheet.create({
card: {
marginTop: 'auto',
marginBottom: 'auto',
},
header: {
fontSize: 36,
marginBottom: 48,
},
text: {
color: '#ffffff',
},
buttonContainer: {
flexWrap: 'wrap',
},
lockButton: {
marginRight: 'auto',
marginBottom: 20,
},
sendButton: {
marginLeft: 'auto',
},
});
const emailRegex = /^.+@.+\..+$/;
/**
* Checks if the entered email is valid (matches the regex)
*
* @returns {boolean}
*/
function isEmailValid(email: string): boolean {
return emailRegex.test(email);
}
/**
* Checks if the user has entered a password
*
* @returns {boolean}
*/
function isPasswordValid(password: string): boolean {
return password !== '';
}
export default function LoginForm(props: Props) {
const theme = useTheme();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isEmailValidated, setIsEmailValidated] = useState(false);
const [isPasswordValidated, setIsPasswordValidated] = useState(false);
const passwordRef = useRef<RNTextInput>(null);
/**
* Checks if we should tell the user his email is invalid.
* We should only show this if his email is invalid and has been checked when un-focusing the input
*
* @returns {boolean|boolean}
*/
const shouldShowEmailError = () => {
return isEmailValidated && !isEmailValid(email);
};
/**
* Checks if we should tell the user his password is invalid.
* We should only show this if his password is invalid and has been checked when un-focusing the input
*
* @returns {boolean|boolean}
*/
const shouldShowPasswordError = () => {
return isPasswordValidated && !isPasswordValid(password);
};
const onEmailSubmit = () => {
if (passwordRef.current) {
passwordRef.current.focus();
}
};
/**
* The user has unfocused the input, his email is ready to be validated
*/
const validateEmail = () => setIsEmailValidated(true);
/**
* The user has unfocused the input, his password is ready to be validated
*/
const validatePassword = () => setIsPasswordValidated(true);
const onEmailChange = (value: string) => {
if (isEmailValidated) {
setIsEmailValidated(false);
}
setEmail(value);
};
const onPasswordChange = (value: string) => {
if (isPasswordValidated) {
setIsPasswordValidated(false);
}
setPassword(value);
};
const shouldEnableLogin = () => {
return isEmailValid(email) && isPasswordValid(password) && !props.loading;
};
const onSubmit = () => {
if (shouldEnableLogin()) {
props.onSubmit(email, password);
}
};
return (
<View style={styles.card}>
<Card.Title
title={i18n.t('screens.login.title')}
titleStyle={styles.text}
subtitle={i18n.t('screens.login.subtitle')}
subtitleStyle={styles.text}
left={({ size }) => (
<Image
source={ICON_AMICALE}
style={{
width: size,
height: size,
}}
/>
)}
/>
<Card.Content>
<View>
<TextInput
label={i18n.t('screens.login.email')}
mode={'outlined'}
value={email}
onChangeText={onEmailChange}
onBlur={validateEmail}
onSubmitEditing={onEmailSubmit}
error={shouldShowEmailError()}
textContentType={'emailAddress'}
autoCapitalize={'none'}
autoCompleteType={'email'}
autoCorrect={false}
keyboardType={'email-address'}
returnKeyType={'next'}
secureTextEntry={false}
/>
<HelperText type={'error'} visible={shouldShowEmailError()}>
{i18n.t('screens.login.emailError')}
</HelperText>
<TextInput
ref={passwordRef}
label={i18n.t('screens.login.password')}
mode={'outlined'}
value={password}
onChangeText={onPasswordChange}
onBlur={validatePassword}
onSubmitEditing={onSubmit}
error={shouldShowPasswordError()}
textContentType={'password'}
autoCapitalize={'none'}
autoCompleteType={'password'}
autoCorrect={false}
keyboardType={'default'}
returnKeyType={'done'}
secureTextEntry={true}
/>
<HelperText type={'error'} visible={shouldShowPasswordError()}>
{i18n.t('screens.login.passwordError')}
</HelperText>
</View>
<Card.Actions style={styles.buttonContainer}>
<Button
icon="lock-question"
mode="contained"
onPress={props.onResetPasswordPress}
color={theme.colors.warning}
style={styles.lockButton}
>
{i18n.t('screens.login.resetPassword')}
</Button>
<Button
icon="send"
mode="contained"
disabled={!shouldEnableLogin()}
loading={props.loading}
onPress={onSubmit}
style={styles.sendButton}
>
{i18n.t('screens.login.title')}
</Button>
</Card.Actions>
<Card.Actions>
<Button
icon="help-circle"
mode="contained"
onPress={props.onHelpPress}
style={GENERAL_STYLES.centerHorizontal}
>
{i18n.t('screens.login.mascotDialog.title')}
</Button>
</Card.Actions>
</Card.Content>
</View>
);
}

View file

@ -1,47 +0,0 @@
// @flow
import * as React from 'react';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
import ConnectionManager from '../../managers/ConnectionManager';
type PropsType = {
navigation: StackNavigationProp,
visible: boolean,
onDismiss: () => void,
};
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(): 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,53 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import i18n from 'i18n-js';
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
import { useLogout } from '../../utils/logout';
type PropsType = {
visible: boolean;
onDismiss: () => void;
};
function LogoutDialog(props: PropsType) {
const onLogout = useLogout();
// Use a loading dialog as it can take some time to update the context
const onClickAccept = async (): Promise<void> => {
return new Promise((resolve: () => void) => {
onLogout();
props.onDismiss();
resolve();
});
};
return (
<LoadingConfirmDialog
visible={props.visible}
onDismiss={props.onDismiss}
onAccept={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,104 @@
import React from 'react';
import { Card, Avatar, Divider, useTheme, List } from 'react-native-paper';
import i18n from 'i18n-js';
import { FlatList, StyleSheet } from 'react-native';
import { ProfileClubType } from '../../../screens/Amicale/ProfileScreen';
import { useNavigation } from '@react-navigation/core';
import { MainRoutes } from '../../../navigation/MainNavigator';
type Props = {
clubs?: Array<ProfileClubType>;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
});
export default function ProfileClubCard(props: Props) {
const theme = useTheme();
const navigation = useNavigation();
const clubKeyExtractor = (item: ProfileClubType) => item.name;
const getClubListItem = ({ item }: { item: ProfileClubType }) => {
const onPress = () =>
navigation.navigate(MainRoutes.ClubInformation, {
type: 'id',
clubId: item.id,
});
let description = i18n.t('screens.profile.isMember');
let icon = (leftProps: {
color: string;
style: {
marginLeft: number;
marginRight: number;
marginVertical?: number;
};
}) => (
<List.Icon
color={leftProps.color}
style={leftProps.style}
icon="chevron-right"
/>
);
if (item.is_manager) {
description = i18n.t('screens.profile.isManager');
icon = (leftProps) => (
<List.Icon
style={leftProps.style}
icon="star"
color={theme.colors.primary}
/>
);
}
return (
<List.Item
title={item.name}
description={description}
left={icon}
onPress={onPress}
/>
);
};
function getClubList(list: Array<ProfileClubType> | undefined) {
if (!list) {
return null;
}
list.sort((a) => (a.is_manager ? -1 : 1));
return (
<FlatList
renderItem={getClubListItem}
keyExtractor={clubKeyExtractor}
data={list}
/>
);
}
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.clubs')}
subtitle={i18n.t('screens.profile.clubsSubtitle')}
left={(iconProps) => (
<Avatar.Icon
size={iconProps.size}
icon="account-group"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<Divider />
{getClubList(props.clubs)}
</Card.Content>
</Card>
);
}

View file

@ -0,0 +1,56 @@
import React from 'react';
import { Avatar, Card, List, useTheme } from 'react-native-paper';
import i18n from 'i18n-js';
import { StyleSheet } from 'react-native';
type Props = {
valid?: boolean;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
});
export default function ProfileMembershipCard(props: Props) {
const theme = useTheme();
const state = props.valid === true;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.membership')}
subtitle={i18n.t('screens.profile.membershipSubtitle')}
left={(iconProps) => (
<Avatar.Icon
size={iconProps.size}
icon="credit-card"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<List.Section>
<List.Item
title={
state
? i18n.t('screens.profile.membershipPayed')
: i18n.t('screens.profile.membershipNotPayed')
}
left={(leftProps) => (
<List.Icon
style={leftProps.style}
color={state ? theme.colors.success : theme.colors.danger}
icon={state ? 'check' : 'close'}
/>
)}
/>
</List.Section>
</Card.Content>
</Card>
);
}

View file

@ -0,0 +1,111 @@
import { useNavigation } from '@react-navigation/core';
import React from 'react';
import { StyleSheet } from 'react-native';
import {
Avatar,
Button,
Card,
Divider,
List,
useTheme,
} from 'react-native-paper';
import Urls from '../../../constants/Urls';
import { ProfileDataType } from '../../../screens/Amicale/ProfileScreen';
import i18n from 'i18n-js';
import { MainRoutes } from '../../../navigation/MainNavigator';
type Props = {
profile?: ProfileDataType;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
},
editButton: {
marginLeft: 'auto',
},
mascot: {
width: 60,
},
title: {
marginLeft: 10,
},
});
function getFieldValue(field?: string): string {
return field ? field : i18n.t('screens.profile.noData');
}
export default function ProfilePersonalCard(props: Props) {
const { profile } = props;
const theme = useTheme();
const navigation = useNavigation();
function getPersonalListItem(field: string | undefined, icon: string) {
const title = field != null ? getFieldValue(field) : ':(';
const subtitle = field != null ? '' : getFieldValue(field);
return (
<List.Item
title={title}
description={subtitle}
left={(leftProps) => (
<List.Icon
style={leftProps.style}
icon={icon}
color={field != null ? leftProps.color : theme.colors.textDisabled}
/>
)}
/>
);
}
return (
<Card style={styles.card}>
<Card.Title
title={`${profile?.first_name} ${profile?.last_name}`}
subtitle={profile?.email}
left={(iconProps) => (
<Avatar.Icon
size={iconProps.size}
icon="account"
color={theme.colors.primary}
style={styles.icon}
/>
)}
/>
<Card.Content>
<Divider />
<List.Section>
<List.Subheader>
{i18n.t('screens.profile.personalInformation')}
</List.Subheader>
{getPersonalListItem(profile?.birthday, 'cake-variant')}
{getPersonalListItem(profile?.phone, 'phone')}
{getPersonalListItem(profile?.email, 'email')}
{getPersonalListItem(profile?.branch, 'school')}
</List.Section>
<Divider />
<Card.Actions>
<Button
icon="account-edit"
mode="contained"
onPress={() => {
navigation.navigate(MainRoutes.Website, {
host: Urls.websites.amicale,
path: profile?.link,
title: i18n.t('screens.websites.amicale'),
});
}}
style={styles.editButton}
>
{i18n.t('screens.profile.editInformation')}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}

View file

@ -0,0 +1,84 @@
import { useNavigation } from '@react-navigation/core';
import React from 'react';
import { Button, Card, Divider, Paragraph } from 'react-native-paper';
import Mascot, { MASCOT_STYLE } from '../../Mascot/Mascot';
import i18n from 'i18n-js';
import { StyleSheet } from 'react-native';
import CardList from '../../Lists/CardList/CardList';
import { getAmicaleServices, SERVICES_KEY } from '../../../utils/Services';
import { MainRoutes } from '../../../navigation/MainNavigator';
type Props = {
firstname?: string;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
editButton: {
marginLeft: 'auto',
},
mascot: {
width: 60,
},
title: {
marginLeft: 10,
},
});
function ProfileWelcomeCard(props: Props) {
const navigation = useNavigation();
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.welcomeTitle', {
name: props.firstname,
})}
left={() => (
<Mascot
style={styles.mascot}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 1000,
}}
/>
)}
titleStyle={styles.title}
/>
<Card.Content>
<Divider />
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
<CardList
dataset={getAmicaleServices(
(route) => navigation.navigate(route),
true,
[SERVICES_KEY.PROFILE]
)}
isHorizontal={true}
/>
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
<Divider />
<Card.Actions>
<Button
icon="bug"
mode="contained"
onPress={() => {
navigation.navigate(MainRoutes.Feedback);
}}
style={styles.editButton}
>
{i18n.t('screens.feedback.homeButtonTitle')}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
export default React.memo(
ProfileWelcomeCard,
(pp, np) => pp.firstname === np.firstname
);

View file

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

@ -0,0 +1,52 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Headline, useTheme } from 'react-native-paper';
import i18n from 'i18n-js';
const styles = StyleSheet.create({
container: {
width: '100%',
marginTop: 10,
marginBottom: 10,
},
headline: {
textAlign: 'center',
},
});
function VoteNotAvailable() {
const theme = useTheme();
return (
<View style={styles.container}>
<Headline
style={{
color: theme.colors.textDisabled,
...styles.headline,
}}
>
{i18n.t('screens.vote.noVote')}
</Headline>
</View>
);
}
export default VoteNotAvailable;

View file

@ -1,4 +1,21 @@
// @flow
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {
@ -9,27 +26,25 @@ import {
Subheading,
withTheme,
} from 'react-native-paper';
import {FlatList, StyleSheet} from 'react-native';
import { FlatList, StyleSheet } from 'react-native';
import i18n from 'i18n-js';
import type {VoteTeamType} from '../../../screens/Amicale/VoteScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {
CardTitleIconPropsType,
ListIconPropsType,
} from '../../../constants/PaperStyles';
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
type PropsType = {
teams: Array<VoteTeamType>,
dateEnd: string,
theme: CustomThemeType,
teams: Array<VoteTeamType>;
dateEnd: string;
theme: ReactNativePaper.Theme;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
icon: {
backgroundColor: 'transparent',
itemCard: {
marginTop: 10,
},
item: {
padding: 0,
},
});
@ -39,10 +54,10 @@ class VoteResults extends React.Component<PropsType> {
winnerIds: Array<number>;
constructor(props: PropsType) {
super();
super(props);
props.teams.sort(this.sortByVotes);
this.getTotalVotes(props.teams);
this.getWinnerIds(props.teams);
this.totalVotes = this.getTotalVotes(props.teams);
this.winnerIds = this.getWinnerIds(props.teams);
}
shouldComponentUpdate(): boolean {
@ -50,39 +65,46 @@ class VoteResults extends React.Component<PropsType> {
}
getTotalVotes(teams: Array<VoteTeamType>) {
this.totalVotes = 0;
let totalVotes = 0;
for (let i = 0; i < teams.length; i += 1) {
this.totalVotes += teams[i].votes;
totalVotes += teams[i].votes;
}
return totalVotes;
}
getWinnerIds(teams: Array<VoteTeamType>) {
const max = teams[0].votes;
this.winnerIds = [];
let winnerIds = [];
for (let i = 0; i < teams.length; i += 1) {
if (teams[i].votes === max) this.winnerIds.push(teams[i].id);
else break;
if (teams[i].votes === max) {
winnerIds.push(teams[i].id);
} else {
break;
}
}
return winnerIds;
}
sortByVotes = (a: VoteTeamType, b: VoteTeamType): number => b.votes - a.votes;
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
resultRenderItem = ({item}: {item: VoteTeamType}): React.Node => {
resultRenderItem = ({ item }: { item: VoteTeamType }) => {
const isWinner = this.winnerIds.indexOf(item.id) !== -1;
const isDraw = this.winnerIds.length > 1;
const {props} = this;
const { props } = this;
const elevation = isWinner ? 5 : 3;
return (
<Card
style={{
marginTop: 10,
elevation: isWinner ? 5 : 3,
}}>
...styles.itemCard,
elevation: elevation,
}}
>
<List.Item
title={item.name}
description={`${item.votes} ${i18n.t('screens.vote.results.votes')}`}
left={(iconProps: ListIconPropsType): React.Node =>
left={(iconProps) =>
isWinner ? (
<List.Icon
style={iconProps.style}
@ -96,7 +118,7 @@ class VoteResults extends React.Component<PropsType> {
? props.theme.colors.primary
: props.theme.colors.text,
}}
style={{padding: 0}}
style={styles.item}
/>
<ProgressBar
progress={item.votes / this.totalVotes}
@ -106,8 +128,8 @@ class VoteResults extends React.Component<PropsType> {
);
};
render(): React.Node {
const {props} = this;
render() {
const { props } = this;
return (
<Card style={styles.card}>
<Card.Title
@ -115,15 +137,14 @@ class VoteResults extends React.Component<PropsType> {
subtitle={`${i18n.t('screens.vote.results.subtitle')} ${
props.dateEnd
}`}
left={(iconProps: CardTitleIconPropsType): React.Node => (
left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="podium-gold" />
)}
/>
<Card.Content>
<Subheading>{`${i18n.t('screens.vote.results.totalVotes')} ${
this.totalVotes
}`}</Subheading>
{/* $FlowFixMe */}
<Subheading>
{`${i18n.t('screens.vote.results.totalVotes')} ${this.totalVotes}`}
</Subheading>
<FlatList
data={props.teams}
keyExtractor={this.voteKeyExtractor}

View file

@ -1,147 +0,0 @@
// @flow
import * as React from 'react';
import {Avatar, Button, Card, RadioButton} from 'react-native-paper';
import {FlatList, StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
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 PropsType = {
teams: Array<VoteTeamType>,
onVoteSuccess: () => void,
onVoteError: () => void,
};
type StateType = {
selectedTeam: string,
voteDialogVisible: boolean,
errorDialogVisible: boolean,
currentError: number,
};
const styles = StyleSheet.create({
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

@ -0,0 +1,143 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import React, { useState } from 'react';
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
import { FlatList, StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../Dialogs/ErrorDialog';
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
import { ApiRejectType } from '../../../utils/WebData';
import { REQUEST_STATUS } from '../../../utils/Requests';
import { useAuthenticatedRequest } from '../../../context/loginContext';
type Props = {
teams: Array<VoteTeamType>;
onVoteSuccess: () => void;
onVoteError: () => void;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
button: {
marginLeft: 'auto',
},
});
function VoteSelect(props: Props) {
const [selectedTeam, setSelectedTeam] = useState('none');
const [voteDialogVisible, setVoteDialogVisible] = useState(false);
const [currentError, setCurrentError] = useState<ApiRejectType>({
status: REQUEST_STATUS.SUCCESS,
});
const request = useAuthenticatedRequest('elections/vote', {
team: parseInt(selectedTeam, 10),
});
const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
<RadioButton.Item label={item.name} value={item.id.toString()} />
);
const showVoteDialog = () => setVoteDialogVisible(true);
const onVoteDialogDismiss = () => setVoteDialogVisible(false);
const onVoteDialogAccept = async (): Promise<void> => {
return new Promise((resolve: () => void) => {
request()
.then(() => {
onVoteDialogDismiss();
props.onVoteSuccess();
resolve();
})
.catch((error: ApiRejectType) => {
onVoteDialogDismiss();
setCurrentError(error);
resolve();
});
});
};
const onErrorDialogDismiss = () => {
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
props.onVoteError();
};
return (
<View>
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.select.title')}
subtitle={i18n.t('screens.vote.select.subtitle')}
left={(iconProps) => (
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
)}
/>
<Card.Content>
<RadioButton.Group
onValueChange={setSelectedTeam}
value={selectedTeam}
>
<FlatList
data={props.teams}
keyExtractor={voteKeyExtractor}
extraData={selectedTeam}
renderItem={voteRenderItem}
/>
</RadioButton.Group>
</Card.Content>
<Card.Actions>
<Button
icon={'send'}
mode={'contained'}
onPress={showVoteDialog}
style={styles.button}
disabled={selectedTeam === 'none'}
>
{i18n.t('screens.vote.select.sendButton')}
</Button>
</Card.Actions>
</Card>
<LoadingConfirmDialog
visible={voteDialogVisible}
onDismiss={onVoteDialogDismiss}
onAccept={onVoteDialogAccept}
title={i18n.t('screens.vote.select.dialogTitle')}
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
message={i18n.t('screens.vote.select.dialogMessage')}
/>
<ErrorDialog
visible={
currentError.status !== REQUEST_STATUS.SUCCESS ||
currentError.code !== undefined
}
onDismiss={onErrorDialogDismiss}
status={currentError.status}
code={currentError.code}
/>
</View>
);
}
export default VoteSelect;

View file

@ -1,46 +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';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
startDate: string,
};
const styles = StyleSheet.create({
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

@ -0,0 +1,50 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Avatar, Card, Paragraph } from 'react-native-paper';
import { StyleSheet } from 'react-native';
import i18n from 'i18n-js';
type PropsType = {
startDate: string;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
});
export default function VoteTease(props: PropsType) {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.vote.tease.title')}
subtitle={i18n.t('screens.vote.tease.subtitle')}
left={(iconProps) => <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,74 +0,0 @@
// @flow
import * as React from 'react';
import {Avatar, Card, Paragraph, withTheme} from 'react-native-paper';
import {StyleSheet} from 'react-native';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {CardTitleIconPropsType} from '../../../constants/PaperStyles';
type PropsType = {
startDate: string | null,
justVoted: boolean,
hasVoted: boolean,
isVoteRunning: boolean,
theme: CustomThemeType,
};
const styles = StyleSheet.create({
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

@ -0,0 +1,77 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Avatar, Card, Paragraph, useTheme } from 'react-native-paper';
import { StyleSheet } from 'react-native';
import i18n from 'i18n-js';
type PropsType = {
startDate: string | null;
justVoted: boolean;
hasVoted: boolean;
isVoteRunning: boolean;
};
const styles = StyleSheet.create({
card: {
margin: 10,
},
});
export default function VoteWait(props: PropsType) {
const theme = useTheme();
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) => (
<Avatar.Icon size={iconProps.size} icon="progress-check" />
)}
/>
<Card.Content>
{props.justVoted ? (
<Paragraph style={{ color: theme.colors.success }}>
{i18n.t('screens.vote.wait.messageSubmitted')}
</Paragraph>
) : null}
{props.hasVoted ? (
<Paragraph style={{ color: 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>
);
}

View file

@ -1,117 +0,0 @@
// @flow
import * as React from 'react';
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 {CustomThemeType} from '../../managers/ThemeManager';
import type {ListIconPropsType} from '../../constants/PaperStyles';
type PropsType = {
theme: CustomThemeType,
title: string,
subtitle?: string,
left?: () => React.Node,
opened?: boolean,
unmountWhenCollapsed?: boolean,
children?: React.Node,
};
type StateType = {
expanded: boolean,
};
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.Component<PropsType, StateType> {
static defaultProps = {
subtitle: '',
left: null,
opened: null,
unmountWhenCollapsed: false,
children: null,
};
chevronRef: {current: null | AnimatedListIcon};
chevronIcon: string;
animStart: string;
animEnd: string;
constructor(props: PropsType) {
super(props);
this.state = {
expanded: props.opened != null ? props.opened : false,
};
this.chevronRef = React.createRef();
this.setupChevron();
}
shouldComponentUpdate(nextProps: PropsType): boolean {
const {state, props} = this;
if (nextProps.opened != null && nextProps.opened !== props.opened)
state.expanded = nextProps.opened;
return true;
}
setupChevron() {
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';
}
}
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);

View file

@ -0,0 +1,137 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import React, { useEffect, useRef } from 'react';
import { View, ViewStyle } from 'react-native';
import { List, useTheme } from 'react-native-paper';
import Collapsible from 'react-native-collapsible';
import * as Animatable from 'react-native-animatable';
import GENERAL_STYLES from '../../constants/Styles';
type PropsType = {
title: string;
subtitle?: string;
style?: ViewStyle;
left?: (props: {
color: string;
style?: {
marginRight: number;
marginVertical?: number;
};
}) => React.ReactNode;
opened?: boolean;
unmountWhenCollapsed?: boolean;
enabled?: boolean;
renderItem: () => React.ReactNode;
};
function AnimatedAccordion(props: PropsType) {
const theme = useTheme();
const [expanded, setExpanded] = React.useState(props.opened);
const lastOpenedProp = useRef(props.opened);
const chevronIcon = useRef(props.opened ? 'chevron-up' : 'chevron-down');
const animStart = useRef(props.opened ? '180deg' : '0deg');
const animEnd = useRef(props.opened ? '0deg' : '180deg');
const enabled = props.enabled !== false;
const getAccordionAnimation = ():
| Animatable.Animation
| string
| Animatable.CustomAnimation => {
// I don't knwo why ts is complaining
// The type definitions must be broken because this is a valid style and it works
if (expanded) {
return {
from: {
// @ts-ignore
rotate: animStart.current,
},
to: {
// @ts-ignore
rotate: animEnd.current,
},
};
} else {
return {
from: {
// @ts-ignore
rotate: animEnd.current,
},
to: {
// @ts-ignore
rotate: animStart.current,
},
};
}
};
useEffect(() => {
// Force the expanded state to follow the prop when changing
if (!enabled) {
setExpanded(false);
} else if (
props.opened !== undefined &&
props.opened !== lastOpenedProp.current
) {
setExpanded(props.opened);
}
}, [enabled, props.opened]);
const toggleAccordion = () => setExpanded(!expanded);
const renderChildren =
!props.unmountWhenCollapsed || (props.unmountWhenCollapsed && expanded);
return (
<View style={props.style}>
<List.Item
title={props.title}
description={props.subtitle}
descriptionNumberOfLines={2}
titleStyle={expanded ? { color: theme.colors.primary } : null}
onPress={enabled ? toggleAccordion : undefined}
right={
enabled
? (iconProps) => (
<Animatable.View
animation={getAccordionAnimation()}
duration={300}
useNativeDriver={true}
>
<List.Icon
style={{ ...iconProps.style, ...GENERAL_STYLES.center }}
icon={chevronIcon.current}
color={expanded ? theme.colors.primary : iconProps.color}
/>
</Animatable.View>
)
: undefined
}
left={props.left}
/>
{enabled ? (
<Collapsible collapsed={!expanded}>
{renderChildren ? props.renderItem() : null}
</Collapsible>
) : null}
</View>
);
}
export default AnimatedAccordion;

View file

@ -1,179 +0,0 @@
// @flow
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import {FAB, IconButton, Surface, withTheme} from 'react-native-paper';
import * as Animatable from 'react-native-animatable';
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 PropsType = {
navigation: StackNavigationProp,
theme: CustomThemeType,
onPress: (action: string, data?: string) => void,
seekAttention: boolean,
};
type StateType = {
currentMode: string,
};
const DISPLAY_MODES = {
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%',
},
});
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,63 +0,0 @@
// @flow
import * as React from 'react';
import {StyleSheet} from 'react-native';
import {FAB} from 'react-native-paper';
import * as Animatable from 'react-native-animatable';
import AutoHideHandler from '../../utils/AutoHideHandler';
import CustomTabBar from '../Tabbar/CustomTabBar';
type PropsType = {
icon: string,
onPress: () => void,
};
const AnimatedFab = Animatable.createAnimatableComponent(FAB);
const styles = StyleSheet.create({
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,92 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
View,
} from 'react-native';
import { FAB } from 'react-native-paper';
import * as Animatable from 'react-native-animatable';
import AutoHideHandler from '../../utils/AutoHideHandler';
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
type PropsType = {
icon: string;
onPress: () => void;
};
const styles = StyleSheet.create({
fab: {
position: 'absolute',
margin: 16,
right: 0,
},
});
export default class AnimatedFAB extends React.Component<PropsType> {
ref: { current: null | (Animatable.View & View) };
hideHandler: AutoHideHandler;
constructor(props: PropsType) {
super(props);
this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
}
onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
this.hideHandler.onScroll(event);
};
onHideChange = (shouldHide: boolean) => {
const ref = this.ref;
if (
ref &&
ref.current &&
ref.current.bounceOutDown &&
ref.current.bounceInUp
) {
if (shouldHide) {
ref.current.bounceOutDown(1000);
} else {
ref.current.bounceInUp(1000);
}
}
};
render() {
const { props } = this;
return (
<Animatable.View
ref={this.ref}
useNativeDriver={true}
style={{
...styles.fab,
bottom: TAB_BAR_HEIGHT,
}}
>
<FAB icon={props.icon} onPress={props.onPress} />
</Animatable.View>
);
}
}

View file

@ -0,0 +1,177 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import React, { useState } from 'react';
import { StyleSheet, View, Animated } from 'react-native';
import { FAB, IconButton, Surface, useTheme } from 'react-native-paper';
import * as Animatable from 'react-native-animatable';
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
import { useNavigation } from '@react-navigation/core';
import { useCollapsible } from '../../context/CollapsibleContext';
import { MainRoutes } from '../../navigation/MainNavigator';
type Props = {
onPress: (action: string, data?: string) => void;
seekAttention: boolean;
};
const DISPLAY_MODES = {
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%',
},
side: {
flexDirection: 'row',
},
icon: {
marginLeft: 5,
},
});
const DISPLAY_MODE_ICONS = {
[DISPLAY_MODES.DAY]: 'calendar-text',
[DISPLAY_MODES.WEEK]: 'calendar-week',
[DISPLAY_MODES.MONTH]: 'calendar-range',
};
function PlanexBottomBar(props: Props) {
const navigation = useNavigation();
const theme = useTheme();
const [currentMode, setCurrentMode] = useState(DISPLAY_MODES.WEEK);
const { collapsible } = useCollapsible();
const changeDisplayMode = () => {
let newMode;
switch (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;
}
setCurrentMode(newMode);
props.onPress('changeView', newMode);
};
let translateY: number | Animated.AnimatedInterpolation = 0;
let opacity: number | Animated.AnimatedInterpolation = 1;
let scale: number | Animated.AnimatedInterpolation = 1;
if (collapsible) {
translateY = Animated.multiply(-3, collapsible.translateY);
opacity = Animated.subtract(1, collapsible.progress);
scale = Animated.add(
0.5,
Animated.multiply(0.5, Animated.subtract(1, collapsible.progress))
);
}
const buttonColor = theme.colors.primary;
return (
<Animated.View
style={{
...styles.container,
bottom: 10 + TAB_BAR_HEIGHT,
transform: [{ translateY: translateY }, { scale: scale }],
opacity: opacity,
}}
>
<Surface style={styles.surface}>
<View style={styles.fabContainer}>
<Animatable.View
style={styles.fab}
animation={props.seekAttention ? 'bounce' : undefined}
easing={'ease-out'}
iterationDelay={500}
iterationCount={'infinite'}
useNativeDriver={true}
>
<FAB
icon={'account-clock'}
onPress={() => navigation.navigate(MainRoutes.GroupSelect)}
/>
</Animatable.View>
</View>
<View style={styles.side}>
<IconButton
icon={DISPLAY_MODE_ICONS[currentMode]}
color={buttonColor}
onPress={changeDisplayMode}
/>
<IconButton
icon="clock-in"
color={buttonColor}
style={styles.icon}
onPress={() => props.onPress('today')}
/>
</View>
<View style={styles.side}>
<IconButton
icon="chevron-left"
color={buttonColor}
onPress={() => props.onPress('prev')}
/>
<IconButton
icon="chevron-right"
color={buttonColor}
style={styles.icon}
onPress={() => props.onPress('next')}
/>
</View>
</Surface>
</Animated.View>
);
}
export default PlanexBottomBar;

View file

@ -1,59 +0,0 @@
// @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,106 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import React, { useCallback } from 'react';
import { useCollapsibleHeader } from 'react-navigation-collapsible';
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
} from 'react-native';
import { useTheme } from 'react-native-paper';
import { useCollapsible } from '../../context/CollapsibleContext';
import { useFocusEffect } from '@react-navigation/core';
export type CollapsibleComponentPropsType = {
children?: React.ReactNode;
hasTab?: boolean;
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
paddedProps?: (paddingTop: number) => Record<string, any>;
headerColors?: string;
};
type Props = CollapsibleComponentPropsType & {
component: React.ComponentType<any>;
};
const styles = StyleSheet.create({
main: {
minHeight: '100%',
},
});
function CollapsibleComponent(props: Props) {
const { paddedProps, headerColors } = props;
const Comp = props.component;
const theme = useTheme();
const { setCollapsible } = useCollapsible();
const collapsible = useCollapsibleHeader({
config: {
collapsedColor: headerColors ? headerColors : theme.colors.surface,
useNativeDriver: true,
},
navigationOptions: {
headerStyle: {
backgroundColor: headerColors ? headerColors : theme.colors.surface,
},
},
});
useFocusEffect(
useCallback(() => {
setCollapsible(collapsible);
}, [collapsible, setCollapsible])
);
const { containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener } =
collapsible;
const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (props.onScroll) {
props.onScroll(event);
}
};
const pprops =
paddedProps !== undefined ? paddedProps(containerPaddingTop) : undefined;
return (
<Comp
{...props}
{...pprops}
onScroll={onScrollWithListener(onScroll)}
contentContainerStyle={{
paddingTop: containerPaddingTop,
paddingBottom: paddingBottom,
...styles.main,
}}
scrollIndicatorInsets={{ top: scrollIndicatorInsetTop }}
>
{props.children}
</Comp>
);
}
export default CollapsibleComponent;

View file

@ -1,26 +0,0 @@
// @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,35 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Animated, FlatListProps } from 'react-native';
import type { CollapsibleComponentPropsType } from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent';
type Props<T> = FlatListProps<T> & CollapsibleComponentPropsType;
function CollapsibleFlatList<T>(props: Props<T>) {
return (
<CollapsibleComponent {...props} component={Animated.FlatList}>
{props.children}
</CollapsibleComponent>
);
}
export default CollapsibleFlatList;

View file

@ -1,26 +0,0 @@
// @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,35 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Animated, ScrollViewProps } from 'react-native';
import type { CollapsibleComponentPropsType } from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent';
type Props = ScrollViewProps & CollapsibleComponentPropsType;
function CollapsibleScrollView(props: Props) {
return (
<CollapsibleComponent {...props} component={Animated.ScrollView}>
{props.children}
</CollapsibleComponent>
);
}
export default CollapsibleScrollView;

View file

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

@ -0,0 +1,35 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Animated, SectionListProps } from 'react-native';
import type { CollapsibleComponentPropsType } from './CollapsibleComponent';
import CollapsibleComponent from './CollapsibleComponent';
type Props<T> = SectionListProps<T> & CollapsibleComponentPropsType;
function CollapsibleSectionList<T>(props: Props<T>) {
return (
<CollapsibleComponent {...props} component={Animated.SectionList}>
{props.children}
</CollapsibleComponent>
);
}
export default CollapsibleSectionList;

View file

@ -1,33 +0,0 @@
// @flow
import * as React from 'react';
import {Button, Dialog, Paragraph, Portal} from 'react-native-paper';
import i18n from 'i18n-js';
type PropsType = {
visible: boolean,
onDismiss: () => void,
title: string,
message: string,
};
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

@ -0,0 +1,53 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Button, Dialog, Paragraph, Portal } from 'react-native-paper';
import i18n from 'i18n-js';
import { ViewStyle } from 'react-native';
type PropsType = {
visible: boolean;
onDismiss: () => void;
title: string | React.ReactNode;
message: string | React.ReactNode;
style?: ViewStyle;
};
function AlertDialog(props: PropsType) {
return (
<Portal>
<Dialog
visible={props.visible}
onDismiss={props.onDismiss}
style={props.style}
>
<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,71 +0,0 @@
// @flow
import * as React from 'react';
import i18n from 'i18n-js';
import {ERROR_TYPE} from '../../utils/WebData';
import AlertDialog from './AlertDialog';
type PropsType = {
visible: boolean,
onDismiss: () => void,
errorCode: number,
};
class ErrorDialog extends React.PureComponent<PropsType> {
title: string;
message: string;
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(): 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

@ -0,0 +1,47 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import i18n from 'i18n-js';
import AlertDialog from './AlertDialog';
import {
API_REQUEST_CODES,
getErrorMessage,
REQUEST_STATUS,
} from '../../utils/Requests';
type PropsType = {
visible: boolean;
onDismiss: () => void;
status?: REQUEST_STATUS;
code?: API_REQUEST_CODES;
};
function ErrorDialog(props: PropsType) {
return (
<AlertDialog
visible={props.visible}
onDismiss={props.onDismiss}
title={i18n.t('errors.title')}
message={getErrorMessage(props).message}
/>
);
}
export default ErrorDialog;

View file

@ -1,4 +1,21 @@
// @flow
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {
@ -9,22 +26,32 @@ import {
Portal,
} from 'react-native-paper';
import i18n from 'i18n-js';
import { StyleSheet } from 'react-native';
type PropsType = {
visible: boolean,
onDismiss?: () => void,
onAccept?: () => Promise<void>, // async function to be executed
title?: string,
titleLoading?: string,
message?: string,
startLoading?: boolean,
visible: boolean;
onDismiss?: () => void;
onAccept?: () => Promise<void>; // async function to be executed
title?: string;
titleLoading?: string;
message?: string;
startLoading?: boolean;
};
type StateType = {
loading: boolean,
loading: boolean;
};
class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
const styles = StyleSheet.create({
button: {
marginRight: 10,
},
});
export default class LoadingConfirmDialog extends React.PureComponent<
PropsType,
StateType
> {
static defaultProps = {
onDismiss: () => {},
onAccept: (): Promise<void> => {
@ -50,30 +77,34 @@ class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
* 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);
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}
* @returns {NodeJS.Timeout}
*/
hideLoading = (): TimeoutID =>
hideLoading = (): NodeJS.Timeout =>
setTimeout(() => {
this.setState({loading: false});
this.setState({ loading: false });
}, 200);
/**
* Hide the dialog if it is not loading
*/
onDismiss = () => {
const {state, props} = this;
if (!state.loading && props.onDismiss != null) props.onDismiss();
const { state, props } = this;
if (!state.loading && props.onDismiss != null) {
props.onDismiss();
}
};
render(): React.Node {
const {state, props} = this;
render() {
const { state, props } = this;
return (
<Portal>
<Dialog visible={props.visible} onDismiss={this.onDismiss}>
@ -89,7 +120,7 @@ class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
</Dialog.Content>
{state.loading ? null : (
<Dialog.Actions>
<Button onPress={this.onDismiss} style={{marginRight: 10}}>
<Button onPress={this.onDismiss} style={styles.button}>
{i18n.t('dialog.cancel')}
</Button>
<Button onPress={this.onClickAccept}>
@ -102,5 +133,3 @@ class LoadingConfirmDialog extends React.PureComponent<PropsType, StateType> {
);
}
}
export default LoadingConfirmDialog;

View file

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

@ -0,0 +1,75 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Button, Dialog, Paragraph, Portal } from 'react-native-paper';
import { FlatList } from 'react-native';
export type OptionsDialogButtonType = {
title: string;
icon?: string;
onPress: () => void;
};
type PropsType = {
visible: boolean;
title: string;
message: string;
buttons: Array<OptionsDialogButtonType>;
onDismiss: () => void;
};
function OptionsDialog(props: PropsType) {
const getButtonRender = ({ item }: { item: OptionsDialogButtonType }) => {
return (
<Button onPress={item.onPress} icon={item.icon}>
{item.title}
</Button>
);
};
const keyExtractor = (item: OptionsDialogButtonType): string => {
if (item.icon != null) {
return item.title + item.icon;
}
return item.title;
};
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={getButtonRender}
keyExtractor={keyExtractor}
horizontal
inverted
/>
</Dialog.Actions>
</Dialog>
</Portal>
);
}
export default OptionsDialog;

View file

@ -1,56 +0,0 @@
// @flow
import * as React from 'react';
import {List, withTheme} from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack';
import type {CustomThemeType} from '../../managers/ThemeManager';
import type {ListIconPropsType} from '../../constants/PaperStyles';
type PropsType = {
navigation: StackNavigationProp,
theme: CustomThemeType,
};
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>
);
}
}
export default withTheme(ActionsDashBoardItem);

View file

@ -0,0 +1,64 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { List } from 'react-native-paper';
import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
import { useNavigation } from '@react-navigation/native';
import { MainRoutes } from '../../navigation/MainNavigator';
const styles = StyleSheet.create({
item: {
paddingTop: 0,
paddingBottom: 0,
marginLeft: 10,
marginRight: 10,
},
});
function ActionsDashBoardItem() {
const navigation = useNavigation();
return (
<View>
<List.Item
title={i18n.t('screens.feedback.homeButtonTitle')}
description={i18n.t('screens.feedback.homeButtonSubtitle')}
left={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="comment-quote"
/>
)}
right={(props) => (
<List.Icon
color={props.color}
style={props.style}
icon="chevron-right"
/>
)}
onPress={(): void => navigation.navigate(MainRoutes.Feedback)}
style={styles.item}
/>
</View>
);
}
export default ActionsDashBoardItem;

View file

@ -1,97 +0,0 @@
// @flow
import * as React from 'react';
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 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<PropsType> {
static defaultProps = {
children: null,
};
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.eventNumber !== props.eventNumber
);
}
render(): React.Node {
const {props} = this;
const {colors} = props.theme;
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable ? colors.planningColor : colors.textDisabled;
const textColor = isAvailable ? colors.text : colors.textDisabled;
let subtitle;
if (isAvailable) {
subtitle = (
<Text>
<Text style={{fontWeight: 'bold'}}>{props.eventNumber}</Text>
<Text>
{props.eventNumber > 1
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
</Text>
</Text>
);
} else subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
return (
<Card style={styles.card}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
<Card.Title
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
titleStyle={{color: textColor}}
subtitle={subtitle}
subtitleStyle={{color: textColor}}
left={(iconProps: CardTitleIconPropsType): React.Node => (
<Avatar.Icon
icon="calendar-range"
color={iconColor}
size={iconProps.size}
style={styles.avatar}
/>
)}
/>
<Card.Content>{props.children}</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
}
export default withTheme(EventDashBoardItem);

View file

@ -0,0 +1,108 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {
Avatar,
Card,
Text,
TouchableRipple,
useTheme,
} from 'react-native-paper';
import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
import GENERAL_STYLES from '../../constants/Styles';
type PropsType = {
eventNumber: number;
clickAction: () => void;
children?: React.ReactNode;
};
const styles = StyleSheet.create({
card: {
width: 'auto',
marginLeft: 10,
marginRight: 10,
marginTop: 10,
overflow: 'hidden',
},
avatar: {
backgroundColor: 'transparent',
},
text: {
fontWeight: 'bold',
},
});
/**
* Component used to display a dashboard item containing a preview event
*/
function EventDashBoardItem(props: PropsType) {
const theme = useTheme();
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable
? theme.colors.planningColor
: theme.colors.textDisabled;
const textColor = isAvailable ? theme.colors.text : theme.colors.textDisabled;
let subtitle;
if (isAvailable) {
subtitle = (
<Text>
<Text style={styles.text}>{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={GENERAL_STYLES.flex} onPress={props.clickAction}>
<View>
<Card.Title
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
titleStyle={{ color: textColor }}
subtitle={subtitle}
subtitleStyle={{ color: textColor }}
left={(iconProps) => (
<Avatar.Icon
icon="calendar-range"
color={iconColor}
size={iconProps.size}
style={styles.avatar}
/>
)}
/>
<Card.Content>{props.children}</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.eventNumber === prevProps.eventNumber;
};
export default React.memo(EventDashBoardItem, areEqual);

View file

@ -1,120 +0,0 @@
// @flow
import * as React from 'react';
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<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(): boolean {
return false;
}
onPress = () => {
const {item, navigation} = this.props;
navigation.navigate('feed-information', {
data: item,
date: FeedItem.getFormattedDate(item.time),
});
};
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,
}}
/>
)}
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

@ -0,0 +1,140 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Button, Card, Text, TouchableRipple } from 'react-native-paper';
import { Image, StyleSheet, View } from 'react-native';
import Autolink from 'react-native-autolink';
import i18n from 'i18n-js';
import type { FeedItemType } from '../../screens/Home/HomeScreen';
import NewsSourcesConstants, {
AvailablePages,
} from '../../constants/NewsSourcesConstants';
import type { NewsSourceType } from '../../constants/NewsSourcesConstants';
import ImageGalleryButton from '../Media/ImageGalleryButton';
import { useNavigation } from '@react-navigation/native';
import GENERAL_STYLES from '../../constants/Styles';
import { MainRoutes } from '../../navigation/MainNavigator';
type PropsType = {
item: FeedItemType;
height: number;
};
/**
* 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
*/
function getFormattedDate(dateString: number): string {
const date = new Date(dateString * 1000);
return date.toLocaleString();
}
const styles = StyleSheet.create({
image: {
width: 48,
height: 48,
},
button: {
marginLeft: 'auto',
marginRight: 'auto',
},
action: {
marginLeft: 'auto',
},
});
/**
* Component used to display a feed item
*/
function FeedItem(props: PropsType) {
const navigation = useNavigation();
const onPress = () => {
navigation.navigate(MainRoutes.FeedInformation, {
data: item,
date: getFormattedDate(props.item.time),
});
};
const { item, height } = props;
const image = item.image !== '' && item.image != null ? item.image : null;
const pageSource: NewsSourceType =
NewsSourcesConstants[item.page_id as AvailablePages];
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={GENERAL_STYLES.flex} onPress={onPress}>
<View>
<Card.Title
title={pageSource.name}
subtitle={getFormattedDate(item.time)}
left={() => <Image source={pageSource.icon} style={styles.image} />}
style={{ height: titleHeight }}
/>
{image != null ? (
<ImageGalleryButton
images={[{ url: image }]}
style={{
...styles.button,
width: imageSize,
height: imageSize,
}}
/>
) : null}
<Card.Content>
{item.message !== undefined ? (
<Autolink
text={item.message}
hashtag={'facebook'}
component={Text}
style={{ height: textHeight }}
truncate={32}
email={true}
url={true}
phone={true}
/>
) : null}
</Card.Content>
<Card.Actions style={{ height: actionsHeight }}>
<Button onPress={onPress} icon="plus" style={styles.action}>
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
export default React.memo(FeedItem, () => true);

View file

@ -1,94 +0,0 @@
// @flow
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
import {getTimeOnlyString, isDescriptionEmpty} from '../../utils/Planning';
import CustomHTML from '../Overrides/CustomHTML';
import type {PlanningEventType} from '../../utils/Planning';
type PropsType = {
event?: PlanningEventType | 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
*/
// eslint-disable-next-line react/prefer-stateless-function
class PreviewEventDashboardItem extends React.Component<PropsType> {
static defaultProps = {
event: null,
};
render(): React.Node {
const {props} = this;
const {event} = props;
const isEmpty =
event == null ? true : isDescriptionEmpty(event.description);
if (event != null) {
const hasImage = event.logo !== '' && event.logo != null;
const getImage = (): React.Node => (
<Avatar.Image
source={{uri: event.logo}}
size={50}
style={styles.avatar}
/>
);
return (
<Card style={styles.card} elevation={3}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
{hasImage ? (
<Card.Title
title={event.title}
subtitle={getTimeOnlyString(event.date_begin)}
left={getImage}
/>
) : (
<Card.Title
title={event.title}
subtitle={getTimeOnlyString(event.date_begin)}
/>
)}
{!isEmpty ? (
<Card.Content style={styles.content}>
<CustomHTML html={event.description} />
</Card.Content>
) : null}
<Card.Actions style={styles.actions}>
<Button icon="chevron-right">
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
return null;
}
}
export default PreviewEventDashboardItem;

View file

@ -0,0 +1,101 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
import { Avatar, Button, Card, TouchableRipple } from 'react-native-paper';
import { getTimeOnlyString, isDescriptionEmpty } from '../../utils/Planning';
import CustomHTML from '../Overrides/CustomHTML';
import type { PlanningEventType } from '../../utils/Planning';
import GENERAL_STYLES from '../../constants/Styles';
type PropsType = {
event?: PlanningEventType | 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
*/
function PreviewEventDashboardItem(props: PropsType) {
const { event } = props;
const isEmpty = event == null ? true : isDescriptionEmpty(event.description);
if (event != null) {
const logo = event.logo;
const getImage = logo
? () => (
<Avatar.Image
source={{ uri: logo }}
size={50}
style={styles.avatar}
/>
)
: () => null;
return (
<Card style={styles.card} elevation={3}>
<TouchableRipple
style={GENERAL_STYLES.flex}
onPress={props.clickAction}
>
<View>
<Card.Title
title={event.title}
subtitle={getTimeOnlyString(event.date_begin)}
left={getImage}
/>
{!isEmpty ? (
<Card.Content style={styles.content}>
<CustomHTML html={event.description} />
</Card.Content>
) : null}
<Card.Actions style={styles.actions}>
<Button icon="chevron-right">
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
return null;
}
export default PreviewEventDashboardItem;

View file

@ -1,85 +0,0 @@
// @flow
import * as React from 'react';
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 PropsType = {
image: string | null,
onPress: () => void | null,
badgeCount: number | null,
theme: CustomThemeType,
};
const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
/**
* Component used to render a small dashboard item
*/
class SmallDashboardItem extends React.Component<PropsType> {
itemSize: number;
constructor(props: PropsType) {
super(props);
this.itemSize = Dimensions.get('window').width / 8;
}
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

@ -0,0 +1,106 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Badge, TouchableRipple, useTheme } from 'react-native-paper';
import { Dimensions, Image, StyleSheet, View } from 'react-native';
import * as Animatable from 'react-native-animatable';
type PropsType = {
image?: string | number;
onPress?: () => void;
badgeCount?: number;
};
const styles = StyleSheet.create({
image: {
width: '80%',
height: '80%',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
marginBottom: 'auto',
},
badgeContainer: {
position: 'absolute',
top: 0,
right: 0,
},
badge: {
borderWidth: 2,
},
});
/**
* Component used to render a small dashboard item
*/
function SmallDashboardItem(props: PropsType) {
const itemSize = Dimensions.get('window').width / 8;
const theme = useTheme();
const { image } = props;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: itemSize / 6,
marginRight: itemSize / 6,
}}
>
<View
style={{
width: itemSize,
height: itemSize,
}}
>
{image ? (
<Image
source={typeof image === 'string' ? { uri: image } : image}
style={styles.image}
/>
) : null}
{props.badgeCount != null && props.badgeCount > 0 ? (
<Animatable.View
animation="zoomIn"
duration={300}
useNativeDriver
style={styles.badgeContainer}
>
<Badge
visible={true}
style={{
backgroundColor: theme.colors.primary,
borderColor: theme.colors.background,
...styles.badge,
}}
>
{props.badgeCount}
</Badge>
</Animatable.View>
) : null}
</View>
</TouchableRipple>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return nextProps.badgeCount === prevProps.badgeCount;
};
export default React.memo(SmallDashboardItem, areEqual);

View file

@ -1,41 +0,0 @@
// @flow
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
type PropsType = {
icon: string,
};
const styles = StyleSheet.create({
center: {
marginTop: 'auto',
marginBottom: 'auto',
marginRight: 'auto',
marginLeft: 'auto',
},
});
class IntroIcon extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
const {icon} = this.props;
return (
<View style={{flex: 1}}>
<Animatable.View
useNativeDriver
style={styles.center}
animation="fadeIn">
<MaterialCommunityIcons name={icon} color="#fff" size={200} />
</Animatable.View>
</View>
);
}
}
export default IntroIcon;

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import GENERAL_STYLES from '../../constants/Styles';
type PropsType = {
icon: string;
};
const styles = StyleSheet.create({
center: {
marginTop: 'auto',
marginBottom: 'auto',
marginRight: 'auto',
marginLeft: 'auto',
},
});
function IntroIcon(props: PropsType) {
return (
<View style={GENERAL_STYLES.flex}>
<Animatable.View useNativeDriver style={styles.center} animation="fadeIn">
<MaterialCommunityIcons name={props.icon} color="#fff" size={200} />
</Animatable.View>
</View>
);
}
export default IntroIcon;

View file

@ -1,46 +0,0 @@
// @flow
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
const styles = StyleSheet.create({
center: {
marginTop: 'auto',
marginBottom: 'auto',
marginRight: 'auto',
marginLeft: 'auto',
},
});
class MascotIntroEnd extends React.Component<null> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
return (
<View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
width: '80%',
}}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'slideInDown',
duration: 2000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
</View>
);
}
}
export default MascotIntroEnd;

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import GENERAL_STYLES from '../../constants/Styles';
import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot';
const styles = StyleSheet.create({
center: {
...GENERAL_STYLES.center,
width: '80%',
},
});
function MascotIntroEnd() {
return (
<View style={GENERAL_STYLES.flex}>
<Mascot
style={{
...styles.center,
}}
emotion={MASCOT_STYLE.COOL}
animated
entryAnimation={{
animation: 'slideInDown',
duration: 2000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
</View>
);
}
export default MascotIntroEnd;

View file

@ -1,76 +0,0 @@
// @flow
import * as React from 'react';
import {StyleSheet, View} from 'react-native';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import Mascot, {MASCOT_STYLE} from '../Mascot/Mascot';
const styles = StyleSheet.create({
center: {
marginTop: 'auto',
marginBottom: 'auto',
marginRight: 'auto',
marginLeft: 'auto',
},
});
class MascotIntroWelcome extends React.Component<null> {
shouldComponentUpdate(): boolean {
return false;
}
render(): React.Node {
return (
<View style={{flex: 1}}>
<Mascot
style={{
...styles.center,
width: '80%',
}}
emotion={MASCOT_STYLE.NORMAL}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 2000,
}}
/>
<Animatable.Text
useNativeDriver
animation="fadeInUp"
duration={500}
style={{
color: '#fff',
textAlign: 'center',
fontSize: 25,
}}>
PABLO
</Animatable.Text>
<Animatable.View
useNativeDriver
animation="fadeInUp"
duration={500}
delay={200}
style={{
position: 'absolute',
bottom: 30,
right: '20%',
width: 50,
height: 50,
}}>
<MaterialCommunityIcons
style={{
...styles.center,
transform: [{rotateZ: '70deg'}],
}}
name="undo"
color="#fff"
size={40}
/>
</Animatable.View>
</View>
);
}
}
export default MascotIntroWelcome;

View file

@ -0,0 +1,88 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import * as Animatable from 'react-native-animatable';
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import GENERAL_STYLES from '../../constants/Styles';
import Mascot, { MASCOT_STYLE } from '../Mascot/Mascot';
const styles = StyleSheet.create({
mascot: {
...GENERAL_STYLES.center,
width: '80%',
},
text: {
color: '#fff',
textAlign: 'center',
fontSize: 25,
},
container: {
position: 'absolute',
bottom: 30,
right: '20%',
width: 50,
height: 50,
},
icon: {
...GENERAL_STYLES.center,
transform: [{ rotateZ: '70deg' }],
},
});
function MascotIntroWelcome() {
return (
<View style={GENERAL_STYLES.flex}>
<Mascot
style={styles.mascot}
emotion={MASCOT_STYLE.NORMAL}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 2000,
}}
/>
<Animatable.Text
useNativeDriver
animation="fadeInUp"
duration={500}
style={styles.text}
>
PABLO
</Animatable.Text>
<Animatable.View
useNativeDriver
animation="fadeInUp"
duration={500}
delay={200}
style={styles.container}
>
<MaterialCommunityIcons
style={styles.icon}
name="undo"
color="#fff"
size={40}
/>
</Animatable.View>
</View>
);
}
export default MascotIntroWelcome;

View file

@ -1,16 +1,32 @@
// @flow
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import {Animated, Dimensions} from 'react-native';
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet';
import { Animated, Dimensions, ViewStyle } from 'react-native';
import ImageListItem from './ImageListItem';
import CardListItem from './CardListItem';
import type {ServiceItemType} from '../../../managers/ServicesManager';
import { ServiceItemType } from '../../../utils/Services';
type PropsType = {
dataset: Array<ServiceItemType>,
isHorizontal?: boolean,
contentContainerStyle?: ViewStyle | null,
dataset: Array<ServiceItemType>;
isHorizontal?: boolean;
contentContainerStyle?: ViewStyle;
};
export default class CardList extends React.Component<PropsType> {
@ -26,12 +42,12 @@ export default class CardList extends React.Component<PropsType> {
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
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)
getRenderItem = ({ item }: { item: ServiceItemType }) => {
const { props } = this;
if (props.isHorizontal) {
return (
<ImageListItem
item={item}
@ -39,13 +55,14 @@ export default class CardList extends React.Component<PropsType> {
width={this.horizontalItemSize}
/>
);
}
return <CardListItem item={item} key={item.title} />;
};
keyExtractor = (item: ServiceItemType): string => item.key;
render(): React.Node {
const {props} = this;
render() {
const { props } = this;
let containerStyle = {};
if (props.isHorizontal) {
containerStyle = {
@ -65,7 +82,7 @@ export default class CardList extends React.Component<PropsType> {
}
pagingEnabled={props.isHorizontal}
snapToInterval={
props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : null
props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : undefined
}
/>
);

View file

@ -1,42 +0,0 @@
// @flow
import * as React from 'react';
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
import {View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager';
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>
);
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Caption, Card, Paragraph, TouchableRipple } from 'react-native-paper';
import { StyleSheet, View } from 'react-native';
import GENERAL_STYLES from '../../../constants/Styles';
import { ServiceItemType } from '../../../utils/Services';
type PropsType = {
item: ServiceItemType;
};
const styles = StyleSheet.create({
card: {
width: '40%',
margin: 5,
marginLeft: 'auto',
marginRight: 'auto',
},
cover: {
height: 80,
},
});
function CardListItem(props: PropsType) {
const { item } = props;
const source =
typeof item.image === 'number' ? item.image : { uri: item.image };
return (
<Card style={styles.card}>
<TouchableRipple style={GENERAL_STYLES.flex} onPress={item.onPress}>
<View>
<Card.Cover style={styles.cover} source={source} />
<Card.Content>
<Paragraph>{item.title}</Paragraph>
<Caption>{item.subtitle}</Caption>
</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
export default React.memo(CardListItem, () => true);

View file

@ -1,54 +0,0 @@
// @flow
import * as React from 'react';
import {Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager';
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>
);
}
}

View file

@ -0,0 +1,70 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Text, TouchableRipple } from 'react-native-paper';
import { Image, StyleSheet, View } from 'react-native';
import GENERAL_STYLES from '../../../constants/Styles';
import { ServiceItemType } from '../../../utils/Services';
type PropsType = {
item: ServiceItemType;
width: number;
};
const styles = StyleSheet.create({
ripple: {
margin: 5,
},
text: {
...GENERAL_STYLES.centerHorizontal,
marginTop: 5,
textAlign: 'center',
},
});
function ImageListItem(props: PropsType) {
const { item } = props;
const source =
typeof item.image === 'number' ? item.image : { uri: item.image };
return (
<TouchableRipple
style={{
width: props.width,
height: props.width + 40,
...styles.ripple,
}}
onPress={item.onPress}
>
<View>
<Image
style={{
width: props.width - 20,
height: props.width - 20,
...GENERAL_STYLES.centerHorizontal,
}}
source={source}
/>
<Text style={styles.text}>{item.title}</Text>
</View>
</TouchableRipple>
);
}
export default React.memo(ImageListItem, () => true);

View file

@ -1,92 +0,0 @@
// @flow
import * as React from 'react';
import {Card, Chip, List, Text} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
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 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,
},
});
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

@ -0,0 +1,117 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Card, Chip, List, Text } from 'react-native-paper';
import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js';
import AnimatedAccordion from '../../Animations/AnimatedAccordion';
import { isItemInCategoryFilter } from '../../../utils/Search';
import type { ClubCategoryType } from '../../../screens/Amicale/Clubs/ClubListScreen';
import GENERAL_STYLES from '../../../constants/Styles';
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,
...GENERAL_STYLES.centerHorizontal,
},
chipContainer: {
justifyContent: 'space-around',
flexDirection: 'row',
flexWrap: 'wrap',
paddingLeft: 0,
marginBottom: 5,
},
chip: {
marginRight: 5,
marginLeft: 5,
marginBottom: 5,
},
});
function ClubListHeader(props: PropsType) {
const getChipRender = (category: ClubCategoryType, key: string) => {
const onPress = (): void => props.onChipSelect(category.id);
return (
<Chip
selected={isItemInCategoryFilter(props.selectedCategories, [
category.id,
null,
])}
mode="outlined"
onPress={onPress}
style={styles.chip}
key={key}
>
{category.name}
</Chip>
);
};
const getCategoriesRender = () => {
const final: Array<React.ReactNode> = [];
props.categories.forEach((cat: ClubCategoryType) => {
final.push(getChipRender(cat, cat.id.toString()));
});
return final;
};
return (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t('screens.clubs.categories')}
left={(iconProps) => (
<List.Icon
color={iconProps.color}
style={iconProps.style}
icon="star"
/>
)}
opened={true}
renderItem={() => (
<View>
<Text style={styles.text}>
{i18n.t('screens.clubs.categoriesFilterMessage')}
</Text>
<View style={styles.chipContainer}>{getCategoriesRender()}</View>
</View>
)}
/>
</Card>
);
}
const areEqual = (prevProps: PropsType, nextProps: PropsType): boolean => {
return (
prevProps.selectedCategories.length === nextProps.selectedCategories.length
);
};
export default React.memo(ClubListHeader, areEqual);

View file

@ -1,94 +0,0 @@
// @flow
import * as React from 'react';
import {Avatar, Chip, List, withTheme} from 'react-native-paper';
import {View} from 'react-native';
import type {
ClubCategoryType,
ClubType,
} from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
type PropsType = {
onPress: () => void,
categoryTranslator: (id: number) => ClubCategoryType,
item: ClubType,
height: number,
theme: CustomThemeType,
};
class ClubListItem extends React.Component<PropsType> {
hasManagers: boolean;
constructor(props: PropsType) {
super(props);
this.hasManagers = props.item.responsibles.length > 0;
}
shouldComponentUpdate(): boolean {
return false;
}
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,125 @@
/*
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
*
* This file is part of Campus INSAT.
*
* Campus INSAT is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Campus INSAT is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from 'react';
import { Avatar, Chip, List, withTheme } from 'react-native-paper';
import { StyleSheet, View } from 'react-native';
import type {
ClubCategoryType,
ClubType,
} from '../../../screens/Amicale/Clubs/ClubListScreen';
import GENERAL_STYLES from '../../../constants/Styles';
type PropsType = {
onPress: () => void;
categoryTranslator: (id: number) => ClubCategoryType | null;
item: ClubType;
height: number;
theme: ReactNativePaper.Theme;
};
const styles = StyleSheet.create({
chip: {
marginRight: 5,
marginBottom: 5,
},
chipContainer: {
flexDirection: 'row',
},
avatar: {
backgroundColor: 'transparent',
marginLeft: 10,
marginRight: 10,
},
icon: {
...GENERAL_STYLES.centerVertical,
backgroundColor: 'transparent',
},
item: {
justifyContent: 'center',
},
});
class ClubListItem extends React.Component<PropsType> {
hasManagers: boolean;
constructor(props: PropsType) {
super(props);
this.hasManagers = props.item.responsibles.length > 0;
}
shouldComponentUpdate(): boolean {
return false;
}
getCategoriesRender(categories: Array<number | null>) {
const { props } = this;
const final: Array<React.ReactNode> = [];
categories.forEach((cat: number | null) => {
if (cat != null) {
const category = props.categoryTranslator(cat);
if (category) {
final.push(
<Chip style={styles.chip} key={`${props.item.id}:${category.id}`}>
{category.name}
</Chip>
);
}
}
});
return <View style={styles.chipContainer}>{final}</View>;
}
render() {
const { props } = this;
const categoriesRender = () =>
this.getCategoriesRender(props.item.category);
const { colors } = props.theme;
return (
<List.Item
title={props.item.name}
description={categoriesRender}
onPress={props.onPress}
left={() => (
<Avatar.Image
style={styles.avatar}
size={64}
source={{ uri: props.item.logo }}
/>
)}
right={() => (
<Avatar.Icon
style={styles.icon}
size={48}
icon={
this.hasManagers ? 'check-circle-outline' : 'alert-circle-outline'
}
color={this.hasManagers ? colors.success : colors.primary}
/>
)}
style={{
height: props.height,
...styles.item,
}}
/>
);
}
}
export default withTheme(ClubListItem);

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