Compare commits

...

35 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
82 changed files with 28485 additions and 4279 deletions

47
App.tsx
View file

@ -21,7 +21,6 @@ import React from 'react';
import { LogBox, Platform } from 'react-native'; import { LogBox, Platform } from 'react-native';
import { setSafeBounceHeight } from 'react-navigation-collapsible'; import { setSafeBounceHeight } from 'react-navigation-collapsible';
import SplashScreen from 'react-native-splash-screen'; import SplashScreen from 'react-native-splash-screen';
import ConnectionManager from './src/managers/ConnectionManager';
import type { ParsedUrlDataType } from './src/utils/URLHandler'; import type { ParsedUrlDataType } from './src/utils/URLHandler';
import URLHandler from './src/utils/URLHandler'; import URLHandler from './src/utils/URLHandler';
import initLocales from './src/utils/Locales'; import initLocales from './src/utils/Locales';
@ -48,15 +47,17 @@ import {
ProxiwashPreferencesProvider, ProxiwashPreferencesProvider,
} from './src/components/providers/PreferencesProvider'; } from './src/components/providers/PreferencesProvider';
import MainApp from './src/screens/MainApp'; 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';
// Native optimizations https://reactnavigation.org/docs/react-native-screens initLocales();
// Crashes app when navigating away from webview on android 9+ setupNotifications();
// enableScreens(true);
LogBox.ignoreLogs([ 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', 'Cannot update a component from inside the function body of a different component',
'`new NativeEventEmitter()` was called with a non-null argument',
]); ]);
type StateType = { type StateType = {
@ -67,14 +68,13 @@ type StateType = {
proxiwash: ProxiwashPreferencesType; proxiwash: ProxiwashPreferencesType;
mascot: MascotPreferencesType; mascot: MascotPreferencesType;
}; };
loginToken?: string;
}; };
export default class App extends React.Component<{}, StateType> { export default class App extends React.Component<{}, StateType> {
navigatorRef: { current: null | NavigationContainerRef }; navigatorRef: { current: null | NavigationContainerRef<any> };
defaultHomeRoute: string | undefined; defaultData?: ParsedUrlDataType;
defaultHomeData: { [key: string]: string } | undefined;
urlHandler: URLHandler; urlHandler: URLHandler;
@ -88,11 +88,10 @@ export default class App extends React.Component<{}, StateType> {
proxiwash: defaultProxiwashPreferences, proxiwash: defaultProxiwashPreferences,
mascot: defaultMascotPreferences, mascot: defaultMascotPreferences,
}, },
loginToken: undefined,
}; };
initLocales();
this.navigatorRef = React.createRef(); this.navigatorRef = React.createRef();
this.defaultHomeRoute = undefined; this.defaultData = undefined;
this.defaultHomeData = undefined;
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL); this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
this.urlHandler.listen(); this.urlHandler.listen();
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20); setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
@ -106,8 +105,7 @@ export default class App extends React.Component<{}, StateType> {
* @param parsedData The data parsed from the url * @param parsedData The data parsed from the url
*/ */
onInitialURLParsed = (parsedData: ParsedUrlDataType) => { onInitialURLParsed = (parsedData: ParsedUrlDataType) => {
this.defaultHomeRoute = parsedData.route; this.defaultData = parsedData;
this.defaultHomeData = parsedData.data;
}; };
/** /**
@ -120,9 +118,9 @@ export default class App extends React.Component<{}, StateType> {
// Navigate to nested navigator and pass data to the index screen // Navigate to nested navigator and pass data to the index screen
const nav = this.navigatorRef.current; const nav = this.navigatorRef.current;
if (nav != null) { if (nav != null) {
nav.navigate('home', { nav.navigate(TabRoutes.Home, {
screen: 'index', nextScreen: parsedData.route,
params: { nextScreen: parsedData.route, data: parsedData.data }, data: parsedData.data,
}); });
} }
}; };
@ -136,10 +134,11 @@ export default class App extends React.Component<{}, StateType> {
| PlanexPreferencesType | PlanexPreferencesType
| ProxiwashPreferencesType | ProxiwashPreferencesType
| MascotPreferencesType | MascotPreferencesType
| void | string
| undefined
> >
) => { ) => {
const [general, planex, proxiwash, mascot] = values; const [general, planex, proxiwash, mascot, token] = values;
this.setState({ this.setState({
isLoading: false, isLoading: false,
initialPreferences: { initialPreferences: {
@ -148,6 +147,7 @@ export default class App extends React.Component<{}, StateType> {
proxiwash: proxiwash as ProxiwashPreferencesType, proxiwash: proxiwash as ProxiwashPreferencesType,
mascot: mascot as MascotPreferencesType, mascot: mascot as MascotPreferencesType,
}, },
loginToken: token as string | undefined,
}); });
SplashScreen.hide(); SplashScreen.hide();
}; };
@ -175,7 +175,7 @@ export default class App extends React.Component<{}, StateType> {
Object.values(MascotPreferenceKeys), Object.values(MascotPreferenceKeys),
defaultMascotPreferences defaultMascotPreferences
), ),
ConnectionManager.getInstance().recoverLogin(), retrieveLoginToken(),
]) ])
.then(this.onLoadFinished) .then(this.onLoadFinished)
.catch(this.onLoadFinished); .catch(this.onLoadFinished);
@ -202,11 +202,12 @@ export default class App extends React.Component<{}, StateType> {
<MascotPreferencesProvider <MascotPreferencesProvider
initialPreferences={this.state.initialPreferences.mascot} initialPreferences={this.state.initialPreferences.mascot}
> >
<LoginProvider initialToken={this.state.loginToken}>
<MainApp <MainApp
ref={this.navigatorRef} ref={this.navigatorRef}
defaultHomeData={this.defaultHomeData} defaultData={this.defaultData}
defaultHomeRoute={this.defaultHomeRoute}
/> />
</LoginProvider>
</MascotPreferencesProvider> </MascotPreferencesProvider>
</ProxiwashPreferencesProvider> </ProxiwashPreferencesProvider>
</PlanexPreferencesProvider> </PlanexPreferencesProvider>

View file

@ -141,17 +141,12 @@ android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig { defaultConfig {
applicationId 'fr.amicaleinsat.application' applicationId 'fr.amicaleinsat.application'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 45 versionCode 49
versionName "4.1.0" versionName "5.0.0-3"
missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-camera', 'general'
} }
splits { splits {
@ -238,7 +233,7 @@ dependencies {
// Run this once to be able to run the application with BUCK // Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use // puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) { task copyDownloadableDepsToLibs(type: Copy) {
from configurations.compile from configurations.implementation
into 'libs' into 'libs'
} }

View file

@ -24,7 +24,7 @@
<!-- 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) --> <!-- 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" <meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
android:value="false"/> android:value="false"/>
Change the resource name to your App's accent color - or any other color you want <!-- 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" <meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
android:resource="@color/colorPrimary"/> android:resource="@color/colorPrimary"/>
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" /> <receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />

View file

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

View file

@ -24,4 +24,8 @@ android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true
# Version of flipper SDK to use with React Native # Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.75.1 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View file

@ -126,6 +126,7 @@
13B07F8E1A680F5B00A75B9A /* Resources */, 13B07F8E1A680F5B00A75B9A /* Resources */,
00DD1BFF1BD5951E006B06BC /* Bundle Expo Assets */, 00DD1BFF1BD5951E006B06BC /* Bundle Expo Assets */,
58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */, 58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */,
2C1F7D7FCACF5494D140CFB7 /* [CP] Embed Pods Frameworks */,
); );
buildRules = ( buildRules = (
); );
@ -199,6 +200,24 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "../node_modules/react-native/scripts/react-native-xcode.sh\n"; 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 */ = { 58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -388,6 +407,7 @@
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES; ENABLE_TESTABILITY = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
GCC_C_LANGUAGE_STANDARD = gnu99; GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO; GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
@ -403,7 +423,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application; PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application;
@ -444,6 +464,7 @@
COPY_PHASE_STRIP = YES; COPY_PHASE_STRIP = YES;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_STRICT_OBJC_MSGSEND = YES;
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
GCC_C_LANGUAGE_STANDARD = gnu99; GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES; GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@ -452,7 +473,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0; IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application; PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application;
PRODUCT_NAME = application; PRODUCT_NAME = application;

View file

@ -17,7 +17,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string> <string>5.0.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -30,7 +30,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string> <string>4</string>
<key>FacebookAdvertiserIDCollectionEnabled</key> <key>FacebookAdvertiserIDCollectionEnabled</key>
<false /> <false />
<key>FacebookAutoInitEnabled</key> <key>FacebookAutoInitEnabled</key>

View file

@ -1,7 +1,7 @@
require_relative '../node_modules/react-native/scripts/react_native_pods' require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules' require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
platform :ios, '10.0' platform :ios, '11.0'
target 'Campus' do target 'Campus' do
config = use_native_modules! config = use_native_modules!
@ -9,7 +9,7 @@ target 'Campus' do
use_react_native!( use_react_native!(
:path => config[:reactNativePath], :path => config[:reactNativePath],
# to enable hermes on iOS, change `false` to `true` and then install pods # to enable hermes on iOS, change `false` to `true` and then install pods
:hermes_enabled => false :hermes_enabled => true
) )
@ -28,5 +28,4 @@ target 'Campus' do
post_install do |installer| post_install do |installer|
react_native_post_install(installer) react_native_post_install(installer)
end end
end end

View file

@ -47,34 +47,38 @@
"paymentTab": "Payment", "paymentTab": "Payment",
"tariffs": "Tariffs", "tariffs": "Tariffs",
"paymentMethods": "Payment Methods", "paymentMethods": "Payment Methods",
"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).", "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.", "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 advised dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.", "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", "procedure": "Procedure",
"tips": "Tips", "tips": "Tips",
"numAvailable": "available", "numAvailable": "available",
"numAvailablePlural": "available", "numAvailablePlural": "available",
"errors": {
"title": "Proxiwash message",
"button": "More info"
},
"washinsa": { "washinsa": {
"title": "INSA laundromat", "title": "INSA laundromat",
"subtitle": "Your favorite 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).", "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€ the washer + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.", "tariff": "Washers 6kg: 3€ per run + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.",
"paymentMethods": "Cash up until 10€.\nCredit Card also accepted." "paymentMethods": "Cash up to 10€.\nCredit Cards also accepted."
}, },
"tripodeB": { "tripodeB": {
"title": "Tripode B laundromat", "title": "Tripode B laundromat",
"subtitle": "For those who live near the metro.", "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.", "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€ the washer + 0.90€ with detergent.\nWashers 10kg: 4.90€ the washer + 1.50€ with detergent.\nDryers 14kg: 0.40€ for 5min of dryer usage.", "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 Card accepted." "paymentMethods": "Credit Cards accepted."
}, },
"modal": { "modal": {
"enableNotifications": "Notify me", "enableNotifications": "Notify me",
"disableNotifications": "Stop notifications", "disableNotifications": "Stop notifications",
"cancel": "Cancel", "cancel": "Cancel",
"finished": "This machine is finished. If you started it, you can get back your laundry.", "finished": "This machine is finished. If you started it, you can pick up your laundry.",
"ready": "This machine is empty and ready to use.", "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}", "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.", "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.", "broken": "This machine is out of order and cannot be used. Thank you for your comprehension.",
@ -93,14 +97,18 @@
"unknown": "UNKNOWN" "unknown": "UNKNOWN"
}, },
"notifications": { "notifications": {
"channel": {
"title": "Laundry reminders",
"description": "Get reminders for watched washers/dryers"
},
"machineFinishedTitle": "Laundry Ready", "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", "machineRunningTitle": "Laundry running: {{time}} minutes left",
"machineRunningBody": "The machine n°{{number}} is still running" "machineRunningBody": "Machine n°{{number}} is still running"
}, },
"mascotDialog": { "mascotDialog": {
"title": "Small tips", "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.\n\nIf you live off campus we have other laundromat available, check the settings !!!!", "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", "ok": "Settings",
"cancel": "Later" "cancel": "Later"
} }
@ -137,12 +145,12 @@
}, },
"planex": { "planex": {
"title": "Planex", "title": "Planex",
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button bellow.", "noGroupSelected": "No group selected. Please select your group using the big beautiful red button below.",
"favorites": { "favorites": {
"title": "Favorites", "title": "Favorites",
"empty": { "empty": {
"title": "No favorites", "title": "No favorites",
"subtitle": "Clic on the star next to a group to add it to the favorites" "subtitle": "Click on the star next to a group to add it to the favorites"
} }
}, },
"mascotDialog": { "mascotDialog": {
@ -156,7 +164,7 @@
"amicaleAbout": { "amicaleAbout": {
"title": "A question ?", "title": "A question ?",
"subtitle": "Ask the Amicale", "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": { "roles": {
"interSchools": "Inter Schools", "interSchools": "Inter Schools",
"culture": "Culture", "culture": "Culture",
@ -181,8 +189,8 @@
"sortPrice": "Price", "sortPrice": "Price",
"sortPriceReverse": "Price (reverse)", "sortPriceReverse": "Price (reverse)",
"inStock": "in stock", "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.", "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": "Openning Hours", "openingHours": "Opening Hours",
"paymentMethods": "Payment Methods", "paymentMethods": "Payment Methods",
"paymentMethodsDescription": "Cash or Lydia", "paymentMethodsDescription": "Cash or Lydia",
"search": "Search", "search": "Search",
@ -212,7 +220,7 @@
"resetPassword": "Forgot Password", "resetPassword": "Forgot Password",
"mascotDialog": { "mascotDialog": {
"title": "An account?", "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" "button": "OK"
} }
}, },
@ -230,8 +238,8 @@
"membershipPayed": "Payed", "membershipPayed": "Payed",
"membershipNotPayed": "Not payed", "membershipNotPayed": "Not payed",
"welcomeTitle": "Welcome %{name}!", "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!", "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 bellow." "welcomeFeedback": "We plan on doing more! If you have any suggestions or found bugs, please tell us by clicking the button below."
}, },
"clubs": { "clubs": {
"title": "Clubs", "title": "Clubs",
@ -245,10 +253,10 @@
"amicaleContact": "Contact the Amicale", "amicaleContact": "Contact the Amicale",
"invalidClub": "Could not find the club. Please make sure the club you are trying to access is valid.", "invalidClub": "Could not find the club. Please make sure the club you are trying to access is valid.",
"about": { "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 ?", "title": "A question ?",
"subtitle": "Ask the Amicale", "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": { "vote": {
@ -257,14 +265,14 @@
"select": { "select": {
"title": "Elections open", "title": "Elections open",
"subtitle": "Vote now!", "subtitle": "Vote now!",
"sendButton": "Send Vote", "sendButton": "Cast Vote",
"dialogTitle": "Send Vote?", "dialogTitle": "Cast Vote?",
"dialogTitleLoading": "Sending vote...", "dialogTitleLoading": "Casting vote...",
"dialogMessage": "Are you sure you want to send your vote? You will not be able to change it." "dialogMessage": "Are you sure you want to cast your vote? You will not be able to change it."
}, },
"tease": { "tease": {
"title": "Elections incoming", "title": "Elections incoming",
"subtitle": "Be ready to vote!", "subtitle": "Get ready to vote!",
"message": "Vote start:" "message": "Vote start:"
}, },
"wait": { "wait": {
@ -284,7 +292,7 @@
}, },
"mascotDialog": { "mascotDialog": {
"title": "Why vote?", "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" "button": "Ok"
} }
}, },
@ -309,7 +317,7 @@
"bookingConfirmedMessage": "Do not forget to come by the Amicale to give your bail in exchange of the equipment.", "bookingConfirmedMessage": "Do not forget to come by the Amicale to give your bail in exchange of the equipment.",
"mascotDialog": { "mascotDialog": {
"title": "How does it work ?", "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" "button": "Ok"
} }
}, },
@ -329,7 +337,7 @@
}, },
"mascotDialog": { "mascotDialog": {
"title": "Scano...what?", "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" "button": "OK"
} }
}, },
@ -340,11 +348,11 @@
"nightModeSubOn": "Your eyes are at peace", "nightModeSubOn": "Your eyes are at peace",
"nightModeSubOff": "Your eyes are burning", "nightModeSubOff": "Your eyes are burning",
"nightModeAuto": "Follow system dark mode", "nightModeAuto": "Follow system dark mode",
"nightModeAutoSub": "Follows the mode chosen by your system", "nightModeAutoSub": "Follows the mode set by your system",
"startScreen": "Start Screen", "startScreen": "Start Screen",
"startScreenSub": "Select which screen to start the app on", "startScreenSub": "Select which screen to start the app on",
"dashboard": "Dashboard", "dashboard": "Dashboard",
"dashboardSub": "Edit what services to display on the dashboard", "dashboardSub": "Edit which services to display on the dashboard",
"proxiwashNotifReminder": "Machine running reminder", "proxiwashNotifReminder": "Machine running reminder",
"proxiwashNotifReminderSub": "How many minutes before", "proxiwashNotifReminderSub": "How many minutes before",
"proxiwashChangeWash": "Laundromat selection", "proxiwashChangeWash": "Laundromat selection",
@ -352,7 +360,7 @@
"information": "Information", "information": "Information",
"dashboardEdit": { "dashboardEdit": {
"title": "Edit dashboard", "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" "undo": "Undo changes"
} }
}, },
@ -375,7 +383,7 @@
"docjyj": "Student in 2MIC FAS (2020). He added some new features and fixed some bugs.", "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.", "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.", "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 would be less cute. She helped to write the text, for communication, and also to create the mascot 🦊.", "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.", "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.", "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." "theo": "Student in 4AE (2020). If the app works on iOS, this is all thanks to his help during his numerous tests."
@ -385,10 +393,10 @@
"title": "Contribute", "title": "Contribute",
"feedback": "Contact the dev", "feedback": "Contact the dev",
"feedbackSubtitle": "A student like you!", "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", "contribute": "Contribute to the project",
"contributeSubtitle": "With a possible \"implication citoyenne\"!", "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", "homeButtonTitle": "Contribute to the project",
"homeButtonSubtitle": "Your help is important" "homeButtonSubtitle": "Your help is important"
}, },
@ -426,11 +434,11 @@
"intro": { "intro": {
"slideMain": { "slideMain": {
"title": "Welcome to CAMPUS!", "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": { "slidePlanex": {
"title": "Prettier Planex", "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": { "slideEvents": {
"title": "Events", "title": "Events",
@ -438,7 +446,7 @@
}, },
"slideServices": { "slideServices": {
"title": "And even more!", "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": { "slideDone": {
"title": "Contribute to the project!", "title": "Contribute to the project!",

View file

@ -55,6 +55,10 @@
"tips": "Conseils", "tips": "Conseils",
"numAvailable": "disponible", "numAvailable": "disponible",
"numAvailablePlural": "disponibles", "numAvailablePlural": "disponibles",
"errors": {
"title": "Message laverie",
"button": "En savoir plus"
},
"washinsa": { "washinsa": {
"title": "Laverie INSA", "title": "Laverie INSA",
"subtitle": "Ta laverie préférée !!", "subtitle": "Ta laverie préférée !!",
@ -93,6 +97,10 @@
"unknown": "INCONNU" "unknown": "INCONNU"
}, },
"notifications": { "notifications": {
"channel": {
"title": "Rappels laverie",
"description": "Recevoir des rappels pour les machines demandées"
},
"machineFinishedTitle": "Linge prêt", "machineFinishedTitle": "Linge prêt",
"machineFinishedBody": "La machine n°{{number}} est terminée et ton linge est prêt à être récupéré", "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", "machineRunningTitle": "Machine en cours: {{time}} minutes restantes",

27482
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "campus", "name": "campus",
"version": "4.1.0", "version": "5.0.0-3",
"private": true, "private": true,
"scripts": { "scripts": {
"android": "react-native run-android", "android": "react-native run-android",
@ -14,75 +14,77 @@
"lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
"full-check": "npm run typescript && npm run lint && npm run test", "full-check": "npm run typescript && npm run lint && npm run test",
"pod": "cd ios && pod install && cd ..", "pod": "cd ios && pod install && cd ..",
"bundle": "npm run full-check && cd android && ./gradlew bundleRelease", "bundle": "cd android && ./gradlew bundleRelease",
"clean": "react-native-clean-project", "clean": "react-native-clean-project",
"postversion": "react-native-version" "postversion": "react-native-version"
}, },
"dependencies": { "dependencies": {
"@nartc/react-native-barcode-mask": "1.2.0", "@nartc/react-native-barcode-mask": "1.2.0",
"@react-native-async-storage/async-storage": "^1.15.4", "@react-native-async-storage/async-storage": "1.15.7",
"@react-native-community/masked-view": "0.1.11", "@react-native-community/masked-view": "0.1.11",
"@react-native-community/push-notification-ios": "1.8.0", "@react-native-community/push-notification-ios": "1.10.1",
"@react-native-community/slider": "3.0.3", "@react-native-community/slider": "4.1.6",
"@react-navigation/bottom-tabs": "5.11.10", "@react-navigation/bottom-tabs": "6.0.5",
"@react-navigation/native": "5.9.4", "@react-navigation/native": "6.0.2",
"@react-navigation/stack": "5.14.4", "@react-navigation/stack": "6.0.7",
"i18n-js": "3.8.0", "i18n-js": "3.8.0",
"moment": "^2.29.1", "moment": "2.29.1",
"react": "17.0.1", "react": "17.0.2",
"react-native": "0.64.1", "react-native": "0.65.1",
"react-native-animatable": "1.3.3", "react-native-animatable": "1.3.3",
"react-native-app-intro-slider": "4.0.4", "react-native-app-intro-slider": "4.0.4",
"react-native-appearance": "0.3.4", "react-native-appearance": "0.3.4",
"react-native-autolink": "4.0.0", "react-native-autolink": "4.0.0",
"react-native-calendars": "1.1260.0", "react-native-calendars": "1.1266.0",
"react-native-camera": "3.43.6", "react-native-camera": "4.1.1",
"react-native-collapsible": "1.6.0", "react-native-collapsible": "1.6.0",
"react-native-gesture-handler": "1.10.3", "react-native-gesture-handler": "1.10.3",
"react-native-image-zoom-viewer": "3.0.1", "react-native-image-zoom-viewer": "3.0.1",
"react-native-keychain": "4.0.5", "react-native-keychain": "4.0.5",
"react-native-linear-gradient": "2.5.6", "react-native-linear-gradient": "2.5.6",
"react-native-localize": "2.0.3", "react-native-localize": "2.1.4",
"react-native-modalize": "2.0.8", "react-native-modalize": "2.0.8",
"react-native-paper": "4.8.1", "react-native-paper": "4.9.2",
"react-native-permissions": "3.0.3", "react-native-permissions": "3.0.5",
"react-native-push-notification": "7.3.0", "react-native-push-notification": "8.1.0",
"react-native-reanimated": "1.13.2", "react-native-reanimated": "1.13.2",
"react-native-render-html": "5.1.0", "react-native-render-html": "6.1.0",
"react-native-safe-area-context": "3.2.0", "react-native-safe-area-context": "3.3.2",
"react-native-screens": "3.1.1", "react-native-screens": "3.7.0",
"react-native-splash-screen": "3.2.0", "react-native-splash-screen": "3.2.0",
"react-native-timeago": "^0.5.0", "react-native-timeago": "0.5.0",
"react-native-vector-icons": "8.1.0", "react-native-vector-icons": "8.1.0",
"react-native-webview": "11.4.3", "react-native-webview": "11.13.0",
"react-navigation-collapsible": "5.9.1", "react-navigation-collapsible": "6.0.0",
"react-navigation-header-buttons": "7.0.1" "react-navigation-header-buttons": "9.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.12.9", "@babel/core": "7.12.9",
"@babel/runtime": "7.12.5", "@babel/runtime": "7.12.5",
"@react-native-community/eslint-config": "2.0.0", "@react-native-community/eslint-config": "3.0.1",
"@types/i18n-js": "3.8.0", "@types/i18n-js": "3.8.2",
"@types/jest": "26.0.23", "@types/jest": "26.0.24",
"@types/react": "17.0.3", "@types/react": "17.0.3",
"@types/react-native": "0.64.4", "@types/react-native": "0.65.0",
"@types/react-native-calendars": "1.20.10", "@types/react-native-calendars": "1.1264.2",
"@types/react-native-push-notification": "^7.2.0", "@types/react-native-push-notification": "7.3.2",
"@types/react-native-vector-icons": "6.4.6", "@types/react-native-vector-icons": "6.4.8",
"@types/react-test-renderer": "17.0.1", "@types/react-test-renderer": "17.0.1",
"@typescript-eslint/eslint-plugin": "4.22.1", "@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.22.1", "@typescript-eslint/parser": "4.31.0",
"babel-jest": "26.6.3", "babel-jest": "26.6.3",
"eslint": "7.25.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.3.0",
"jest": "26.6.3", "jest": "26.6.3",
"jest-extended": "0.11.5", "jest-extended": "0.11.5",
"jest-fetch-mock": "3.0.3", "jest-fetch-mock": "3.0.3",
"metro-react-native-babel-preset": "0.64.0", "metro-react-native-babel-preset": "0.66.0",
"prettier": "2.2.1", "prettier": "2.4.0",
"react-native-clean-project": "^3.6.3", "react-native-clean-project": "3.6.7",
"react-native-codegen": "0.0.7",
"react-native-version": "4.0.0", "react-native-version": "4.0.0",
"react-test-renderer": "17.0.1", "react-test-renderer": "17.0.2",
"typescript": "4.2.4" "typescript": "4.4.2"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

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

@ -20,8 +20,7 @@
import * as React from 'react'; import * as React from 'react';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
import ConnectionManager from '../../managers/ConnectionManager'; import { useLogout } from '../../utils/logout';
import { useNavigation } from '@react-navigation/native';
type PropsType = { type PropsType = {
visible: boolean; visible: boolean;
@ -29,20 +28,14 @@ type PropsType = {
}; };
function LogoutDialog(props: PropsType) { function LogoutDialog(props: PropsType) {
const navigation = useNavigation(); const onLogout = useLogout();
// Use a loading dialog as it can take some time to update the context
const onClickAccept = async (): Promise<void> => { const onClickAccept = async (): Promise<void> => {
return new Promise((resolve: () => void) => { return new Promise((resolve: () => void) => {
ConnectionManager.getInstance() onLogout();
.disconnect()
.then(() => {
navigation.reset({
index: 0,
routes: [{ name: 'main' }],
});
props.onDismiss(); props.onDismiss();
resolve(); resolve();
}); });
});
}; };
return ( return (

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

@ -17,30 +17,23 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useState } from 'react';
import { Avatar, Button, Card, RadioButton } from 'react-native-paper'; import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
import { FlatList, StyleSheet, View } from 'react-native'; import { FlatList, StyleSheet, View } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import ConnectionManager from '../../../managers/ConnectionManager';
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../Dialogs/ErrorDialog'; import ErrorDialog from '../../Dialogs/ErrorDialog';
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen'; import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
import { ApiRejectType } from '../../../utils/WebData'; import { ApiRejectType } from '../../../utils/WebData';
import { REQUEST_STATUS } from '../../../utils/Requests'; import { REQUEST_STATUS } from '../../../utils/Requests';
import { useAuthenticatedRequest } from '../../../context/loginContext';
type PropsType = { type Props = {
teams: Array<VoteTeamType>; teams: Array<VoteTeamType>;
onVoteSuccess: () => void; onVoteSuccess: () => void;
onVoteError: () => void; onVoteError: () => void;
}; };
type StateType = {
selectedTeam: string;
voteDialogVisible: boolean;
errorDialogVisible: boolean;
currentError: ApiRejectType;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
margin: 10, margin: 10,
@ -50,68 +43,47 @@ const styles = StyleSheet.create({
}, },
}); });
export default class VoteSelect extends React.PureComponent< function VoteSelect(props: Props) {
PropsType, const [selectedTeam, setSelectedTeam] = useState('none');
StateType const [voteDialogVisible, setVoteDialogVisible] = useState(false);
> { const [currentError, setCurrentError] = useState<ApiRejectType>({
constructor(props: PropsType) { status: REQUEST_STATUS.SUCCESS,
super(props); });
this.state = { const request = useAuthenticatedRequest('elections/vote', {
selectedTeam: 'none', team: parseInt(selectedTeam, 10),
voteDialogVisible: false, });
errorDialogVisible: false,
currentError: { status: REQUEST_STATUS.SUCCESS },
};
}
onVoteSelectionChange = (teamName: string): void => const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
this.setState({ selectedTeam: teamName });
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString(); const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
voteRenderItem = ({ item }: { item: VoteTeamType }) => (
<RadioButton.Item label={item.name} value={item.id.toString()} /> <RadioButton.Item label={item.name} value={item.id.toString()} />
); );
showVoteDialog = (): void => this.setState({ voteDialogVisible: true }); const showVoteDialog = () => setVoteDialogVisible(true);
onVoteDialogDismiss = (): void => this.setState({ voteDialogVisible: false }); const onVoteDialogDismiss = () => setVoteDialogVisible(false);
onVoteDialogAccept = async (): Promise<void> => { const onVoteDialogAccept = async (): Promise<void> => {
return new Promise((resolve: () => void) => { return new Promise((resolve: () => void) => {
const { state } = this; request()
ConnectionManager.getInstance()
.authenticatedRequest('elections/vote', {
team: parseInt(state.selectedTeam, 10),
})
.then(() => { .then(() => {
this.onVoteDialogDismiss(); onVoteDialogDismiss();
const { props } = this;
props.onVoteSuccess(); props.onVoteSuccess();
resolve(); resolve();
}) })
.catch((error: ApiRejectType) => { .catch((error: ApiRejectType) => {
this.onVoteDialogDismiss(); onVoteDialogDismiss();
this.showErrorDialog(error); setCurrentError(error);
resolve(); resolve();
}); });
}); });
}; };
showErrorDialog = (error: ApiRejectType): void => const onErrorDialogDismiss = () => {
this.setState({ setCurrentError({ status: REQUEST_STATUS.SUCCESS });
errorDialogVisible: true,
currentError: error,
});
onErrorDialogDismiss = () => {
this.setState({ errorDialogVisible: false });
const { props } = this;
props.onVoteError(); props.onVoteError();
}; };
render() {
const { state, props } = this;
return ( return (
<View> <View>
<Card style={styles.card}> <Card style={styles.card}>
@ -124,44 +96,48 @@ export default class VoteSelect extends React.PureComponent<
/> />
<Card.Content> <Card.Content>
<RadioButton.Group <RadioButton.Group
onValueChange={this.onVoteSelectionChange} onValueChange={setSelectedTeam}
value={state.selectedTeam} value={selectedTeam}
> >
<FlatList <FlatList
data={props.teams} data={props.teams}
keyExtractor={this.voteKeyExtractor} keyExtractor={voteKeyExtractor}
extraData={state.selectedTeam} extraData={selectedTeam}
renderItem={this.voteRenderItem} renderItem={voteRenderItem}
/> />
</RadioButton.Group> </RadioButton.Group>
</Card.Content> </Card.Content>
<Card.Actions> <Card.Actions>
<Button <Button
icon="send" icon={'send'}
mode="contained" mode={'contained'}
onPress={this.showVoteDialog} onPress={showVoteDialog}
style={styles.button} style={styles.button}
disabled={state.selectedTeam === 'none'} disabled={selectedTeam === 'none'}
> >
{i18n.t('screens.vote.select.sendButton')} {i18n.t('screens.vote.select.sendButton')}
</Button> </Button>
</Card.Actions> </Card.Actions>
</Card> </Card>
<LoadingConfirmDialog <LoadingConfirmDialog
visible={state.voteDialogVisible} visible={voteDialogVisible}
onDismiss={this.onVoteDialogDismiss} onDismiss={onVoteDialogDismiss}
onAccept={this.onVoteDialogAccept} onAccept={onVoteDialogAccept}
title={i18n.t('screens.vote.select.dialogTitle')} title={i18n.t('screens.vote.select.dialogTitle')}
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')} titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
message={i18n.t('screens.vote.select.dialogMessage')} message={i18n.t('screens.vote.select.dialogMessage')}
/> />
<ErrorDialog <ErrorDialog
visible={state.errorDialogVisible} visible={
onDismiss={this.onErrorDialogDismiss} currentError.status !== REQUEST_STATUS.SUCCESS ||
status={state.currentError.status} currentError.code !== undefined
code={state.currentError.code} }
onDismiss={onErrorDialogDismiss}
status={currentError.status}
code={currentError.code}
/> />
</View> </View>
); );
} }
}
export default VoteSelect;

View file

@ -24,6 +24,7 @@ import * as Animatable from 'react-native-animatable';
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar'; import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
import { useNavigation } from '@react-navigation/core'; import { useNavigation } from '@react-navigation/core';
import { useCollapsible } from '../../context/CollapsibleContext'; import { useCollapsible } from '../../context/CollapsibleContext';
import { MainRoutes } from '../../navigation/MainNavigator';
type Props = { type Props = {
onPress: (action: string, data?: string) => void; onPress: (action: string, data?: string) => void;
@ -138,7 +139,7 @@ function PlanexBottomBar(props: Props) {
> >
<FAB <FAB
icon={'account-clock'} icon={'account-clock'}
onPress={() => navigation.navigate('group-select')} onPress={() => navigation.navigate(MainRoutes.GroupSelect)}
/> />
</Animatable.View> </Animatable.View>
</View> </View>

View file

@ -72,11 +72,8 @@ function CollapsibleComponent(props: Props) {
}, [collapsible, setCollapsible]) }, [collapsible, setCollapsible])
); );
const { const { containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener } =
containerPaddingTop, collapsible;
scrollIndicatorInsetTop,
onScrollWithListener,
} = collapsible;
const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0; const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;

View file

@ -22,6 +22,7 @@ import { List } from 'react-native-paper';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { MainRoutes } from '../../navigation/MainNavigator';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
item: { item: {
@ -53,7 +54,7 @@ function ActionsDashBoardItem() {
icon="chevron-right" icon="chevron-right"
/> />
)} )}
onPress={(): void => navigation.navigate('feedback')} onPress={(): void => navigation.navigate(MainRoutes.Feedback)}
style={styles.item} style={styles.item}
/> />
</View> </View>

View file

@ -30,6 +30,7 @@ import type { NewsSourceType } from '../../constants/NewsSourcesConstants';
import ImageGalleryButton from '../Media/ImageGalleryButton'; import ImageGalleryButton from '../Media/ImageGalleryButton';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import { MainRoutes } from '../../navigation/MainNavigator';
type PropsType = { type PropsType = {
item: FeedItemType; item: FeedItemType;
@ -67,7 +68,7 @@ const styles = StyleSheet.create({
function FeedItem(props: PropsType) { function FeedItem(props: PropsType) {
const navigation = useNavigation(); const navigation = useNavigation();
const onPress = () => { const onPress = () => {
navigation.navigate('feed-information', { navigation.navigate(MainRoutes.FeedInformation, {
data: item, data: item,
date: getFormattedDate(props.item.time), date: getFormattedDate(props.item.time),
}); });

View file

@ -20,7 +20,6 @@
import * as React from 'react'; import * as React from 'react';
import { Avatar, List, useTheme } from 'react-native-paper'; import { Avatar, List, useTheme } from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen'; import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import { import {
getFirstEquipmentAvailability, getFirstEquipmentAvailability,
@ -29,9 +28,10 @@ import {
} from '../../../utils/EquipmentBooking'; } from '../../../utils/EquipmentBooking';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import { useNavigation } from '@react-navigation/native';
import { MainRoutes } from '../../../navigation/MainNavigator';
type PropsType = { type PropsType = {
navigation: StackNavigationProp<any>;
userDeviceRentDates: [string, string] | null; userDeviceRentDates: [string, string] | null;
item: DeviceType; item: DeviceType;
height: number; height: number;
@ -48,7 +48,8 @@ const styles = StyleSheet.create({
function EquipmentListItem(props: PropsType) { function EquipmentListItem(props: PropsType) {
const theme = useTheme(); const theme = useTheme();
const { item, userDeviceRentDates, navigation, height } = props; const navigation = useNavigation();
const { item, userDeviceRentDates, height } = props;
const isRented = userDeviceRentDates != null; const isRented = userDeviceRentDates != null;
const isAvailable = isEquipmentAvailable(item); const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item); const firstAvailability = getFirstEquipmentAvailability(item);
@ -56,14 +57,14 @@ function EquipmentListItem(props: PropsType) {
let onPress; let onPress;
if (isRented) { if (isRented) {
onPress = () => { onPress = () => {
navigation.navigate('equipment-confirm', { navigation.navigate(MainRoutes.EquipmentConfirm, {
item, item,
dates: userDeviceRentDates, dates: userDeviceRentDates,
}); });
}; };
} else { } else {
onPress = () => { onPress = () => {
navigation.navigate('equipment-rent', { item }); navigation.navigate(MainRoutes.EquipmentRent, { item });
}; };
} }

View file

@ -1,64 +0,0 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import { Avatar, Button, Card, Text } from 'react-native-paper';
import TimeAgo from 'react-native-timeago';
import i18n from 'i18n-js';
import { useNavigation } from '@react-navigation/core';
import { MainRoutes } from '../../../navigation/MainNavigator';
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
let moment = require('moment'); //load moment module to set local language
require('moment/locale/fr'); // import moment local language file during the application build
moment.locale('fr');
type Props = {
date?: Date;
selectedWash: 'tripodeB' | 'washinsa';
};
const styles = StyleSheet.create({
card: {
marginHorizontal: 5,
},
actions: {
justifyContent: 'center',
},
});
function ProximoListHeader(props: Props) {
const navigation = useNavigation();
const { date, selectedWash } = props;
let title = i18n.t('screens.proxiwash.washinsa.title');
let icon = ProxiwashConstants.washinsa.icon;
if (selectedWash === 'tripodeB') {
title = i18n.t('screens.proxiwash.tripodeB.title');
icon = ProxiwashConstants.tripodeB.icon;
}
return (
<Card style={styles.card}>
<Card.Title
title={title}
subtitle={
date ? (
<Text>
{i18n.t('screens.proxiwash.updated')}
<TimeAgo time={date} interval={2000} />
</Text>
) : null
}
left={(iconProps) => <Avatar.Icon icon={icon} size={iconProps.size} />}
/>
<Card.Actions style={styles.actions}>
<Button
mode={'contained'}
onPress={() => navigation.navigate(MainRoutes.Settings)}
icon={'swap-horizontal'}
>
{i18n.t('screens.proxiwash.switch')}
</Button>
</Card.Actions>
</Card>
);
}
export default ProximoListHeader;

View file

@ -0,0 +1,129 @@
import React from 'react';
import { Linking, StyleSheet } from 'react-native';
import {
Avatar,
Button,
Card,
Paragraph,
Text,
useTheme,
} from 'react-native-paper';
import TimeAgo from 'react-native-timeago';
import i18n from 'i18n-js';
import { useNavigation } from '@react-navigation/core';
import { MainRoutes } from '../../../navigation/MainNavigator';
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
import { ProxiwashInfoType } from '../../../screens/Proxiwash/ProxiwashScreen';
import * as Animatable from 'react-native-animatable';
let moment = require('moment'); //load moment module to set local language
require('moment/locale/fr'); // import moment local language file during the application build
moment.locale('fr');
type Props = {
date?: Date;
selectedWash: 'tripodeB' | 'washinsa';
info?: ProxiwashInfoType;
};
const styles = StyleSheet.create({
card: {
marginHorizontal: 5,
},
messageCard: {
marginTop: 50,
marginBottom: 10,
},
actions: {
justifyContent: 'center',
},
});
function ProxiwashListHeader(props: Props) {
const navigation = useNavigation();
const theme = useTheme();
const { date, selectedWash } = props;
let title = i18n.t('screens.proxiwash.washinsa.title');
let icon = ProxiwashConstants.washinsa.icon;
if (selectedWash === 'tripodeB') {
title = i18n.t('screens.proxiwash.tripodeB.title');
icon = ProxiwashConstants.tripodeB.icon;
}
const message = props.info?.message;
return (
<>
<Card style={styles.card}>
<Card.Title
title={title}
subtitle={
date ? (
<Text>
{i18n.t('screens.proxiwash.updated')}
<TimeAgo time={date} interval={2000} />
</Text>
) : null
}
left={(iconProps) => (
<Avatar.Icon icon={icon} size={iconProps.size} />
)}
/>
<Card.Actions style={styles.actions}>
<Button
mode={'contained'}
onPress={() => navigation.navigate(MainRoutes.Settings)}
icon={'swap-horizontal'}
>
{i18n.t('screens.proxiwash.switch')}
</Button>
</Card.Actions>
</Card>
{message ? (
<Card
style={{
...styles.card,
...styles.messageCard,
}}
>
<Animatable.View
useNativeDriver={false}
animation={'flash'}
iterationCount={'infinite'}
duration={2000}
>
<Card.Title
title={i18n.t('screens.proxiwash.errors.title')}
titleStyle={{
color: theme.colors.primary,
}}
left={(iconProps) => (
<Avatar.Icon icon={'alert'} size={iconProps.size} />
)}
/>
</Animatable.View>
<Card.Content>
<Paragraph
style={{
color: theme.colors.warning,
}}
>
{message}
</Paragraph>
</Card.Content>
<Card.Actions style={styles.actions}>
<Button
mode={'contained'}
onPress={() =>
Linking.openURL(ProxiwashConstants[selectedWash].webPageUrl)
}
icon={'open-in-new'}
>
{i18n.t('screens.proxiwash.errors.button')}
</Button>
</Card.Actions>
</Card>
) : null}
</>
);
}
export default ProxiwashListHeader;

View file

@ -22,6 +22,7 @@ import { TouchableRipple } from 'react-native-paper';
import { Image } from 'react-native-animatable'; import { Image } from 'react-native-animatable';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { StyleSheet, ViewStyle } from 'react-native'; import { StyleSheet, ViewStyle } from 'react-native';
import { MainRoutes } from '../../navigation/MainNavigator';
type PropsType = { type PropsType = {
images: Array<{ url: string }>; images: Array<{ url: string }>;
@ -39,7 +40,7 @@ function ImageGalleryButton(props: PropsType) {
const navigation = useNavigation(); const navigation = useNavigation();
const onPress = () => { const onPress = () => {
navigation.navigate('gallery', { images: props.images }); navigation.navigate(MainRoutes.Gallery, { images: props.images });
}; };
return ( return (

View file

@ -18,9 +18,13 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { Text } from 'react-native-paper'; import { Text, useTheme } from 'react-native-paper';
import HTML from 'react-native-render-html'; import HTML, {
import { GestureResponderEvent, Linking } from 'react-native'; CustomRendererProps,
TBlock,
TText,
} from 'react-native-render-html';
import { Dimensions, GestureResponderEvent, Linking } from 'react-native';
type PropsType = { type PropsType = {
html: string; html: string;
@ -30,37 +34,54 @@ type PropsType = {
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
function CustomHTML(props: PropsType) { function CustomHTML(props: PropsType) {
const theme = useTheme();
const openWebLink = (_event: GestureResponderEvent, link: string) => { const openWebLink = (_event: GestureResponderEvent, link: string) => {
Linking.openURL(link); Linking.openURL(link);
}; };
const getBasicText = ( // Why is this so complex?? I just want to replace the default Text element with the one
_htmlAttribs: any, // from react-native-paper
children: any, // Might need to read the doc a bit more: https://meliorence.github.io/react-native-render-html/
_convertedCSSStyles: any, // For now this seems to work
passProps: any const getBasicText = (rendererProps: CustomRendererProps<TBlock>) => {
) => { let text: TText | undefined;
return <Text {...passProps}>{children}</Text>; if (rendererProps.tnode.children.length > 0) {
const phrasing = rendererProps.tnode.children[0];
if (phrasing.children.length > 0) {
text = phrasing.children[0] as TText;
}
}
if (text) {
return <Text>{text.data}</Text>;
} else {
return null;
}
}; };
const getListBullet = () => {
return <Text>- </Text>;
};
// Surround description with p to allow text styling if the description is not html
return ( return (
<HTML <HTML
html={`<p>${props.html}</p>`} // Surround description with p to allow text styling if the description is not html
source={{ html: `<p>${props.html}</p>` }}
// Use Paper Text instead of React
renderers={{ renderers={{
p: getBasicText, p: getBasicText,
li: getBasicText, li: getBasicText,
}} }}
listsPrefixesRenderers={{ // Sometimes we have images inside the text, just ignore them
ul: getListBullet, ignoredDomTags={['img']}
// Ignore text color
ignoredStyles={['color', 'backgroundColor']}
contentWidth={Dimensions.get('window').width - 50}
renderersProps={{
a: {
onPress: openWebLink,
},
ul: {
markerTextStyle: {
color: theme.colors.text,
},
},
}} }}
ignoredTags={['img']}
ignoredStyles={['color', 'background-color']}
onLinkPress={openWebLink}
/> />
); );
} }

View file

@ -127,6 +127,7 @@ function PlanexWebview(props: Props) {
onMessage={props.onMessage} onMessage={props.onMessage}
showAdvancedControls={false} showAdvancedControls={false}
showControls={props.currentGroup !== undefined} showControls={props.currentGroup !== undefined}
incognito={true}
/> />
{!props.currentGroup ? ( {!props.currentGroup ? (
<ErrorView <ErrorView

View file

@ -11,7 +11,7 @@ import i18n from 'i18n-js';
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests'; import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import { MainRoutes } from '../../navigation/MainNavigator'; import { MainRoutes } from '../../navigation/MainNavigator';
import ConnectionManager from '../../managers/ConnectionManager'; import { useLogout } from '../../utils/logout';
export type RequestScreenProps<T> = { export type RequestScreenProps<T> = {
request: () => Promise<T>; request: () => Promise<T>;
@ -44,17 +44,12 @@ type Props<T> = RequestScreenProps<T>;
const MIN_REFRESH_TIME = 3 * 1000; const MIN_REFRESH_TIME = 3 * 1000;
export default function RequestScreen<T>(props: Props<T>) { export default function RequestScreen<T>(props: Props<T>) {
const onLogout = useLogout();
const navigation = useNavigation<StackNavigationProp<any>>(); const navigation = useNavigation<StackNavigationProp<any>>();
const route = useRoute(); const route = useRoute();
const refreshInterval = useRef<number>(); const refreshInterval = useRef<number>();
const [ const [loading, lastRefreshDate, status, code, data, refreshData] =
loading, useRequestLogic<T>(
lastRefreshDate,
status,
code,
data,
refreshData,
] = useRequestLogic<T>(
props.request, props.request,
props.cache, props.cache,
props.onCacheUpdate, props.onCacheUpdate,
@ -76,7 +71,8 @@ export default function RequestScreen<T>(props: Props<T>) {
if (props.refresh !== lastRefresh.current) { if (props.refresh !== lastRefresh.current) {
lastRefresh.current = props.refresh === true; lastRefresh.current = props.refresh === true;
} }
}, [props, loading, refreshData]); // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props, loading]);
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
@ -94,7 +90,8 @@ export default function RequestScreen<T>(props: Props<T>) {
clearInterval(refreshInterval.current); clearInterval(refreshInterval.current);
} }
}; };
}, [props.cache, props.refreshOnFocus, props.autoRefreshTime, refreshData]) // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.cache, props.refreshOnFocus, props.autoRefreshTime])
); );
const isErrorCritical = (e: API_REQUEST_CODES | undefined) => { const isErrorCritical = (e: API_REQUEST_CODES | undefined) => {
@ -103,13 +100,10 @@ export default function RequestScreen<T>(props: Props<T>) {
useEffect(() => { useEffect(() => {
if (isErrorCritical(code)) { if (isErrorCritical(code)) {
ConnectionManager.getInstance() onLogout();
.disconnect()
.then(() => {
navigation.replace(MainRoutes.Login, { nextScreen: route.name }); navigation.replace(MainRoutes.Login, { nextScreen: route.name });
});
} }
}, [code, navigation, route]); }, [code, navigation, route, onLogout]);
if (data === undefined && loading && props.showLoading !== false) { if (data === undefined && loading && props.showLoading !== false) {
return <BasicLoadingScreen />; return <BasicLoadingScreen />;

View file

@ -141,11 +141,15 @@ function WebSectionList<ItemT, RawData>(props: Props<ItemT, RawData>) {
<ErrorView <ErrorView
status={status} status={status}
code={code} code={code}
button={{ button={
code !== API_REQUEST_CODES.BAD_TOKEN
? {
icon: 'refresh', icon: 'refresh',
text: i18n.t('general.retry'), text: i18n.t('general.retry'),
onPress: () => refreshData(), onPress: () => refreshData(),
}} }
: undefined
}
/> />
) )
} }

View file

@ -58,6 +58,7 @@ type Props = {
customPaddingFunction?: null | ((padding: number) => string); customPaddingFunction?: null | ((padding: number) => string);
showAdvancedControls?: boolean; showAdvancedControls?: boolean;
showControls?: boolean; showControls?: boolean;
incognito?: boolean;
}; };
const AnimatedWebView = Animated.createAnimatedComponent(WebView); const AnimatedWebView = Animated.createAnimatedComponent(WebView);
@ -272,6 +273,7 @@ function WebViewScreen(props: Props) {
onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))} onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
// Animations // Animations
onScroll={onScrollWithListener(onScroll)} onScroll={onScrollWithListener(onScroll)}
incognito={props.incognito}
/> />
); );
} }

View file

@ -27,7 +27,7 @@ import { useCollapsible } from '../../context/CollapsibleContext';
export const TAB_BAR_HEIGHT = 50; export const TAB_BAR_HEIGHT = 50;
function CustomTabBar( function CustomTabBar(
props: BottomTabBarProps<any> & { props: BottomTabBarProps & {
icons: { icons: {
[key: string]: { [key: string]: {
normal: string; normal: string;
@ -94,10 +94,7 @@ const styles = StyleSheet.create({
}, },
}); });
function areEqual( function areEqual(prevProps: BottomTabBarProps, nextProps: BottomTabBarProps) {
prevProps: BottomTabBarProps<any>,
nextProps: BottomTabBarProps<any>
) {
return prevProps.state.index === nextProps.state.index; return prevProps.state.index === nextProps.state.index;
} }

View file

@ -17,10 +17,8 @@ export default function CollapsibleProvider(props: Props) {
})); }));
}; };
const [ const [currentCollapsible, setCurrentCollapsible] =
currentCollapsible, useState<CollapsibleContextType>({
setCurrentCollapsible,
] = useState<CollapsibleContextType>({
collapsible: undefined, collapsible: undefined,
setCollapsible: setCollapsible, setCollapsible: setCollapsible,
}); });

View file

@ -0,0 +1,27 @@
import React, { useState } from 'react';
import { LoginContext, LoginContextType } from '../../context/loginContext';
type Props = {
children: React.ReactChild;
initialToken: string | undefined;
};
export default function LoginProvider(props: Props) {
const setLogin = (token: string | undefined) => {
setLoginState((prevState) => ({
...prevState,
token,
}));
};
const [loginState, setLoginState] = useState<LoginContextType>({
token: props.initialToken,
setLogin: setLogin,
});
return (
<LoginContext.Provider value={loginState}>
{props.children}
</LoginContext.Provider>
);
}

View file

@ -48,6 +48,7 @@ export default {
paymentMethods: 'screens.proxiwash.washinsa.paymentMethods', paymentMethods: 'screens.proxiwash.washinsa.paymentMethods',
icon: 'school-outline', icon: 'school-outline',
url: Urls.app.api + 'washinsa/washinsa_data.json', url: Urls.app.api + 'washinsa/washinsa_data.json',
webPageUrl: Urls.proxiwash.washinsa,
}, },
tripodeB: { tripodeB: {
id: 'tripodeB', id: 'tripodeB',
@ -58,5 +59,6 @@ export default {
paymentMethods: 'screens.proxiwash.tripodeB.paymentMethods', paymentMethods: 'screens.proxiwash.tripodeB.paymentMethods',
icon: 'domain', icon: 'domain',
url: Urls.app.api + 'washinsa/tripode_b_data.json', url: Urls.app.api + 'washinsa/tripode_b_data.json',
webPageUrl: Urls.proxiwash.tripodeB,
}, },
}; };

View file

@ -30,6 +30,8 @@ const PROXIMO_ENDPOINT = STUDENT_SERVER + '~proximo/v2/api/';
const PROXIMO_IMAGES_ENDPOINT = const PROXIMO_IMAGES_ENDPOINT =
STUDENT_SERVER + '~proximo/api_proximo/storage/app/public/'; STUDENT_SERVER + '~proximo/api_proximo/storage/app/public/';
const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/'; const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/';
const PROXIWASH_ENDPOINT =
'https://www.proxiwash.com/weblaverie/ma-laverie-2?s=';
export default { export default {
amicale: { amicale: {
@ -48,6 +50,10 @@ export default {
images: PROXIMO_IMAGES_ENDPOINT + 'img/', images: PROXIMO_IMAGES_ENDPOINT + 'img/',
icons: PROXIMO_IMAGES_ENDPOINT + 'icon/', icons: PROXIMO_IMAGES_ENDPOINT + 'icon/',
}, },
proxiwash: {
washinsa: PROXIWASH_ENDPOINT + 'cf4f39',
tripodeB: PROXIWASH_ENDPOINT + 'b310b7',
},
planex: { planex: {
planning: PLANEX_SERVER, planning: PLANEX_SERVER,
groups: PLANEX_SERVER + 'wsAdeGrp.php?projectId=1', groups: PLANEX_SERVER + 'wsAdeGrp.php?projectId=1',

View file

@ -0,0 +1,46 @@
import React, { useContext } from 'react';
import { apiRequest } from '../utils/WebData';
export type LoginContextType = {
token: string | undefined;
setLogin: (token: string | undefined) => void;
};
export const LoginContext = React.createContext<LoginContextType>({
token: undefined,
setLogin: () => undefined,
});
/**
* Hook used to retrieve the user token and puid.
* @returns Login context with token and puid to undefined if user is not logged in
*/
export function useLogin() {
return useContext(LoginContext);
}
/**
* Checks if the user is connected
* @returns True if the user is connected
*/
export function useLoginState() {
const { token } = useLogin();
return token !== undefined;
}
/**
* Gets the current user token.
* @returns The token, or empty string if the user is not logged in
*/
export function useLoginToken() {
const { token } = useLogin();
return token ? token : '';
}
export function useAuthenticatedRequest<T>(
path: string,
params?: { [key: string]: any }
) {
const token = useLoginToken();
return () => apiRequest<T>(path, 'POST', params, token);
}

View file

@ -25,6 +25,7 @@ import {
getSpecialServices, getSpecialServices,
getStudentServices, getStudentServices,
} from '../utils/Services'; } from '../utils/Services';
import { useLoginState } from './loginContext';
const colorScheme = Appearance.getColorScheme(); const colorScheme = Appearance.getColorScheme();
@ -135,6 +136,7 @@ export function useDarkTheme() {
export function useCurrentDashboard() { export function useCurrentDashboard() {
const { preferences, updatePreferences } = usePreferences(); const { preferences, updatePreferences } = usePreferences();
const navigation = useNavigation(); const navigation = useNavigation();
const isLoggedIn = useLoginState();
const dashboardIdList = getPreferenceObject( const dashboardIdList = getPreferenceObject(
GeneralPreferenceKeys.dashboardItems, GeneralPreferenceKeys.dashboardItems,
preferences preferences
@ -145,10 +147,10 @@ export function useCurrentDashboard() {
}; };
const allDatasets = [ const allDatasets = [
...getAmicaleServices(navigation.navigate), ...getAmicaleServices((route) => navigation.navigate(route), isLoggedIn),
...getStudentServices(navigation.navigate), ...getStudentServices((route) => navigation.navigate(route)),
...getINSAServices(navigation.navigate), ...getINSAServices((route) => navigation.navigate(route)),
...getSpecialServices(navigation.navigate), ...getSpecialServices((route) => navigation.navigate(route)),
]; ];
return { return {
currentDashboard: allDatasets.filter((item) => currentDashboard: allDatasets.filter((item) =>

View file

@ -1,205 +0,0 @@
/*
* 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 Keychain from 'react-native-keychain';
import { REQUEST_STATUS } from '../utils/Requests';
import type { ApiDataLoginType, ApiRejectType } from '../utils/WebData';
import { apiRequest } from '../utils/WebData';
/**
* champ: error
*
* 0 : SUCCESS -> pas d'erreurs
* 1 : BAD_CREDENTIALS -> email ou mdp invalide
* 2 : BAD_TOKEN -> session expirée
* 3 : NO_CONSENT
* 403 : FORBIDDEN -> accès a la ressource interdit
* 500 : SERVER_ERROR -> pb coté serveur
*/
const AUTH_PATH = 'password';
export default class ConnectionManager {
static instance: ConnectionManager | null = null;
private token: string | null;
constructor() {
this.token = null;
}
/**
* Gets this class instance or create one if none is found
*
* @returns {ConnectionManager}
*/
static getInstance(): ConnectionManager {
if (ConnectionManager.instance == null) {
ConnectionManager.instance = new ConnectionManager();
}
return ConnectionManager.instance;
}
/**
* Gets the current token
*
* @returns {string | null}
*/
getToken(): string | null {
return this.token;
}
/**
* Tries to recover login token from the secure keychain
*
* @returns Promise<void>
*/
async recoverLogin(): Promise<void> {
return new Promise((resolve: () => void) => {
const token = this.getToken();
if (token != null) {
resolve();
} else {
Keychain.getGenericPassword()
.then((data: Keychain.UserCredentials | false) => {
if (data && data.password != null) {
this.token = data.password;
}
resolve();
})
.catch(() => resolve());
}
});
}
/**
* Check if the user has a valid token
*
* @returns {boolean}
*/
isLoggedIn(): boolean {
return this.getToken() !== null;
}
/**
* Saves the login token in the secure keychain
*
* @param email
* @param token
* @returns Promise<void>
*/
async saveLogin(_email: string, token: string): Promise<void> {
return new Promise((resolve: () => void, reject: () => void) => {
Keychain.setGenericPassword('token', token)
.then(() => {
this.token = token;
resolve();
})
.catch((): void => reject());
});
}
/**
* Deletes the login token from the keychain
*
* @returns Promise<void>
*/
async disconnect(): Promise<void> {
return new Promise((resolve: () => void, reject: () => void) => {
Keychain.resetGenericPassword()
.then(() => {
this.token = null;
resolve();
})
.catch((): void => reject());
});
}
/**
* Sends the given login and password to the api.
* If the combination is valid, the login token is received and saved in the secure keychain.
* If not, the promise is rejected with the corresponding error code.
*
* @param email
* @param password
* @returns Promise<void>
*/
async connect(email: string, password: string): Promise<void> {
return new Promise(
(resolve: () => void, reject: (error: ApiRejectType) => void) => {
const data = {
email,
password,
};
apiRequest<ApiDataLoginType>(AUTH_PATH, 'POST', data)
.then((response: ApiDataLoginType) => {
if (response.token != null) {
this.saveLogin(email, response.token)
.then(() => resolve())
.catch(() =>
reject({
status: REQUEST_STATUS.TOKEN_SAVE,
})
);
} else {
reject({
status: REQUEST_STATUS.SERVER_ERROR,
});
}
})
.catch((err) => {
reject(err);
});
}
);
}
/**
* Sends an authenticated request with the login token to the API
*
* @param path
* @param params
* @returns Promise<ApiGenericDataType>
*/
async authenticatedRequest<T>(
path: string,
params?: { [key: string]: any }
): Promise<T> {
return new Promise(
(
resolve: (response: T) => void,
reject: (error: ApiRejectType) => void
) => {
if (this.getToken() !== null) {
const data = {
...params,
token: this.getToken(),
};
apiRequest<T>(path, 'POST', data)
.then((response: T) => resolve(response))
.catch(reject);
} else {
reject({
status: REQUEST_STATUS.TOKEN_RETRIEVE,
});
}
}
);
}
}

View file

@ -24,7 +24,7 @@ import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
import AboutScreen from '../screens/About/AboutScreen'; import AboutScreen from '../screens/About/AboutScreen';
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen'; import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
import DebugScreen from '../screens/About/DebugScreen'; import DebugScreen from '../screens/About/DebugScreen';
import TabNavigator from './TabNavigator'; import TabNavigator, { TabRoutes } from './TabNavigator';
import GameMainScreen from '../screens/Game/screens/GameMainScreen'; import GameMainScreen from '../screens/Game/screens/GameMainScreen';
import VoteScreen from '../screens/Amicale/VoteScreen'; import VoteScreen from '../screens/Amicale/VoteScreen';
import LoginScreen from '../screens/Amicale/LoginScreen'; import LoginScreen from '../screens/Amicale/LoginScreen';
@ -33,7 +33,10 @@ import ProximoMainScreen from '../screens/Services/Proximo/ProximoMainScreen';
import ProximoListScreen from '../screens/Services/Proximo/ProximoListScreen'; import ProximoListScreen from '../screens/Services/Proximo/ProximoListScreen';
import ProximoAboutScreen from '../screens/Services/Proximo/ProximoAboutScreen'; import ProximoAboutScreen from '../screens/Services/Proximo/ProximoAboutScreen';
import ProfileScreen from '../screens/Amicale/ProfileScreen'; import ProfileScreen from '../screens/Amicale/ProfileScreen';
import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen'; import ClubListScreen, {
ClubCategoryType,
ClubType,
} from '../screens/Amicale/Clubs/ClubListScreen';
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen'; import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen'; import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
import BugReportScreen from '../screens/Other/FeedbackScreen'; import BugReportScreen from '../screens/Other/FeedbackScreen';
@ -52,6 +55,18 @@ import {
GeneralPreferenceKeys, GeneralPreferenceKeys,
} from '../utils/asyncStorage'; } from '../utils/asyncStorage';
import IntroScreen from '../screens/Intro/IntroScreen'; import IntroScreen from '../screens/Intro/IntroScreen';
import { useLoginState } from '../context/loginContext';
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
import ScannerScreen from '../screens/Home/ScannerScreen';
import FeedItemScreen from '../screens/Home/FeedItemScreen';
import GroupSelectionScreen from '../screens/Planex/GroupSelectionScreen';
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
import { FeedItemType } from '../screens/Home/HomeScreen';
import { PlanningEventType } from '../utils/Planning';
import { ServiceCategoryType } from '../utils/Services';
import { ParsedUrlDataType } from '../utils/URLHandler';
export enum MainRoutes { export enum MainRoutes {
Main = 'main', Main = 'main',
@ -69,6 +84,7 @@ export enum MainRoutes {
Proximo = 'proximo', Proximo = 'proximo',
ProximoList = 'proximo-list', ProximoList = 'proximo-list',
ProximoAbout = 'proximo-about', ProximoAbout = 'proximo-about',
ProxiwashAbout = 'proxiwash-about',
Profile = 'profile', Profile = 'profile',
ClubList = 'club-list', ClubList = 'club-list',
ClubInformation = 'club-information', ClubInformation = 'club-information',
@ -79,28 +95,72 @@ export enum MainRoutes {
Vote = 'vote', Vote = 'vote',
Feedback = 'feedback', Feedback = 'feedback',
Website = 'website', Website = 'website',
PlanningInformation = 'planning-information',
Scanner = 'scanner',
FeedInformation = 'feed-information',
GroupSelect = 'group-select',
ServicesSection = 'services-section',
AmicaleContact = 'amicale-contact',
} }
type DefaultParams = { [key in MainRoutes]: object | undefined }; type DefaultParams = { [key in MainRoutes]: object | undefined } & {
[key in TabRoutes]: object | undefined;
};
export type FullParamsList = DefaultParams & { export type MainStackParamsList = DefaultParams & {
'login': { nextScreen: string }; [MainRoutes.Login]: { nextScreen: string };
'equipment-confirm': { [MainRoutes.EquipmentConfirm]: {
item?: DeviceType; item?: DeviceType;
dates: [string, string]; dates: [string, string];
}; };
'equipment-rent': { item?: DeviceType }; [MainRoutes.EquipmentRent]: { item?: DeviceType };
'gallery': { images: Array<{ url: string }> }; [MainRoutes.Gallery]: { images: Array<{ url: string }> };
[MainRoutes.ProximoList]: { [MainRoutes.ProximoList]: {
shouldFocusSearchBar: boolean; shouldFocusSearchBar: boolean;
category: number; category: number;
}; };
[MainRoutes.ClubInformation]: ClubInformationScreenParams;
[MainRoutes.Website]: {
host: string;
path?: string;
title: string;
};
[MainRoutes.FeedInformation]: {
data: FeedItemType;
date: string;
};
[MainRoutes.PlanningInformation]: PlanningInformationScreenParams;
[MainRoutes.ServicesSection]: {
data: ServiceCategoryType;
};
}; };
// Don't know why but TS is complaining without this export type ClubInformationScreenParams =
// See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined | {
export type MainStackParamsList = FullParamsList & type: 'full';
Record<string, object | undefined>; data: ClubType;
categories: Array<ClubCategoryType>;
}
| {
type: 'id';
clubId: number;
};
export type PlanningInformationScreenParams =
| {
type: 'full';
data: PlanningEventType;
}
| {
type: 'id';
eventId: number;
};
declare global {
namespace ReactNavigation {
interface RootParamList extends MainStackParamsList {}
}
}
const MainStack = createStackNavigator<MainStackParamsList>(); const MainStack = createStackNavigator<MainStackParamsList>();
@ -118,6 +178,62 @@ function getIntroScreens() {
); );
} }
function getAmicaleScreens() {
return (
<>
<MainStack.Screen
name={MainRoutes.Profile}
component={ProfileScreen}
options={{
title: i18n.t('screens.profile.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubList}
component={ClubListScreen}
options={{
title: i18n.t('screens.clubs.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubInformation}
component={ClubDisplayScreen}
options={{
title: i18n.t('screens.clubs.details'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubAbout}
component={ClubAboutScreen}
options={{
title: i18n.t('screens.clubs.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentList}
component={EquipmentScreen}
options={{
title: i18n.t('screens.equipment.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentRent}
component={EquipmentLendScreen}
options={{
title: i18n.t('screens.equipment.book'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentConfirm}
component={EquipmentConfirmScreen}
options={{
title: i18n.t('screens.equipment.confirm'),
}}
/>
</>
);
}
function getRegularScreens(createTabNavigator: () => React.ReactElement) { function getRegularScreens(createTabNavigator: () => React.ReactElement) {
return ( return (
<> <>
@ -233,55 +349,6 @@ function getRegularScreens(createTabNavigator: () => React.ReactElement) {
title: i18n.t('screens.proximo.title'), title: i18n.t('screens.proximo.title'),
}} }}
/> />
<MainStack.Screen
name={MainRoutes.Profile}
component={ProfileScreen}
options={{
title: i18n.t('screens.profile.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubList}
component={ClubListScreen}
options={{
title: i18n.t('screens.clubs.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubInformation}
component={ClubDisplayScreen}
options={{
title: i18n.t('screens.clubs.details'),
}}
/>
<MainStack.Screen
name={MainRoutes.ClubAbout}
component={ClubAboutScreen}
options={{
title: i18n.t('screens.clubs.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentList}
component={EquipmentScreen}
options={{
title: i18n.t('screens.equipment.title'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentRent}
component={EquipmentLendScreen}
options={{
title: i18n.t('screens.equipment.book'),
}}
/>
<MainStack.Screen
name={MainRoutes.EquipmentConfirm}
component={EquipmentConfirmScreen}
options={{
title: i18n.t('screens.equipment.confirm'),
}}
/>
<MainStack.Screen <MainStack.Screen
name={MainRoutes.Vote} name={MainRoutes.Vote}
component={VoteScreen} component={VoteScreen}
@ -296,32 +363,75 @@ function getRegularScreens(createTabNavigator: () => React.ReactElement) {
title: i18n.t('screens.feedback.title'), title: i18n.t('screens.feedback.title'),
}} }}
/> />
<MainStack.Screen
name={MainRoutes.ProxiwashAbout}
component={ProxiwashAboutScreen}
options={{ title: i18n.t('screens.proxiwash.title') }}
/>
<MainStack.Screen
name={MainRoutes.PlanningInformation}
component={PlanningDisplayScreen}
options={{ title: i18n.t('screens.planning.eventDetails') }}
/>
<MainStack.Screen
name={MainRoutes.Scanner}
component={ScannerScreen}
options={{ title: i18n.t('screens.scanner.title') }}
/>
<MainStack.Screen
name={MainRoutes.FeedInformation}
component={FeedItemScreen}
options={{
title: i18n.t('screens.home.feed'),
}}
/>
<MainStack.Screen
name={MainRoutes.GroupSelect}
component={GroupSelectionScreen}
options={{
title: '',
}}
/>
<MainStack.Screen
name={MainRoutes.ServicesSection}
component={ServicesSectionScreen}
options={{ title: 'SECTION' }}
/>
<MainStack.Screen
name={MainRoutes.AmicaleContact}
component={AmicaleContactScreen}
options={{ title: i18n.t('screens.amicaleAbout.title') }}
/>
</> </>
); );
} }
function MainStackComponent(props: { function MainStackComponent(props: {
showIntro: boolean; showIntro: boolean;
isloggedIn: boolean;
createTabNavigator: () => React.ReactElement; createTabNavigator: () => React.ReactElement;
}) { }) {
const { showIntro, createTabNavigator } = props; const { showIntro, isloggedIn, createTabNavigator } = props;
return ( return (
<MainStack.Navigator <MainStack.Navigator
initialRouteName={showIntro ? MainRoutes.Intro : MainRoutes.Main} initialRouteName={showIntro ? MainRoutes.Intro : MainRoutes.Main}
headerMode={'screen'} screenOptions={{
headerMode: 'float',
}}
> >
{showIntro ? getIntroScreens() : getRegularScreens(createTabNavigator)} {showIntro ? getIntroScreens() : getRegularScreens(createTabNavigator)}
{isloggedIn ? getAmicaleScreens() : null}
</MainStack.Navigator> </MainStack.Navigator>
); );
} }
type PropsType = { type PropsType = {
defaultHomeRoute?: string; defaultData?: ParsedUrlDataType;
defaultHomeData?: { [key: string]: string };
}; };
function MainNavigator(props: PropsType) { function MainNavigator(props: PropsType) {
const { preferences } = usePreferences(); const { preferences } = usePreferences();
const isloggedIn = useLoginState();
const showIntro = getPreferenceBool( const showIntro = getPreferenceBool(
GeneralPreferenceKeys.showIntro, GeneralPreferenceKeys.showIntro,
preferences preferences
@ -330,6 +440,7 @@ function MainNavigator(props: PropsType) {
return ( return (
<MainStackComponent <MainStackComponent
showIntro={showIntro !== false} showIntro={showIntro !== false}
isloggedIn={isloggedIn}
createTabNavigator={createTabNavigator} createTabNavigator={createTabNavigator}
/> />
); );
@ -337,7 +448,5 @@ function MainNavigator(props: PropsType) {
export default React.memo( export default React.memo(
MainNavigator, MainNavigator,
(pp: PropsType, np: PropsType) => (pp: PropsType, np: PropsType) => pp.defaultData === np.defaultData
pp.defaultHomeRoute === np.defaultHomeRoute &&
pp.defaultHomeData === np.defaultHomeData
); );

View file

@ -18,7 +18,6 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Title, useTheme } from 'react-native-paper'; import { Title, useTheme } from 'react-native-paper';
@ -27,24 +26,17 @@ import i18n from 'i18n-js';
import { View } from 'react-native-animatable'; import { View } from 'react-native-animatable';
import HomeScreen from '../screens/Home/HomeScreen'; import HomeScreen from '../screens/Home/HomeScreen';
import PlanningScreen from '../screens/Planning/PlanningScreen'; import PlanningScreen from '../screens/Planning/PlanningScreen';
import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen'; import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
import PlanexScreen from '../screens/Planex/PlanexScreen'; import PlanexScreen from '../screens/Planex/PlanexScreen';
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
import ScannerScreen from '../screens/Home/ScannerScreen';
import FeedItemScreen from '../screens/Home/FeedItemScreen';
import GroupSelectionScreen from '../screens/Planex/GroupSelectionScreen';
import CustomTabBar from '../components/Tabbar/CustomTabBar'; import CustomTabBar from '../components/Tabbar/CustomTabBar';
import WebsitesHomeScreen from '../screens/Services/ServicesScreen'; import WebsitesHomeScreen from '../screens/Services/ServicesScreen';
import ServicesSectionScreen from '../screens/Services/ServicesSectionScreen';
import AmicaleContactScreen from '../screens/Amicale/AmicaleContactScreen';
import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot'; import Mascot, { MASCOT_STYLE } from '../components/Mascot/Mascot';
import { usePreferences } from '../context/preferencesContext'; import { usePreferences } from '../context/preferencesContext';
import { import {
getPreferenceString, getPreferenceString,
GeneralPreferenceKeys, GeneralPreferenceKeys,
} from '../utils/asyncStorage'; } from '../utils/asyncStorage';
import { ParsedUrlDataType } from '../utils/URLHandler';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
header: { header: {
@ -60,183 +52,24 @@ const styles = StyleSheet.create({
}, },
}); });
export enum TabRoutes {
Services = 'services',
Proxiwash = 'proxiwash',
Home = 'home',
Planning = 'events',
Planex = 'planex',
}
type DefaultParams = { [key in TabRoutes]: object | undefined }; type DefaultParams = { [key in TabRoutes]: object | undefined };
export type FullParamsList = DefaultParams & { export type TabStackParamsList = DefaultParams & {
[TabRoutes.Home]: { [TabRoutes.Home]: ParsedUrlDataType;
nextScreen: string;
data: Record<string, object | undefined>;
}; };
};
// Don't know why but TS is complaining without this
// See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined
export type TabStackParamsList = FullParamsList &
Record<string, object | undefined>;
const ServicesStack = createStackNavigator();
function ServicesStackComponent() {
return (
<ServicesStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<ServicesStack.Screen
name={'index'}
component={WebsitesHomeScreen}
options={{ title: i18n.t('screens.services.title') }}
/>
<ServicesStack.Screen
name={'services-section'}
component={ServicesSectionScreen}
options={{ title: 'SECTION' }}
/>
<ServicesStack.Screen
name={'amicale-contact'}
component={AmicaleContactScreen}
options={{ title: i18n.t('screens.amicaleAbout.title') }}
/>
</ServicesStack.Navigator>
);
}
const ProxiwashStack = createStackNavigator();
function ProxiwashStackComponent() {
return (
<ProxiwashStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<ProxiwashStack.Screen
name={'index-contact'}
component={ProxiwashScreen}
options={{ title: i18n.t('screens.proxiwash.title') }}
/>
<ProxiwashStack.Screen
name={'proxiwash-about'}
component={ProxiwashAboutScreen}
options={{ title: i18n.t('screens.proxiwash.title') }}
/>
</ProxiwashStack.Navigator>
);
}
const PlanningStack = createStackNavigator();
function PlanningStackComponent() {
return (
<PlanningStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<PlanningStack.Screen
name={'index'}
component={PlanningScreen}
options={{ title: i18n.t('screens.planning.title') }}
/>
<PlanningStack.Screen
name={'planning-information'}
component={PlanningDisplayScreen}
options={{ title: i18n.t('screens.planning.eventDetails') }}
/>
</PlanningStack.Navigator>
);
}
const HomeStack = createStackNavigator();
function HomeStackComponent(
initialRoute?: string,
defaultData?: { [key: string]: string }
) {
let params;
if (initialRoute) {
params = { data: defaultData, nextScreen: initialRoute, shouldOpen: true };
}
const { colors } = useTheme();
return (
<HomeStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<HomeStack.Screen
name={'index'}
component={HomeScreen}
options={{
title: i18n.t('screens.home.title'),
headerStyle: {
backgroundColor: colors.surface,
},
headerTitle: (headerProps) => (
<View style={styles.header}>
<Mascot
style={styles.mascot}
emotion={MASCOT_STYLE.RANDOM}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 1000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
<Title style={styles.title}>{headerProps.children}</Title>
</View>
),
}}
initialParams={params}
/>
<HomeStack.Screen
name={'scanner'}
component={ScannerScreen}
options={{ title: i18n.t('screens.scanner.title') }}
/>
<HomeStack.Screen
name={'club-information'}
component={ClubDisplayScreen}
options={{
title: i18n.t('screens.clubs.details'),
}}
/>
<HomeStack.Screen
name={'feed-information'}
component={FeedItemScreen}
options={{
title: i18n.t('screens.home.feed'),
}}
/>
<HomeStack.Screen
name={'planning-information'}
component={PlanningDisplayScreen}
options={{
title: i18n.t('screens.planning.eventDetails'),
}}
/>
</HomeStack.Navigator>
);
}
const PlanexStack = createStackNavigator();
function PlanexStackComponent() {
return (
<PlanexStack.Navigator initialRouteName={'index'} headerMode={'screen'}>
<PlanexStack.Screen
name={'index'}
component={PlanexScreen}
options={{
title: i18n.t('screens.planex.title'),
}}
/>
<PlanexStack.Screen
name={'group-select'}
component={GroupSelectionScreen}
options={{
title: '',
}}
/>
</PlanexStack.Navigator>
);
}
const Tab = createBottomTabNavigator<TabStackParamsList>(); const Tab = createBottomTabNavigator<TabStackParamsList>();
type PropsType = { type PropsType = {
defaultHomeRoute?: string; defaultData?: ParsedUrlDataType;
defaultHomeData?: { [key: string]: string };
}; };
const ICONS: { const ICONS: {
@ -278,9 +111,7 @@ function TabNavigator(props: PropsType) {
} else { } else {
defaultRoute = defaultRoute.toLowerCase(); defaultRoute = defaultRoute.toLowerCase();
} }
const { colors } = useTheme();
const createHomeStackComponent = () =>
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
const LABELS: { const LABELS: {
[key: string]: string; [key: string]: string;
@ -293,35 +124,62 @@ function TabNavigator(props: PropsType) {
}; };
return ( return (
<Tab.Navigator <Tab.Navigator
initialRouteName={defaultRoute} initialRouteName={defaultRoute as TabRoutes}
tabBar={(tabProps) => ( tabBar={(tabProps) => (
<CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} /> <CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
)} )}
> >
<Tab.Screen <Tab.Screen
name={'services'} name={TabRoutes.Services}
component={ServicesStackComponent} component={WebsitesHomeScreen}
options={{ title: i18n.t('screens.services.title') }} options={{ title: i18n.t('screens.services.title') }}
/> />
<Tab.Screen <Tab.Screen
name={'proxiwash'} name={TabRoutes.Proxiwash}
component={ProxiwashStackComponent} component={ProxiwashScreen}
options={{ title: i18n.t('screens.proxiwash.title') }} options={{ title: i18n.t('screens.proxiwash.title') }}
/> />
<Tab.Screen <Tab.Screen
name={'home'} name={TabRoutes.Home}
component={createHomeStackComponent} component={HomeScreen}
options={{ title: i18n.t('screens.home.title') }} options={{
title: i18n.t('screens.home.title'),
headerStyle: {
backgroundColor: colors.surface,
},
headerTitle: (headerProps) => (
<View style={styles.header}>
<Mascot
style={styles.mascot}
emotion={MASCOT_STYLE.RANDOM}
animated
entryAnimation={{
animation: 'bounceIn',
duration: 1000,
}}
loopAnimation={{
animation: 'pulse',
duration: 2000,
iterationCount: 'infinite',
}}
/>
<Title style={styles.title}>{headerProps.children}</Title>
</View>
),
}}
initialParams={props.defaultData}
/> />
<Tab.Screen <Tab.Screen
name={'events'} name={TabRoutes.Planning}
component={PlanningStackComponent} component={PlanningScreen}
options={{ title: i18n.t('screens.planning.title') }} options={{ title: i18n.t('screens.planning.title') }}
/> />
<Tab.Screen <Tab.Screen
name={'planex'} name={TabRoutes.Planex}
component={PlanexStackComponent} component={PlanexScreen}
options={{ title: i18n.t('screens.planex.title') }} options={{
title: i18n.t('screens.planex.title'),
}}
/> />
</Tab.Navigator> </Tab.Navigator>
); );
@ -329,15 +187,5 @@ function TabNavigator(props: PropsType) {
export default React.memo( export default React.memo(
TabNavigator, TabNavigator,
(pp: PropsType, np: PropsType) => (pp: PropsType, np: PropsType) => pp.defaultData === np.defaultData
pp.defaultHomeRoute === np.defaultHomeRoute &&
pp.defaultHomeData === np.defaultHomeData
); );
export enum TabRoutes {
Services = 'services',
Proxiwash = 'proxiwash',
Home = 'home',
Planning = 'events',
Planex = 'planex',
}

View file

@ -35,6 +35,7 @@ import OptionsDialog from '../../components/Dialogs/OptionsDialog';
import type { OptionsDialogButtonType } from '../../components/Dialogs/OptionsDialog'; import type { OptionsDialogButtonType } from '../../components/Dialogs/OptionsDialog';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import { MainRoutes } from '../../navigation/MainNavigator';
const APP_LOGO = require('../../../assets/android.icon.round.png'); const APP_LOGO = require('../../../assets/android.icon.round.png');
@ -179,7 +180,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
{ {
onPressCallback: () => { onPressCallback: () => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('feedback'); navigation.navigate(MainRoutes.Feedback);
}, },
icon: 'bug', icon: 'bug',
text: i18n.t('screens.feedback.homeButtonTitle'), text: i18n.t('screens.feedback.homeButtonTitle'),
@ -236,7 +237,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
{ {
onPressCallback: () => { onPressCallback: () => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('dependencies'); navigation.navigate(MainRoutes.Dependencies);
}, },
icon: 'developer-board', icon: 'developer-board',
text: i18n.t('screens.about.libs'), text: i18n.t('screens.about.libs'),
@ -275,7 +276,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
{ {
onPressCallback: () => { onPressCallback: () => {
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('feedback'); navigation.navigate(MainRoutes.Feedback);
}, },
icon: 'hand-pointing-right', icon: 'hand-pointing-right',
text: i18n.t('screens.about.user.you'), text: i18n.t('screens.about.user.you'),

View file

@ -64,10 +64,8 @@ function DebugScreen() {
const modalRef = useRef<Modalize>(null); const modalRef = useRef<Modalize>(null);
const [modalInputValue, setModalInputValue] = useState<string>(''); const [modalInputValue, setModalInputValue] = useState<string>('');
const [ const [modalCurrentDisplayItem, setModalCurrentDisplayItem] =
modalCurrentDisplayItem, useState<PreferenceItemType | null>(null);
setModalCurrentDisplayItem,
] = useState<PreferenceItemType | null>(null);
const currentPreferences: Array<PreferenceItemType> = []; const currentPreferences: Array<PreferenceItemType> = [];
Object.values(GeneralPreferenceKeys).forEach((key) => { Object.values(GeneralPreferenceKeys).forEach((key) => {

View file

@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useState } from 'react';
import { Linking, StyleSheet, View } from 'react-native'; import { Linking, StyleSheet, View } from 'react-native';
import { import {
Avatar, Avatar,
@ -25,29 +25,26 @@ import {
Card, Card,
Chip, Chip,
Paragraph, Paragraph,
withTheme, useTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import CustomHTML from '../../../components/Overrides/CustomHTML'; import CustomHTML from '../../../components/Overrides/CustomHTML';
import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar'; import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
import type { ClubCategoryType, ClubType } from './ClubListScreen'; import type { ClubCategoryType, ClubType } from './ClubListScreen';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import ImageGalleryButton from '../../../components/Media/ImageGalleryButton'; import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
import RequestScreen from '../../../components/Screens/RequestScreen'; import RequestScreen from '../../../components/Screens/RequestScreen';
import ConnectionManager from '../../../managers/ConnectionManager'; import { useFocusEffect } from '@react-navigation/core';
import { useCallback } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useAuthenticatedRequest } from '../../../context/loginContext';
import { StackScreenProps } from '@react-navigation/stack';
import {
MainRoutes,
MainStackParamsList,
} from '../../../navigation/MainNavigator';
type PropsType = { type Props = StackScreenProps<MainStackParamsList, MainRoutes.ClubInformation>;
navigation: StackNavigationProp<any>;
route: {
params?: {
data?: ClubType;
categories?: Array<ClubCategoryType>;
clubId?: number;
};
};
theme: ReactNativePaper.Theme;
};
type ResponseType = ClubType; type ResponseType = ClubType;
@ -89,33 +86,28 @@ const styles = StyleSheet.create({
* If called with data and categories navigation parameters, will use those to display the data. * If called with data and categories navigation parameters, will use those to display the data.
* If called with clubId parameter, will fetch the information on the server * If called with clubId parameter, will fetch the information on the server
*/ */
class ClubDisplayScreen extends React.Component<PropsType> { function ClubDisplayScreen(props: Props) {
displayData: ClubType | undefined; const navigation = useNavigation();
const theme = useTheme();
categories: Array<ClubCategoryType> | null; const [displayData, setDisplayData] = useState<ClubType | undefined>();
const [categories, setCategories] = useState<
Array<ClubCategoryType> | undefined
>();
const [clubId, setClubId] = useState<number | undefined>();
clubId: number; useFocusEffect(
useCallback(() => {
shouldFetchData: boolean; if (props.route.params.type === 'full') {
setDisplayData(props.route.params.data);
constructor(props: PropsType) { setCategories(props.route.params.categories);
super(props); setClubId(props.route.params.data.id);
this.displayData = undefined; } else {
this.categories = null; const id = props.route.params.clubId;
this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0; setClubId(id ? id : 0);
this.shouldFetchData = true;
if (
props.route.params &&
props.route.params.data &&
props.route.params.categories
) {
this.displayData = props.route.params.data;
this.categories = props.route.params.categories;
this.clubId = props.route.params.data.id;
this.shouldFetchData = false;
}
} }
}, [props.route.params])
);
/** /**
* Gets the name of the category with the given ID * Gets the name of the category with the given ID
@ -123,17 +115,17 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param id The category's ID * @param id The category's ID
* @returns {string|*} * @returns {string|*}
*/ */
getCategoryName(id: number): string { const getCategoryName = (id: number): string => {
let categoryName = ''; let categoryName = '';
if (this.categories !== null) { if (categories) {
this.categories.forEach((item: ClubCategoryType) => { categories.forEach((item: ClubCategoryType) => {
if (id === item.id) { if (id === item.id) {
categoryName = item.name; categoryName = item.name;
} }
}); });
} }
return categoryName; return categoryName;
} };
/** /**
* Gets the view for rendering categories * Gets the view for rendering categories
@ -141,23 +133,23 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param categories The categories to display (max 2) * @param categories The categories to display (max 2)
* @returns {null|*} * @returns {null|*}
*/ */
getCategoriesRender(categories: Array<number | null>) { const getCategoriesRender = (c: Array<number | null>) => {
if (this.categories == null) { if (!categories) {
return null; return null;
} }
const final: Array<React.ReactNode> = []; const final: Array<React.ReactNode> = [];
categories.forEach((cat: number | null) => { c.forEach((cat: number | null) => {
if (cat != null) { if (cat != null) {
final.push( final.push(
<Chip style={styles.category} key={cat}> <Chip style={styles.category} key={cat}>
{this.getCategoryName(cat)} {getCategoryName(cat)}
</Chip> </Chip>
); );
} }
}); });
return <View style={styles.categoryContainer}>{final}</View>; return <View style={styles.categoryContainer}>{final}</View>;
} };
/** /**
* Gets the view for rendering club managers if any * Gets the view for rendering club managers if any
@ -166,8 +158,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param email The club contact email * @param email The club contact email
* @returns {*} * @returns {*}
*/ */
getManagersRender(managers: Array<string>, email: string | null) { const getManagersRender = (managers: Array<string>, email: string | null) => {
const { props } = this;
const managersListView: Array<React.ReactNode> = []; const managersListView: Array<React.ReactNode> = [];
managers.forEach((item: string) => { managers.forEach((item: string) => {
managersListView.push(<Paragraph key={item}>{item}</Paragraph>); managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
@ -191,22 +182,18 @@ class ClubDisplayScreen extends React.Component<PropsType> {
<Avatar.Icon <Avatar.Icon
size={iconProps.size} size={iconProps.size}
style={styles.icon} style={styles.icon}
color={ color={hasManagers ? theme.colors.success : theme.colors.primary}
hasManagers
? props.theme.colors.success
: props.theme.colors.primary
}
icon="account-tie" icon="account-tie"
/> />
)} )}
/> />
<Card.Content> <Card.Content>
{managersListView} {managersListView}
{ClubDisplayScreen.getEmailButton(email, hasManagers)} {getEmailButton(email, hasManagers)}
</Card.Content> </Card.Content>
</Card> </Card>
); );
} };
/** /**
* Gets the email button to contact the club, or the amicale if the club does not have any managers * Gets the email button to contact the club, or the amicale if the club does not have any managers
@ -215,7 +202,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param hasManagers True if the club has managers * @param hasManagers True if the club has managers
* @returns {*} * @returns {*}
*/ */
static getEmailButton(email: string | null, hasManagers: boolean) { const getEmailButton = (email: string | null, hasManagers: boolean) => {
const destinationEmail = const destinationEmail =
email != null && hasManagers ? email : AMICALE_MAIL; email != null && hasManagers ? email : AMICALE_MAIL;
const text = const text =
@ -236,14 +223,14 @@ class ClubDisplayScreen extends React.Component<PropsType> {
</Button> </Button>
</Card.Actions> </Card.Actions>
); );
} };
getScreen = (data: ResponseType | undefined) => { const getScreen = (data: ResponseType | undefined) => {
if (data) { if (data) {
this.updateHeaderTitle(data); updateHeaderTitle(data);
return ( return (
<CollapsibleScrollView style={styles.scroll} hasTab> <CollapsibleScrollView style={styles.scroll} hasTab>
{this.getCategoriesRender(data.category)} {getCategoriesRender(data.category)}
{data.logo !== null ? ( {data.logo !== null ? (
<ImageGalleryButton <ImageGalleryButton
images={[{ url: data.logo }]} images={[{ url: data.logo }]}
@ -261,7 +248,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
) : ( ) : (
<View /> <View />
)} )}
{this.getManagersRender(data.responsibles, data.email)} {getManagersRender(data.responsibles, data.email)}
</CollapsibleScrollView> </CollapsibleScrollView>
); );
} }
@ -273,27 +260,22 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* *
* @param data The club data * @param data The club data
*/ */
updateHeaderTitle(data: ClubType) { const updateHeaderTitle = (data: ClubType) => {
const { props } = this; navigation.setOptions({ title: data.name });
props.navigation.setOptions({ title: data.name }); };
}
const request = useAuthenticatedRequest<ClubType>('clubs/info', {
id: clubId,
});
render() {
if (this.shouldFetchData) {
return ( return (
<RequestScreen <RequestScreen
request={() => request={request}
ConnectionManager.getInstance().authenticatedRequest<ResponseType>( render={getScreen}
'clubs/info', cache={displayData}
{ id: this.clubId } onCacheUpdate={setDisplayData}
)
}
render={this.getScreen}
/> />
); );
} }
return this.getScreen(this.displayData);
}
}
export default withTheme(ClubDisplayScreen); export default ClubDisplayScreen;

View file

@ -17,11 +17,10 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useLayoutEffect, useRef, useState } from 'react';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { Searchbar } from 'react-native-paper'; import { Searchbar } from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem'; import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
import { import {
isItemInCategoryFilter, isItemInCategoryFilter,
@ -31,8 +30,10 @@ import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../../components/Overrides/CustomHeaderButton'; } from '../../../components/Overrides/CustomHeaderButton';
import ConnectionManager from '../../../managers/ConnectionManager';
import WebSectionList from '../../../components/Screens/WebSectionList'; import WebSectionList from '../../../components/Screens/WebSectionList';
import { useNavigation } from '@react-navigation/native';
import { useAuthenticatedRequest } from '../../../context/loginContext';
import { MainRoutes } from '../../../navigation/MainNavigator';
export type ClubCategoryType = { export type ClubCategoryType = {
id: number; id: number;
@ -49,15 +50,6 @@ export type ClubType = {
responsibles: Array<string>; responsibles: Array<string>;
}; };
type PropsType = {
navigation: StackNavigationProp<any>;
};
type StateType = {
currentlySelectedCategories: Array<number>;
currentSearchString: string;
};
type ResponseType = { type ResponseType = {
categories: Array<ClubCategoryType>; categories: Array<ClubCategoryType>;
clubs: Array<ClubType>; clubs: Array<ClubType>;
@ -65,33 +57,50 @@ type ResponseType = {
const LIST_ITEM_HEIGHT = 96; const LIST_ITEM_HEIGHT = 96;
class ClubListScreen extends React.Component<PropsType, StateType> { function ClubListScreen() {
categories: Array<ClubCategoryType>; const navigation = useNavigation();
const request = useAuthenticatedRequest<ResponseType>('clubs/list');
const [currentlySelectedCategories, setCurrentlySelectedCategories] =
useState<Array<number>>([]);
const [currentSearchString, setCurrentSearchString] = useState('');
const categories = useRef<Array<ClubCategoryType>>([]);
constructor(props: PropsType) { useLayoutEffect(() => {
super(props); const getSearchBar = () => {
this.categories = []; return (
this.state = { // @ts-ignore
currentlySelectedCategories: [], <Searchbar
currentSearchString: '', placeholder={i18n.t('screens.proximo.search')}
onChangeText={onSearchStringChange}
/>
);
}; };
} const getHeaderButtons = () => {
return (
/** <MaterialHeaderButtons>
* Creates the header content <Item
*/ title="main"
componentDidMount() { iconName="information"
const { props } = this; onPress={() => navigation.navigate(MainRoutes.ClubAbout)}
props.navigation.setOptions({ />
headerTitle: this.getSearchBar, </MaterialHeaderButtons>
headerRight: this.getHeaderButtons, );
};
navigation.setOptions({
headerTitle: getSearchBar,
headerRight: getHeaderButtons,
headerBackTitleVisible: false, headerBackTitleVisible: false,
headerTitleContainerStyle: headerTitleContainerStyle:
Platform.OS === 'ios' Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' } ? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 }, : { width: '100%' },
}); });
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, [navigation]);
const onSearchStringChange = (str: string) => {
updateFilteredData(str, null);
};
/** /**
* Callback used when clicking an article in the list. * Callback used when clicking an article in the list.
@ -99,61 +108,21 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
* *
* @param item The article pressed * @param item The article pressed
*/ */
onListItemPress(item: ClubType) { const onListItemPress = (item: ClubType) => {
const { props } = this; navigation.navigate(MainRoutes.ClubInformation, {
props.navigation.navigate('club-information', { type: 'full',
data: item, data: item,
categories: this.categories, categories: categories.current,
}); });
}
/**
* Callback used when the search changes
*
* @param str The new search string
*/
onSearchStringChange = (str: string) => {
this.updateFilteredData(str, null);
}; };
/** const onChipSelect = (id: number) => {
* Gets the header search bar updateFilteredData(null, id);
*
* @return {*}
*/
getSearchBar = () => {
return (
// @ts-ignore
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange}
/>
);
}; };
onChipSelect = (id: number) => { const createDataset = (data: ResponseType | undefined) => {
this.updateFilteredData(null, id);
};
/**
* Gets the header button
* @return {*}
*/
getHeaderButtons = () => {
const onPress = () => {
const { props } = this;
props.navigation.navigate('club-about');
};
return (
<MaterialHeaderButtons>
<Item title="main" iconName="information" onPress={onPress} />
</MaterialHeaderButtons>
);
};
createDataset = (data: ResponseType | undefined) => {
if (data) { if (data) {
this.categories = data?.categories; categories.current = data.categories;
return [{ title: '', data: data.clubs }]; return [{ title: '', data: data.clubs }];
} else { } else {
return []; return [];
@ -165,30 +134,23 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
* *
* @returns {*} * @returns {*}
*/ */
getListHeader(data: ResponseType | undefined) { const getListHeader = (data: ResponseType | undefined) => {
const { state } = this;
if (data) { if (data) {
return ( return (
<ClubListHeader <ClubListHeader
categories={this.categories} categories={categories.current}
selectedCategories={state.currentlySelectedCategories} selectedCategories={currentlySelectedCategories}
onChipSelect={this.onChipSelect} onChipSelect={onChipSelect}
/> />
); );
} else { } else {
return null; return null;
} }
} };
/** const getCategoryOfId = (id: number): ClubCategoryType | null => {
* Gets the category object of the given ID
*
* @param id The ID of the category to find
* @returns {*}
*/
getCategoryOfId = (id: number): ClubCategoryType | null => {
let cat = null; let cat = null;
this.categories.forEach((item: ClubCategoryType) => { categories.current.forEach((item: ClubCategoryType) => {
if (id === item.id) { if (id === item.id) {
cat = item; cat = item;
} }
@ -196,14 +158,14 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
return cat; return cat;
}; };
getRenderItem = ({ item }: { item: ClubType }) => { const getRenderItem = ({ item }: { item: ClubType }) => {
const onPress = () => { const onPress = () => {
this.onListItemPress(item); onListItemPress(item);
}; };
if (this.shouldRenderItem(item)) { if (shouldRenderItem(item)) {
return ( return (
<ClubListItem <ClubListItem
categoryTranslator={this.getCategoryOfId} categoryTranslator={getCategoryOfId}
item={item} item={item}
onPress={onPress} onPress={onPress}
height={LIST_ITEM_HEIGHT} height={LIST_ITEM_HEIGHT}
@ -213,7 +175,7 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
return null; return null;
}; };
keyExtractor = (item: ClubType): string => item.id.toString(); const keyExtractor = (item: ClubType): string => item.id.toString();
/** /**
* Updates the search string and category filter, saving them to the State. * Updates the search string and category filter, saving them to the State.
@ -224,10 +186,12 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
* @param filterStr The new filter string to use * @param filterStr The new filter string to use
* @param categoryId The category to add/remove from the filter * @param categoryId The category to add/remove from the filter
*/ */
updateFilteredData(filterStr: string | null, categoryId: number | null) { const updateFilteredData = (
const { state } = this; filterStr: string | null,
const newCategoriesState = [...state.currentlySelectedCategories]; categoryId: number | null
let newStrState = state.currentSearchString; ) => {
const newCategoriesState = [...currentlySelectedCategories];
let newStrState = currentSearchString;
if (filterStr !== null) { if (filterStr !== null) {
newStrState = filterStr; newStrState = filterStr;
} }
@ -240,12 +204,10 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
} }
} }
if (filterStr !== null || categoryId !== null) { if (filterStr !== null || categoryId !== null) {
this.setState({ setCurrentSearchString(newStrState);
currentSearchString: newStrState, setCurrentlySelectedCategories(newCategoriesState);
currentlySelectedCategories: newCategoriesState,
});
}
} }
};
/** /**
* Checks if the given item should be rendered according to current name and category filters * Checks if the given item should be rendered according to current name and category filters
@ -253,35 +215,28 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
* @param item The club to check * @param item The club to check
* @returns {boolean} * @returns {boolean}
*/ */
shouldRenderItem(item: ClubType): boolean { const shouldRenderItem = (item: ClubType): boolean => {
const { state } = this;
let shouldRender = let shouldRender =
state.currentlySelectedCategories.length === 0 || currentlySelectedCategories.length === 0 ||
isItemInCategoryFilter(state.currentlySelectedCategories, item.category); isItemInCategoryFilter(currentlySelectedCategories, item.category);
if (shouldRender) { if (shouldRender) {
shouldRender = stringMatchQuery(item.name, state.currentSearchString); shouldRender = stringMatchQuery(item.name, currentSearchString);
} }
return shouldRender; return shouldRender;
} };
render() {
return ( return (
<WebSectionList <WebSectionList
request={() => request={request}
ConnectionManager.getInstance().authenticatedRequest<ResponseType>( createDataset={createDataset}
'clubs/list' keyExtractor={keyExtractor}
) renderItem={getRenderItem}
} renderListHeaderComponent={getListHeader}
createDataset={this.createDataset}
keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem}
renderListHeaderComponent={(data) => this.getListHeader(data)}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews={true} removeClippedSubviews={true}
itemHeight={LIST_ITEM_HEIGHT} itemHeight={LIST_ITEM_HEIGHT}
/> />
); );
} }
}
export default ClubListScreen; export default ClubListScreen;

View file

@ -31,12 +31,15 @@ import i18n from 'i18n-js';
import { getRelativeDateString } from '../../../utils/EquipmentBooking'; import { getRelativeDateString } from '../../../utils/EquipmentBooking';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import { StackScreenProps } from '@react-navigation/stack'; import { StackScreenProps } from '@react-navigation/stack';
import { MainStackParamsList } from '../../../navigation/MainNavigator'; import {
MainRoutes,
MainStackParamsList,
} from '../../../navigation/MainNavigator';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
type EquipmentConfirmScreenNavigationProp = StackScreenProps< type EquipmentConfirmScreenNavigationProp = StackScreenProps<
MainStackParamsList, MainStackParamsList,
'equipment-confirm' MainRoutes.EquipmentConfirm
>; >;
type Props = EquipmentConfirmScreenNavigationProp; type Props = EquipmentConfirmScreenNavigationProp;

View file

@ -17,26 +17,17 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { StackNavigationProp } from '@react-navigation/stack';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem'; import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
import MascotPopup from '../../../components/Mascot/MascotPopup'; import MascotPopup from '../../../components/Mascot/MascotPopup';
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import ConnectionManager from '../../../managers/ConnectionManager';
import { ApiRejectType } from '../../../utils/WebData'; import { ApiRejectType } from '../../../utils/WebData';
import WebSectionList from '../../../components/Screens/WebSectionList'; import WebSectionList from '../../../components/Screens/WebSectionList';
import { useAuthenticatedRequest } from '../../../context/loginContext';
type PropsType = {
navigation: StackNavigationProp<any>;
};
type StateType = {
mascotDialogVisible: boolean | undefined;
};
export type DeviceType = { export type DeviceType = {
id: number; id: number;
@ -67,69 +58,63 @@ const styles = StyleSheet.create({
}, },
}); });
class EquipmentListScreen extends React.Component<PropsType, StateType> { function EquipmentListScreen() {
userRents: null | Array<RentedDeviceType>; const userRents = useRef<undefined | Array<RentedDeviceType>>();
const [mascotDialogVisible, setMascotDialogVisible] = useState<
undefined | boolean
>(undefined);
constructor(props: PropsType) { const requestAll =
super(props); useAuthenticatedRequest<{ devices: Array<DeviceType> }>('location/all');
this.userRents = null; const requestOwn = useAuthenticatedRequest<{
this.state = { locations: Array<RentedDeviceType>;
mascotDialogVisible: undefined, }>('location/my');
};
}
getRenderItem = ({ item }: { item: DeviceType }) => { const getRenderItem = ({ item }: { item: DeviceType }) => {
const { navigation } = this.props;
return ( return (
<EquipmentListItem <EquipmentListItem
navigation={navigation}
item={item} item={item}
userDeviceRentDates={this.getUserDeviceRentDates(item)} userDeviceRentDates={getUserDeviceRentDates(item)}
height={LIST_ITEM_HEIGHT} height={LIST_ITEM_HEIGHT}
/> />
); );
}; };
getUserDeviceRentDates(item: DeviceType): [string, string] | null { const getUserDeviceRentDates = (
item: DeviceType
): [string, string] | null => {
let dates = null; let dates = null;
if (this.userRents != null) { if (userRents.current) {
this.userRents.forEach((device: RentedDeviceType) => { userRents.current.forEach((device: RentedDeviceType) => {
if (item.id === device.device_id) { if (item.id === device.device_id) {
dates = [device.begin, device.end]; dates = [device.begin, device.end];
} }
}); });
} }
return dates; return dates;
} };
/** const getListHeader = () => {
* Gets the list header, with explains this screen's purpose
*
* @returns {*}
*/
getListHeader() {
return ( return (
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<Button <Button
mode="contained" mode="contained"
icon="help-circle" icon="help-circle"
onPress={this.showMascotDialog} onPress={showMascotDialog}
style={GENERAL_STYLES.centerHorizontal} style={GENERAL_STYLES.centerHorizontal}
> >
{i18n.t('screens.equipment.mascotDialog.title')} {i18n.t('screens.equipment.mascotDialog.title')}
</Button> </Button>
</View> </View>
); );
} };
keyExtractor = (item: DeviceType): string => item.id.toString(); const keyExtractor = (item: DeviceType): string => item.id.toString();
createDataset = (data: ResponseType | undefined) => { const createDataset = (data: ResponseType | undefined) => {
if (data) { if (data) {
const userRents = data.locations; if (data.locations) {
userRents.current = data.locations;
if (userRents) {
this.userRents = userRents;
} }
return [{ title: '', data: data.devices }]; return [{ title: '', data: data.devices }];
} else { } else {
@ -137,27 +122,19 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
} }
}; };
showMascotDialog = () => { const showMascotDialog = () => setMascotDialogVisible(true);
this.setState({ mascotDialogVisible: true });
};
hideMascotDialog = () => { const hideMascotDialog = () => setMascotDialogVisible(false);
this.setState({ mascotDialogVisible: false });
};
request = () => { const request = () => {
return new Promise( return new Promise(
( (
resolve: (data: ResponseType) => void, resolve: (data: ResponseType) => void,
reject: (error: ApiRejectType) => void reject: (error: ApiRejectType) => void
) => { ) => {
ConnectionManager.getInstance() requestAll()
.authenticatedRequest<{ devices: Array<DeviceType> }>('location/all')
.then((devicesData) => { .then((devicesData) => {
ConnectionManager.getInstance() requestOwn()
.authenticatedRequest<{
locations: Array<RentedDeviceType>;
}>('location/my')
.then((rentsData) => { .then((rentsData) => {
resolve({ resolve({
devices: devicesData.devices, devices: devicesData.devices,
@ -175,19 +152,17 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
); );
}; };
render() {
const { state } = this;
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
<WebSectionList <WebSectionList
request={this.request} request={request}
createDataset={this.createDataset} createDataset={createDataset}
keyExtractor={this.keyExtractor} keyExtractor={keyExtractor}
renderItem={this.getRenderItem} renderItem={getRenderItem}
renderListHeaderComponent={() => this.getListHeader()} renderListHeaderComponent={getListHeader}
/> />
<MascotPopup <MascotPopup
visible={state.mascotDialogVisible} visible={mascotDialogVisible}
title={i18n.t('screens.equipment.mascotDialog.title')} title={i18n.t('screens.equipment.mascotDialog.title')}
message={i18n.t('screens.equipment.mascotDialog.message')} message={i18n.t('screens.equipment.mascotDialog.message')}
icon="vote" icon="vote"
@ -195,7 +170,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
cancel: { cancel: {
message: i18n.t('screens.equipment.mascotDialog.button'), message: i18n.t('screens.equipment.mascotDialog.button'),
icon: 'check', icon: 'check',
onPress: this.hideMascotDialog, onPress: hideMascotDialog,
}, },
}} }}
emotion={MASCOT_STYLE.WINK} emotion={MASCOT_STYLE.WINK}
@ -203,6 +178,5 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
</View> </View>
); );
} }
}
export default EquipmentListScreen; export default EquipmentListScreen;

View file

@ -17,21 +17,20 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useCallback, useRef, useState } from 'react';
import { import {
Button, Button,
Caption, Caption,
Card, Card,
Headline, Headline,
Subheading, Subheading,
withTheme, useTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'; import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
import { BackHandler, StyleSheet, View } from 'react-native'; import { BackHandler, StyleSheet, View } from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { CalendarList, PeriodMarking } from 'react-native-calendars'; import { CalendarList, PeriodMarking } from 'react-native-calendars';
import type { DeviceType } from './EquipmentListScreen';
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../../components/Dialogs/ErrorDialog'; import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
import { import {
@ -42,34 +41,24 @@ import {
getValidRange, getValidRange,
isEquipmentAvailable, isEquipmentAvailable,
} from '../../../utils/EquipmentBooking'; } from '../../../utils/EquipmentBooking';
import ConnectionManager from '../../../managers/ConnectionManager';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import { MainStackParamsList } from '../../../navigation/MainNavigator'; import {
MainRoutes,
MainStackParamsList,
} from '../../../navigation/MainNavigator';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import { ApiRejectType } from '../../../utils/WebData'; import { ApiRejectType } from '../../../utils/WebData';
import { REQUEST_STATUS } from '../../../utils/Requests'; import { REQUEST_STATUS } from '../../../utils/Requests';
import { useFocusEffect } from '@react-navigation/core';
import { useNavigation } from '@react-navigation/native';
import { useAuthenticatedRequest } from '../../../context/loginContext';
type EquipmentRentScreenNavigationProp = StackScreenProps< type Props = StackScreenProps<MainStackParamsList, MainRoutes.EquipmentRent>;
MainStackParamsList,
'equipment-rent'
>;
type Props = EquipmentRentScreenNavigationProp & {
navigation: StackNavigationProp<any>;
theme: ReactNativePaper.Theme;
};
export type MarkedDatesObjectType = { export type MarkedDatesObjectType = {
[key: string]: PeriodMarking; [key: string]: PeriodMarking;
}; };
type StateType = {
dialogVisible: boolean;
errorDialogVisible: boolean;
markedDates: MarkedDatesObjectType;
currentError: ApiRejectType;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
titleContainer: { titleContainer: {
marginLeft: 'auto', marginLeft: 'auto',
@ -114,98 +103,101 @@ const styles = StyleSheet.create({
}, },
}); });
class EquipmentRentScreen extends React.Component<Props, StateType> { function EquipmentRentScreen(props: Props) {
item: DeviceType | null; const theme = useTheme();
const navigation = useNavigation<StackNavigationProp<any>>();
const [currentError, setCurrentError] = useState<ApiRejectType>({
status: REQUEST_STATUS.SUCCESS,
});
const [markedDates, setMarkedDates] = useState<MarkedDatesObjectType>({});
const [dialogVisible, setDialogVisible] = useState(false);
bookedDates: Array<string>; const item = props.route.params.item;
bookRef: { current: null | (Animatable.View & View) }; const bookedDates = useRef<Array<string>>([]);
const canBookEquipment = useRef(false);
canBookEquipment: boolean; const bookRef = useRef<Animatable.View & View>(null);
lockedDates: { let lockedDates: {
[key: string]: PeriodMarking; [key: string]: PeriodMarking;
}; } = {};
constructor(props: Props) { if (item) {
super(props);
this.item = null;
this.lockedDates = {};
this.state = {
dialogVisible: false,
errorDialogVisible: false,
markedDates: {},
currentError: { status: REQUEST_STATUS.SUCCESS },
};
this.resetSelection();
this.bookRef = React.createRef();
this.canBookEquipment = false;
this.bookedDates = [];
if (props.route.params != null) {
if (props.route.params.item != null) {
this.item = props.route.params.item;
} else {
this.item = null;
}
}
const { item } = this;
if (item != null) {
this.lockedDates = {};
item.booked_at.forEach((date: { begin: string; end: string }) => { item.booked_at.forEach((date: { begin: string; end: string }) => {
const range = getValidRange( const range = getValidRange(
new Date(date.begin), new Date(date.begin),
new Date(date.end), new Date(date.end),
null null
); );
this.lockedDates = { lockedDates = {
...this.lockedDates, ...lockedDates,
...generateMarkedDates(false, props.theme, range), ...generateMarkedDates(false, theme, range),
}; };
}); });
} }
}
/** useFocusEffect(
* Captures focus and blur events to hook on android back button useCallback(() => {
*/
componentDidMount() {
const { navigation } = this.props;
navigation.addListener('focus', () => {
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid onBackButtonPressAndroid
); );
}); return () => {
navigation.addListener('blur', () => {
BackHandler.removeEventListener( BackHandler.removeEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid onBackButtonPressAndroid
);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
); );
});
}
/** /**
* Overrides default android back button behaviour to deselect date if any is selected. * Overrides default android back button behaviour to deselect date if any is selected.
* *
* @return {boolean} * @return {boolean}
*/ */
onBackButtonPressAndroid = (): boolean => { const onBackButtonPressAndroid = (): boolean => {
if (this.bookedDates.length > 0) { if (bookedDates.current.length > 0) {
this.resetSelection(); resetSelection();
this.updateMarkedSelection(); updateMarkedSelection();
return true; return true;
} }
return false; return false;
}; };
onDialogDismiss = () => { const showDialog = () => setDialogVisible(true);
this.setState({ dialogVisible: false });
const onDialogDismiss = () => setDialogVisible(false);
const onErrorDialogDismiss = () =>
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
const getBookStartDate = (): Date | null => {
return bookedDates.current.length > 0
? new Date(bookedDates.current[0])
: null;
}; };
onErrorDialogDismiss = () => { const getBookEndDate = (): Date | null => {
this.setState({ errorDialogVisible: false }); const { length } = bookedDates.current;
return length > 0 ? new Date(bookedDates.current[length - 1]) : null;
}; };
const start = getBookStartDate();
const end = getBookEndDate();
const request = useAuthenticatedRequest(
'location/booking',
item && start && end
? {
device: item.id,
begin: getISODate(start),
end: getISODate(end),
}
: undefined
);
/** /**
* Sends the selected data to the server and waits for a response. * Sends the selected data to the server and waits for a response.
* If the request is a success, navigate to the recap screen. * If the request is a success, navigate to the recap screen.
@ -213,54 +205,37 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
* *
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
onDialogAccept = (): Promise<void> => { const onDialogAccept = (): Promise<void> => {
return new Promise((resolve: () => void) => { return new Promise((resolve: () => void) => {
const { item, props } = this;
const start = this.getBookStartDate();
const end = this.getBookEndDate();
if (item != null && start != null && end != null) { if (item != null && start != null && end != null) {
ConnectionManager.getInstance() request()
.authenticatedRequest('location/booking', {
device: item.id,
begin: getISODate(start),
end: getISODate(end),
})
.then(() => { .then(() => {
this.onDialogDismiss(); onDialogDismiss();
props.navigation.replace('equipment-confirm', { navigation.replace('equipment-confirm', {
item: this.item, item: item,
dates: [getISODate(start), getISODate(end)], dates: [getISODate(start), getISODate(end)],
}); });
resolve(); resolve();
}) })
.catch((error: ApiRejectType) => { .catch((error: ApiRejectType) => {
this.onDialogDismiss(); onDialogDismiss();
this.showErrorDialog(error); setCurrentError(error);
resolve(); resolve();
}); });
} else { } else {
this.onDialogDismiss(); onDialogDismiss();
resolve(); resolve();
} }
}); });
}; };
getBookStartDate(): Date | null {
return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
}
getBookEndDate(): Date | null {
const { length } = this.bookedDates;
return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
}
/** /**
* Selects a new date on the calendar. * Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all. * If both start and end dates are already selected, unselect all.
* *
* @param day The day selected * @param day The day selected
*/ */
selectNewDate = (day: { const selectNewDate = (day: {
dateString: string; dateString: string;
day: number; day: number;
month: number; month: number;
@ -268,84 +243,64 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
year: number; year: number;
}) => { }) => {
const selected = new Date(day.dateString); const selected = new Date(day.dateString);
const start = this.getBookStartDate();
if (!this.lockedDates[day.dateString] != null) { if (!lockedDates[day.dateString] != null) {
if (start === null) { if (start === null) {
this.updateSelectionRange(selected, selected); updateSelectionRange(selected, selected);
this.enableBooking(); enableBooking();
} else if (start.getTime() === selected.getTime()) { } else if (start.getTime() === selected.getTime()) {
this.resetSelection(); resetSelection();
} else if (this.bookedDates.length === 1) { } else if (bookedDates.current.length === 1) {
this.updateSelectionRange(start, selected); updateSelectionRange(start, selected);
this.enableBooking(); enableBooking();
} else { } else {
this.resetSelection(); resetSelection();
} }
this.updateMarkedSelection(); updateMarkedSelection();
} }
}; };
showErrorDialog = (error: ApiRejectType) => { const showBookButton = () => {
this.setState({ if (bookRef.current && bookRef.current.fadeInUp) {
errorDialogVisible: true, bookRef.current.fadeInUp(500);
currentError: error, }
});
}; };
showDialog = () => { const hideBookButton = () => {
this.setState({ dialogVisible: true }); if (bookRef.current && bookRef.current.fadeOutDown) {
bookRef.current.fadeOutDown(500);
}
}; };
/** const enableBooking = () => {
* Shows the book button by plying a fade animation if (!canBookEquipment.current) {
*/ showBookButton();
showBookButton() { canBookEquipment.current = true;
if (this.bookRef.current && this.bookRef.current.fadeInUp) {
this.bookRef.current.fadeInUp(500);
}
} }
};
/** const resetSelection = () => {
* Hides the book button by plying a fade animation if (canBookEquipment.current) {
*/ hideBookButton();
hideBookButton() {
if (this.bookRef.current && this.bookRef.current.fadeOutDown) {
this.bookRef.current.fadeOutDown(500);
}
} }
canBookEquipment.current = false;
bookedDates.current = [];
};
enableBooking() { const updateSelectionRange = (s: Date, e: Date) => {
if (!this.canBookEquipment) { if (item) {
this.showBookButton(); bookedDates.current = getValidRange(s, e, item);
this.canBookEquipment = true; } else {
} bookedDates.current = [];
} }
};
resetSelection() { const updateMarkedSelection = () => {
if (this.canBookEquipment) { setMarkedDates(generateMarkedDates(true, theme, bookedDates.current));
this.hideBookButton(); };
}
this.canBookEquipment = false;
this.bookedDates = [];
}
updateSelectionRange(start: Date, end: Date) {
this.bookedDates = getValidRange(start, end, this.item);
}
updateMarkedSelection() {
const { theme } = this.props;
this.setState({
markedDates: generateMarkedDates(true, theme, this.bookedDates),
});
}
render() {
const { item, props, state } = this;
const start = this.getBookStartDate();
const end = this.getBookEndDate();
let subHeadingText; let subHeadingText;
if (start == null) { if (start == null) {
subHeadingText = i18n.t('screens.equipment.booking'); subHeadingText = i18n.t('screens.equipment.booking');
} else if (end != null && start.getTime() !== end.getTime()) { } else if (end != null && start.getTime() !== end.getTime()) {
@ -358,7 +313,8 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
date: getRelativeDateString(start), date: getRelativeDateString(start),
}); });
} }
if (item != null) {
if (item) {
const isAvailable = isEquipmentAvailable(item); const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item); const firstAvailability = getFirstEquipmentAvailability(item);
return ( return (
@ -370,9 +326,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Headline style={styles.title}>{item.name}</Headline> <Headline style={styles.title}>{item.name}</Headline>
<Caption style={styles.caption}> <Caption style={styles.caption}>
( ({i18n.t('screens.equipment.bail', { cost: item.caution })})
{i18n.t('screens.equipment.bail', { cost: item.caution })}
)
</Caption> </Caption>
</View> </View>
</View> </View>
@ -380,9 +334,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
<Button <Button
icon={isAvailable ? 'check-circle-outline' : 'update'} icon={isAvailable ? 'check-circle-outline' : 'update'}
color={ color={
isAvailable isAvailable ? theme.colors.success : theme.colors.primary
? props.theme.colors.success
: props.theme.colors.primary
} }
mode="text" mode="text"
> >
@ -390,9 +342,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
date: getRelativeDateString(firstAvailability), date: getRelativeDateString(firstAvailability),
})} })}
</Button> </Button>
<Subheading style={styles.subtitle}> <Subheading style={styles.subtitle}>{subHeadingText}</Subheading>
{subHeadingText}
</Subheading>
</Card.Content> </Card.Content>
</Card> </Card>
<CalendarList <CalendarList
@ -407,28 +357,28 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
// Enable paging on horizontal, default = false // Enable paging on horizontal, default = false
pagingEnabled pagingEnabled
// Handler which gets executed on day press. Default = undefined // Handler which gets executed on day press. Default = undefined
onDayPress={this.selectNewDate} onDayPress={selectNewDate}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday. // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1} firstDay={1}
// Hide month navigation arrows. // Hide month navigation arrows.
hideArrows={false} hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple' // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType={'period'} markingType={'period'}
markedDates={{ ...this.lockedDates, ...state.markedDates }} markedDates={{ ...lockedDates, ...markedDates }}
theme={{ theme={{
'backgroundColor': props.theme.colors.agendaBackgroundColor, 'backgroundColor': theme.colors.agendaBackgroundColor,
'calendarBackground': props.theme.colors.background, 'calendarBackground': theme.colors.background,
'textSectionTitleColor': props.theme.colors.agendaDayTextColor, 'textSectionTitleColor': theme.colors.agendaDayTextColor,
'selectedDayBackgroundColor': props.theme.colors.primary, 'selectedDayBackgroundColor': theme.colors.primary,
'selectedDayTextColor': '#ffffff', 'selectedDayTextColor': '#ffffff',
'todayTextColor': props.theme.colors.text, 'todayTextColor': theme.colors.text,
'dayTextColor': props.theme.colors.text, 'dayTextColor': theme.colors.text,
'textDisabledColor': props.theme.colors.agendaDayTextColor, 'textDisabledColor': theme.colors.agendaDayTextColor,
'dotColor': props.theme.colors.primary, 'dotColor': theme.colors.primary,
'selectedDotColor': '#ffffff', 'selectedDotColor': '#ffffff',
'arrowColor': props.theme.colors.primary, 'arrowColor': theme.colors.primary,
'monthTextColor': props.theme.colors.text, 'monthTextColor': theme.colors.text,
'indicatorColor': props.theme.colors.primary, 'indicatorColor': theme.colors.primary,
'textDayFontFamily': 'monospace', 'textDayFontFamily': 'monospace',
'textMonthFontFamily': 'monospace', 'textMonthFontFamily': 'monospace',
'textDayHeaderFontFamily': 'monospace', 'textDayHeaderFontFamily': 'monospace',
@ -451,29 +401,32 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
/> />
</CollapsibleScrollView> </CollapsibleScrollView>
<LoadingConfirmDialog <LoadingConfirmDialog
visible={state.dialogVisible} visible={dialogVisible}
onDismiss={this.onDialogDismiss} onDismiss={onDialogDismiss}
onAccept={this.onDialogAccept} onAccept={onDialogAccept}
title={i18n.t('screens.equipment.dialogTitle')} title={i18n.t('screens.equipment.dialogTitle')}
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')} titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
message={i18n.t('screens.equipment.dialogMessage')} message={i18n.t('screens.equipment.dialogMessage')}
/> />
<ErrorDialog <ErrorDialog
visible={state.errorDialogVisible} visible={
onDismiss={this.onErrorDialogDismiss} currentError.status !== REQUEST_STATUS.SUCCESS ||
status={state.currentError.status} currentError.code !== undefined
code={state.currentError.code} }
onDismiss={onErrorDialogDismiss}
status={currentError.status}
code={currentError.code}
/> />
<Animatable.View <Animatable.View
ref={this.bookRef} ref={bookRef}
useNativeDriver useNativeDriver
style={styles.buttonContainer} style={styles.buttonContainer}
> >
<Button <Button
icon="bookmark-check" icon="bookmark-check"
mode="contained" mode="contained"
onPress={this.showDialog} onPress={showDialog}
style={styles.button} style={styles.button}
> >
{i18n.t('screens.equipment.bookButton')} {i18n.t('screens.equipment.bookButton')}
@ -484,6 +437,5 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
} }
return null; return null;
} }
}
export default withTheme(EquipmentRentScreen); export default EquipmentRentScreen;

View file

@ -17,158 +17,59 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useCallback, useState } from 'react';
import { Image, KeyboardAvoidingView, StyleSheet, View } from 'react-native'; import { KeyboardAvoidingView, View } from 'react-native';
import {
Button,
Card,
HelperText,
TextInput,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'; import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
import LinearGradient from 'react-native-linear-gradient'; import LinearGradient from 'react-native-linear-gradient';
import ConnectionManager from '../../managers/ConnectionManager';
import ErrorDialog from '../../components/Dialogs/ErrorDialog'; import ErrorDialog from '../../components/Dialogs/ErrorDialog';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
import { MainStackParamsList } from '../../navigation/MainNavigator'; import {
MainRoutes,
MainStackParamsList,
} from '../../navigation/MainNavigator';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import { ApiRejectType } from '../../utils/WebData'; import { ApiRejectType, connectToAmicale } from '../../utils/WebData';
import { REQUEST_STATUS } from '../../utils/Requests'; import { REQUEST_STATUS } from '../../utils/Requests';
import LoginForm from '../../components/Amicale/Login/LoginForm';
import { useFocusEffect, useNavigation } from '@react-navigation/native';
import { TabRoutes } from '../../navigation/TabNavigator';
import { useShouldShowMascot } from '../../context/preferencesContext';
import { useLogin } from '../../context/loginContext';
import { saveLoginToken } from '../../utils/loginToken';
type LoginScreenNavigationProp = StackScreenProps<MainStackParamsList, 'login'>; type Props = StackScreenProps<MainStackParamsList, MainRoutes.Login>;
type Props = LoginScreenNavigationProp & { function LoginScreen(props: Props) {
navigation: StackNavigationProp<any>; const navigation = useNavigation<StackNavigationProp<any>>();
theme: ReactNativePaper.Theme; const { setLogin } = useLogin();
}; const [loading, setLoading] = useState(false);
const [nextScreen, setNextScreen] = useState<string | undefined>(undefined);
type StateType = { const [mascotDialogVisible, setMascotDialogVisible] = useState<
email: string; undefined | boolean
password: string; >(undefined);
isEmailValidated: boolean; const [currentError, setCurrentError] = useState<ApiRejectType>({
isPasswordValidated: boolean; status: REQUEST_STATUS.SUCCESS,
loading: boolean;
dialogVisible: boolean;
dialogError: ApiRejectType;
mascotDialogVisible: boolean | undefined;
};
const ICON_AMICALE = require('../../../assets/amicale.png');
const emailRegex = /^.+@.+\..+$/;
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 homeMascot = useShouldShowMascot(TabRoutes.Home);
class LoginScreen extends React.Component<Props, StateType> { useFocusEffect(
onEmailChange: (value: string) => void; useCallback(() => {
setNextScreen(props.route.params?.nextScreen);
}, [props.route.params])
);
onPasswordChange: (value: string) => void; const onResetPasswordClick = () => {
navigation.navigate(MainRoutes.Website, {
passwordInputRef: {
// @ts-ignore
current: null | TextInput;
};
nextScreen: string | null;
constructor(props: Props) {
super(props);
this.nextScreen = null;
this.passwordInputRef = React.createRef();
this.onEmailChange = (value: string) => {
this.onInputChange(true, value);
};
this.onPasswordChange = (value: string) => {
this.onInputChange(false, value);
};
props.navigation.addListener('focus', this.onScreenFocus);
this.state = {
email: '',
password: '',
isEmailValidated: false,
isPasswordValidated: false,
loading: false,
dialogVisible: false,
dialogError: { status: REQUEST_STATUS.SUCCESS },
mascotDialogVisible: undefined,
};
}
onScreenFocus = () => {
this.handleNavigationParams();
};
/**
* Navigates to the Amicale website screen with the reset password link as navigation parameters
*/
onResetPasswordClick = () => {
const { navigation } = this.props;
navigation.navigate('website', {
host: Urls.websites.amicale, host: Urls.websites.amicale,
path: Urls.amicale.resetPassword, path: Urls.amicale.resetPassword,
title: i18n.t('screens.websites.amicale'), title: i18n.t('screens.websites.amicale'),
}); });
}; };
/**
* Called when the user input changes in the email or password field.
* This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
*
* @param isEmail True if the field is the email field
* @param value The new field value
*/
onInputChange(isEmail: boolean, value: string) {
if (isEmail) {
this.setState({
email: value,
isEmailValidated: false,
});
} else {
this.setState({
password: value,
isPasswordValidated: false,
});
}
}
/**
* Focuses the password field when the email field is done
*
* @returns {*}
*/
onEmailSubmit = () => {
if (this.passwordInputRef.current != null) {
this.passwordInputRef.current.focus();
}
};
/** /**
* Called when the user clicks on login or finishes to type his password. * Called when the user clicks on login or finishes to type his password.
* *
@ -176,253 +77,39 @@ class LoginScreen extends React.Component<Props, StateType> {
* then makes the login request and enters a loading state until the request finishes * then makes the login request and enters a loading state until the request finishes
* *
*/ */
onSubmit = () => { const onSubmit = (email: string, password: string) => {
const { email, password } = this.state; setLoading(true);
if (this.shouldEnableLogin()) { connectToAmicale(email, password)
this.setState({ loading: true }); .then(handleSuccess)
ConnectionManager.getInstance() .catch(setCurrentError)
.connect(email, password) .finally(() => setLoading(false));
.then(this.handleSuccess)
.catch(this.showErrorDialog)
.finally(() => {
this.setState({ loading: false });
});
}
}; };
/** const hideMascotDialog = () => setMascotDialogVisible(false);
* Gets the form input
*
* @returns {*}
*/
getFormInput() {
const { email, password } = this.state;
return (
<View>
<TextInput
label={i18n.t('screens.login.email')}
mode="outlined"
value={email}
onChangeText={this.onEmailChange}
onBlur={this.validateEmail}
onSubmitEditing={this.onEmailSubmit}
error={this.shouldShowEmailError()}
textContentType="emailAddress"
autoCapitalize="none"
autoCompleteType="email"
autoCorrect={false}
keyboardType="email-address"
returnKeyType="next"
secureTextEntry={false}
/>
<HelperText type="error" visible={this.shouldShowEmailError()}>
{i18n.t('screens.login.emailError')}
</HelperText>
<TextInput
ref={this.passwordInputRef}
label={i18n.t('screens.login.password')}
mode="outlined"
value={password}
onChangeText={this.onPasswordChange}
onBlur={this.validatePassword}
onSubmitEditing={this.onSubmit}
error={this.shouldShowPasswordError()}
textContentType="password"
autoCapitalize="none"
autoCompleteType="password"
autoCorrect={false}
keyboardType="default"
returnKeyType="done"
secureTextEntry
/>
<HelperText type="error" visible={this.shouldShowPasswordError()}>
{i18n.t('screens.login.passwordError')}
</HelperText>
</View>
);
}
/** const showMascotDialog = () => setMascotDialogVisible(true);
* Gets the card containing the input form
* @returns {*}
*/
getMainCard() {
const { props, state } = this;
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>
{this.getFormInput()}
<Card.Actions style={styles.buttonContainer}>
<Button
icon="lock-question"
mode="contained"
onPress={this.onResetPasswordClick}
color={props.theme.colors.warning}
style={styles.lockButton}
>
{i18n.t('screens.login.resetPassword')}
</Button>
<Button
icon="send"
mode="contained"
disabled={!this.shouldEnableLogin()}
loading={state.loading}
onPress={this.onSubmit}
style={styles.sendButton}
>
{i18n.t('screens.login.title')}
</Button>
</Card.Actions>
<Card.Actions>
<Button
icon="help-circle"
mode="contained"
onPress={this.showMascotDialog}
style={GENERAL_STYLES.centerHorizontal}
>
{i18n.t('screens.login.mascotDialog.title')}
</Button>
</Card.Actions>
</Card.Content>
</View>
);
}
/** const hideErrorDialog = () =>
* The user has unfocused the input, his email is ready to be validated setCurrentError({ status: REQUEST_STATUS.SUCCESS });
*/
validateEmail = () => {
this.setState({ isEmailValidated: true });
};
/**
* The user has unfocused the input, his password is ready to be validated
*/
validatePassword = () => {
this.setState({ isPasswordValidated: true });
};
hideMascotDialog = () => {
this.setState({ mascotDialogVisible: false });
};
showMascotDialog = () => {
this.setState({ mascotDialogVisible: true });
};
/**
* Shows an error dialog with the corresponding login error
*
* @param error The error given by the login request
*/
showErrorDialog = (error: ApiRejectType) => {
console.log(error);
this.setState({
dialogVisible: true,
dialogError: error,
});
};
hideErrorDialog = () => {
this.setState({ dialogVisible: false });
};
/** /**
* Navigates to the screen specified in navigation parameters or simply go back tha stack. * Navigates to the screen specified in navigation parameters or simply go back tha stack.
* Saves in user preferences to not show the login banner again. * Saves in user preferences to not show the login banner again.
*/ */
handleSuccess = () => { const handleSuccess = (token: string) => {
const { navigation } = this.props;
// Do not show the home login banner again // Do not show the home login banner again
// TODO if (homeMascot.shouldShow) {
// AsyncStorageManager.set( homeMascot.setShouldShow(false);
// AsyncStorageManager.PREFERENCES.homeShowMascot.key, }
// false saveLoginToken(token);
// ); setLogin(token);
if (this.nextScreen == null) { if (!nextScreen) {
navigation.goBack(); navigation.goBack();
} else { } else {
navigation.replace(this.nextScreen); navigation.replace(nextScreen);
} }
}; };
/**
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
*/
handleNavigationParams() {
this.nextScreen = this.props.route.params?.nextScreen;
}
/**
* Checks if the entered email is valid (matches the regex)
*
* @returns {boolean}
*/
isEmailValid(): boolean {
const { email } = this.state;
return emailRegex.test(email);
}
/**
* Checks if we should tell the user his email is invalid.
* We should only show this if his email is invalid and has been checked when un-focusing the input
*
* @returns {boolean|boolean}
*/
shouldShowEmailError(): boolean {
const { isEmailValidated } = this.state;
return isEmailValidated && !this.isEmailValid();
}
/**
* Checks if the user has entered a password
*
* @returns {boolean}
*/
isPasswordValid(): boolean {
const { password } = this.state;
return password !== '';
}
/**
* Checks if we should tell the user his password is invalid.
* We should only show this if his password is invalid and has been checked when un-focusing the input
*
* @returns {boolean|boolean}
*/
shouldShowPasswordError(): boolean {
const { isPasswordValidated } = this.state;
return isPasswordValidated && !this.isPasswordValid();
}
/**
* If the email and password are valid, and we are not loading a request, then the login button can be enabled
*
* @returns {boolean}
*/
shouldEnableLogin(): boolean {
const { loading } = this.state;
return this.isEmailValid() && this.isPasswordValid() && !loading;
}
render() {
const { mascotDialogVisible, dialogVisible, dialogError } = this.state;
return ( return (
<LinearGradient <LinearGradient
style={GENERAL_STYLES.flex} style={GENERAL_STYLES.flex}
@ -438,7 +125,14 @@ class LoginScreen extends React.Component<Props, StateType> {
keyboardVerticalOffset={100} keyboardVerticalOffset={100}
> >
<CollapsibleScrollView headerColors={'transparent'}> <CollapsibleScrollView headerColors={'transparent'}>
<View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View> <View style={GENERAL_STYLES.flex}>
<LoginForm
loading={loading}
onSubmit={onSubmit}
onResetPasswordPress={onResetPasswordClick}
onHelpPress={showMascotDialog}
/>
</View>
<MascotPopup <MascotPopup
visible={mascotDialogVisible} visible={mascotDialogVisible}
title={i18n.t('screens.login.mascotDialog.title')} title={i18n.t('screens.login.mascotDialog.title')}
@ -448,22 +142,24 @@ class LoginScreen extends React.Component<Props, StateType> {
cancel: { cancel: {
message: i18n.t('screens.login.mascotDialog.button'), message: i18n.t('screens.login.mascotDialog.button'),
icon: 'check', icon: 'check',
onPress: this.hideMascotDialog, onPress: hideMascotDialog,
}, },
}} }}
emotion={MASCOT_STYLE.NORMAL} emotion={MASCOT_STYLE.NORMAL}
/> />
<ErrorDialog <ErrorDialog
visible={dialogVisible} visible={
onDismiss={this.hideErrorDialog} currentError.status !== REQUEST_STATUS.SUCCESS ||
status={dialogError.status} currentError.code !== undefined
code={dialogError.code} }
onDismiss={hideErrorDialog}
status={currentError.status}
code={currentError.code}
/> />
</CollapsibleScrollView> </CollapsibleScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</LinearGradient> </LinearGradient>
); );
} }
}
export default withTheme(LoginScreen); export default LoginScreen;

View file

@ -17,52 +17,29 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useLayoutEffect, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native'; import { View } from 'react-native';
import {
Avatar,
Button,
Card,
Divider,
List,
Paragraph,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import LogoutDialog from '../../components/Amicale/LogoutDialog'; import LogoutDialog from '../../components/Amicale/LogoutDialog';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../components/Overrides/CustomHeaderButton'; } from '../../components/Overrides/CustomHeaderButton';
import CardList from '../../components/Lists/CardList/CardList';
import Mascot, { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList'; import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls';
import RequestScreen from '../../components/Screens/RequestScreen'; import RequestScreen from '../../components/Screens/RequestScreen';
import ConnectionManager from '../../managers/ConnectionManager'; import ProfileWelcomeCard from '../../components/Amicale/Profile/ProfileWelcomeCard';
import { import ProfilePersonalCard from '../../components/Amicale/Profile/ProfilePersonalCard';
getAmicaleServices, import ProfileClubCard from '../../components/Amicale/Profile/ProfileClubCard';
ServiceItemType, import ProfileMembershipCard from '../../components/Amicale/Profile/ProfileMembershipCard';
SERVICES_KEY, import { useNavigation } from '@react-navigation/core';
} from '../../utils/Services'; import { useAuthenticatedRequest } from '../../context/loginContext';
type PropsType = { export type ProfileClubType = {
navigation: StackNavigationProp<any>;
theme: ReactNativePaper.Theme;
};
type StateType = {
dialogVisible: boolean;
};
type ClubType = {
id: number; id: number;
name: string; name: string;
is_manager: boolean; is_manager: boolean;
}; };
type ProfileDataType = { export type ProfileDataType = {
first_name: string; first_name: string;
last_name: string; last_name: string;
email: string; email: string;
@ -71,87 +48,68 @@ type ProfileDataType = {
branch: string; branch: string;
link: string; link: string;
validity: boolean; validity: boolean;
clubs: Array<ClubType>; clubs: Array<ProfileClubType>;
}; };
const styles = StyleSheet.create({ function ProfileScreen() {
card: { const navigation = useNavigation();
margin: 10, const [dialogVisible, setDialogVisible] = useState(false);
}, const request = useAuthenticatedRequest<ProfileDataType>('user/profile');
icon: {
backgroundColor: 'transparent',
},
editButton: {
marginLeft: 'auto',
},
mascot: {
width: 60,
},
title: {
marginLeft: 10,
},
});
class ProfileScreen extends React.Component<PropsType, StateType> { useLayoutEffect(() => {
data: ProfileDataType | undefined; const getHeaderButton = () => (
flatListData: Array<{ id: string }>;
amicaleDataset: Array<ServiceItemType>;
constructor(props: PropsType) {
super(props);
this.data = undefined;
this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
this.amicaleDataset = getAmicaleServices(props.navigation.navigate, [
SERVICES_KEY.PROFILE,
]);
this.state = {
dialogVisible: false,
};
}
componentDidMount() {
const { navigation } = this.props;
navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
/**
* Gets the logout header button
*
* @returns {*}
*/
getHeaderButton = () => (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
title="logout" title={'logout'}
iconName="logout" iconName={'logout'}
onPress={this.showDisconnectDialog} onPress={showDisconnectDialog}
/> />
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
navigation.setOptions({
headerRight: getHeaderButton,
});
}, [navigation]);
/** const getScreen = (data: ProfileDataType | undefined) => {
* Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: ProfileDataType | undefined) => {
const { dialogVisible } = this.state;
if (data) { if (data) {
this.data = data; const flatListData: Array<{
id: string;
render: () => React.ReactElement;
}> = [];
for (let i = 0; i < 4; i++) {
switch (i) {
case 0:
flatListData.push({
id: i.toString(),
render: () => <ProfileWelcomeCard firstname={data?.first_name} />,
});
break;
case 1:
flatListData.push({
id: i.toString(),
render: () => <ProfilePersonalCard profile={data} />,
});
break;
case 2:
flatListData.push({
id: i.toString(),
render: () => <ProfileClubCard clubs={data?.clubs} />,
});
break;
default:
flatListData.push({
id: i.toString(),
render: () => <ProfileMembershipCard valid={data?.validity} />,
});
}
}
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
<CollapsibleFlatList <CollapsibleFlatList renderItem={getRenderItem} data={flatListData} />
renderItem={this.getRenderItem}
data={this.flatListData}
/>
<LogoutDialog <LogoutDialog
visible={dialogVisible} visible={dialogVisible}
onDismiss={this.hideDisconnectDialog} onDismiss={hideDisconnectDialog}
/> />
</View> </View>
); );
@ -160,346 +118,17 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
} }
}; };
getRenderItem = ({ item }: { item: { id: string } }) => { const getRenderItem = ({
switch (item.id) { item,
case '0': }: {
return this.getWelcomeCard(); item: { id: string; render: () => React.ReactElement };
case '1': }) => item.render();
return this.getPersonalCard();
case '2':
return this.getClubCard();
default:
return this.getMembershipCar();
}
};
/** const showDisconnectDialog = () => setDialogVisible(true);
* Gets the list of services available with the Amicale account
* const hideDisconnectDialog = () => setDialogVisible(false);
* @returns {*}
*/ return <RequestScreen request={request} render={getScreen} />;
getServicesList() {
return <CardList dataset={this.amicaleDataset} isHorizontal />;
} }
/** export default ProfileScreen;
* Gets a card welcoming the user to his account
*
* @returns {*}
*/
getWelcomeCard() {
const { navigation } = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t('screens.profile.welcomeTitle', {
name: this.data?.first_name,
})}
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>
{this.getServicesList()}
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
<Divider />
<Card.Actions>
<Button
icon="bug"
mode="contained"
onPress={() => {
navigation.navigate('feedback');
}}
style={styles.editButton}
>
{i18n.t('screens.feedback.homeButtonTitle')}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Gets the given field value.
* If the field does not have a value, returns a placeholder text
*
* @param field The field to get the value from
* @return {*}
*/
static getFieldValue(field?: string): string {
return field ? field : i18n.t('screens.profile.noData');
}
/**
* Gets a list item showing personal information
*
* @param field The field to display
* @param icon The icon to use
* @return {*}
*/
getPersonalListItem(field: string | undefined, icon: string) {
const { theme } = this.props;
const title = field != null ? ProfileScreen.getFieldValue(field) : ':(';
const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
return (
<List.Item
title={title}
description={subtitle}
left={(props) => (
<List.Icon
style={props.style}
icon={icon}
color={field != null ? props.color : theme.colors.textDisabled}
/>
)}
/>
);
}
/**
* Gets a card containing user personal information
*
* @return {*}
*/
getPersonalCard() {
const { theme, navigation } = this.props;
return (
<Card style={styles.card}>
<Card.Title
title={`${this.data?.first_name} ${this.data?.last_name}`}
subtitle={this.data?.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>
{this.getPersonalListItem(this.data?.birthday, 'cake-variant')}
{this.getPersonalListItem(this.data?.phone, 'phone')}
{this.getPersonalListItem(this.data?.email, 'email')}
{this.getPersonalListItem(this.data?.branch, 'school')}
</List.Section>
<Divider />
<Card.Actions>
<Button
icon="account-edit"
mode="contained"
onPress={() => {
navigation.navigate('website', {
host: Urls.websites.amicale,
path: this.data?.link,
title: i18n.t('screens.websites.amicale'),
});
}}
style={styles.editButton}
>
{i18n.t('screens.profile.editInformation')}
</Button>
</Card.Actions>
</Card.Content>
</Card>
);
}
/**
* Gets a cars containing clubs the user is part of
*
* @return {*}
*/
getClubCard() {
const { theme } = this.props;
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 />
{this.getClubList(this.data?.clubs)}
</Card.Content>
</Card>
);
}
/**
* Gets a card showing if the user has payed his membership
*
* @return {*}
*/
getMembershipCar() {
const { theme } = this.props;
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>
{this.getMembershipItem(this.data?.validity === true)}
</List.Section>
</Card.Content>
</Card>
);
}
/**
* Gets the item showing if the user has payed his membership
*
* @return {*}
*/
getMembershipItem(state: boolean) {
const { theme } = this.props;
return (
<List.Item
title={
state
? i18n.t('screens.profile.membershipPayed')
: i18n.t('screens.profile.membershipNotPayed')
}
left={(props) => (
<List.Icon
style={props.style}
color={state ? theme.colors.success : theme.colors.danger}
icon={state ? 'check' : 'close'}
/>
)}
/>
);
}
/**
* Gets a list item for the club list
*
* @param item The club to render
* @return {*}
*/
getClubListItem = ({ item }: { item: ClubType }) => {
const { theme } = this.props;
const onPress = () => {
this.openClubDetailsScreen(item.id);
};
let description = i18n.t('screens.profile.isMember');
let icon = (props: {
color: string;
style: {
marginLeft: number;
marginRight: number;
marginVertical?: number;
};
}) => (
<List.Icon color={props.color} style={props.style} icon="chevron-right" />
);
if (item.is_manager) {
description = i18n.t('screens.profile.isManager');
icon = (props) => (
<List.Icon
style={props.style}
icon="star"
color={theme.colors.primary}
/>
);
}
return (
<List.Item
title={item.name}
description={description}
left={icon}
onPress={onPress}
/>
);
};
/**
* Renders the list of clubs the user is part of
*
* @param list The club list
* @return {*}
*/
getClubList(list: Array<ClubType> | undefined) {
if (!list) {
return null;
}
list.sort(this.sortClubList);
return (
<FlatList
renderItem={this.getClubListItem}
keyExtractor={this.clubKeyExtractor}
data={list}
/>
);
}
clubKeyExtractor = (item: ClubType): string => item.name;
sortClubList = (a: ClubType): number => (a.is_manager ? -1 : 1);
showDisconnectDialog = () => {
this.setState({ dialogVisible: true });
};
hideDisconnectDialog = () => {
this.setState({ dialogVisible: false });
};
/**
* Opens the club details screen for the club of given ID
* @param id The club's id to open
*/
openClubDetailsScreen(id: number) {
const { navigation } = this.props;
navigation.navigate('club-information', { clubId: id });
}
render() {
return (
<RequestScreen<ProfileDataType>
request={() =>
ConnectionManager.getInstance().authenticatedRequest('user/profile')
}
render={this.getScreen}
/>
);
}
}
export default withTheme(ProfileScreen);

View file

@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useRef, useState } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { Button } from 'react-native-paper'; import { Button } from 'react-native-paper';
@ -30,10 +30,10 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable'; import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import ConnectionManager from '../../managers/ConnectionManager';
import WebSectionList, { import WebSectionList, {
SectionListDataType, SectionListDataType,
} from '../../components/Screens/WebSectionList'; } from '../../components/Screens/WebSectionList';
import { useAuthenticatedRequest } from '../../context/loginContext';
export type VoteTeamType = { export type VoteTeamType = {
id: number; id: number;
@ -65,6 +65,13 @@ type ResponseType = {
dates?: VoteDatesStringType; dates?: VoteDatesStringType;
}; };
type FlatlistType = {
teams: Array<VoteTeamType>;
hasVoted: boolean;
datesString?: VoteDatesStringType;
dates?: VoteDatesObjectType;
};
// const FAKE_DATE = { // const FAKE_DATE = {
// "date_begin": "2020-08-19 15:50", // "date_begin": "2020-08-19 15:50",
// "date_end": "2020-08-19 15:50", // "date_end": "2020-08-19 15:50",
@ -113,13 +120,6 @@ type ResponseType = {
// ], // ],
// }; // };
type PropsType = {};
type StateType = {
hasVoted: boolean;
mascotDialogVisible: boolean | undefined;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
marginLeft: 'auto', marginLeft: 'auto',
@ -131,38 +131,19 @@ const styles = StyleSheet.create({
/** /**
* Screen displaying vote information and controls * Screen displaying vote information and controls
*/ */
export default class VoteScreen extends React.Component<PropsType, StateType> { export default function VoteScreen() {
teams: Array<VoteTeamType>; const [hasVoted, setHasVoted] = useState(false);
const [mascotDialogVisible, setMascotDialogVisible] = useState<
undefined | boolean
>(undefined);
hasVoted: boolean; const datesRequest =
useAuthenticatedRequest<VoteDatesStringType>('elections/dates');
datesString: undefined | VoteDatesStringType; const teamsRequest =
useAuthenticatedRequest<TeamResponseType>('elections/teams');
dates: undefined | VoteDatesObjectType;
today: Date;
mainFlatListData: SectionListDataType<{ key: string }>;
refreshData: () => void;
constructor(props: PropsType) {
super(props);
this.teams = [];
this.datesString = undefined;
this.dates = undefined;
this.state = {
hasVoted: false,
mascotDialogVisible: undefined,
};
this.hasVoted = false;
this.today = new Date();
this.refreshData = () => undefined;
this.mainFlatListData = [
{ title: '', data: [{ key: 'main' }, { key: 'info' }] },
];
}
const today = new Date();
const refresh = useRef<() => void | undefined>();
/** /**
* Gets the string representation of the given date. * Gets the string representation of the given date.
* *
@ -173,22 +154,26 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
* @param dateString The string representation of the wanted date * @param dateString The string representation of the wanted date
* @returns {string} * @returns {string}
*/ */
getDateString(date: Date, dateString: string): string { const getDateString = (date: Date, dateString: string) => {
if (this.today.getDate() === date.getDate()) { if (today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString); const str = getTimeOnlyString(dateString);
return str != null ? str : ''; return str != null ? str : '';
} }
return dateString; return dateString;
} };
getMainRenderItem = ({ item }: { item: { key: string } }) => { const getMainRenderItem = ({
item,
}: {
item: { key: string; data?: FlatlistType };
}) => {
if (item.key === 'info') { if (item.key === 'info') {
return ( return (
<View> <View>
<Button <Button
mode="contained" mode="contained"
icon="help-circle" icon="help-circle"
onPress={this.showMascotDialog} onPress={showMascotDialog}
style={styles.button} style={styles.button}
> >
{i18n.t('screens.vote.mascotDialog.title')} {i18n.t('screens.vote.mascotDialog.title')}
@ -196,10 +181,14 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
</View> </View>
); );
} }
return this.getContent(); if (item.data) {
return getContent(item.data);
} else {
return <View />;
}
}; };
createDataset = ( const createDataset = (
data: ResponseType | undefined, data: ResponseType | undefined,
_loading: boolean, _loading: boolean,
_lastRefreshDate: Date | undefined, _lastRefreshDate: Date | undefined,
@ -207,157 +196,158 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
) => { ) => {
// data[0] = FAKE_TEAMS2; // data[0] = FAKE_TEAMS2;
// data[1] = FAKE_DATE; // data[1] = FAKE_DATE;
this.refreshData = refreshData;
const mainFlatListData: SectionListDataType<{
key: string;
data?: FlatlistType;
}> = [
{
title: '',
data: [{ key: 'main' }, { key: 'info' }],
},
];
refresh.current = refreshData;
if (data) { if (data) {
const { teams, dates } = data; const { teams, dates } = data;
const flatlistData: FlatlistType = {
if (dates && dates.date_begin == null) { teams: [],
this.datesString = undefined; hasVoted: false,
} else { };
this.datesString = dates; if (dates && dates.date_begin != null) {
flatlistData.datesString = dates;
} }
if (teams) { if (teams) {
this.teams = teams.teams; flatlistData.teams = teams.teams;
this.hasVoted = teams.has_voted; flatlistData.hasVoted = teams.has_voted;
} }
flatlistData.dates = generateDateObject(flatlistData.datesString);
this.generateDateObject();
} }
return this.mainFlatListData; return mainFlatListData;
}; };
getContent() { const getContent = (data: FlatlistType) => {
const { state } = this; const { dates } = data;
if (!this.isVoteStarted()) { if (!isVoteStarted(dates)) {
return this.getTeaseVoteCard(); return getTeaseVoteCard(data);
} }
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) { if (isVoteRunning(dates) && !data.hasVoted && !hasVoted) {
return this.getVoteCard(); return getVoteCard(data);
} }
if (!this.isResultStarted()) { if (!isResultStarted(dates)) {
return this.getWaitVoteCard(); return getWaitVoteCard(data);
} }
if (this.isResultRunning()) { if (isResultRunning(dates)) {
return this.getVoteResultCard(); return getVoteResultCard(data);
} }
return <VoteNotAvailable />; return <VoteNotAvailable />;
} };
onVoteSuccess = (): void => this.setState({ hasVoted: true });
const onVoteSuccess = () => setHasVoted(true);
/** /**
* The user has not voted yet, and the votes are open * The user has not voted yet, and the votes are open
*/ */
getVoteCard() { const getVoteCard = (data: FlatlistType) => {
return ( return (
<VoteSelect <VoteSelect
teams={this.teams} teams={data.teams}
onVoteSuccess={this.onVoteSuccess} onVoteSuccess={onVoteSuccess}
onVoteError={this.refreshData} onVoteError={() => {
if (refresh.current) {
refresh.current();
}
}}
/> />
); );
} };
/** /**
* Votes have ended, results can be displayed * Votes have ended, results can be displayed
*/ */
getVoteResultCard() { const getVoteResultCard = (data: FlatlistType) => {
if (this.dates != null && this.datesString != null) { if (data.dates != null && data.datesString != null) {
return ( return (
<VoteResults <VoteResults
teams={this.teams} teams={data.teams}
dateEnd={this.getDateString( dateEnd={getDateString(
this.dates.date_result_end, data.dates.date_result_end,
this.datesString.date_result_end data.datesString.date_result_end
)} )}
/> />
); );
} }
return <VoteNotAvailable />; return <VoteNotAvailable />;
} };
/** /**
* Vote will open shortly * Vote will open shortly
*/ */
getTeaseVoteCard() { const getTeaseVoteCard = (data: FlatlistType) => {
if (this.dates != null && this.datesString != null) { if (data.dates != null && data.datesString != null) {
return ( return (
<VoteTease <VoteTease
startDate={this.getDateString( startDate={getDateString(
this.dates.date_begin, data.dates.date_begin,
this.datesString.date_begin data.datesString.date_begin
)} )}
/> />
); );
} }
return <VoteNotAvailable />; return <VoteNotAvailable />;
} };
/** /**
* Votes have ended, or user has voted waiting for results * Votes have ended, or user has voted waiting for results
*/ */
getWaitVoteCard() { const getWaitVoteCard = (data: FlatlistType) => {
const { state } = this;
let startDate = null; let startDate = null;
if ( if (
this.dates != null && data.dates != null &&
this.datesString != null && data.datesString != null &&
this.dates.date_result_begin != null data.dates.date_result_begin != null
) { ) {
startDate = this.getDateString( startDate = getDateString(
this.dates.date_result_begin, data.dates.date_result_begin,
this.datesString.date_result_begin data.datesString.date_result_begin
); );
} }
return ( return (
<VoteWait <VoteWait
startDate={startDate} startDate={startDate}
hasVoted={this.hasVoted || state.hasVoted} hasVoted={data.hasVoted}
justVoted={state.hasVoted} justVoted={hasVoted}
isVoteRunning={this.isVoteRunning()} isVoteRunning={isVoteRunning()}
/> />
); );
}
showMascotDialog = () => {
this.setState({ mascotDialogVisible: true });
}; };
hideMascotDialog = () => { const showMascotDialog = () => setMascotDialogVisible(true);
this.setState({ mascotDialogVisible: false });
const hideMascotDialog = () => setMascotDialogVisible(false);
const isVoteStarted = (dates?: VoteDatesObjectType) => {
return dates != null && today > dates.date_begin;
}; };
isVoteStarted(): boolean { const isResultRunning = (dates?: VoteDatesObjectType) => {
return this.dates != null && this.today > this.dates.date_begin;
}
isResultRunning(): boolean {
return ( return (
this.dates != null && dates != null &&
this.today > this.dates.date_result_begin && today > dates.date_result_begin &&
this.today < this.dates.date_result_end today < dates.date_result_end
); );
} };
isResultStarted(): boolean { const isResultStarted = (dates?: VoteDatesObjectType) => {
return this.dates != null && this.today > this.dates.date_result_begin; return dates != null && today > dates.date_result_begin;
} };
isVoteRunning(): boolean { const isVoteRunning = (dates?: VoteDatesObjectType) => {
return ( return dates != null && today > dates.date_begin && today < dates.date_end;
this.dates != null && };
this.today > this.dates.date_begin &&
this.today < this.dates.date_end
);
}
/** /**
* Generates the objects containing string and Date representations of key vote dates * Generates the objects containing string and Date representations of key vote dates
*/ */
generateDateObject() { const generateDateObject = (
const strings = this.datesString; strings?: VoteDatesStringType
if (strings != null) { ): VoteDatesObjectType | undefined => {
if (strings) {
const dateBegin = stringToDate(strings.date_begin); const dateBegin = stringToDate(strings.date_begin);
const dateEnd = stringToDate(strings.date_end); const dateEnd = stringToDate(strings.date_end);
const dateResultBegin = stringToDate(strings.date_result_begin); const dateResultBegin = stringToDate(strings.date_result_begin);
@ -368,27 +358,25 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
dateResultBegin != null && dateResultBegin != null &&
dateResultEnd != null dateResultEnd != null
) { ) {
this.dates = { return {
date_begin: dateBegin, date_begin: dateBegin,
date_end: dateEnd, date_end: dateEnd,
date_result_begin: dateResultBegin, date_result_begin: dateResultBegin,
date_result_end: dateResultEnd, date_result_end: dateResultEnd,
}; };
} else { } else {
this.dates = undefined; return undefined;
} }
} else { } else {
this.dates = undefined; return undefined;
}
} }
};
request = () => { const request = () => {
return new Promise((resolve: (data: ResponseType) => void) => { return new Promise((resolve: (data: ResponseType) => void) => {
ConnectionManager.getInstance() datesRequest()
.authenticatedRequest<VoteDatesStringType>('elections/dates')
.then((datesData) => { .then((datesData) => {
ConnectionManager.getInstance() teamsRequest()
.authenticatedRequest<TeamResponseType>('elections/teams')
.then((teamsData) => { .then((teamsData) => {
resolve({ resolve({
dates: datesData, dates: datesData,
@ -405,25 +393,16 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
}); });
}; };
/**
* Renders the authenticated screen.
*
* Teams and dates are not mandatory to allow showing the information box even if api requests fail
*
* @returns {*}
*/
render() {
const { state } = this;
return ( return (
<View style={GENERAL_STYLES.flex}> <View style={GENERAL_STYLES.flex}>
<WebSectionList <WebSectionList
request={this.request} request={request}
createDataset={this.createDataset} createDataset={createDataset}
extraData={state.hasVoted.toString()} extraData={hasVoted.toString()}
renderItem={this.getMainRenderItem} renderItem={getMainRenderItem}
/> />
<MascotPopup <MascotPopup
visible={state.mascotDialogVisible} visible={mascotDialogVisible}
title={i18n.t('screens.vote.mascotDialog.title')} title={i18n.t('screens.vote.mascotDialog.title')}
message={i18n.t('screens.vote.mascotDialog.message')} message={i18n.t('screens.vote.mascotDialog.message')}
icon="vote" icon="vote"
@ -431,7 +410,7 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
cancel: { cancel: {
message: i18n.t('screens.vote.mascotDialog.button'), message: i18n.t('screens.vote.mascotDialog.button'),
icon: 'check', icon: 'check',
onPress: this.hideMascotDialog, onPress: hideMascotDialog,
}, },
}} }}
emotion={MASCOT_STYLE.CUTE} emotion={MASCOT_STYLE.CUTE}
@ -439,4 +418,3 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
</View> </View>
); );
} }
}

View file

@ -285,8 +285,9 @@ export default class GameLogic {
getNextPiecesPreviews(): Array<GridType> { getNextPiecesPreviews(): Array<GridType> {
const finalArray = []; const finalArray = [];
for (let i = 0; i < this.nextPieces.length; i += 1) { for (let i = 0; i < this.nextPieces.length; i += 1) {
const gridSize = this.nextPieces[i].getCurrentShape().getCurrentShape()[0] const gridSize = this.nextPieces[i]
.length; .getCurrentShape()
.getCurrentShape()[0].length;
finalArray.push(this.gridManager.getEmptyGrid(gridSize, gridSize)); finalArray.push(this.gridManager.getEmptyGrid(gridSize, gridSize));
this.nextPieces[i].toGrid(finalArray[i], true); this.nextPieces[i].toGrid(finalArray[i], true);
} }

View file

@ -65,9 +65,8 @@ export default class Piece {
* @param grid The grid to remove the piece from * @param grid The grid to remove the piece from
*/ */
removeFromGrid(grid: GridType) { removeFromGrid(grid: GridType) {
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates( const pos: Array<CoordinatesType> =
true this.currentShape.getCellsCoordinates(true);
);
pos.forEach((coordinates: CoordinatesType) => { pos.forEach((coordinates: CoordinatesType) => {
grid[coordinates.y][coordinates.x] = { grid[coordinates.y][coordinates.x] = {
color: this.theme.colors.tetrisBackground, color: this.theme.colors.tetrisBackground,
@ -106,9 +105,8 @@ export default class Piece {
*/ */
isPositionValid(grid: GridType, width: number, height: number): boolean { isPositionValid(grid: GridType, width: number, height: number): boolean {
let isValid = true; let isValid = true;
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates( const pos: Array<CoordinatesType> =
true this.currentShape.getCellsCoordinates(true);
);
for (let i = 0; i < pos.length; i += 1) { for (let i = 0; i < pos.length; i += 1) {
if ( if (
pos[i].x >= width || pos[i].x >= width ||

View file

@ -21,7 +21,7 @@ import * as React from 'react';
import { Linking, Image, StyleSheet } from 'react-native'; import { Linking, Image, StyleSheet } from 'react-native';
import { Card, Text } from 'react-native-paper'; import { Card, Text } from 'react-native-paper';
import Autolink from 'react-native-autolink'; import Autolink from 'react-native-autolink';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackScreenProps } from '@react-navigation/stack';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
} from '../../components/Overrides/CustomHeaderButton'; } from '../../components/Overrides/CustomHeaderButton';
@ -33,11 +33,15 @@ import NewsSourcesConstants, {
AvailablePages, AvailablePages,
} from '../../constants/NewsSourcesConstants'; } from '../../constants/NewsSourcesConstants';
import type { NewsSourceType } from '../../constants/NewsSourcesConstants'; import type { NewsSourceType } from '../../constants/NewsSourcesConstants';
import {
MainRoutes,
MainStackParamsList,
} from '../../navigation/MainNavigator';
type PropsType = { type PropsType = StackScreenProps<
navigation: StackNavigationProp<any>; MainStackParamsList,
route: { params: { data: FeedItemType; date: string } }; MainRoutes.FeedInformation
}; >;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {

View file

@ -46,7 +46,6 @@ import MaterialHeaderButtons, {
Item, Item,
} from '../../components/Overrides/CustomHeaderButton'; } from '../../components/Overrides/CustomHeaderButton';
import AnimatedFAB from '../../components/Animations/AnimatedFAB'; import AnimatedFAB from '../../components/Animations/AnimatedFAB';
import ConnectionManager from '../../managers/ConnectionManager';
import LogoutDialog from '../../components/Amicale/LogoutDialog'; import LogoutDialog from '../../components/Amicale/LogoutDialog';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
@ -59,6 +58,7 @@ import { TabRoutes, TabStackParamsList } from '../../navigation/TabNavigator';
import { ServiceItemType } from '../../utils/Services'; import { ServiceItemType } from '../../utils/Services';
import { useCurrentDashboard } from '../../context/preferencesContext'; import { useCurrentDashboard } from '../../context/preferencesContext';
import { MainRoutes } from '../../navigation/MainNavigator'; import { MainRoutes } from '../../navigation/MainNavigator';
import { useLoginState } from '../../context/loginContext';
const FEED_ITEM_HEIGHT = 500; const FEED_ITEM_HEIGHT = 500;
@ -146,9 +146,7 @@ function HomeScreen(props: Props) {
const [dialogVisible, setDialogVisible] = useState(false); const [dialogVisible, setDialogVisible] = useState(false);
const fabRef = useRef<AnimatedFAB>(null); const fabRef = useRef<AnimatedFAB>(null);
const [isLoggedIn, setIsLoggedIn] = useState( const isLoggedIn = useLoginState();
ConnectionManager.getInstance().isLoggedIn()
);
const { currentDashboard } = useCurrentDashboard(); const { currentDashboard } = useCurrentDashboard();
let homeDashboard: FullDashboardType | null = null; let homeDashboard: FullDashboardType | null = null;
@ -156,7 +154,7 @@ function HomeScreen(props: Props) {
useLayoutEffect(() => { useLayoutEffect(() => {
const getHeaderButton = () => { const getHeaderButton = () => {
let onPressLog = () => let onPressLog = () =>
navigation.navigate('login', { nextScreen: 'profile' }); navigation.navigate(MainRoutes.Login, { nextScreen: 'profile' });
let logIcon = 'login'; let logIcon = 'login';
let logColor = theme.colors.primary; let logColor = theme.colors.primary;
if (isLoggedIn) { if (isLoggedIn) {
@ -192,20 +190,15 @@ function HomeScreen(props: Props) {
const handleNavigationParams = () => { const handleNavigationParams = () => {
const { route } = props; const { route } = props;
if (route.params != null) { if (route.params != null) {
if (route.params.nextScreen != null) { if (route.params.route != null) {
navigation.navigate(route.params.nextScreen, route.params.data); navigation.navigate(route.params.route, route.params.data);
// reset params to prevent infinite loop // reset params to prevent infinite loop
navigation.dispatch(CommonActions.setParams({ nextScreen: null })); navigation.dispatch(CommonActions.setParams({ nextScreen: null }));
} }
} }
}; };
if (ConnectionManager.getInstance().isLoggedIn() !== isLoggedIn) {
setIsLoggedIn(ConnectionManager.getInstance().isLoggedIn());
}
// handle link open when home is not focused or created // handle link open when home is not focused or created
handleNavigationParams(); handleNavigationParams();
return () => {};
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoggedIn]) }, [isLoggedIn])
); );
@ -335,7 +328,7 @@ function HomeScreen(props: Props) {
const hideDisconnectDialog = () => setDialogVisible(false); const hideDisconnectDialog = () => setDialogVisible(false);
const openScanner = () => navigation.navigate('scanner'); const openScanner = () => navigation.navigate(MainRoutes.Scanner);
/** /**
* Creates the dataset to be used in the FlatList * Creates the dataset to be used in the FlatList

View file

@ -26,7 +26,6 @@ import i18n from 'i18n-js';
import { PERMISSIONS, request, RESULTS } from 'react-native-permissions'; import { PERMISSIONS, request, RESULTS } from 'react-native-permissions';
import URLHandler from '../../utils/URLHandler'; import URLHandler from '../../utils/URLHandler';
import AlertDialog from '../../components/Dialogs/AlertDialog'; import AlertDialog from '../../components/Dialogs/AlertDialog';
import { TAB_BAR_HEIGHT } from '../../components/Tabbar/CustomTabBar';
import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog';
import { MASCOT_STYLE } from '../../components/Mascot/Mascot'; import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
@ -223,7 +222,6 @@ class ScannerScreen extends React.Component<{}, StateType> {
<View <View
style={{ style={{
...styles.container, ...styles.container,
marginBottom: TAB_BAR_HEIGHT,
}} }}
> >
{state.hasPermission ? this.getScanner() : this.getPermissionScreen()} {state.hasPermission ? this.getScanner() : this.getPermissionScreen()}

View file

@ -13,13 +13,13 @@ import { Platform, SafeAreaView, View } from 'react-native';
import { useDarkTheme } from '../context/preferencesContext'; import { useDarkTheme } from '../context/preferencesContext';
import { CustomDarkTheme, CustomWhiteTheme } from '../utils/Themes'; import { CustomDarkTheme, CustomWhiteTheme } from '../utils/Themes';
import { setupStatusBar } from '../utils/Utils'; import { setupStatusBar } from '../utils/Utils';
import { ParsedUrlDataType } from '../utils/URLHandler';
type Props = { type Props = {
defaultHomeRoute?: string; defaultData?: ParsedUrlDataType;
defaultHomeData?: { [key: string]: string };
}; };
function MainApp(props: Props, ref?: Ref<NavigationContainerRef>) { function MainApp(props: Props, ref?: Ref<NavigationContainerRef<any>>) {
const darkTheme = useDarkTheme(); const darkTheme = useDarkTheme();
const theme = darkTheme ? CustomDarkTheme : CustomWhiteTheme; const theme = darkTheme ? CustomDarkTheme : CustomWhiteTheme;
@ -44,10 +44,7 @@ function MainApp(props: Props, ref?: Ref<NavigationContainerRef>) {
> >
<SafeAreaView style={GENERAL_STYLES.flex}> <SafeAreaView style={GENERAL_STYLES.flex}>
<NavigationContainer theme={theme} ref={ref}> <NavigationContainer theme={theme} ref={ref}>
<MainNavigator <MainNavigator defaultData={props.defaultData} />
defaultHomeRoute={props.defaultHomeRoute}
defaultHomeData={props.defaultHomeData}
/>
</NavigationContainer> </NavigationContainer>
</SafeAreaView> </SafeAreaView>
</View> </View>

View file

@ -23,11 +23,14 @@ import ImageViewer from 'react-native-image-zoom-viewer';
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack'; import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { MainStackParamsList } from '../../navigation/MainNavigator'; import {
MainRoutes,
MainStackParamsList,
} from '../../navigation/MainNavigator';
type ImageGalleryScreenNavigationProp = StackScreenProps< type ImageGalleryScreenNavigationProp = StackScreenProps<
MainStackParamsList, MainStackParamsList,
'gallery' MainRoutes.Gallery
>; >;
type Props = ImageGalleryScreenNavigationProp & { type Props = ImageGalleryScreenNavigationProp & {

View file

@ -32,6 +32,7 @@ import {
} from '../../../utils/Services'; } from '../../../utils/Services';
import { useNavigation } from '@react-navigation/core'; import { useNavigation } from '@react-navigation/core';
import { useCurrentDashboard } from '../../../context/preferencesContext'; import { useCurrentDashboard } from '../../../context/preferencesContext';
import { useLoginState } from '../../../context/loginContext';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
dashboardContainer: { dashboardContainer: {
@ -63,12 +64,10 @@ const styles = StyleSheet.create({
*/ */
function DashboardEditScreen() { function DashboardEditScreen() {
const navigation = useNavigation(); const navigation = useNavigation();
const isLoggedIn = useLoginState();
const { const { currentDashboard, currentDashboardIdList, updateCurrentDashboard } =
currentDashboard, useCurrentDashboard();
currentDashboardIdList,
updateCurrentDashboard,
} = useCurrentDashboard();
const initialDashboard = useRef(currentDashboardIdList); const initialDashboard = useRef(currentDashboardIdList);
const [activeItem, setActiveItem] = useState(0); const [activeItem, setActiveItem] = useState(0);
@ -150,7 +149,8 @@ function DashboardEditScreen() {
return ( return (
<CollapsibleFlatList <CollapsibleFlatList
data={getCategories(navigation.navigate)} //@ts-ignore
data={getCategories(navigation.navigate, isLoggedIn)}
renderItem={getRenderItem} renderItem={getRenderItem}
ListHeaderComponent={getListHeader()} ListHeaderComponent={getListHeader()}
style={{}} style={{}}

View file

@ -44,6 +44,7 @@ import {
GeneralPreferenceKeys, GeneralPreferenceKeys,
ProxiwashPreferenceKeys, ProxiwashPreferenceKeys,
} from '../../../utils/asyncStorage'; } from '../../../utils/asyncStorage';
import { MainRoutes } from '../../../navigation/MainNavigator';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
slider: { slider: {
@ -204,7 +205,7 @@ function SettingsScreen() {
}; };
const getNavigateItem = ( const getNavigateItem = (
route: string, route: MainRoutes,
icon: string, icon: string,
title: string, title: string,
subtitle: string, subtitle: string,
@ -283,7 +284,7 @@ function SettingsScreen() {
/> />
{getStartScreenPicker()} {getStartScreenPicker()}
{getNavigateItem( {getNavigateItem(
'dashboard-edit', MainRoutes.DashboardEdit,
'view-dashboard', 'view-dashboard',
i18n.t('screens.settings.dashboard'), i18n.t('screens.settings.dashboard'),
i18n.t('screens.settings.dashboardSub') i18n.t('screens.settings.dashboardSub')
@ -328,21 +329,21 @@ function SettingsScreen() {
<List.Section> <List.Section>
{isDebugUnlocked {isDebugUnlocked
? getNavigateItem( ? getNavigateItem(
'debug', MainRoutes.Debug,
'bug-check', 'bug-check',
i18n.t('screens.debug.title'), i18n.t('screens.debug.title'),
'' ''
) )
: null} : null}
{getNavigateItem( {getNavigateItem(
'about', MainRoutes.About,
'information', 'information',
i18n.t('screens.about.title'), i18n.t('screens.about.title'),
i18n.t('screens.about.buttonDesc'), i18n.t('screens.about.buttonDesc'),
unlockDebugMode unlockDebugMode
)} )}
{getNavigateItem( {getNavigateItem(
'feedback', MainRoutes.Feedback,
'comment-quote', 'comment-quote',
i18n.t('screens.feedback.homeButtonTitle'), i18n.t('screens.feedback.homeButtonTitle'),
i18n.t('screens.feedback.homeButtonSubtitle') i18n.t('screens.feedback.homeButtonSubtitle')

View file

@ -81,16 +81,6 @@ function GroupSelectionScreen() {
const favoriteGroups = getFavoriteGroups(); const favoriteGroups = getFavoriteGroups();
useLayoutEffect(() => { useLayoutEffect(() => {
navigation.setOptions({
headerTitle: getSearchBar,
headerBackTitleVisible: false,
headerTitleContainerStyle:
Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 },
});
}, [navigation]);
const getSearchBar = () => { const getSearchBar = () => {
return ( return (
// @ts-ignore // @ts-ignore
@ -100,6 +90,15 @@ function GroupSelectionScreen() {
/> />
); );
}; };
navigation.setOptions({
headerTitle: getSearchBar,
headerBackTitleVisible: false,
headerTitleContainerStyle:
Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' }
: { width: '100%' },
});
}, [navigation]);
/** /**
* Gets a render item for the given article * Gets a render item for the given article

View file

@ -42,6 +42,7 @@ import {
} from '../../utils/asyncStorage'; } from '../../utils/asyncStorage';
import { usePlanexPreferences } from '../../context/preferencesContext'; import { usePlanexPreferences } from '../../context/preferencesContext';
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen'; import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
import { MainRoutes } from '../../navigation/MainNavigator';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -105,7 +106,7 @@ function PlanexScreen() {
* Callback used when the user clicks on the navigate to settings button. * Callback used when the user clicks on the navigate to settings button.
* This will hide the banner and open the SettingsScreen * This will hide the banner and open the SettingsScreen
*/ */
const onGoToSettings = () => navigation.navigate('settings'); const onGoToSettings = () => navigation.navigate(MainRoutes.Settings);
/** /**
* Sends a FullCalendar action to the web page inside the webview. * Sends a FullCalendar action to the web page inside the webview.

View file

@ -21,7 +21,7 @@ import * as React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { Card } from 'react-native-paper'; import { Card } from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackScreenProps } from '@react-navigation/stack';
import { getDateOnlyString, getTimeOnlyString } from '../../utils/Planning'; import { getDateOnlyString, getTimeOnlyString } from '../../utils/Planning';
import DateManager from '../../managers/DateManager'; import DateManager from '../../managers/DateManager';
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen'; import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
@ -33,11 +33,15 @@ import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrol
import type { PlanningEventType } from '../../utils/Planning'; import type { PlanningEventType } from '../../utils/Planning';
import ImageGalleryButton from '../../components/Media/ImageGalleryButton'; import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests'; import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
import {
MainRoutes,
MainStackParamsList,
} from '../../navigation/MainNavigator';
type PropsType = { type PropsType = StackScreenProps<
navigation: StackNavigationProp<any>; MainStackParamsList,
route: { params: { data: PlanningEventType; id: number; eventId: number } }; MainRoutes.PlanningInformation
}; >;
type StateType = { type StateType = {
loading: boolean; loading: boolean;
@ -78,7 +82,7 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
if (props.route.params.data != null) { if (props.route.params.type === 'full') {
this.displayData = props.route.params.data; this.displayData = props.route.params.data;
this.eventId = this.displayData.id; this.eventId = this.displayData.id;
this.shouldFetchData = false; this.shouldFetchData = false;

View file

@ -36,6 +36,7 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup'; import MascotPopup from '../../components/Mascot/MascotPopup';
import GENERAL_STYLES from '../../constants/Styles'; import GENERAL_STYLES from '../../constants/Styles';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import { MainRoutes } from '../../navigation/MainNavigator';
LocaleConfig.locales.fr = { LocaleConfig.locales.fr = {
monthNames: [ monthNames: [
@ -76,6 +77,7 @@ LocaleConfig.locales.fr = {
'Samedi', 'Samedi',
], ],
dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'], dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
today: "Aujourd'hui",
}; };
type PropsType = { type PropsType = {
@ -216,7 +218,7 @@ class PlanningScreen extends React.Component<PropsType, StateType> {
getRenderItem = (item: PlanningEventType) => { getRenderItem = (item: PlanningEventType) => {
const { navigation } = this.props; const { navigation } = this.props;
const onPress = () => { const onPress = () => {
navigation.navigate('planning-information', { navigation.navigate(MainRoutes.PlanningInformation, {
data: item, data: item,
}); });
}; };

View file

@ -19,6 +19,7 @@
import React, { useLayoutEffect, useRef, useState } from 'react'; import React, { useLayoutEffect, useRef, useState } from 'react';
import { import {
Linking,
SectionListData, SectionListData,
SectionListRenderItemInfo, SectionListRenderItemInfo,
StyleSheet, StyleSheet,
@ -51,7 +52,7 @@ import GENERAL_STYLES from '../../constants/Styles';
import { readData } from '../../utils/WebData'; import { readData } from '../../utils/WebData';
import { useNavigation } from '@react-navigation/core'; import { useNavigation } from '@react-navigation/core';
import { setupMachineNotification } from '../../utils/Notifications'; import { setupMachineNotification } from '../../utils/Notifications';
import ProximoListHeader from '../../components/Lists/Proximo/ProximoListHeader'; import ProxiwashListHeader from '../../components/Lists/Proxiwash/ProxiwashListHeader';
import { import {
getPreferenceNumber, getPreferenceNumber,
getPreferenceObject, getPreferenceObject,
@ -60,6 +61,7 @@ import {
} from '../../utils/asyncStorage'; } from '../../utils/asyncStorage';
import { useProxiwashPreferences } from '../../context/preferencesContext'; import { useProxiwashPreferences } from '../../context/preferencesContext';
import { useSubsequentEffect } from '../../utils/customHooks'; import { useSubsequentEffect } from '../../utils/customHooks';
import { MainRoutes } from '../../navigation/MainNavigator';
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -75,7 +77,13 @@ export type ProxiwashMachineType = {
program: string; program: string;
}; };
export type ProxiwashInfoType = {
message: string;
last_checked: number;
};
type FetchedDataType = { type FetchedDataType = {
info: ProxiwashInfoType;
dryers: Array<ProxiwashMachineType>; dryers: Array<ProxiwashMachineType>;
washers: Array<ProxiwashMachineType>; washers: Array<ProxiwashMachineType>;
}; };
@ -99,10 +107,8 @@ function ProxiwashScreen() {
const navigation = useNavigation(); const navigation = useNavigation();
const theme = useTheme(); const theme = useTheme();
const { preferences, updatePreferences } = useProxiwashPreferences(); const { preferences, updatePreferences } = useProxiwashPreferences();
const [ const [modalCurrentDisplayItem, setModalCurrentDisplayItem] =
modalCurrentDisplayItem, useState<React.ReactElement | null>(null);
setModalCurrentDisplayItem,
] = useState<React.ReactElement | null>(null);
const reminder = getPreferenceNumber( const reminder = getPreferenceNumber(
ProxiwashPreferenceKeys.proxiwashNotifications, ProxiwashPreferenceKeys.proxiwashNotifications,
preferences preferences
@ -155,15 +161,22 @@ function ProxiwashScreen() {
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item
title={'web'}
iconName={'open-in-new'}
onPress={() =>
Linking.openURL(ProxiwashConstants[selectedWash].webPageUrl)
}
/>
<Item <Item
title={'information'} title={'information'}
iconName={'information'} iconName={'information'}
onPress={() => navigation.navigate('proxiwash-about')} onPress={() => navigation.navigate(MainRoutes.ProxiwashAbout)}
/> />
</MaterialHeaderButtons> </MaterialHeaderButtons>
), ),
}); });
}, [navigation]); }, [navigation, selectedWash]);
/** /**
* Callback used when the user clicks on enable notifications for a machine * Callback used when the user clicks on enable notifications for a machine
@ -438,7 +451,11 @@ function ProxiwashScreen() {
) => { ) => {
if (data) { if (data) {
return ( return (
<ProximoListHeader date={lastRefreshDate} selectedWash={selectedWash} /> <ProxiwashListHeader
date={lastRefreshDate}
selectedWash={selectedWash}
info={data?.info}
/>
); );
} else { } else {
return null; return null;
@ -478,7 +495,7 @@ function ProxiwashScreen() {
action: { action: {
message: i18n.t('screens.proxiwash.mascotDialog.ok'), message: i18n.t('screens.proxiwash.mascotDialog.ok'),
icon: 'cog', icon: 'cog',
onPress: () => navigation.navigate('settings'), onPress: () => navigation.navigate(MainRoutes.Settings),
}, },
cancel: { cancel: {
message: i18n.t('screens.proxiwash.mascotDialog.cancel'), message: i18n.t('screens.proxiwash.mascotDialog.cancel'),

View file

@ -120,6 +120,7 @@ function ProximoListScreen(props: Props) {
const theme = useTheme(); const theme = useTheme();
const { articles, setArticles } = useCachedProximoArticles(); const { articles, setArticles } = useCachedProximoArticles();
const modalRef = useRef<Modalize>(null); const modalRef = useRef<Modalize>(null);
const navParams = props.route.params;
const [currentSearchString, setCurrentSearchString] = useState(''); const [currentSearchString, setCurrentSearchString] = useState('');
const [currentSortMode, setCurrentSortMode] = useState(2); const [currentSortMode, setCurrentSortMode] = useState(2);
@ -130,6 +131,70 @@ function ProximoListScreen(props: Props) {
const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse]; const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse];
useLayoutEffect(() => { useLayoutEffect(() => {
const getSearchBar = () => {
return (
// @ts-ignore
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={setCurrentSearchString}
autoFocus={navParams.shouldFocusSearchBar}
/>
);
};
const getModalSortMenu = () => {
return (
<View style={styles.modalContainer}>
<Title style={styles.sortTitle}>
{i18n.t('screens.proximo.sortOrder')}
</Title>
<RadioButton.Group
onValueChange={setSortMode}
value={currentSortMode.toString()}
>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPrice')}
value={'0'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPriceReverse')}
value={'1'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortName')}
value={'2'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortNameReverse')}
value={'3'}
/>
</RadioButton.Group>
</View>
);
};
const setSortMode = (mode: string) => {
const currentMode = parseInt(mode, 10);
setCurrentSortMode(currentMode);
if (modalRef.current && currentMode !== currentSortMode) {
modalRef.current.close();
}
};
const getSortMenuButton = () => {
return (
<MaterialHeaderButtons>
<Item
title="main"
iconName="sort"
onPress={() => {
setModalCurrentDisplayItem(getModalSortMenu());
if (modalRef.current) {
modalRef.current.open();
}
}}
/>
</MaterialHeaderButtons>
);
};
navigation.setOptions({ navigation.setOptions({
headerRight: getSortMenuButton, headerRight: getSortMenuButton,
headerTitle: getSearchBar, headerTitle: getSearchBar,
@ -137,21 +202,9 @@ function ProximoListScreen(props: Props) {
headerTitleContainerStyle: headerTitleContainerStyle:
Platform.OS === 'ios' Platform.OS === 'ios'
? { marginHorizontal: 0, width: '70%' } ? { marginHorizontal: 0, width: '70%' }
: { marginHorizontal: 0, right: 50, left: 50 }, : { width: '100%' },
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps }, [navigation, currentSortMode, navParams.shouldFocusSearchBar]);
}, [navigation, currentSortMode]);
/**
* Callback used when clicking on the sort menu button.
* It will open the modal to show a sort selection
*/
const onSortMenuPress = () => {
setModalCurrentDisplayItem(getModalSortMenu());
if (modalRef.current) {
modalRef.current.open();
}
};
/** /**
* Callback used when clicking an article in the list. * Callback used when clicking an article in the list.
@ -166,19 +219,6 @@ function ProximoListScreen(props: Props) {
} }
}; };
/**
* Sets the current sort mode.
*
* @param mode The number representing the mode
*/
const setSortMode = (mode: string) => {
const currentMode = parseInt(mode, 10);
setCurrentSortMode(currentMode);
if (modalRef.current && currentMode !== currentSortMode) {
modalRef.current.close();
}
};
/** /**
* Gets a color depending on the quantity available * Gets a color depending on the quantity available
* *
@ -197,35 +237,6 @@ function ProximoListScreen(props: Props) {
return color; return color;
}; };
/**
* Gets the sort menu header button
*
* @return {*}
*/
const getSortMenuButton = () => {
return (
<MaterialHeaderButtons>
<Item title="main" iconName="sort" onPress={onSortMenuPress} />
</MaterialHeaderButtons>
);
};
/**
* Gets the header search bar
*
* @return {*}
*/
const getSearchBar = () => {
return (
// @ts-ignore
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={setCurrentSearchString}
autoFocus={props.route.params.shouldFocusSearchBar}
/>
);
};
/** /**
* Gets the modal content depending on the given article * Gets the modal content depending on the given article
* *
@ -262,42 +273,6 @@ function ProximoListScreen(props: Props) {
); );
}; };
/**
* Gets the modal content to display a sort menu
*
* @return {*}
*/
const getModalSortMenu = () => {
return (
<View style={styles.modalContainer}>
<Title style={styles.sortTitle}>
{i18n.t('screens.proximo.sortOrder')}
</Title>
<RadioButton.Group
onValueChange={setSortMode}
value={currentSortMode.toString()}
>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPrice')}
value={'0'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortPriceReverse')}
value={'1'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortName')}
value={'2'}
/>
<RadioButton.Item
label={i18n.t('screens.proximo.sortNameReverse')}
value={'3'}
/>
</RadioButton.Group>
</View>
);
};
/** /**
* Gets a render item for the given article * Gets a render item for the given article
* *
@ -341,8 +316,8 @@ function ProximoListScreen(props: Props) {
data: data data: data
.filter( .filter(
(d) => (d) =>
props.route.params.category === -1 || navParams.category === -1 ||
props.route.params.category === d.category_id navParams.category === d.category_id
) )
.sort(sortModes[currentSortMode]), .sort(sortModes[currentSortMode]),
keyExtractor: keyExtractor, keyExtractor: keyExtractor,

View file

@ -32,6 +32,7 @@ import { useNavigation } from '@react-navigation/core';
import { useLayoutEffect } from 'react'; import { useLayoutEffect } from 'react';
import { useCachedProximoCategories } from '../../../context/cacheContext'; import { useCachedProximoCategories } from '../../../context/cacheContext';
import GENERAL_STYLES from '../../../constants/Styles'; import GENERAL_STYLES from '../../../constants/Styles';
import { MainRoutes } from '../../../navigation/MainNavigator';
const LIST_ITEM_HEIGHT = 84; const LIST_ITEM_HEIGHT = 84;
@ -122,10 +123,10 @@ function ProximoMainScreen() {
shouldFocusSearchBar: true, shouldFocusSearchBar: true,
category: -1, category: -1,
}; };
navigation.navigate('proximo-list', searchScreenData); navigation.navigate(MainRoutes.ProximoList, searchScreenData);
}; };
const onPressAboutBtn = () => navigation.navigate('proximo-about'); const onPressAboutBtn = () => navigation.navigate(MainRoutes.ProximoAbout);
const getHeaderButtons = () => { const getHeaderButtons = () => {
return ( return (
@ -170,7 +171,8 @@ function ProximoMainScreen() {
? i18n.t('screens.proximo.articles') ? i18n.t('screens.proximo.articles')
: i18n.t('screens.proximo.article') : i18n.t('screens.proximo.article')
}`; }`;
const onPress = () => navigation.navigate('proximo-list', dataToSend); const onPress = () =>
navigation.navigate(MainRoutes.ProximoList, dataToSend);
if (article_number > 0) { if (article_number > 0) {
return ( return (
<List.Item <List.Item

View file

@ -17,7 +17,7 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as React from 'react'; import React, { useLayoutEffect } from 'react';
import { Image, StyleSheet, View } from 'react-native'; import { Image, StyleSheet, View } from 'react-native';
import { import {
Avatar, Avatar,
@ -25,10 +25,9 @@ import {
Divider, Divider,
List, List,
TouchableRipple, TouchableRipple,
withTheme, useTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import { StackNavigationProp } from '@react-navigation/stack';
import CardList from '../../components/Lists/CardList/CardList'; import CardList from '../../components/Lists/CardList/CardList';
import MaterialHeaderButtons, { import MaterialHeaderButtons, {
Item, Item,
@ -41,11 +40,9 @@ import {
ServiceCategoryType, ServiceCategoryType,
SERVICES_CATEGORIES_KEY, SERVICES_CATEGORIES_KEY,
} from '../../utils/Services'; } from '../../utils/Services';
import { useNavigation } from '@react-navigation/native';
type PropsType = { import { useLoginState } from '../../context/loginContext';
navigation: StackNavigationProp<any>; import { MainRoutes } from '../../navigation/MainNavigator';
theme: ReactNativePaper.Theme;
};
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -61,37 +58,30 @@ const styles = StyleSheet.create({
}, },
}); });
class ServicesScreen extends React.Component<PropsType> { function ServicesScreen() {
finalDataset: Array<ServiceCategoryType>; const navigation = useNavigation();
const theme = useTheme();
const isLoggedIn = useLoginState();
constructor(props: PropsType) { //@ts-ignore
super(props); const finalDataset = getCategories(navigation.navigate, isLoggedIn, [
this.finalDataset = getCategories(props.navigation.navigate, [
SERVICES_CATEGORIES_KEY.SPECIAL, SERVICES_CATEGORIES_KEY.SPECIAL,
]); ]);
}
componentDidMount() { useLayoutEffect(() => {
const { props } = this; const getAboutButton = () => (
props.navigation.setOptions({
headerRight: this.getAboutButton,
});
}
getAboutButton = () => (
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item
title="information" title="information"
iconName="information" iconName="information"
onPress={this.onAboutPress} onPress={() => navigation.navigate(MainRoutes.AmicaleContact)}
/> />
</MaterialHeaderButtons> </MaterialHeaderButtons>
); );
navigation.setOptions({
onAboutPress = () => { headerRight: getAboutButton,
const { props } = this; });
props.navigation.navigate('amicale-contact'); }, [navigation]);
};
/** /**
* Gets the list title image for the list. * Gets the list title image for the list.
@ -102,8 +92,7 @@ class ServicesScreen extends React.Component<PropsType> {
* @param source The source image to display. Can be a string for icons or a number for local images * @param source The source image to display. Can be a string for icons or a number for local images
* @returns {*} * @returns {*}
*/ */
getListTitleImage(source: string | number) { const getListTitleImage = (source: string | number) => {
const { props } = this;
if (typeof source === 'number') { if (typeof source === 'number') {
return <Image source={source} style={styles.image} />; return <Image source={source} style={styles.image} />;
} }
@ -111,11 +100,11 @@ class ServicesScreen extends React.Component<PropsType> {
<Avatar.Icon <Avatar.Icon
size={48} size={48}
icon={source} icon={source}
color={props.theme.colors.primary} color={theme.colors.primary}
style={styles.icon} style={styles.icon}
/> />
); );
} };
/** /**
* A list item showing a list of available services for the current category * A list item showing a list of available services for the current category
@ -123,20 +112,19 @@ class ServicesScreen extends React.Component<PropsType> {
* @param item * @param item
* @returns {*} * @returns {*}
*/ */
getRenderItem = ({ item }: { item: ServiceCategoryType }) => { const getRenderItem = ({ item }: { item: ServiceCategoryType }) => {
const { props } = this;
return ( return (
<TouchableRipple <TouchableRipple
style={styles.container} style={styles.container}
onPress={() => { onPress={() =>
props.navigation.navigate('services-section', { data: item }); navigation.navigate(MainRoutes.ServicesSection, { data: item })
}} }
> >
<View> <View>
<Card.Title <Card.Title
title={item.title} title={item.title}
subtitle={item.subtitle} subtitle={item.subtitle}
left={() => this.getListTitleImage(item.image)} left={() => getListTitleImage(item.image)}
right={() => <List.Icon icon="chevron-right" />} right={() => <List.Icon icon="chevron-right" />}
/> />
<CardList dataset={item.content} isHorizontal /> <CardList dataset={item.content} isHorizontal />
@ -145,15 +133,14 @@ class ServicesScreen extends React.Component<PropsType> {
); );
}; };
keyExtractor = (item: ServiceCategoryType): string => item.title; const keyExtractor = (item: ServiceCategoryType): string => item.title;
render() {
return ( return (
<View> <View>
<CollapsibleFlatList <CollapsibleFlatList
data={this.finalDataset} data={finalDataset}
renderItem={this.getRenderItem} renderItem={getRenderItem}
keyExtractor={this.keyExtractor} keyExtractor={keyExtractor}
ItemSeparatorComponent={() => <Divider />} ItemSeparatorComponent={() => <Divider />}
hasTab hasTab
/> />
@ -172,6 +159,5 @@ class ServicesScreen extends React.Component<PropsType> {
</View> </View>
); );
} }
}
export default withTheme(ServicesScreen); export default ServicesScreen;

View file

@ -18,17 +18,19 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { Collapsible } from 'react-navigation-collapsible';
import { CommonActions } from '@react-navigation/native'; import { CommonActions } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackScreenProps } from '@react-navigation/stack';
import CardList from '../../components/Lists/CardList/CardList'; import CardList from '../../components/Lists/CardList/CardList';
import { ServiceCategoryType } from '../../utils/Services'; import { ServiceCategoryType } from '../../utils/Services';
import {
MainRoutes,
MainStackParamsList,
} from '../../navigation/MainNavigator';
type PropsType = { type PropsType = StackScreenProps<
navigation: StackNavigationProp<any>; MainStackParamsList,
route: { params: { data: ServiceCategoryType | null } }; MainRoutes.ServicesSection
collapsibleStack: Collapsible; >;
};
class ServicesSectionScreen extends React.Component<PropsType> { class ServicesSectionScreen extends React.Component<PropsType> {
finalDataset: null | ServiceCategoryType; finalDataset: null | ServiceCategoryType;

View file

@ -18,15 +18,16 @@
*/ */
import * as React from 'react'; import * as React from 'react';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackScreenProps } from '@react-navigation/stack';
import WebViewScreen from '../../components/Screens/WebViewScreen'; import WebViewScreen from '../../components/Screens/WebViewScreen';
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen'; import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
import Urls from '../../constants/Urls'; import Urls from '../../constants/Urls';
import {
MainRoutes,
MainStackParamsList,
} from '../../navigation/MainNavigator';
type Props = { type Props = StackScreenProps<MainStackParamsList, MainRoutes.Website>;
navigation: StackNavigationProp<any>;
route: { params: { host: string; path: string | null; title: string } };
};
type State = { type State = {
url: string; url: string;

View file

@ -19,24 +19,51 @@
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import PushNotificationIOS from '@react-native-community/push-notification-ios'; import PushNotificationIOS from '@react-native-community/push-notification-ios';
import PushNotification from 'react-native-push-notification'; import PushNotification, {
Importance,
PushNotificationObject,
} from 'react-native-push-notification';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import Update from '../constants/Update';
// Used to multiply the normal notification id to create the reminder one. It allows to find it back easily // Used to multiply the normal notification id to create the reminder one. It allows to find it back easily
const reminderIdFactor = 100; const reminderIdFactor = 100;
// Allows the channel to be updated when the app updates
const channelId = 'reminders' + Update.number;
/**
* Clean channels before creating a new one
*/
function cleanChannels() {
PushNotification.getChannels((idList) => {
idList.forEach((i) => {
if (i !== channelId) {
PushNotification.deleteChannel(i);
}
});
});
}
export function setupNotifications() {
cleanChannels();
PushNotification.channelExists(channelId, (exists) => {
if (!exists) {
PushNotification.createChannel( PushNotification.createChannel(
{ {
channelId: 'reminders', // (required) channelId: channelId, // (required)
channelName: 'Reminders', // (required) channelName: i18n.t('screens.proxiwash.notifications.channel.title'), // (required)
channelDescription: 'Get laundry reminders', // (optional) default: undefined. channelDescription: i18n.t(
'screens.proxiwash.notifications.channel.description'
), // (optional) default: undefined.
playSound: true, // (optional) default: true playSound: true, // (optional) default: true
soundName: 'default', // (optional) See `soundName` parameter of `localNotification` function soundName: 'default', // (optional) See `soundName` parameter of `localNotification` function
importance: 4, // (optional) default: 4. Int value of the Android notification importance importance: Importance.HIGH, // (optional) default: Importance.HIGH. Int value of the Android notification importance
vibrate: true, // (optional) default: true. Creates the default vibration patten if true. vibrate: true, // (optional) default: true. Creates the default vibration patten if true.
}, },
(created) => console.log(`createChannel returned '${created}'`) // (optional) callback returns whether the channel was created, false means it already existed. (created) => console.log(`createChannel returned '${created}'`) // (optional) callback returns whether the channel was created, false means it already existed.
); );
}
});
PushNotification.configure({ PushNotification.configure({
// (required) Called when a remote is received or opened, or local notification is opened // (required) Called when a remote is received or opened, or local notification is opened
@ -69,6 +96,30 @@ PushNotification.configure({
*/ */
requestPermissions: Platform.OS === 'ios', requestPermissions: Platform.OS === 'ios',
}); });
}
const DEFAULT_NOTIFICATIONS_OPTIONS: Partial<PushNotificationObject> = {
/* Android Only Properties */
channelId: channelId, // (required) channelId, if the channel doesn't exist, notification will not trigger.
showWhen: true, // (optional) default: true
autoCancel: true, // (optional) default: true
vibrate: true, // (optional) default: true
vibration: 300, // vibration length in milliseconds, ignored if vibrate=false, default: 1000
priority: 'high', // (optional) set notification priority, default: high
visibility: 'public', // (optional) set notification visibility, default: private
ignoreInForeground: false, // (optional) if true, the notification will not be visible when the app is in the foreground (useful for parity with how iOS notifications appear). should be used in combine with `com.dieam.reactnativepushnotification.notification_foreground` setting
onlyAlertOnce: false, // (optional) alert will open only once with sound and notify, default: false
when: null, // (optional) Add a timestamp (Unix timestamp value in milliseconds) pertaining to the notification (usually the time the event occurred). For apps targeting Build.VERSION_CODES.N and above, this time is not shown anymore by default and must be opted into by using `showWhen`, default: null.
usesChronometer: false, // (optional) Show the `when` field as a stopwatch. Instead of presenting `when` as a timestamp, the notification will show an automatically updating display of the minutes and seconds since when. Useful when showing an elapsed time (like an ongoing phone call), default: false.
timeoutAfter: null, // (optional) Specifies a duration in milliseconds after which this notification should be canceled, if it is not already canceled, default: null
invokeApp: true, // (optional) This enable click on actions to bring back the application to foreground or stay in background, default: true
/* iOS and Android properties */
playSound: true, // (optional) default: true
soundName: 'default', // (optional) Sound to play when the notification is shown. Value of 'default' plays the default sound. It can be set to a custom sound such as 'android.resource://com.xyz/raw/my_sound'. It will look for the 'my_sound' audio file in 'res/raw' directory and play it. default: 'default' (default sound is played)
};
/** /**
* Creates a notification for the given machine id at the given date. * Creates a notification for the given machine id at the given date.
@ -84,6 +135,7 @@ function createNotifications(machineID: string, date: Date, reminder?: number) {
const reminderDate = new Date(date); const reminderDate = new Date(date);
reminderDate.setMinutes(reminderDate.getMinutes() - reminder); reminderDate.setMinutes(reminderDate.getMinutes() - reminder);
PushNotification.localNotificationSchedule({ PushNotification.localNotificationSchedule({
...DEFAULT_NOTIFICATIONS_OPTIONS,
title: i18n.t('screens.proxiwash.notifications.machineRunningTitle', { title: i18n.t('screens.proxiwash.notifications.machineRunningTitle', {
time: reminder, time: reminder,
}), }),
@ -96,6 +148,7 @@ function createNotifications(machineID: string, date: Date, reminder?: number) {
} }
PushNotification.localNotificationSchedule({ PushNotification.localNotificationSchedule({
...DEFAULT_NOTIFICATIONS_OPTIONS,
title: i18n.t('screens.proxiwash.notifications.machineFinishedTitle'), title: i18n.t('screens.proxiwash.notifications.machineFinishedTitle'),
message: i18n.t('screens.proxiwash.notifications.machineFinishedBody', { message: i18n.t('screens.proxiwash.notifications.machineFinishedBody', {
number: machineID, number: machineID,
@ -124,8 +177,8 @@ export function setupMachineNotification(
if (isEnabled && endDate) { if (isEnabled && endDate) {
createNotifications(machineID, endDate, reminder); createNotifications(machineID, endDate, reminder);
} else { } else {
PushNotification.cancelLocalNotifications({ id: machineID }); PushNotification.cancelLocalNotification(machineID);
const reminderId = reminderIdFactor * parseInt(machineID, 10); const reminderId = reminderIdFactor * parseInt(machineID, 10);
PushNotification.cancelLocalNotifications({ id: reminderId.toString() }); PushNotification.cancelLocalNotification(reminderId.toString());
} }
} }

View file

@ -178,9 +178,9 @@ export function isDescriptionEmpty(description?: string): boolean {
* @param numberOfMonths The number of months to create, starting from the current date * @param numberOfMonths The number of months to create, starting from the current date
* @return {Object} * @return {Object}
*/ */
export function generateEmptyCalendar( export function generateEmptyCalendar(numberOfMonths: number): {
numberOfMonths: number [key: string]: Array<PlanningEventType>;
): { [key: string]: Array<PlanningEventType> } { } {
const end = new Date(Date.now()); const end = new Date(Date.now());
end.setMonth(end.getMonth() + numberOfMonths); end.setMonth(end.getMonth() + numberOfMonths);
const daysOfYear: { [key: string]: Array<PlanningEventType> } = {}; const daysOfYear: { [key: string]: Array<PlanningEventType> } = {};

View file

@ -86,8 +86,21 @@ export type ServiceCategoryType = {
content: Array<ServiceItemType>; content: Array<ServiceItemType>;
}; };
function getAmicaleOnPress(
route: MainRoutes,
onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
isLoggedIn: boolean
) {
if (isLoggedIn) {
return () => onPress(route);
} else {
return () => onPress(MainRoutes.Login, { nextScreen: route });
}
}
export function getAmicaleServices( export function getAmicaleServices(
onPress: (route: string, params?: { [key: string]: any }) => void, onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
isLoggedIn: boolean,
excludedItems?: Array<string> excludedItems?: Array<string>
): Array<ServiceItemType> { ): Array<ServiceItemType> {
const amicaleDataset = [ const amicaleDataset = [
@ -96,21 +109,21 @@ export function getAmicaleServices(
title: i18n.t('screens.clubs.title'), title: i18n.t('screens.clubs.title'),
subtitle: i18n.t('screens.services.descriptions.clubs'), subtitle: i18n.t('screens.services.descriptions.clubs'),
image: Urls.images.clubs, image: Urls.images.clubs,
onPress: () => onPress(MainRoutes.ClubList), onPress: getAmicaleOnPress(MainRoutes.ClubList, onPress, isLoggedIn),
}, },
{ {
key: SERVICES_KEY.PROFILE, key: SERVICES_KEY.PROFILE,
title: i18n.t('screens.profile.title'), title: i18n.t('screens.profile.title'),
subtitle: i18n.t('screens.services.descriptions.profile'), subtitle: i18n.t('screens.services.descriptions.profile'),
image: Urls.images.profile, image: Urls.images.profile,
onPress: () => onPress(MainRoutes.Profile), onPress: getAmicaleOnPress(MainRoutes.Profile, onPress, isLoggedIn),
}, },
{ {
key: SERVICES_KEY.EQUIPMENT, key: SERVICES_KEY.EQUIPMENT,
title: i18n.t('screens.equipment.title'), title: i18n.t('screens.equipment.title'),
subtitle: i18n.t('screens.services.descriptions.equipment'), subtitle: i18n.t('screens.services.descriptions.equipment'),
image: Urls.images.equipment, image: Urls.images.equipment,
onPress: () => onPress(MainRoutes.EquipmentList), onPress: getAmicaleOnPress(MainRoutes.EquipmentList, onPress, isLoggedIn),
}, },
{ {
key: SERVICES_KEY.AMICALE_WEBSITE, key: SERVICES_KEY.AMICALE_WEBSITE,
@ -135,7 +148,7 @@ export function getAmicaleServices(
} }
export function getStudentServices( export function getStudentServices(
onPress: (route: string, params?: { [key: string]: any }) => void, onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
excludedItems?: Array<string> excludedItems?: Array<string>
): Array<ServiceItemType> { ): Array<ServiceItemType> {
const studentsDataset = [ const studentsDataset = [
@ -188,7 +201,7 @@ export function getStudentServices(
} }
export function getINSAServices( export function getINSAServices(
onPress: (route: string, params?: { [key: string]: any }) => void, onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
excludedItems?: Array<string> excludedItems?: Array<string>
): Array<ServiceItemType> { ): Array<ServiceItemType> {
const insaDataset = [ const insaDataset = [
@ -261,7 +274,10 @@ export function getINSAServices(
} }
export function getSpecialServices( export function getSpecialServices(
onPress: (route: string, params?: { [key: string]: any }) => void, onPress: (
route: MainRoutes | TabRoutes,
params?: { [key: string]: any }
) => void,
excludedItems?: Array<string> excludedItems?: Array<string>
): Array<ServiceItemType> { ): Array<ServiceItemType> {
const specialDataset = [ const specialDataset = [
@ -288,7 +304,11 @@ export function getSpecialServices(
} }
export function getCategories( export function getCategories(
onPress: (route: string, params?: { [key: string]: any }) => void, onPress: (
route: MainRoutes | TabRoutes,
params?: { [key: string]: any }
) => void,
isLoggedIn: boolean,
excludedItems?: Array<string> excludedItems?: Array<string>
): Array<ServiceCategoryType> { ): Array<ServiceCategoryType> {
const categoriesDataset = [ const categoriesDataset = [
@ -297,7 +317,7 @@ export function getCategories(
title: i18n.t('screens.services.categories.amicale'), title: i18n.t('screens.services.categories.amicale'),
subtitle: i18n.t('screens.services.more'), subtitle: i18n.t('screens.services.more'),
image: AMICALE_LOGO, image: AMICALE_LOGO,
content: getAmicaleServices(onPress), content: getAmicaleServices(onPress, isLoggedIn),
}, },
{ {
key: SERVICES_CATEGORIES_KEY.STUDENTS, key: SERVICES_CATEGORIES_KEY.STUDENTS,

View file

@ -101,13 +101,13 @@ export const CustomWhiteTheme: ReactNativePaper.Theme = {
// Tetris // Tetris
tetrisBackground: '#f0f0f0', tetrisBackground: '#f0f0f0',
tetrisScore: '#e2bd33', tetrisScore: '#e2bd33',
tetrisI: '#3cd9e6', tetrisI: '#be1522',
tetrisO: '#ffdd00', tetrisO: '#EB6C1F',
tetrisT: '#a716e5', tetrisT: '#5cb85c',
tetrisS: '#09c528', tetrisS: '#5294E2',
tetrisZ: '#ff0009', tetrisZ: '#dede00',
tetrisJ: '#2a67e3', tetrisJ: '#69009d',
tetrisL: '#da742d', tetrisL: '#553716',
gameGold: '#ffd610', gameGold: '#ffd610',
gameSilver: '#7b7b7b', gameSilver: '#7b7b7b',
@ -160,13 +160,13 @@ export const CustomDarkTheme: ReactNativePaper.Theme = {
// Tetris // Tetris
tetrisBackground: '#181818', tetrisBackground: '#181818',
tetrisScore: '#e2d707', tetrisScore: '#e2d707',
tetrisI: '#30b3be', tetrisI: '#be1522',
tetrisO: '#c1a700', tetrisO: '#EB6C1F',
tetrisT: '#9114c7', tetrisT: '#5cb85c',
tetrisS: '#08a121', tetrisS: '#5294E2',
tetrisZ: '#b50008', tetrisZ: '#dede00',
tetrisJ: '#0f37b9', tetrisJ: '#69009d',
tetrisL: '#b96226', tetrisL: '#553716',
gameGold: '#ffd610', gameGold: '#ffd610',
gameSilver: '#7b7b7b', gameSilver: '#7b7b7b',

View file

@ -18,10 +18,15 @@
*/ */
import { Linking } from 'react-native'; import { Linking } from 'react-native';
import {
ClubInformationScreenParams,
MainRoutes,
PlanningInformationScreenParams,
} from '../navigation/MainNavigator';
export type ParsedUrlDataType = { export type ParsedUrlDataType = {
route: string; route: MainRoutes.ClubInformation | MainRoutes.PlanningInformation;
data: { [key: string]: string }; data: ClubInformationScreenParams | PlanningInformationScreenParams;
}; };
export type ParsedUrlCallbackType = (parsedData: ParsedUrlDataType) => void; export type ParsedUrlCallbackType = (parsedData: ParsedUrlDataType) => void;
@ -41,10 +46,6 @@ export default class URLHandler {
static EVENT_INFO_URL_PATH = 'event'; static EVENT_INFO_URL_PATH = 'event';
static CLUB_INFO_ROUTE = 'club-information';
static EVENT_INFO_ROUTE = 'planning-information';
onInitialURLParsed: ParsedUrlCallbackType; onInitialURLParsed: ParsedUrlCallbackType;
onDetectURL: ParsedUrlCallbackType; onDetectURL: ParsedUrlCallbackType;
@ -152,8 +153,11 @@ export default class URLHandler {
const id = parseInt(params.id, 10); const id = parseInt(params.id, 10);
if (!Number.isNaN(id)) { if (!Number.isNaN(id)) {
return { return {
route: URLHandler.CLUB_INFO_ROUTE, route: MainRoutes.ClubInformation,
data: { clubId: id.toString() }, data: {
type: 'id',
clubId: id,
},
}; };
} }
} }
@ -173,8 +177,11 @@ export default class URLHandler {
const id = parseInt(params.id, 10); const id = parseInt(params.id, 10);
if (!Number.isNaN(id)) { if (!Number.isNaN(id)) {
return { return {
route: URLHandler.EVENT_INFO_ROUTE, route: MainRoutes.PlanningInformation,
data: { eventId: id.toString() }, data: {
type: 'id',
eventId: id,
},
}; };
} }
} }

View file

@ -80,7 +80,8 @@ export function isApiResponseValid<T>(response: ApiResponseType<T>): boolean {
export async function apiRequest<T>( export async function apiRequest<T>(
path: string, path: string,
method: string, method: string,
params?: object params?: object,
token?: string
): Promise<T> { ): Promise<T> {
return new Promise( return new Promise(
(resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => { (resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
@ -88,7 +89,9 @@ export async function apiRequest<T>(
if (params != null) { if (params != null) {
requestParams = { ...params }; requestParams = { ...params };
} }
console.log(Urls.amicale.api + path); if (token) {
requestParams = { ...requestParams, token: token };
}
fetch(Urls.amicale.api + path, { fetch(Urls.amicale.api + path, {
method, method,
@ -101,11 +104,9 @@ export async function apiRequest<T>(
.then((response: Response) => { .then((response: Response) => {
const status = response.status; const status = response.status;
if (status === REQUEST_STATUS.SUCCESS) { if (status === REQUEST_STATUS.SUCCESS) {
return response.json().then( return response.json().then((data): ApiResponseType<T> => {
(data): ApiResponseType<T> => {
return { status: status, error: data.error, data: data.data }; return { status: status, error: data.error, data: data.data };
} });
);
} else { } else {
return { status: status }; return { status: status };
} }
@ -135,6 +136,33 @@ export async function apiRequest<T>(
); );
} }
export async function connectToAmicale(email: string, password: string) {
return new Promise(
(
resolve: (token: string) => void,
reject: (error: ApiRejectType) => void
) => {
const data = {
email,
password,
};
apiRequest<ApiDataLoginType>('password', 'POST', data)
.then((response: ApiDataLoginType) => {
if (response.token != null) {
resolve(response.token);
} else {
reject({
status: REQUEST_STATUS.SERVER_ERROR,
});
}
})
.catch((err) => {
reject(err);
});
}
);
}
/** /**
* Reads data from the given url and returns it. * Reads data from the given url and returns it.
* *

View file

@ -110,13 +110,17 @@ export const defaultPreferences: { [key in GeneralPreferenceKeys]: string } = {
export function isValidGeneralPreferenceKey( export function isValidGeneralPreferenceKey(
key: string key: string
): key is GeneralPreferenceKeys { ): key is GeneralPreferenceKeys {
return key in Object.values(GeneralPreferenceKeys); return Object.values(GeneralPreferenceKeys).includes(
key as GeneralPreferenceKeys
);
} }
export function isValidMascotPreferenceKey( export function isValidMascotPreferenceKey(
key: string key: string
): key is MascotPreferenceKeys { ): key is MascotPreferenceKeys {
return key in Object.values(MascotPreferenceKeys); return Object.values(MascotPreferenceKeys).includes(
key as MascotPreferenceKeys
);
} }
/** /**

42
src/utils/loginToken.ts Normal file
View file

@ -0,0 +1,42 @@
import * as Keychain from 'react-native-keychain';
/**
* Tries to recover login token from the secure keychain
*
* @returns Promise<string | undefined>
*/
export async function retrieveLoginToken(): Promise<string | undefined> {
return new Promise((resolve: (token: string | undefined) => void) => {
Keychain.getGenericPassword()
.then((data: Keychain.UserCredentials | false) => {
if (data && data.password) {
resolve(data.password);
} else {
resolve(undefined);
}
})
.catch(() => resolve(undefined));
});
}
/**
* Saves the login token in the secure keychain
*
* @param token
* @returns Promise<void>
*/
export async function saveLoginToken(token: string): Promise<void> {
return new Promise((resolve: () => void, reject: () => void) => {
Keychain.setGenericPassword('amicale', token).then(resolve).catch(reject);
});
}
/**
* Deletes the login token from the keychain
*
* @returns Promise<void>
*/
export async function deleteLoginToken(): Promise<void> {
return new Promise((resolve: () => void, reject: () => void) => {
Keychain.resetGenericPassword().then(resolve).catch(reject);
});
}

13
src/utils/logout.ts Normal file
View file

@ -0,0 +1,13 @@
import { useCallback } from 'react';
import { useLogin } from '../context/loginContext';
import { deleteLoginToken } from './loginToken';
export const useLogout = () => {
const { setLogin } = useLogin();
const onLogout = useCallback(() => {
deleteLoginToken();
setLogin(undefined);
}, [setLogin]);
return onLogout;
};

View file

@ -30,17 +30,19 @@
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */ /* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */ // "noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ "typeRoots": [ /* List of folders to include type definitions from. */
"node_modules/@types"
],
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
@ -51,12 +53,11 @@
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
"skipLibCheck": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false
/* Experimental Options */ /* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true
}, },
"exclude": [ "exclude": [
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js" "node_modules", "babel.config.js", "metro.config.js", "jest.config.js"