Compare commits
No commits in common. "master" and "functional-migration" have entirely different histories.
master
...
functional
82 changed files with 4273 additions and 28479 deletions
53
App.tsx
53
App.tsx
|
@ -21,6 +21,7 @@ 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';
|
||||||
|
@ -47,17 +48,15 @@ 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';
|
|
||||||
|
|
||||||
initLocales();
|
// Native optimizations https://reactnavigation.org/docs/react-native-screens
|
||||||
setupNotifications();
|
// Crashes app when navigating away from webview on android 9+
|
||||||
|
// 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 = {
|
||||||
|
@ -68,13 +67,14 @@ 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<any> };
|
navigatorRef: { current: null | NavigationContainerRef };
|
||||||
|
|
||||||
defaultData?: ParsedUrlDataType;
|
defaultHomeRoute: string | undefined;
|
||||||
|
|
||||||
|
defaultHomeData: { [key: string]: string } | undefined;
|
||||||
|
|
||||||
urlHandler: URLHandler;
|
urlHandler: URLHandler;
|
||||||
|
|
||||||
|
@ -88,10 +88,11 @@ 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.defaultData = undefined;
|
this.defaultHomeRoute = 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);
|
||||||
|
@ -105,7 +106,8 @@ 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.defaultData = parsedData;
|
this.defaultHomeRoute = parsedData.route;
|
||||||
|
this.defaultHomeData = parsedData.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,9 +120,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(TabRoutes.Home, {
|
nav.navigate('home', {
|
||||||
nextScreen: parsedData.route,
|
screen: 'index',
|
||||||
data: parsedData.data,
|
params: { nextScreen: parsedData.route, data: parsedData.data },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -134,11 +136,10 @@ export default class App extends React.Component<{}, StateType> {
|
||||||
| PlanexPreferencesType
|
| PlanexPreferencesType
|
||||||
| ProxiwashPreferencesType
|
| ProxiwashPreferencesType
|
||||||
| MascotPreferencesType
|
| MascotPreferencesType
|
||||||
| string
|
| void
|
||||||
| undefined
|
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
const [general, planex, proxiwash, mascot, token] = values;
|
const [general, planex, proxiwash, mascot] = values;
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
initialPreferences: {
|
initialPreferences: {
|
||||||
|
@ -147,7 +148,6 @@ 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
|
||||||
),
|
),
|
||||||
retrieveLoginToken(),
|
ConnectionManager.getInstance().recoverLogin(),
|
||||||
])
|
])
|
||||||
.then(this.onLoadFinished)
|
.then(this.onLoadFinished)
|
||||||
.catch(this.onLoadFinished);
|
.catch(this.onLoadFinished);
|
||||||
|
@ -202,12 +202,11 @@ 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>
|
||||||
|
|
|
@ -141,12 +141,17 @@ 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 49
|
versionCode 45
|
||||||
versionName "5.0.0-3"
|
versionName "4.1.0"
|
||||||
missingDimensionStrategy 'react-native-camera', 'general'
|
missingDimensionStrategy 'react-native-camera', 'general'
|
||||||
}
|
}
|
||||||
splits {
|
splits {
|
||||||
|
@ -233,7 +238,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.implementation
|
from configurations.compile
|
||||||
into 'libs'
|
into 'libs'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -2,18 +2,18 @@
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext {
|
ext {
|
||||||
buildToolsVersion = "30.0.2"
|
buildToolsVersion = "29.0.3"
|
||||||
minSdkVersion = 23
|
minSdkVersion = 23
|
||||||
compileSdkVersion = 30
|
compileSdkVersion = 29
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 29
|
||||||
ndkVersion = "20.1.5948944"
|
ndkVersion = "20.1.5948944"
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:4.2.1")
|
classpath("com.android.tools.build:gradle:4.1.0")
|
||||||
|
|
||||||
// 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,7 +22,6 @@ 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
|
||||||
|
@ -37,6 +36,7 @@ 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' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,4 @@ 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.93.0
|
FLIPPER_VERSION=0.75.1
|
||||||
# Increase Java heap size for compilation
|
|
||||||
org.gradle.jvmargs=-Xmx2048M
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.9-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -126,7 +126,6 @@
|
||||||
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 = (
|
||||||
);
|
);
|
||||||
|
@ -200,24 +199,6 @@
|
||||||
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;
|
||||||
|
@ -407,7 +388,6 @@
|
||||||
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;
|
||||||
|
@ -423,7 +403,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 = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 9.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;
|
||||||
|
@ -464,7 +444,6 @@
|
||||||
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;
|
||||||
|
@ -473,7 +452,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 = 11.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 9.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;
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>5.0.0</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
|
@ -30,25 +30,25 @@
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>4</string>
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
<key>FacebookAdvertiserIDCollectionEnabled</key>
|
<key>FacebookAdvertiserIDCollectionEnabled</key>
|
||||||
<false />
|
<false/>
|
||||||
<key>FacebookAutoInitEnabled</key>
|
<key>FacebookAutoInitEnabled</key>
|
||||||
<false />
|
<false/>
|
||||||
<key>FacebookAutoLogAppEventsEnabled</key>
|
<key>FacebookAutoLogAppEventsEnabled</key>
|
||||||
<false />
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>NSExceptionDomains</key>
|
<key>NSExceptionDomains</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>localhost</key>
|
<key>localhost</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
<true />
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
<string>armv7</string>
|
<string>armv7</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIRequiresFullScreen</key>
|
<key>UIRequiresFullScreen</key>
|
||||||
<true />
|
<true/>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
@ -74,6 +74,6 @@
|
||||||
<key>UIUserInterfaceStyle</key>
|
<key>UIUserInterfaceStyle</key>
|
||||||
<string>Automatic</string>
|
<string>Automatic</string>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<false />
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
@ -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, '11.0'
|
platform :ios, '10.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 => true
|
:hermes_enabled => false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,4 +28,5 @@ 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
|
||||||
|
|
|
@ -47,38 +47,34 @@
|
||||||
"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 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.",
|
"washerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the command central, then press the START button on the machine.\n\nWhen the program is finished, the screen indicates 'Programme terminé', press the yellow button to open the lid and retrieve your laundry.",
|
||||||
"washerTips": "Program 'blanc/couleur': 6kg of dry laundry (cotton linen, linen, underwear, sheets, jeans, towels).\n\nProgram 'non repassable': 3,5 kg of dry laundry (synthetic fibre linen, cotton and polyester mixed).\n\nProgram 'fin 30°C': 2,5 kg of dry laundry (delicate linen in synthetic fibres).\n\nProgram 'laine 30°C': 2,5 kg of dry laundry (wool textiles).",
|
"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 central command , 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 command central, then press the START button on the machine.",
|
||||||
"dryerTips": "The recommended dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.",
|
"dryerTips": "The advised dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.",
|
||||||
"procedure": "Procedure",
|
"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€ per run + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.",
|
"tariff": "Washers 6kg: 3€ the washer + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.",
|
||||||
"paymentMethods": "Cash up to 10€.\nCredit Cards also accepted."
|
"paymentMethods": "Cash up until 10€.\nCredit Card 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€ 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.",
|
"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.",
|
||||||
"paymentMethods": "Credit Cards accepted."
|
"paymentMethods": "Credit Card 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 pick up your laundry.",
|
"finished": "This machine is finished. If you started it, you can get back your laundry.",
|
||||||
"ready": "This machine is empty and ready for use.",
|
"ready": "This machine is empty and ready to use.",
|
||||||
"running": "This machine has been started at %{start} and will end at %{end}.\n\nRemaining time: %{remaining} min.\nProgram: %{program}",
|
"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.",
|
||||||
|
@ -97,18 +93,14 @@
|
||||||
"unknown": "UNKNOWN"
|
"unknown": "UNKNOWN"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"channel": {
|
|
||||||
"title": "Laundry reminders",
|
|
||||||
"description": "Get reminders for watched washers/dryers"
|
|
||||||
},
|
|
||||||
"machineFinishedTitle": "Laundry Ready",
|
"machineFinishedTitle": "Laundry Ready",
|
||||||
"machineFinishedBody": "Machine n°{{number}} is finished and your laundry is ready for pickup",
|
"machineFinishedBody": "The machine n°{{number}} is finished and your laundry is ready to pickup",
|
||||||
"machineRunningTitle": "Laundry running: {{time}} minutes left",
|
"machineRunningTitle": "Laundry running: {{time}} minutes left",
|
||||||
"machineRunningBody": "Machine n°{{number}} is still running"
|
"machineRunningBody": "The 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 another available laundromat, 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 other laundromat available, check the settings !!!!",
|
||||||
"ok": "Settings",
|
"ok": "Settings",
|
||||||
"cancel": "Later"
|
"cancel": "Later"
|
||||||
}
|
}
|
||||||
|
@ -145,12 +137,12 @@
|
||||||
},
|
},
|
||||||
"planex": {
|
"planex": {
|
||||||
"title": "Planex",
|
"title": "Planex",
|
||||||
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button below.",
|
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button bellow.",
|
||||||
"favorites": {
|
"favorites": {
|
||||||
"title": "Favorites",
|
"title": "Favorites",
|
||||||
"empty": {
|
"empty": {
|
||||||
"title": "No favorites",
|
"title": "No favorites",
|
||||||
"subtitle": "Click on the star next to a group to add it to the favorites"
|
"subtitle": "Clic on the star next to a group to add it to the favorites"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mascotDialog": {
|
"mascotDialog": {
|
||||||
|
@ -164,7 +156,7 @@
|
||||||
"amicaleAbout": {
|
"amicaleAbout": {
|
||||||
"title": "A question ?",
|
"title": "A question ?",
|
||||||
"subtitle": "Ask the Amicale",
|
"subtitle": "Ask the Amicale",
|
||||||
"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!",
|
"message": "You want to revive a club?\nYou want to start a new project?\nHere are al the contacts you need! Do not hesitate to write a mail or send a message to the Amicale's Facebook page!",
|
||||||
"roles": {
|
"roles": {
|
||||||
"interSchools": "Inter Schools",
|
"interSchools": "Inter Schools",
|
||||||
"culture": "Culture",
|
"culture": "Culture",
|
||||||
|
@ -189,8 +181,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 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.",
|
"description": "The Proximo is your small grocery store maintained by students directly on the campus. Open every day from 18h30 to 19h30, we welcome you when you are short on pastas or sodas ! Different products for different problems, everything at cost price. You can pay by Lydia or cash.",
|
||||||
"openingHours": "Opening Hours",
|
"openingHours": "Openning Hours",
|
||||||
"paymentMethods": "Payment Methods",
|
"paymentMethods": "Payment Methods",
|
||||||
"paymentMethodsDescription": "Cash or Lydia",
|
"paymentMethodsDescription": "Cash or Lydia",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
@ -220,7 +212,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 opening 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 open hours to create one.",
|
||||||
"button": "OK"
|
"button": "OK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -238,8 +230,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. 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!",
|
"welcomeDescription": "This is your Amicale INSA Toulouse personal space. Bellow are the current services you can access thanks to your account. Feels empty? You're right and we plan on fixing that, so stay tuned!",
|
||||||
"welcomeFeedback": "We plan on doing more! If you have any suggestions or found bugs, please tell us by clicking the button below."
|
"welcomeFeedback": "We plan on doing more! If you have any suggestions or found bugs, please tell us by clicking the button bellow."
|
||||||
},
|
},
|
||||||
"clubs": {
|
"clubs": {
|
||||||
"title": "Clubs",
|
"title": "Clubs",
|
||||||
|
@ -253,10 +245,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 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!",
|
"text": "The clubs, making the campus live, with more than sixty clubs offering various activities! From the philosophy club to the PABI (Production Artisanale de Bière Insaienne), without forgetting the multiple music and dance clubs, you will surely find an activity that suits you!",
|
||||||
"title": "A question ?",
|
"title": "A question ?",
|
||||||
"subtitle": "Ask the Amicale",
|
"subtitle": "Ask the Amicale",
|
||||||
"message": "Do you have a question regarding clubs?\nWant to revive or create a club?\nContact the Amicale at the following address:"
|
"message": "You have a question concerning the clubs?\nYou want to revive or create a club?\nContact the Amicale at the following address:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vote": {
|
"vote": {
|
||||||
|
@ -265,14 +257,14 @@
|
||||||
"select": {
|
"select": {
|
||||||
"title": "Elections open",
|
"title": "Elections open",
|
||||||
"subtitle": "Vote now!",
|
"subtitle": "Vote now!",
|
||||||
"sendButton": "Cast Vote",
|
"sendButton": "Send Vote",
|
||||||
"dialogTitle": "Cast Vote?",
|
"dialogTitle": "Send Vote?",
|
||||||
"dialogTitleLoading": "Casting vote...",
|
"dialogTitleLoading": "Sending vote...",
|
||||||
"dialogMessage": "Are you sure you want to cast your vote? You will not be able to change it."
|
"dialogMessage": "Are you sure you want to send your vote? You will not be able to change it."
|
||||||
},
|
},
|
||||||
"tease": {
|
"tease": {
|
||||||
"title": "Elections incoming",
|
"title": "Elections incoming",
|
||||||
"subtitle": "Get ready to vote!",
|
"subtitle": "Be ready to vote!",
|
||||||
"message": "Vote start:"
|
"message": "Vote start:"
|
||||||
},
|
},
|
||||||
"wait": {
|
"wait": {
|
||||||
|
@ -292,7 +284,7 @@
|
||||||
},
|
},
|
||||||
"mascotDialog": {
|
"mascotDialog": {
|
||||||
"title": "Why vote?",
|
"title": "Why vote?",
|
||||||
"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",
|
"message": "The Amicale's elections is the right moment for you to choose the next team, which will handle different projects on the campus, help organizing your favorite events, animate the campus life during the whole year, and relay your ideas to the administration, so that your campus life is the most enjoyable possible!\nYour turn to make a change!\uD83D\uDE09\n\nNote: If there is only one list, it is still important to vote to show your support, so that the administration knows the current list is supported by students. It is always a plus when taking difficult decisions! \uD83D\uDE09",
|
||||||
"button": "Ok"
|
"button": "Ok"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -317,7 +309,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, 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.",
|
"message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, click the equipment of your choice in the list bellow, enter your lend dates, then come around the Amicale to claim it and give your bail.",
|
||||||
"button": "Ok"
|
"button": "Ok"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -337,7 +329,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 purpose.",
|
"message": "Scanotron 3000 allows you to scan Campus QR codes, created by clubs or event managers, to get more detailed info!\n\nThe camera will never be used for any other purposes.",
|
||||||
"button": "OK"
|
"button": "OK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -348,11 +340,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 set by your system",
|
"nightModeAutoSub": "Follows the mode chosen 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 which services to display on the dashboard",
|
"dashboardSub": "Edit what 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",
|
||||||
|
@ -360,7 +352,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 below.",
|
"message": "The five items above represent your dashboard.\nYou can replace one of its services by selecting it, and then by clicking on the desired new service in the list bellow.",
|
||||||
"undo": "Undo changes"
|
"undo": "Undo changes"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -383,7 +375,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 wouldn't be as cute. She helped to write the text, for communication, and also to create the mascot 🦊.",
|
"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 🦊.",
|
||||||
"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."
|
||||||
|
@ -393,10 +385,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 below.",
|
"feedbackDescription": "Feedback or bugs, you are always welcome.\nChoose your preferred way from the buttons bellow.",
|
||||||
"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 below 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 bellow 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"
|
||||||
},
|
},
|
||||||
|
@ -434,11 +426,11 @@
|
||||||
"intro": {
|
"intro": {
|
||||||
"slideMain": {
|
"slideMain": {
|
||||||
"title": "Welcome to CAMPUS!",
|
"title": "Welcome to CAMPUS!",
|
||||||
"text": "INSA Toulouse's student app! Read along to see everything you can do."
|
"text": "The students app of the INSA Toulouse! Read along to see everything you can do."
|
||||||
},
|
},
|
||||||
"slidePlanex": {
|
"slidePlanex": {
|
||||||
"title": "Prettier Planex",
|
"title": "Prettier Planex",
|
||||||
"text": "Lookup your friends' and your own timetables with a mobile friendly Planex!"
|
"text": "Lookup your and your friends timetable with a mobile friendly Planex!"
|
||||||
},
|
},
|
||||||
"slideEvents": {
|
"slideEvents": {
|
||||||
"title": "Events",
|
"title": "Events",
|
||||||
|
@ -446,7 +438,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 for yourself!"
|
"text": "You can do much more with CAMPUS, but I can't explain everything here. Explore the app to find out!"
|
||||||
},
|
},
|
||||||
"slideDone": {
|
"slideDone": {
|
||||||
"title": "Contribute to the project!",
|
"title": "Contribute to the project!",
|
||||||
|
|
|
@ -55,10 +55,6 @@
|
||||||
"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 !!",
|
||||||
|
@ -97,10 +93,6 @@
|
||||||
"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",
|
||||||
|
|
27204
package-lock.json
generated
27204
package-lock.json
generated
File diff suppressed because it is too large
Load diff
80
package.json
80
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "campus",
|
"name": "campus",
|
||||||
"version": "5.0.0-3",
|
"version": "4.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"android": "react-native run-android",
|
"android": "react-native run-android",
|
||||||
|
@ -14,77 +14,75 @@
|
||||||
"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": "cd android && ./gradlew bundleRelease",
|
"bundle": "npm run full-check && 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.7",
|
"@react-native-async-storage/async-storage": "^1.15.4",
|
||||||
"@react-native-community/masked-view": "0.1.11",
|
"@react-native-community/masked-view": "0.1.11",
|
||||||
"@react-native-community/push-notification-ios": "1.10.1",
|
"@react-native-community/push-notification-ios": "1.8.0",
|
||||||
"@react-native-community/slider": "4.1.6",
|
"@react-native-community/slider": "3.0.3",
|
||||||
"@react-navigation/bottom-tabs": "6.0.5",
|
"@react-navigation/bottom-tabs": "5.11.10",
|
||||||
"@react-navigation/native": "6.0.2",
|
"@react-navigation/native": "5.9.4",
|
||||||
"@react-navigation/stack": "6.0.7",
|
"@react-navigation/stack": "5.14.4",
|
||||||
"i18n-js": "3.8.0",
|
"i18n-js": "3.8.0",
|
||||||
"moment": "2.29.1",
|
"moment": "^2.29.1",
|
||||||
"react": "17.0.2",
|
"react": "17.0.1",
|
||||||
"react-native": "0.65.1",
|
"react-native": "0.64.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.1266.0",
|
"react-native-calendars": "1.1260.0",
|
||||||
"react-native-camera": "4.1.1",
|
"react-native-camera": "3.43.6",
|
||||||
"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.1.4",
|
"react-native-localize": "2.0.3",
|
||||||
"react-native-modalize": "2.0.8",
|
"react-native-modalize": "2.0.8",
|
||||||
"react-native-paper": "4.9.2",
|
"react-native-paper": "4.8.1",
|
||||||
"react-native-permissions": "3.0.5",
|
"react-native-permissions": "3.0.3",
|
||||||
"react-native-push-notification": "8.1.0",
|
"react-native-push-notification": "7.3.0",
|
||||||
"react-native-reanimated": "1.13.2",
|
"react-native-reanimated": "1.13.2",
|
||||||
"react-native-render-html": "6.1.0",
|
"react-native-render-html": "5.1.0",
|
||||||
"react-native-safe-area-context": "3.3.2",
|
"react-native-safe-area-context": "3.2.0",
|
||||||
"react-native-screens": "3.7.0",
|
"react-native-screens": "3.1.1",
|
||||||
"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.13.0",
|
"react-native-webview": "11.4.3",
|
||||||
"react-navigation-collapsible": "6.0.0",
|
"react-navigation-collapsible": "5.9.1",
|
||||||
"react-navigation-header-buttons": "9.0.0"
|
"react-navigation-header-buttons": "7.0.1"
|
||||||
},
|
},
|
||||||
"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": "3.0.1",
|
"@react-native-community/eslint-config": "2.0.0",
|
||||||
"@types/i18n-js": "3.8.2",
|
"@types/i18n-js": "3.8.0",
|
||||||
"@types/jest": "26.0.24",
|
"@types/jest": "26.0.23",
|
||||||
"@types/react": "17.0.3",
|
"@types/react": "17.0.3",
|
||||||
"@types/react-native": "0.65.0",
|
"@types/react-native": "0.64.4",
|
||||||
"@types/react-native-calendars": "1.1264.2",
|
"@types/react-native-calendars": "1.20.10",
|
||||||
"@types/react-native-push-notification": "7.3.2",
|
"@types/react-native-push-notification": "^7.2.0",
|
||||||
"@types/react-native-vector-icons": "6.4.8",
|
"@types/react-native-vector-icons": "6.4.6",
|
||||||
"@types/react-test-renderer": "17.0.1",
|
"@types/react-test-renderer": "17.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "4.31.0",
|
"@typescript-eslint/eslint-plugin": "4.22.1",
|
||||||
"@typescript-eslint/parser": "4.31.0",
|
"@typescript-eslint/parser": "4.22.1",
|
||||||
"babel-jest": "26.6.3",
|
"babel-jest": "26.6.3",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.25.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.66.0",
|
"metro-react-native-babel-preset": "0.64.0",
|
||||||
"prettier": "2.4.0",
|
"prettier": "2.2.1",
|
||||||
"react-native-clean-project": "3.6.7",
|
"react-native-clean-project": "^3.6.3",
|
||||||
"react-native-codegen": "0.0.7",
|
|
||||||
"react-native-version": "4.0.0",
|
"react-native-version": "4.0.0",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.1",
|
||||||
"typescript": "4.4.2"
|
"typescript": "4.2.4"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|
|
@ -1,231 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -20,7 +20,8 @@
|
||||||
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 { useLogout } from '../../utils/logout';
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
@ -28,13 +29,19 @@ type PropsType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function LogoutDialog(props: PropsType) {
|
function LogoutDialog(props: PropsType) {
|
||||||
const onLogout = useLogout();
|
const navigation = useNavigation();
|
||||||
// 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) => {
|
||||||
onLogout();
|
ConnectionManager.getInstance()
|
||||||
props.onDismiss();
|
.disconnect()
|
||||||
resolve();
|
.then(() => {
|
||||||
|
navigation.reset({
|
||||||
|
index: 0,
|
||||||
|
routes: [{ name: 'main' }],
|
||||||
|
});
|
||||||
|
props.onDismiss();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
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
|
|
||||||
);
|
|
|
@ -17,23 +17,30 @@
|
||||||
* 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 React, { useState } from 'react';
|
import * as React from 'react';
|
||||||
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
|
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
|
||||||
import { FlatList, StyleSheet, View } from 'react-native';
|
import { FlatList, StyleSheet, View } from 'react-native';
|
||||||
import 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 Props = {
|
type PropsType = {
|
||||||
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,
|
||||||
|
@ -43,101 +50,118 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function VoteSelect(props: Props) {
|
export default class VoteSelect extends React.PureComponent<
|
||||||
const [selectedTeam, setSelectedTeam] = useState('none');
|
PropsType,
|
||||||
const [voteDialogVisible, setVoteDialogVisible] = useState(false);
|
StateType
|
||||||
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
> {
|
||||||
status: REQUEST_STATUS.SUCCESS,
|
constructor(props: PropsType) {
|
||||||
});
|
super(props);
|
||||||
const request = useAuthenticatedRequest('elections/vote', {
|
this.state = {
|
||||||
team: parseInt(selectedTeam, 10),
|
selectedTeam: 'none',
|
||||||
});
|
voteDialogVisible: false,
|
||||||
|
errorDialogVisible: false,
|
||||||
|
currentError: { status: REQUEST_STATUS.SUCCESS },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
|
onVoteSelectionChange = (teamName: string): void =>
|
||||||
|
this.setState({ selectedTeam: teamName });
|
||||||
|
|
||||||
const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||||
|
|
||||||
|
voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
||||||
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const showVoteDialog = () => setVoteDialogVisible(true);
|
showVoteDialog = (): void => this.setState({ voteDialogVisible: true });
|
||||||
|
|
||||||
const onVoteDialogDismiss = () => setVoteDialogVisible(false);
|
onVoteDialogDismiss = (): void => this.setState({ voteDialogVisible: false });
|
||||||
|
|
||||||
const onVoteDialogAccept = async (): Promise<void> => {
|
onVoteDialogAccept = async (): Promise<void> => {
|
||||||
return new Promise((resolve: () => void) => {
|
return new Promise((resolve: () => void) => {
|
||||||
request()
|
const { state } = this;
|
||||||
|
ConnectionManager.getInstance()
|
||||||
|
.authenticatedRequest('elections/vote', {
|
||||||
|
team: parseInt(state.selectedTeam, 10),
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onVoteDialogDismiss();
|
this.onVoteDialogDismiss();
|
||||||
|
const { props } = this;
|
||||||
props.onVoteSuccess();
|
props.onVoteSuccess();
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error: ApiRejectType) => {
|
.catch((error: ApiRejectType) => {
|
||||||
onVoteDialogDismiss();
|
this.onVoteDialogDismiss();
|
||||||
setCurrentError(error);
|
this.showErrorDialog(error);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onErrorDialogDismiss = () => {
|
showErrorDialog = (error: ApiRejectType): void =>
|
||||||
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
this.setState({
|
||||||
|
errorDialogVisible: true,
|
||||||
|
currentError: error,
|
||||||
|
});
|
||||||
|
|
||||||
|
onErrorDialogDismiss = () => {
|
||||||
|
this.setState({ errorDialogVisible: false });
|
||||||
|
const { props } = this;
|
||||||
props.onVoteError();
|
props.onVoteError();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
render() {
|
||||||
<View>
|
const { state, props } = this;
|
||||||
<Card style={styles.card}>
|
return (
|
||||||
<Card.Title
|
<View>
|
||||||
title={i18n.t('screens.vote.select.title')}
|
<Card style={styles.card}>
|
||||||
subtitle={i18n.t('screens.vote.select.subtitle')}
|
<Card.Title
|
||||||
left={(iconProps) => (
|
title={i18n.t('screens.vote.select.title')}
|
||||||
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
subtitle={i18n.t('screens.vote.select.subtitle')}
|
||||||
)}
|
left={(iconProps) => (
|
||||||
|
<Avatar.Icon size={iconProps.size} icon="alert-decagram" />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Card.Content>
|
||||||
|
<RadioButton.Group
|
||||||
|
onValueChange={this.onVoteSelectionChange}
|
||||||
|
value={state.selectedTeam}
|
||||||
|
>
|
||||||
|
<FlatList
|
||||||
|
data={props.teams}
|
||||||
|
keyExtractor={this.voteKeyExtractor}
|
||||||
|
extraData={state.selectedTeam}
|
||||||
|
renderItem={this.voteRenderItem}
|
||||||
|
/>
|
||||||
|
</RadioButton.Group>
|
||||||
|
</Card.Content>
|
||||||
|
<Card.Actions>
|
||||||
|
<Button
|
||||||
|
icon="send"
|
||||||
|
mode="contained"
|
||||||
|
onPress={this.showVoteDialog}
|
||||||
|
style={styles.button}
|
||||||
|
disabled={state.selectedTeam === 'none'}
|
||||||
|
>
|
||||||
|
{i18n.t('screens.vote.select.sendButton')}
|
||||||
|
</Button>
|
||||||
|
</Card.Actions>
|
||||||
|
</Card>
|
||||||
|
<LoadingConfirmDialog
|
||||||
|
visible={state.voteDialogVisible}
|
||||||
|
onDismiss={this.onVoteDialogDismiss}
|
||||||
|
onAccept={this.onVoteDialogAccept}
|
||||||
|
title={i18n.t('screens.vote.select.dialogTitle')}
|
||||||
|
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
||||||
|
message={i18n.t('screens.vote.select.dialogMessage')}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<ErrorDialog
|
||||||
<RadioButton.Group
|
visible={state.errorDialogVisible}
|
||||||
onValueChange={setSelectedTeam}
|
onDismiss={this.onErrorDialogDismiss}
|
||||||
value={selectedTeam}
|
status={state.currentError.status}
|
||||||
>
|
code={state.currentError.code}
|
||||||
<FlatList
|
/>
|
||||||
data={props.teams}
|
</View>
|
||||||
keyExtractor={voteKeyExtractor}
|
);
|
||||||
extraData={selectedTeam}
|
}
|
||||||
renderItem={voteRenderItem}
|
|
||||||
/>
|
|
||||||
</RadioButton.Group>
|
|
||||||
</Card.Content>
|
|
||||||
<Card.Actions>
|
|
||||||
<Button
|
|
||||||
icon={'send'}
|
|
||||||
mode={'contained'}
|
|
||||||
onPress={showVoteDialog}
|
|
||||||
style={styles.button}
|
|
||||||
disabled={selectedTeam === 'none'}
|
|
||||||
>
|
|
||||||
{i18n.t('screens.vote.select.sendButton')}
|
|
||||||
</Button>
|
|
||||||
</Card.Actions>
|
|
||||||
</Card>
|
|
||||||
<LoadingConfirmDialog
|
|
||||||
visible={voteDialogVisible}
|
|
||||||
onDismiss={onVoteDialogDismiss}
|
|
||||||
onAccept={onVoteDialogAccept}
|
|
||||||
title={i18n.t('screens.vote.select.dialogTitle')}
|
|
||||||
titleLoading={i18n.t('screens.vote.select.dialogTitleLoading')}
|
|
||||||
message={i18n.t('screens.vote.select.dialogMessage')}
|
|
||||||
/>
|
|
||||||
<ErrorDialog
|
|
||||||
visible={
|
|
||||||
currentError.status !== REQUEST_STATUS.SUCCESS ||
|
|
||||||
currentError.code !== undefined
|
|
||||||
}
|
|
||||||
onDismiss={onErrorDialogDismiss}
|
|
||||||
status={currentError.status}
|
|
||||||
code={currentError.code}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VoteSelect;
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ 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;
|
||||||
|
@ -139,7 +138,7 @@ function PlanexBottomBar(props: Props) {
|
||||||
>
|
>
|
||||||
<FAB
|
<FAB
|
||||||
icon={'account-clock'}
|
icon={'account-clock'}
|
||||||
onPress={() => navigation.navigate(MainRoutes.GroupSelect)}
|
onPress={() => navigation.navigate('group-select')}
|
||||||
/>
|
/>
|
||||||
</Animatable.View>
|
</Animatable.View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -72,8 +72,11 @@ function CollapsibleComponent(props: Props) {
|
||||||
}, [collapsible, setCollapsible])
|
}, [collapsible, setCollapsible])
|
||||||
);
|
);
|
||||||
|
|
||||||
const { containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener } =
|
const {
|
||||||
collapsible;
|
containerPaddingTop,
|
||||||
|
scrollIndicatorInsetTop,
|
||||||
|
onScrollWithListener,
|
||||||
|
} = collapsible;
|
||||||
|
|
||||||
const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;
|
const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ 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: {
|
||||||
|
@ -54,7 +53,7 @@ function ActionsDashBoardItem() {
|
||||||
icon="chevron-right"
|
icon="chevron-right"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onPress={(): void => navigation.navigate(MainRoutes.Feedback)}
|
onPress={(): void => navigation.navigate('feedback')}
|
||||||
style={styles.item}
|
style={styles.item}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -30,7 +30,6 @@ 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;
|
||||||
|
@ -68,7 +67,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(MainRoutes.FeedInformation, {
|
navigation.navigate('feed-information', {
|
||||||
data: item,
|
data: item,
|
||||||
date: getFormattedDate(props.item.time),
|
date: getFormattedDate(props.item.time),
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
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,
|
||||||
|
@ -28,10 +29,9 @@ 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,8 +48,7 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
function EquipmentListItem(props: PropsType) {
|
function EquipmentListItem(props: PropsType) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigation = useNavigation();
|
const { item, userDeviceRentDates, navigation, height } = props;
|
||||||
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);
|
||||||
|
@ -57,14 +56,14 @@ function EquipmentListItem(props: PropsType) {
|
||||||
let onPress;
|
let onPress;
|
||||||
if (isRented) {
|
if (isRented) {
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
navigation.navigate(MainRoutes.EquipmentConfirm, {
|
navigation.navigate('equipment-confirm', {
|
||||||
item,
|
item,
|
||||||
dates: userDeviceRentDates,
|
dates: userDeviceRentDates,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
onPress = () => {
|
onPress = () => {
|
||||||
navigation.navigate(MainRoutes.EquipmentRent, { item });
|
navigation.navigate('equipment-rent', { item });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
64
src/components/Lists/Proximo/ProximoListHeader.tsx
Normal file
64
src/components/Lists/Proximo/ProximoListHeader.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
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;
|
|
@ -1,129 +0,0 @@
|
||||||
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;
|
|
|
@ -22,7 +22,6 @@ 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 }>;
|
||||||
|
@ -40,7 +39,7 @@ function ImageGalleryButton(props: PropsType) {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
navigation.navigate(MainRoutes.Gallery, { images: props.images });
|
navigation.navigate('gallery', { images: props.images });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -18,13 +18,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Text, useTheme } from 'react-native-paper';
|
import { Text } from 'react-native-paper';
|
||||||
import HTML, {
|
import HTML from 'react-native-render-html';
|
||||||
CustomRendererProps,
|
import { GestureResponderEvent, Linking } from 'react-native';
|
||||||
TBlock,
|
|
||||||
TText,
|
|
||||||
} from 'react-native-render-html';
|
|
||||||
import { Dimensions, GestureResponderEvent, Linking } from 'react-native';
|
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
html: string;
|
html: string;
|
||||||
|
@ -34,54 +30,37 @@ 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);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Why is this so complex?? I just want to replace the default Text element with the one
|
const getBasicText = (
|
||||||
// from react-native-paper
|
_htmlAttribs: any,
|
||||||
// Might need to read the doc a bit more: https://meliorence.github.io/react-native-render-html/
|
children: any,
|
||||||
// For now this seems to work
|
_convertedCSSStyles: any,
|
||||||
const getBasicText = (rendererProps: CustomRendererProps<TBlock>) => {
|
passProps: any
|
||||||
let text: TText | undefined;
|
) => {
|
||||||
if (rendererProps.tnode.children.length > 0) {
|
return <Text {...passProps}>{children}</Text>;
|
||||||
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
|
||||||
// Surround description with p to allow text styling if the description is not html
|
html={`<p>${props.html}</p>`}
|
||||||
source={{ html: `<p>${props.html}</p>` }}
|
|
||||||
// Use Paper Text instead of React
|
|
||||||
renderers={{
|
renderers={{
|
||||||
p: getBasicText,
|
p: getBasicText,
|
||||||
li: getBasicText,
|
li: getBasicText,
|
||||||
}}
|
}}
|
||||||
// Sometimes we have images inside the text, just ignore them
|
listsPrefixesRenderers={{
|
||||||
ignoredDomTags={['img']}
|
ul: getListBullet,
|
||||||
// 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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,7 +127,6 @@ 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
|
||||||
|
|
|
@ -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 { useLogout } from '../../utils/logout';
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
|
|
||||||
export type RequestScreenProps<T> = {
|
export type RequestScreenProps<T> = {
|
||||||
request: () => Promise<T>;
|
request: () => Promise<T>;
|
||||||
|
@ -44,18 +44,23 @@ 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 [loading, lastRefreshDate, status, code, data, refreshData] =
|
const [
|
||||||
useRequestLogic<T>(
|
loading,
|
||||||
props.request,
|
lastRefreshDate,
|
||||||
props.cache,
|
status,
|
||||||
props.onCacheUpdate,
|
code,
|
||||||
props.refreshOnFocus,
|
data,
|
||||||
MIN_REFRESH_TIME
|
refreshData,
|
||||||
);
|
] = useRequestLogic<T>(
|
||||||
|
props.request,
|
||||||
|
props.cache,
|
||||||
|
props.onCacheUpdate,
|
||||||
|
props.refreshOnFocus,
|
||||||
|
MIN_REFRESH_TIME
|
||||||
|
);
|
||||||
// Store last refresh prop value
|
// Store last refresh prop value
|
||||||
const lastRefresh = useRef<boolean>(false);
|
const lastRefresh = useRef<boolean>(false);
|
||||||
|
|
||||||
|
@ -71,8 +76,7 @@ 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;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [props, loading, refreshData]);
|
||||||
}, [props, loading]);
|
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
React.useCallback(() => {
|
React.useCallback(() => {
|
||||||
|
@ -90,8 +94,7 @@ export default function RequestScreen<T>(props: Props<T>) {
|
||||||
clearInterval(refreshInterval.current);
|
clearInterval(refreshInterval.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
}, [props.cache, props.refreshOnFocus, props.autoRefreshTime, refreshData])
|
||||||
}, [props.cache, props.refreshOnFocus, props.autoRefreshTime])
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const isErrorCritical = (e: API_REQUEST_CODES | undefined) => {
|
const isErrorCritical = (e: API_REQUEST_CODES | undefined) => {
|
||||||
|
@ -100,10 +103,13 @@ export default function RequestScreen<T>(props: Props<T>) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isErrorCritical(code)) {
|
if (isErrorCritical(code)) {
|
||||||
onLogout();
|
ConnectionManager.getInstance()
|
||||||
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
|
.disconnect()
|
||||||
|
.then(() => {
|
||||||
|
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [code, navigation, route, onLogout]);
|
}, [code, navigation, route]);
|
||||||
|
|
||||||
if (data === undefined && loading && props.showLoading !== false) {
|
if (data === undefined && loading && props.showLoading !== false) {
|
||||||
return <BasicLoadingScreen />;
|
return <BasicLoadingScreen />;
|
||||||
|
|
|
@ -141,15 +141,11 @@ 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',
|
||||||
? {
|
text: i18n.t('general.retry'),
|
||||||
icon: 'refresh',
|
onPress: () => refreshData(),
|
||||||
text: i18n.t('general.retry'),
|
}}
|
||||||
onPress: () => refreshData(),
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,6 @@ 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);
|
||||||
|
@ -273,7 +272,6 @@ function WebViewScreen(props: Props) {
|
||||||
onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
|
onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
|
||||||
// Animations
|
// Animations
|
||||||
onScroll={onScrollWithListener(onScroll)}
|
onScroll={onScrollWithListener(onScroll)}
|
||||||
incognito={props.incognito}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 & {
|
props: BottomTabBarProps<any> & {
|
||||||
icons: {
|
icons: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
normal: string;
|
normal: string;
|
||||||
|
@ -94,7 +94,10 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function areEqual(prevProps: BottomTabBarProps, nextProps: BottomTabBarProps) {
|
function areEqual(
|
||||||
|
prevProps: BottomTabBarProps<any>,
|
||||||
|
nextProps: BottomTabBarProps<any>
|
||||||
|
) {
|
||||||
return prevProps.state.index === nextProps.state.index;
|
return prevProps.state.index === nextProps.state.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,13 @@ export default function CollapsibleProvider(props: Props) {
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const [currentCollapsible, setCurrentCollapsible] =
|
const [
|
||||||
useState<CollapsibleContextType>({
|
currentCollapsible,
|
||||||
collapsible: undefined,
|
setCurrentCollapsible,
|
||||||
setCollapsible: setCollapsible,
|
] = useState<CollapsibleContextType>({
|
||||||
});
|
collapsible: undefined,
|
||||||
|
setCollapsible: setCollapsible,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleContext.Provider value={currentCollapsible}>
|
<CollapsibleContext.Provider value={currentCollapsible}>
|
||||||
|
|
|
@ -1,27 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -48,7 +48,6 @@ 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',
|
||||||
|
@ -59,6 +58,5 @@ 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,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,8 +30,6 @@ 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: {
|
||||||
|
@ -50,10 +48,6 @@ 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',
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
|
@ -25,7 +25,6 @@ import {
|
||||||
getSpecialServices,
|
getSpecialServices,
|
||||||
getStudentServices,
|
getStudentServices,
|
||||||
} from '../utils/Services';
|
} from '../utils/Services';
|
||||||
import { useLoginState } from './loginContext';
|
|
||||||
|
|
||||||
const colorScheme = Appearance.getColorScheme();
|
const colorScheme = Appearance.getColorScheme();
|
||||||
|
|
||||||
|
@ -136,7 +135,6 @@ 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
|
||||||
|
@ -147,10 +145,10 @@ export function useCurrentDashboard() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const allDatasets = [
|
const allDatasets = [
|
||||||
...getAmicaleServices((route) => navigation.navigate(route), isLoggedIn),
|
...getAmicaleServices(navigation.navigate),
|
||||||
...getStudentServices((route) => navigation.navigate(route)),
|
...getStudentServices(navigation.navigate),
|
||||||
...getINSAServices((route) => navigation.navigate(route)),
|
...getINSAServices(navigation.navigate),
|
||||||
...getSpecialServices((route) => navigation.navigate(route)),
|
...getSpecialServices(navigation.navigate),
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
currentDashboard: allDatasets.filter((item) =>
|
currentDashboard: allDatasets.filter((item) =>
|
||||||
|
|
205
src/managers/ConnectionManager.ts
Normal file
205
src/managers/ConnectionManager.ts
Normal file
|
@ -0,0 +1,205 @@
|
||||||
|
/*
|
||||||
|
* 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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, { TabRoutes } from './TabNavigator';
|
import TabNavigator 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,10 +33,7 @@ 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, {
|
import ClubListScreen from '../screens/Amicale/Clubs/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';
|
||||||
|
@ -55,18 +52,6 @@ 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',
|
||||||
|
@ -84,7 +69,6 @@ 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',
|
||||||
|
@ -95,72 +79,28 @@ 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 MainStackParamsList = DefaultParams & {
|
export type FullParamsList = DefaultParams & {
|
||||||
[MainRoutes.Login]: { nextScreen: string };
|
'login': { nextScreen: string };
|
||||||
[MainRoutes.EquipmentConfirm]: {
|
'equipment-confirm': {
|
||||||
item?: DeviceType;
|
item?: DeviceType;
|
||||||
dates: [string, string];
|
dates: [string, string];
|
||||||
};
|
};
|
||||||
[MainRoutes.EquipmentRent]: { item?: DeviceType };
|
'equipment-rent': { item?: DeviceType };
|
||||||
[MainRoutes.Gallery]: { images: Array<{ url: string }> };
|
'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;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ClubInformationScreenParams =
|
// 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
|
||||||
type: 'full';
|
export type MainStackParamsList = FullParamsList &
|
||||||
data: ClubType;
|
Record<string, object | undefined>;
|
||||||
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>();
|
||||||
|
|
||||||
|
@ -178,62 +118,6 @@ 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 (
|
||||||
<>
|
<>
|
||||||
|
@ -349,6 +233,55 @@ 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}
|
||||||
|
@ -363,75 +296,32 @@ 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, isloggedIn, createTabNavigator } = props;
|
const { showIntro, createTabNavigator } = props;
|
||||||
return (
|
return (
|
||||||
<MainStack.Navigator
|
<MainStack.Navigator
|
||||||
initialRouteName={showIntro ? MainRoutes.Intro : MainRoutes.Main}
|
initialRouteName={showIntro ? MainRoutes.Intro : MainRoutes.Main}
|
||||||
screenOptions={{
|
headerMode={'screen'}
|
||||||
headerMode: 'float',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{showIntro ? getIntroScreens() : getRegularScreens(createTabNavigator)}
|
{showIntro ? getIntroScreens() : getRegularScreens(createTabNavigator)}
|
||||||
{isloggedIn ? getAmicaleScreens() : null}
|
|
||||||
</MainStack.Navigator>
|
</MainStack.Navigator>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
defaultData?: ParsedUrlDataType;
|
defaultHomeRoute?: string;
|
||||||
|
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
|
||||||
|
@ -440,7 +330,6 @@ function MainNavigator(props: PropsType) {
|
||||||
return (
|
return (
|
||||||
<MainStackComponent
|
<MainStackComponent
|
||||||
showIntro={showIntro !== false}
|
showIntro={showIntro !== false}
|
||||||
isloggedIn={isloggedIn}
|
|
||||||
createTabNavigator={createTabNavigator}
|
createTabNavigator={createTabNavigator}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -448,5 +337,7 @@ function MainNavigator(props: PropsType) {
|
||||||
|
|
||||||
export default React.memo(
|
export default React.memo(
|
||||||
MainNavigator,
|
MainNavigator,
|
||||||
(pp: PropsType, np: PropsType) => pp.defaultData === np.defaultData
|
(pp: PropsType, np: PropsType) =>
|
||||||
|
pp.defaultHomeRoute === np.defaultHomeRoute &&
|
||||||
|
pp.defaultHomeData === np.defaultHomeData
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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';
|
||||||
|
@ -26,17 +27,24 @@ 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: {
|
||||||
|
@ -52,24 +60,183 @@ 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 TabStackParamsList = DefaultParams & {
|
export type FullParamsList = DefaultParams & {
|
||||||
[TabRoutes.Home]: ParsedUrlDataType;
|
[TabRoutes.Home]: {
|
||||||
|
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 = {
|
||||||
defaultData?: ParsedUrlDataType;
|
defaultHomeRoute?: string;
|
||||||
|
defaultHomeData?: { [key: string]: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
const ICONS: {
|
const ICONS: {
|
||||||
|
@ -111,7 +278,9 @@ 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;
|
||||||
|
@ -124,62 +293,35 @@ function TabNavigator(props: PropsType) {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
initialRouteName={defaultRoute as TabRoutes}
|
initialRouteName={defaultRoute}
|
||||||
tabBar={(tabProps) => (
|
tabBar={(tabProps) => (
|
||||||
<CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
|
<CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name={TabRoutes.Services}
|
name={'services'}
|
||||||
component={WebsitesHomeScreen}
|
component={ServicesStackComponent}
|
||||||
options={{ title: i18n.t('screens.services.title') }}
|
options={{ title: i18n.t('screens.services.title') }}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name={TabRoutes.Proxiwash}
|
name={'proxiwash'}
|
||||||
component={ProxiwashScreen}
|
component={ProxiwashStackComponent}
|
||||||
options={{ title: i18n.t('screens.proxiwash.title') }}
|
options={{ title: i18n.t('screens.proxiwash.title') }}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name={TabRoutes.Home}
|
name={'home'}
|
||||||
component={HomeScreen}
|
component={createHomeStackComponent}
|
||||||
options={{
|
options={{ title: i18n.t('screens.home.title') }}
|
||||||
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={TabRoutes.Planning}
|
name={'events'}
|
||||||
component={PlanningScreen}
|
component={PlanningStackComponent}
|
||||||
options={{ title: i18n.t('screens.planning.title') }}
|
options={{ title: i18n.t('screens.planning.title') }}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name={TabRoutes.Planex}
|
name={'planex'}
|
||||||
component={PlanexScreen}
|
component={PlanexStackComponent}
|
||||||
options={{
|
options={{ title: i18n.t('screens.planex.title') }}
|
||||||
title: i18n.t('screens.planex.title'),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
);
|
);
|
||||||
|
@ -187,5 +329,15 @@ function TabNavigator(props: PropsType) {
|
||||||
|
|
||||||
export default React.memo(
|
export default React.memo(
|
||||||
TabNavigator,
|
TabNavigator,
|
||||||
(pp: PropsType, np: PropsType) => pp.defaultData === np.defaultData
|
(pp: PropsType, np: PropsType) =>
|
||||||
|
pp.defaultHomeRoute === np.defaultHomeRoute &&
|
||||||
|
pp.defaultHomeData === np.defaultHomeData
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export enum TabRoutes {
|
||||||
|
Services = 'services',
|
||||||
|
Proxiwash = 'proxiwash',
|
||||||
|
Home = 'home',
|
||||||
|
Planning = 'events',
|
||||||
|
Planex = 'planex',
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,6 @@ 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');
|
||||||
|
|
||||||
|
@ -180,7 +179,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
|
||||||
{
|
{
|
||||||
onPressCallback: () => {
|
onPressCallback: () => {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.navigate(MainRoutes.Feedback);
|
navigation.navigate('feedback');
|
||||||
},
|
},
|
||||||
icon: 'bug',
|
icon: 'bug',
|
||||||
text: i18n.t('screens.feedback.homeButtonTitle'),
|
text: i18n.t('screens.feedback.homeButtonTitle'),
|
||||||
|
@ -237,7 +236,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
|
||||||
{
|
{
|
||||||
onPressCallback: () => {
|
onPressCallback: () => {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.navigate(MainRoutes.Dependencies);
|
navigation.navigate('dependencies');
|
||||||
},
|
},
|
||||||
icon: 'developer-board',
|
icon: 'developer-board',
|
||||||
text: i18n.t('screens.about.libs'),
|
text: i18n.t('screens.about.libs'),
|
||||||
|
@ -276,7 +275,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
|
||||||
{
|
{
|
||||||
onPressCallback: () => {
|
onPressCallback: () => {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.navigate(MainRoutes.Feedback);
|
navigation.navigate('feedback');
|
||||||
},
|
},
|
||||||
icon: 'hand-pointing-right',
|
icon: 'hand-pointing-right',
|
||||||
text: i18n.t('screens.about.user.you'),
|
text: i18n.t('screens.about.user.you'),
|
||||||
|
|
|
@ -64,8 +64,10 @@ function DebugScreen() {
|
||||||
const modalRef = useRef<Modalize>(null);
|
const modalRef = useRef<Modalize>(null);
|
||||||
|
|
||||||
const [modalInputValue, setModalInputValue] = useState<string>('');
|
const [modalInputValue, setModalInputValue] = useState<string>('');
|
||||||
const [modalCurrentDisplayItem, setModalCurrentDisplayItem] =
|
const [
|
||||||
useState<PreferenceItemType | null>(null);
|
modalCurrentDisplayItem,
|
||||||
|
setModalCurrentDisplayItem,
|
||||||
|
] = useState<PreferenceItemType | null>(null);
|
||||||
|
|
||||||
const currentPreferences: Array<PreferenceItemType> = [];
|
const currentPreferences: Array<PreferenceItemType> = [];
|
||||||
Object.values(GeneralPreferenceKeys).forEach((key) => {
|
Object.values(GeneralPreferenceKeys).forEach((key) => {
|
||||||
|
|
|
@ -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 React, { useState } from 'react';
|
import * as React from 'react';
|
||||||
import { Linking, StyleSheet, View } from 'react-native';
|
import { Linking, StyleSheet, View } from 'react-native';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -25,26 +25,29 @@ import {
|
||||||
Card,
|
Card,
|
||||||
Chip,
|
Chip,
|
||||||
Paragraph,
|
Paragraph,
|
||||||
useTheme,
|
withTheme,
|
||||||
} 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 { useFocusEffect } from '@react-navigation/core';
|
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||||
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 Props = StackScreenProps<MainStackParamsList, MainRoutes.ClubInformation>;
|
type PropsType = {
|
||||||
|
navigation: StackNavigationProp<any>;
|
||||||
|
route: {
|
||||||
|
params?: {
|
||||||
|
data?: ClubType;
|
||||||
|
categories?: Array<ClubCategoryType>;
|
||||||
|
clubId?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
theme: ReactNativePaper.Theme;
|
||||||
|
};
|
||||||
|
|
||||||
type ResponseType = ClubType;
|
type ResponseType = ClubType;
|
||||||
|
|
||||||
|
@ -86,28 +89,33 @@ 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
|
||||||
*/
|
*/
|
||||||
function ClubDisplayScreen(props: Props) {
|
class ClubDisplayScreen extends React.Component<PropsType> {
|
||||||
const navigation = useNavigation();
|
displayData: ClubType | undefined;
|
||||||
const theme = useTheme();
|
|
||||||
|
|
||||||
const [displayData, setDisplayData] = useState<ClubType | undefined>();
|
categories: Array<ClubCategoryType> | null;
|
||||||
const [categories, setCategories] = useState<
|
|
||||||
Array<ClubCategoryType> | undefined
|
|
||||||
>();
|
|
||||||
const [clubId, setClubId] = useState<number | undefined>();
|
|
||||||
|
|
||||||
useFocusEffect(
|
clubId: number;
|
||||||
useCallback(() => {
|
|
||||||
if (props.route.params.type === 'full') {
|
shouldFetchData: boolean;
|
||||||
setDisplayData(props.route.params.data);
|
|
||||||
setCategories(props.route.params.categories);
|
constructor(props: PropsType) {
|
||||||
setClubId(props.route.params.data.id);
|
super(props);
|
||||||
} else {
|
this.displayData = undefined;
|
||||||
const id = props.route.params.clubId;
|
this.categories = null;
|
||||||
setClubId(id ? id : 0);
|
this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0;
|
||||||
}
|
this.shouldFetchData = true;
|
||||||
}, [props.route.params])
|
|
||||||
);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the name of the category with the given ID
|
* Gets the name of the category with the given ID
|
||||||
|
@ -115,17 +123,17 @@ function ClubDisplayScreen(props: Props) {
|
||||||
* @param id The category's ID
|
* @param id The category's ID
|
||||||
* @returns {string|*}
|
* @returns {string|*}
|
||||||
*/
|
*/
|
||||||
const getCategoryName = (id: number): string => {
|
getCategoryName(id: number): string {
|
||||||
let categoryName = '';
|
let categoryName = '';
|
||||||
if (categories) {
|
if (this.categories !== null) {
|
||||||
categories.forEach((item: ClubCategoryType) => {
|
this.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
|
||||||
|
@ -133,23 +141,23 @@ function ClubDisplayScreen(props: Props) {
|
||||||
* @param categories The categories to display (max 2)
|
* @param categories The categories to display (max 2)
|
||||||
* @returns {null|*}
|
* @returns {null|*}
|
||||||
*/
|
*/
|
||||||
const getCategoriesRender = (c: Array<number | null>) => {
|
getCategoriesRender(categories: Array<number | null>) {
|
||||||
if (!categories) {
|
if (this.categories == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const final: Array<React.ReactNode> = [];
|
const final: Array<React.ReactNode> = [];
|
||||||
c.forEach((cat: number | null) => {
|
categories.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}>
|
||||||
{getCategoryName(cat)}
|
{this.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
|
||||||
|
@ -158,7 +166,8 @@ function ClubDisplayScreen(props: Props) {
|
||||||
* @param email The club contact email
|
* @param email The club contact email
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
const getManagersRender = (managers: Array<string>, email: string | null) => {
|
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>);
|
||||||
|
@ -182,18 +191,22 @@ function ClubDisplayScreen(props: Props) {
|
||||||
<Avatar.Icon
|
<Avatar.Icon
|
||||||
size={iconProps.size}
|
size={iconProps.size}
|
||||||
style={styles.icon}
|
style={styles.icon}
|
||||||
color={hasManagers ? theme.colors.success : theme.colors.primary}
|
color={
|
||||||
|
hasManagers
|
||||||
|
? props.theme.colors.success
|
||||||
|
: props.theme.colors.primary
|
||||||
|
}
|
||||||
icon="account-tie"
|
icon="account-tie"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
{managersListView}
|
{managersListView}
|
||||||
{getEmailButton(email, hasManagers)}
|
{ClubDisplayScreen.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
|
||||||
|
@ -202,7 +215,7 @@ function ClubDisplayScreen(props: Props) {
|
||||||
* @param hasManagers True if the club has managers
|
* @param hasManagers True if the club has managers
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
const getEmailButton = (email: string | null, hasManagers: boolean) => {
|
static 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 =
|
||||||
|
@ -223,14 +236,14 @@ function ClubDisplayScreen(props: Props) {
|
||||||
</Button>
|
</Button>
|
||||||
</Card.Actions>
|
</Card.Actions>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const getScreen = (data: ResponseType | undefined) => {
|
getScreen = (data: ResponseType | undefined) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
updateHeaderTitle(data);
|
this.updateHeaderTitle(data);
|
||||||
return (
|
return (
|
||||||
<CollapsibleScrollView style={styles.scroll} hasTab>
|
<CollapsibleScrollView style={styles.scroll} hasTab>
|
||||||
{getCategoriesRender(data.category)}
|
{this.getCategoriesRender(data.category)}
|
||||||
{data.logo !== null ? (
|
{data.logo !== null ? (
|
||||||
<ImageGalleryButton
|
<ImageGalleryButton
|
||||||
images={[{ url: data.logo }]}
|
images={[{ url: data.logo }]}
|
||||||
|
@ -248,7 +261,7 @@ function ClubDisplayScreen(props: Props) {
|
||||||
) : (
|
) : (
|
||||||
<View />
|
<View />
|
||||||
)}
|
)}
|
||||||
{getManagersRender(data.responsibles, data.email)}
|
{this.getManagersRender(data.responsibles, data.email)}
|
||||||
</CollapsibleScrollView>
|
</CollapsibleScrollView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -260,22 +273,27 @@ function ClubDisplayScreen(props: Props) {
|
||||||
*
|
*
|
||||||
* @param data The club data
|
* @param data The club data
|
||||||
*/
|
*/
|
||||||
const updateHeaderTitle = (data: ClubType) => {
|
updateHeaderTitle(data: ClubType) {
|
||||||
navigation.setOptions({ title: data.name });
|
const { props } = this;
|
||||||
};
|
props.navigation.setOptions({ title: data.name });
|
||||||
|
}
|
||||||
|
|
||||||
const request = useAuthenticatedRequest<ClubType>('clubs/info', {
|
render() {
|
||||||
id: clubId,
|
if (this.shouldFetchData) {
|
||||||
});
|
return (
|
||||||
|
<RequestScreen
|
||||||
return (
|
request={() =>
|
||||||
<RequestScreen
|
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
||||||
request={request}
|
'clubs/info',
|
||||||
render={getScreen}
|
{ id: this.clubId }
|
||||||
cache={displayData}
|
)
|
||||||
onCacheUpdate={setDisplayData}
|
}
|
||||||
/>
|
render={this.getScreen}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.getScreen(this.displayData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ClubDisplayScreen;
|
export default withTheme(ClubDisplayScreen);
|
||||||
|
|
|
@ -17,10 +17,11 @@
|
||||||
* 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 React, { useLayoutEffect, useRef, useState } from 'react';
|
import * as React from 'react';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
import { Searchbar } from 'react-native-paper';
|
import { Searchbar } from 'react-native-paper';
|
||||||
import 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,
|
||||||
|
@ -30,10 +31,8 @@ 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;
|
||||||
|
@ -50,6 +49,15 @@ 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>;
|
||||||
|
@ -57,50 +65,33 @@ type ResponseType = {
|
||||||
|
|
||||||
const LIST_ITEM_HEIGHT = 96;
|
const LIST_ITEM_HEIGHT = 96;
|
||||||
|
|
||||||
function ClubListScreen() {
|
class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||||
const navigation = useNavigation();
|
categories: Array<ClubCategoryType>;
|
||||||
const request = useAuthenticatedRequest<ResponseType>('clubs/list');
|
|
||||||
const [currentlySelectedCategories, setCurrentlySelectedCategories] =
|
|
||||||
useState<Array<number>>([]);
|
|
||||||
const [currentSearchString, setCurrentSearchString] = useState('');
|
|
||||||
const categories = useRef<Array<ClubCategoryType>>([]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
constructor(props: PropsType) {
|
||||||
const getSearchBar = () => {
|
super(props);
|
||||||
return (
|
this.categories = [];
|
||||||
// @ts-ignore
|
this.state = {
|
||||||
<Searchbar
|
currentlySelectedCategories: [],
|
||||||
placeholder={i18n.t('screens.proximo.search')}
|
currentSearchString: '',
|
||||||
onChangeText={onSearchStringChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
const getHeaderButtons = () => {
|
}
|
||||||
return (
|
|
||||||
<MaterialHeaderButtons>
|
/**
|
||||||
<Item
|
* Creates the header content
|
||||||
title="main"
|
*/
|
||||||
iconName="information"
|
componentDidMount() {
|
||||||
onPress={() => navigation.navigate(MainRoutes.ClubAbout)}
|
const { props } = this;
|
||||||
/>
|
props.navigation.setOptions({
|
||||||
</MaterialHeaderButtons>
|
headerTitle: this.getSearchBar,
|
||||||
);
|
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%' }
|
||||||
: { width: '100%' },
|
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||||
});
|
});
|
||||||
// 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.
|
||||||
|
@ -108,21 +99,61 @@ function ClubListScreen() {
|
||||||
*
|
*
|
||||||
* @param item The article pressed
|
* @param item The article pressed
|
||||||
*/
|
*/
|
||||||
const onListItemPress = (item: ClubType) => {
|
onListItemPress(item: ClubType) {
|
||||||
navigation.navigate(MainRoutes.ClubInformation, {
|
const { props } = this;
|
||||||
type: 'full',
|
props.navigation.navigate('club-information', {
|
||||||
data: item,
|
data: item,
|
||||||
categories: categories.current,
|
categories: this.categories,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback used when the search changes
|
||||||
|
*
|
||||||
|
* @param str The new search string
|
||||||
|
*/
|
||||||
|
onSearchStringChange = (str: string) => {
|
||||||
|
this.updateFilteredData(str, null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChipSelect = (id: number) => {
|
/**
|
||||||
updateFilteredData(null, id);
|
* Gets the header search bar
|
||||||
|
*
|
||||||
|
* @return {*}
|
||||||
|
*/
|
||||||
|
getSearchBar = () => {
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<Searchbar
|
||||||
|
placeholder={i18n.t('screens.proximo.search')}
|
||||||
|
onChangeText={this.onSearchStringChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDataset = (data: ResponseType | undefined) => {
|
onChipSelect = (id: number) => {
|
||||||
|
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) {
|
||||||
categories.current = data.categories;
|
this.categories = data?.categories;
|
||||||
return [{ title: '', data: data.clubs }];
|
return [{ title: '', data: data.clubs }];
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
|
@ -134,23 +165,30 @@ function ClubListScreen() {
|
||||||
*
|
*
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
const getListHeader = (data: ResponseType | undefined) => {
|
getListHeader(data: ResponseType | undefined) {
|
||||||
|
const { state } = this;
|
||||||
if (data) {
|
if (data) {
|
||||||
return (
|
return (
|
||||||
<ClubListHeader
|
<ClubListHeader
|
||||||
categories={categories.current}
|
categories={this.categories}
|
||||||
selectedCategories={currentlySelectedCategories}
|
selectedCategories={state.currentlySelectedCategories}
|
||||||
onChipSelect={onChipSelect}
|
onChipSelect={this.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;
|
||||||
categories.current.forEach((item: ClubCategoryType) => {
|
this.categories.forEach((item: ClubCategoryType) => {
|
||||||
if (id === item.id) {
|
if (id === item.id) {
|
||||||
cat = item;
|
cat = item;
|
||||||
}
|
}
|
||||||
|
@ -158,14 +196,14 @@ function ClubListScreen() {
|
||||||
return cat;
|
return cat;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRenderItem = ({ item }: { item: ClubType }) => {
|
getRenderItem = ({ item }: { item: ClubType }) => {
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
onListItemPress(item);
|
this.onListItemPress(item);
|
||||||
};
|
};
|
||||||
if (shouldRenderItem(item)) {
|
if (this.shouldRenderItem(item)) {
|
||||||
return (
|
return (
|
||||||
<ClubListItem
|
<ClubListItem
|
||||||
categoryTranslator={getCategoryOfId}
|
categoryTranslator={this.getCategoryOfId}
|
||||||
item={item}
|
item={item}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
height={LIST_ITEM_HEIGHT}
|
height={LIST_ITEM_HEIGHT}
|
||||||
|
@ -175,7 +213,7 @@ function ClubListScreen() {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyExtractor = (item: ClubType): string => item.id.toString();
|
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.
|
||||||
|
@ -186,12 +224,10 @@ function ClubListScreen() {
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
const updateFilteredData = (
|
updateFilteredData(filterStr: string | null, categoryId: number | null) {
|
||||||
filterStr: string | null,
|
const { state } = this;
|
||||||
categoryId: number | null
|
const newCategoriesState = [...state.currentlySelectedCategories];
|
||||||
) => {
|
let newStrState = state.currentSearchString;
|
||||||
const newCategoriesState = [...currentlySelectedCategories];
|
|
||||||
let newStrState = currentSearchString;
|
|
||||||
if (filterStr !== null) {
|
if (filterStr !== null) {
|
||||||
newStrState = filterStr;
|
newStrState = filterStr;
|
||||||
}
|
}
|
||||||
|
@ -204,10 +240,12 @@ function ClubListScreen() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filterStr !== null || categoryId !== null) {
|
if (filterStr !== null || categoryId !== null) {
|
||||||
setCurrentSearchString(newStrState);
|
this.setState({
|
||||||
setCurrentlySelectedCategories(newCategoriesState);
|
currentSearchString: newStrState,
|
||||||
|
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
|
||||||
|
@ -215,28 +253,35 @@ function ClubListScreen() {
|
||||||
* @param item The club to check
|
* @param item The club to check
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
const shouldRenderItem = (item: ClubType): boolean => {
|
shouldRenderItem(item: ClubType): boolean {
|
||||||
|
const { state } = this;
|
||||||
let shouldRender =
|
let shouldRender =
|
||||||
currentlySelectedCategories.length === 0 ||
|
state.currentlySelectedCategories.length === 0 ||
|
||||||
isItemInCategoryFilter(currentlySelectedCategories, item.category);
|
isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
|
||||||
if (shouldRender) {
|
if (shouldRender) {
|
||||||
shouldRender = stringMatchQuery(item.name, currentSearchString);
|
shouldRender = stringMatchQuery(item.name, state.currentSearchString);
|
||||||
}
|
}
|
||||||
return shouldRender;
|
return shouldRender;
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
render() {
|
||||||
<WebSectionList
|
return (
|
||||||
request={request}
|
<WebSectionList
|
||||||
createDataset={createDataset}
|
request={() =>
|
||||||
keyExtractor={keyExtractor}
|
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
||||||
renderItem={getRenderItem}
|
'clubs/list'
|
||||||
renderListHeaderComponent={getListHeader}
|
)
|
||||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
}
|
||||||
removeClippedSubviews={true}
|
createDataset={this.createDataset}
|
||||||
itemHeight={LIST_ITEM_HEIGHT}
|
keyExtractor={this.keyExtractor}
|
||||||
/>
|
renderItem={this.getRenderItem}
|
||||||
);
|
renderListHeaderComponent={(data) => this.getListHeader(data)}
|
||||||
|
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
itemHeight={LIST_ITEM_HEIGHT}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ClubListScreen;
|
export default ClubListScreen;
|
||||||
|
|
|
@ -31,15 +31,12 @@ 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 {
|
import { MainStackParamsList } from '../../../navigation/MainNavigator';
|
||||||
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,
|
||||||
MainRoutes.EquipmentConfirm
|
'equipment-confirm'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type Props = EquipmentConfirmScreenNavigationProp;
|
type Props = EquipmentConfirmScreenNavigationProp;
|
||||||
|
|
|
@ -17,17 +17,26 @@
|
||||||
* 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 React, { useRef, useState } from 'react';
|
import * as React 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;
|
||||||
|
@ -58,63 +67,69 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function EquipmentListScreen() {
|
class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
||||||
const userRents = useRef<undefined | Array<RentedDeviceType>>();
|
userRents: null | Array<RentedDeviceType>;
|
||||||
const [mascotDialogVisible, setMascotDialogVisible] = useState<
|
|
||||||
undefined | boolean
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
const requestAll =
|
constructor(props: PropsType) {
|
||||||
useAuthenticatedRequest<{ devices: Array<DeviceType> }>('location/all');
|
super(props);
|
||||||
const requestOwn = useAuthenticatedRequest<{
|
this.userRents = null;
|
||||||
locations: Array<RentedDeviceType>;
|
this.state = {
|
||||||
}>('location/my');
|
mascotDialogVisible: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const getRenderItem = ({ item }: { item: DeviceType }) => {
|
getRenderItem = ({ item }: { item: DeviceType }) => {
|
||||||
|
const { navigation } = this.props;
|
||||||
return (
|
return (
|
||||||
<EquipmentListItem
|
<EquipmentListItem
|
||||||
|
navigation={navigation}
|
||||||
item={item}
|
item={item}
|
||||||
userDeviceRentDates={getUserDeviceRentDates(item)}
|
userDeviceRentDates={this.getUserDeviceRentDates(item)}
|
||||||
height={LIST_ITEM_HEIGHT}
|
height={LIST_ITEM_HEIGHT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserDeviceRentDates = (
|
getUserDeviceRentDates(item: DeviceType): [string, string] | null {
|
||||||
item: DeviceType
|
|
||||||
): [string, string] | null => {
|
|
||||||
let dates = null;
|
let dates = null;
|
||||||
if (userRents.current) {
|
if (this.userRents != null) {
|
||||||
userRents.current.forEach((device: RentedDeviceType) => {
|
this.userRents.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={showMascotDialog}
|
onPress={this.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>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const keyExtractor = (item: DeviceType): string => item.id.toString();
|
keyExtractor = (item: DeviceType): string => item.id.toString();
|
||||||
|
|
||||||
const createDataset = (data: ResponseType | undefined) => {
|
createDataset = (data: ResponseType | undefined) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
if (data.locations) {
|
const userRents = data.locations;
|
||||||
userRents.current = data.locations;
|
|
||||||
|
if (userRents) {
|
||||||
|
this.userRents = userRents;
|
||||||
}
|
}
|
||||||
return [{ title: '', data: data.devices }];
|
return [{ title: '', data: data.devices }];
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,19 +137,27 @@ function EquipmentListScreen() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showMascotDialog = () => setMascotDialogVisible(true);
|
showMascotDialog = () => {
|
||||||
|
this.setState({ mascotDialogVisible: true });
|
||||||
|
};
|
||||||
|
|
||||||
const hideMascotDialog = () => setMascotDialogVisible(false);
|
hideMascotDialog = () => {
|
||||||
|
this.setState({ mascotDialogVisible: false });
|
||||||
|
};
|
||||||
|
|
||||||
const request = () => {
|
request = () => {
|
||||||
return new Promise(
|
return new Promise(
|
||||||
(
|
(
|
||||||
resolve: (data: ResponseType) => void,
|
resolve: (data: ResponseType) => void,
|
||||||
reject: (error: ApiRejectType) => void
|
reject: (error: ApiRejectType) => void
|
||||||
) => {
|
) => {
|
||||||
requestAll()
|
ConnectionManager.getInstance()
|
||||||
|
.authenticatedRequest<{ devices: Array<DeviceType> }>('location/all')
|
||||||
.then((devicesData) => {
|
.then((devicesData) => {
|
||||||
requestOwn()
|
ConnectionManager.getInstance()
|
||||||
|
.authenticatedRequest<{
|
||||||
|
locations: Array<RentedDeviceType>;
|
||||||
|
}>('location/my')
|
||||||
.then((rentsData) => {
|
.then((rentsData) => {
|
||||||
resolve({
|
resolve({
|
||||||
devices: devicesData.devices,
|
devices: devicesData.devices,
|
||||||
|
@ -152,31 +175,34 @@ function EquipmentListScreen() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
render() {
|
||||||
<View style={GENERAL_STYLES.flex}>
|
const { state } = this;
|
||||||
<WebSectionList
|
return (
|
||||||
request={request}
|
<View style={GENERAL_STYLES.flex}>
|
||||||
createDataset={createDataset}
|
<WebSectionList
|
||||||
keyExtractor={keyExtractor}
|
request={this.request}
|
||||||
renderItem={getRenderItem}
|
createDataset={this.createDataset}
|
||||||
renderListHeaderComponent={getListHeader}
|
keyExtractor={this.keyExtractor}
|
||||||
/>
|
renderItem={this.getRenderItem}
|
||||||
<MascotPopup
|
renderListHeaderComponent={() => this.getListHeader()}
|
||||||
visible={mascotDialogVisible}
|
/>
|
||||||
title={i18n.t('screens.equipment.mascotDialog.title')}
|
<MascotPopup
|
||||||
message={i18n.t('screens.equipment.mascotDialog.message')}
|
visible={state.mascotDialogVisible}
|
||||||
icon="vote"
|
title={i18n.t('screens.equipment.mascotDialog.title')}
|
||||||
buttons={{
|
message={i18n.t('screens.equipment.mascotDialog.message')}
|
||||||
cancel: {
|
icon="vote"
|
||||||
message: i18n.t('screens.equipment.mascotDialog.button'),
|
buttons={{
|
||||||
icon: 'check',
|
cancel: {
|
||||||
onPress: hideMascotDialog,
|
message: i18n.t('screens.equipment.mascotDialog.button'),
|
||||||
},
|
icon: 'check',
|
||||||
}}
|
onPress: this.hideMascotDialog,
|
||||||
emotion={MASCOT_STYLE.WINK}
|
},
|
||||||
/>
|
}}
|
||||||
</View>
|
emotion={MASCOT_STYLE.WINK}
|
||||||
);
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EquipmentListScreen;
|
export default EquipmentListScreen;
|
||||||
|
|
|
@ -17,20 +17,21 @@
|
||||||
* 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 React, { useCallback, useRef, useState } from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Caption,
|
Caption,
|
||||||
Card,
|
Card,
|
||||||
Headline,
|
Headline,
|
||||||
Subheading,
|
Subheading,
|
||||||
useTheme,
|
withTheme,
|
||||||
} 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 {
|
||||||
|
@ -41,24 +42,34 @@ 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 {
|
import { MainStackParamsList } from '../../../navigation/MainNavigator';
|
||||||
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 Props = StackScreenProps<MainStackParamsList, MainRoutes.EquipmentRent>;
|
type EquipmentRentScreenNavigationProp = StackScreenProps<
|
||||||
|
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',
|
||||||
|
@ -103,101 +114,98 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function EquipmentRentScreen(props: Props) {
|
class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||||
const theme = useTheme();
|
item: DeviceType | null;
|
||||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
|
||||||
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
|
||||||
status: REQUEST_STATUS.SUCCESS,
|
|
||||||
});
|
|
||||||
const [markedDates, setMarkedDates] = useState<MarkedDatesObjectType>({});
|
|
||||||
const [dialogVisible, setDialogVisible] = useState(false);
|
|
||||||
|
|
||||||
const item = props.route.params.item;
|
bookedDates: Array<string>;
|
||||||
|
|
||||||
const bookedDates = useRef<Array<string>>([]);
|
bookRef: { current: null | (Animatable.View & View) };
|
||||||
const canBookEquipment = useRef(false);
|
|
||||||
|
|
||||||
const bookRef = useRef<Animatable.View & View>(null);
|
canBookEquipment: boolean;
|
||||||
|
|
||||||
let lockedDates: {
|
lockedDates: {
|
||||||
[key: string]: PeriodMarking;
|
[key: string]: PeriodMarking;
|
||||||
} = {};
|
};
|
||||||
|
|
||||||
if (item) {
|
constructor(props: Props) {
|
||||||
item.booked_at.forEach((date: { begin: string; end: string }) => {
|
super(props);
|
||||||
const range = getValidRange(
|
this.item = null;
|
||||||
new Date(date.begin),
|
this.lockedDates = {};
|
||||||
new Date(date.end),
|
this.state = {
|
||||||
null
|
dialogVisible: false,
|
||||||
);
|
errorDialogVisible: false,
|
||||||
lockedDates = {
|
markedDates: {},
|
||||||
...lockedDates,
|
currentError: { status: REQUEST_STATUS.SUCCESS },
|
||||||
...generateMarkedDates(false, theme, range),
|
};
|
||||||
};
|
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 }) => {
|
||||||
|
const range = getValidRange(
|
||||||
|
new Date(date.begin),
|
||||||
|
new Date(date.end),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
this.lockedDates = {
|
||||||
|
...this.lockedDates,
|
||||||
|
...generateMarkedDates(false, props.theme, range),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useFocusEffect(
|
/**
|
||||||
useCallback(() => {
|
* Captures focus and blur events to hook on android back button
|
||||||
|
*/
|
||||||
|
componentDidMount() {
|
||||||
|
const { navigation } = this.props;
|
||||||
|
navigation.addListener('focus', () => {
|
||||||
BackHandler.addEventListener(
|
BackHandler.addEventListener(
|
||||||
'hardwareBackPress',
|
'hardwareBackPress',
|
||||||
onBackButtonPressAndroid
|
this.onBackButtonPressAndroid
|
||||||
);
|
);
|
||||||
return () => {
|
});
|
||||||
BackHandler.removeEventListener(
|
navigation.addListener('blur', () => {
|
||||||
'hardwareBackPress',
|
BackHandler.removeEventListener(
|
||||||
onBackButtonPressAndroid
|
'hardwareBackPress',
|
||||||
);
|
this.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}
|
||||||
*/
|
*/
|
||||||
const onBackButtonPressAndroid = (): boolean => {
|
onBackButtonPressAndroid = (): boolean => {
|
||||||
if (bookedDates.current.length > 0) {
|
if (this.bookedDates.length > 0) {
|
||||||
resetSelection();
|
this.resetSelection();
|
||||||
updateMarkedSelection();
|
this.updateMarkedSelection();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const showDialog = () => setDialogVisible(true);
|
onDialogDismiss = () => {
|
||||||
|
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBookEndDate = (): Date | null => {
|
onErrorDialogDismiss = () => {
|
||||||
const { length } = bookedDates.current;
|
this.setState({ errorDialogVisible: false });
|
||||||
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.
|
||||||
|
@ -205,37 +213,54 @@ function EquipmentRentScreen(props: Props) {
|
||||||
*
|
*
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const onDialogAccept = (): Promise<void> => {
|
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) {
|
||||||
request()
|
ConnectionManager.getInstance()
|
||||||
|
.authenticatedRequest('location/booking', {
|
||||||
|
device: item.id,
|
||||||
|
begin: getISODate(start),
|
||||||
|
end: getISODate(end),
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onDialogDismiss();
|
this.onDialogDismiss();
|
||||||
navigation.replace('equipment-confirm', {
|
props.navigation.replace('equipment-confirm', {
|
||||||
item: item,
|
item: this.item,
|
||||||
dates: [getISODate(start), getISODate(end)],
|
dates: [getISODate(start), getISODate(end)],
|
||||||
});
|
});
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error: ApiRejectType) => {
|
.catch((error: ApiRejectType) => {
|
||||||
onDialogDismiss();
|
this.onDialogDismiss();
|
||||||
setCurrentError(error);
|
this.showErrorDialog(error);
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
onDialogDismiss();
|
this.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
|
||||||
*/
|
*/
|
||||||
const selectNewDate = (day: {
|
selectNewDate = (day: {
|
||||||
dateString: string;
|
dateString: string;
|
||||||
day: number;
|
day: number;
|
||||||
month: number;
|
month: number;
|
||||||
|
@ -243,199 +268,222 @@ function EquipmentRentScreen(props: Props) {
|
||||||
year: number;
|
year: number;
|
||||||
}) => {
|
}) => {
|
||||||
const selected = new Date(day.dateString);
|
const selected = new Date(day.dateString);
|
||||||
|
const start = this.getBookStartDate();
|
||||||
|
|
||||||
if (!lockedDates[day.dateString] != null) {
|
if (!this.lockedDates[day.dateString] != null) {
|
||||||
if (start === null) {
|
if (start === null) {
|
||||||
updateSelectionRange(selected, selected);
|
this.updateSelectionRange(selected, selected);
|
||||||
enableBooking();
|
this.enableBooking();
|
||||||
} else if (start.getTime() === selected.getTime()) {
|
} else if (start.getTime() === selected.getTime()) {
|
||||||
resetSelection();
|
this.resetSelection();
|
||||||
} else if (bookedDates.current.length === 1) {
|
} else if (this.bookedDates.length === 1) {
|
||||||
updateSelectionRange(start, selected);
|
this.updateSelectionRange(start, selected);
|
||||||
enableBooking();
|
this.enableBooking();
|
||||||
} else {
|
} else {
|
||||||
resetSelection();
|
this.resetSelection();
|
||||||
}
|
}
|
||||||
updateMarkedSelection();
|
this.updateMarkedSelection();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const showBookButton = () => {
|
showErrorDialog = (error: ApiRejectType) => {
|
||||||
if (bookRef.current && bookRef.current.fadeInUp) {
|
this.setState({
|
||||||
bookRef.current.fadeInUp(500);
|
errorDialogVisible: true,
|
||||||
}
|
currentError: error,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideBookButton = () => {
|
showDialog = () => {
|
||||||
if (bookRef.current && bookRef.current.fadeOutDown) {
|
this.setState({ dialogVisible: true });
|
||||||
bookRef.current.fadeOutDown(500);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableBooking = () => {
|
/**
|
||||||
if (!canBookEquipment.current) {
|
* Shows the book button by plying a fade animation
|
||||||
showBookButton();
|
*/
|
||||||
canBookEquipment.current = true;
|
showBookButton() {
|
||||||
|
if (this.bookRef.current && this.bookRef.current.fadeInUp) {
|
||||||
|
this.bookRef.current.fadeInUp(500);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const resetSelection = () => {
|
/**
|
||||||
if (canBookEquipment.current) {
|
* Hides the book button by plying a fade animation
|
||||||
hideBookButton();
|
*/
|
||||||
|
hideBookButton() {
|
||||||
|
if (this.bookRef.current && this.bookRef.current.fadeOutDown) {
|
||||||
|
this.bookRef.current.fadeOutDown(500);
|
||||||
}
|
}
|
||||||
canBookEquipment.current = false;
|
}
|
||||||
bookedDates.current = [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSelectionRange = (s: Date, e: Date) => {
|
enableBooking() {
|
||||||
if (item) {
|
if (!this.canBookEquipment) {
|
||||||
bookedDates.current = getValidRange(s, e, item);
|
this.showBookButton();
|
||||||
|
this.canBookEquipment = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSelection() {
|
||||||
|
if (this.canBookEquipment) {
|
||||||
|
this.hideBookButton();
|
||||||
|
}
|
||||||
|
this.canBookEquipment = false;
|
||||||
|
this.bookedDates = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSelectionRange(start: Date, end: Date) {
|
||||||
|
this.bookedDates = getValidRange(start, end, this.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMarkedSelection() {
|
||||||
|
const { theme } = this.props;
|
||||||
|
this.setState({
|
||||||
|
markedDates: generateMarkedDates(true, theme, this.bookedDates),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { item, props, state } = this;
|
||||||
|
const start = this.getBookStartDate();
|
||||||
|
const end = this.getBookEndDate();
|
||||||
|
let subHeadingText;
|
||||||
|
if (start == null) {
|
||||||
|
subHeadingText = i18n.t('screens.equipment.booking');
|
||||||
|
} else if (end != null && start.getTime() !== end.getTime()) {
|
||||||
|
subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
|
||||||
|
begin: getRelativeDateString(start),
|
||||||
|
end: getRelativeDateString(end),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
bookedDates.current = [];
|
subHeadingText = i18n.t('screens.equipment.bookingDay', {
|
||||||
|
date: getRelativeDateString(start),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
if (item != null) {
|
||||||
|
const isAvailable = isEquipmentAvailable(item);
|
||||||
const updateMarkedSelection = () => {
|
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||||
setMarkedDates(generateMarkedDates(true, theme, bookedDates.current));
|
return (
|
||||||
};
|
<View style={GENERAL_STYLES.flex}>
|
||||||
|
<CollapsibleScrollView>
|
||||||
let subHeadingText;
|
<Card style={styles.card}>
|
||||||
|
<Card.Content>
|
||||||
if (start == null) {
|
<View style={GENERAL_STYLES.flex}>
|
||||||
subHeadingText = i18n.t('screens.equipment.booking');
|
<View style={styles.titleContainer}>
|
||||||
} else if (end != null && start.getTime() !== end.getTime()) {
|
<Headline style={styles.title}>{item.name}</Headline>
|
||||||
subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
|
<Caption style={styles.caption}>
|
||||||
begin: getRelativeDateString(start),
|
(
|
||||||
end: getRelativeDateString(end),
|
{i18n.t('screens.equipment.bail', { cost: item.caution })}
|
||||||
});
|
)
|
||||||
} else {
|
</Caption>
|
||||||
subHeadingText = i18n.t('screens.equipment.bookingDay', {
|
</View>
|
||||||
date: getRelativeDateString(start),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item) {
|
|
||||||
const isAvailable = isEquipmentAvailable(item);
|
|
||||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
|
||||||
return (
|
|
||||||
<View style={GENERAL_STYLES.flex}>
|
|
||||||
<CollapsibleScrollView>
|
|
||||||
<Card style={styles.card}>
|
|
||||||
<Card.Content>
|
|
||||||
<View style={GENERAL_STYLES.flex}>
|
|
||||||
<View style={styles.titleContainer}>
|
|
||||||
<Headline style={styles.title}>{item.name}</Headline>
|
|
||||||
<Caption style={styles.caption}>
|
|
||||||
({i18n.t('screens.equipment.bail', { cost: item.caution })})
|
|
||||||
</Caption>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
||||||
color={
|
color={
|
||||||
isAvailable ? theme.colors.success : theme.colors.primary
|
isAvailable
|
||||||
}
|
? props.theme.colors.success
|
||||||
mode="text"
|
: props.theme.colors.primary
|
||||||
>
|
}
|
||||||
{i18n.t('screens.equipment.available', {
|
mode="text"
|
||||||
date: getRelativeDateString(firstAvailability),
|
>
|
||||||
})}
|
{i18n.t('screens.equipment.available', {
|
||||||
</Button>
|
date: getRelativeDateString(firstAvailability),
|
||||||
<Subheading style={styles.subtitle}>{subHeadingText}</Subheading>
|
})}
|
||||||
</Card.Content>
|
</Button>
|
||||||
</Card>
|
<Subheading style={styles.subtitle}>
|
||||||
<CalendarList
|
{subHeadingText}
|
||||||
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
|
</Subheading>
|
||||||
minDate={new Date()}
|
</Card.Content>
|
||||||
// Max amount of months allowed to scroll to the past. Default = 50
|
</Card>
|
||||||
pastScrollRange={0}
|
<CalendarList
|
||||||
// Max amount of months allowed to scroll to the future. Default = 50
|
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
|
||||||
futureScrollRange={3}
|
minDate={new Date()}
|
||||||
// Enable horizontal scrolling, default = false
|
// Max amount of months allowed to scroll to the past. Default = 50
|
||||||
horizontal
|
pastScrollRange={0}
|
||||||
// Enable paging on horizontal, default = false
|
// Max amount of months allowed to scroll to the future. Default = 50
|
||||||
pagingEnabled
|
futureScrollRange={3}
|
||||||
// Handler which gets executed on day press. Default = undefined
|
// Enable horizontal scrolling, default = false
|
||||||
onDayPress={selectNewDate}
|
horizontal
|
||||||
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
// Enable paging on horizontal, default = false
|
||||||
firstDay={1}
|
pagingEnabled
|
||||||
// Hide month navigation arrows.
|
// Handler which gets executed on day press. Default = undefined
|
||||||
hideArrows={false}
|
onDayPress={this.selectNewDate}
|
||||||
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
||||||
markingType={'period'}
|
firstDay={1}
|
||||||
markedDates={{ ...lockedDates, ...markedDates }}
|
// Hide month navigation arrows.
|
||||||
theme={{
|
hideArrows={false}
|
||||||
'backgroundColor': theme.colors.agendaBackgroundColor,
|
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
||||||
'calendarBackground': theme.colors.background,
|
markingType={'period'}
|
||||||
'textSectionTitleColor': theme.colors.agendaDayTextColor,
|
markedDates={{ ...this.lockedDates, ...state.markedDates }}
|
||||||
'selectedDayBackgroundColor': theme.colors.primary,
|
theme={{
|
||||||
'selectedDayTextColor': '#ffffff',
|
'backgroundColor': props.theme.colors.agendaBackgroundColor,
|
||||||
'todayTextColor': theme.colors.text,
|
'calendarBackground': props.theme.colors.background,
|
||||||
'dayTextColor': theme.colors.text,
|
'textSectionTitleColor': props.theme.colors.agendaDayTextColor,
|
||||||
'textDisabledColor': theme.colors.agendaDayTextColor,
|
'selectedDayBackgroundColor': props.theme.colors.primary,
|
||||||
'dotColor': theme.colors.primary,
|
'selectedDayTextColor': '#ffffff',
|
||||||
'selectedDotColor': '#ffffff',
|
'todayTextColor': props.theme.colors.text,
|
||||||
'arrowColor': theme.colors.primary,
|
'dayTextColor': props.theme.colors.text,
|
||||||
'monthTextColor': theme.colors.text,
|
'textDisabledColor': props.theme.colors.agendaDayTextColor,
|
||||||
'indicatorColor': theme.colors.primary,
|
'dotColor': props.theme.colors.primary,
|
||||||
'textDayFontFamily': 'monospace',
|
'selectedDotColor': '#ffffff',
|
||||||
'textMonthFontFamily': 'monospace',
|
'arrowColor': props.theme.colors.primary,
|
||||||
'textDayHeaderFontFamily': 'monospace',
|
'monthTextColor': props.theme.colors.text,
|
||||||
'textDayFontWeight': '300',
|
'indicatorColor': props.theme.colors.primary,
|
||||||
'textMonthFontWeight': 'bold',
|
'textDayFontFamily': 'monospace',
|
||||||
'textDayHeaderFontWeight': '300',
|
'textMonthFontFamily': 'monospace',
|
||||||
'textDayFontSize': 16,
|
'textDayHeaderFontFamily': 'monospace',
|
||||||
'textMonthFontSize': 16,
|
'textDayFontWeight': '300',
|
||||||
'textDayHeaderFontSize': 16,
|
'textMonthFontWeight': 'bold',
|
||||||
'stylesheet.day.period': {
|
'textDayHeaderFontWeight': '300',
|
||||||
base: {
|
'textDayFontSize': 16,
|
||||||
overflow: 'hidden',
|
'textMonthFontSize': 16,
|
||||||
height: 34,
|
'textDayHeaderFontSize': 16,
|
||||||
width: 34,
|
'stylesheet.day.period': {
|
||||||
alignItems: 'center',
|
base: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
height: 34,
|
||||||
|
width: 34,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}}
|
||||||
}}
|
style={styles.calendar}
|
||||||
style={styles.calendar}
|
/>
|
||||||
|
</CollapsibleScrollView>
|
||||||
|
<LoadingConfirmDialog
|
||||||
|
visible={state.dialogVisible}
|
||||||
|
onDismiss={this.onDialogDismiss}
|
||||||
|
onAccept={this.onDialogAccept}
|
||||||
|
title={i18n.t('screens.equipment.dialogTitle')}
|
||||||
|
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
|
||||||
|
message={i18n.t('screens.equipment.dialogMessage')}
|
||||||
/>
|
/>
|
||||||
</CollapsibleScrollView>
|
|
||||||
<LoadingConfirmDialog
|
|
||||||
visible={dialogVisible}
|
|
||||||
onDismiss={onDialogDismiss}
|
|
||||||
onAccept={onDialogAccept}
|
|
||||||
title={i18n.t('screens.equipment.dialogTitle')}
|
|
||||||
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
|
|
||||||
message={i18n.t('screens.equipment.dialogMessage')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ErrorDialog
|
<ErrorDialog
|
||||||
visible={
|
visible={state.errorDialogVisible}
|
||||||
currentError.status !== REQUEST_STATUS.SUCCESS ||
|
onDismiss={this.onErrorDialogDismiss}
|
||||||
currentError.code !== undefined
|
status={state.currentError.status}
|
||||||
}
|
code={state.currentError.code}
|
||||||
onDismiss={onErrorDialogDismiss}
|
/>
|
||||||
status={currentError.status}
|
<Animatable.View
|
||||||
code={currentError.code}
|
ref={this.bookRef}
|
||||||
/>
|
useNativeDriver
|
||||||
<Animatable.View
|
style={styles.buttonContainer}
|
||||||
ref={bookRef}
|
|
||||||
useNativeDriver
|
|
||||||
style={styles.buttonContainer}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
icon="bookmark-check"
|
|
||||||
mode="contained"
|
|
||||||
onPress={showDialog}
|
|
||||||
style={styles.button}
|
|
||||||
>
|
>
|
||||||
{i18n.t('screens.equipment.bookButton')}
|
<Button
|
||||||
</Button>
|
icon="bookmark-check"
|
||||||
</Animatable.View>
|
mode="contained"
|
||||||
</View>
|
onPress={this.showDialog}
|
||||||
);
|
style={styles.button}
|
||||||
|
>
|
||||||
|
{i18n.t('screens.equipment.bookButton')}
|
||||||
|
</Button>
|
||||||
|
</Animatable.View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EquipmentRentScreen;
|
export default withTheme(EquipmentRentScreen);
|
||||||
|
|
|
@ -17,59 +17,158 @@
|
||||||
* 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 React, { useCallback, useState } from 'react';
|
import * as React from 'react';
|
||||||
import { KeyboardAvoidingView, View } from 'react-native';
|
import { Image, KeyboardAvoidingView, StyleSheet, 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 {
|
import { MainStackParamsList } from '../../navigation/MainNavigator';
|
||||||
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, connectToAmicale } from '../../utils/WebData';
|
import { ApiRejectType } 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 Props = StackScreenProps<MainStackParamsList, MainRoutes.Login>;
|
type LoginScreenNavigationProp = StackScreenProps<MainStackParamsList, 'login'>;
|
||||||
|
|
||||||
function LoginScreen(props: Props) {
|
type Props = LoginScreenNavigationProp & {
|
||||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
navigation: StackNavigationProp<any>;
|
||||||
const { setLogin } = useLogin();
|
theme: ReactNativePaper.Theme;
|
||||||
const [loading, setLoading] = useState(false);
|
};
|
||||||
const [nextScreen, setNextScreen] = useState<string | undefined>(undefined);
|
|
||||||
const [mascotDialogVisible, setMascotDialogVisible] = useState<
|
|
||||||
undefined | boolean
|
|
||||||
>(undefined);
|
|
||||||
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
|
||||||
status: REQUEST_STATUS.SUCCESS,
|
|
||||||
});
|
|
||||||
const homeMascot = useShouldShowMascot(TabRoutes.Home);
|
|
||||||
|
|
||||||
useFocusEffect(
|
type StateType = {
|
||||||
useCallback(() => {
|
email: string;
|
||||||
setNextScreen(props.route.params?.nextScreen);
|
password: string;
|
||||||
}, [props.route.params])
|
isEmailValidated: boolean;
|
||||||
);
|
isPasswordValidated: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
dialogVisible: boolean;
|
||||||
|
dialogError: ApiRejectType;
|
||||||
|
mascotDialogVisible: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const onResetPasswordClick = () => {
|
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||||
navigation.navigate(MainRoutes.Website, {
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class LoginScreen extends React.Component<Props, StateType> {
|
||||||
|
onEmailChange: (value: string) => void;
|
||||||
|
|
||||||
|
onPasswordChange: (value: string) => void;
|
||||||
|
|
||||||
|
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.
|
||||||
*
|
*
|
||||||
|
@ -77,89 +176,294 @@ function LoginScreen(props: Props) {
|
||||||
* 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
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const onSubmit = (email: string, password: string) => {
|
onSubmit = () => {
|
||||||
setLoading(true);
|
const { email, password } = this.state;
|
||||||
connectToAmicale(email, password)
|
if (this.shouldEnableLogin()) {
|
||||||
.then(handleSuccess)
|
this.setState({ loading: true });
|
||||||
.catch(setCurrentError)
|
ConnectionManager.getInstance()
|
||||||
.finally(() => setLoading(false));
|
.connect(email, password)
|
||||||
|
.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 = () =>
|
/**
|
||||||
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
* The user has unfocused the input, his email is ready to be validated
|
||||||
|
*/
|
||||||
|
validateEmail = () => {
|
||||||
|
this.setState({ isEmailValidated: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has unfocused the input, his password is ready to be validated
|
||||||
|
*/
|
||||||
|
validatePassword = () => {
|
||||||
|
this.setState({ isPasswordValidated: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
hideMascotDialog = () => {
|
||||||
|
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.
|
||||||
*/
|
*/
|
||||||
const handleSuccess = (token: string) => {
|
handleSuccess = () => {
|
||||||
|
const { navigation } = this.props;
|
||||||
// Do not show the home login banner again
|
// Do not show the home login banner again
|
||||||
if (homeMascot.shouldShow) {
|
// TODO
|
||||||
homeMascot.setShouldShow(false);
|
// AsyncStorageManager.set(
|
||||||
}
|
// AsyncStorageManager.PREFERENCES.homeShowMascot.key,
|
||||||
saveLoginToken(token);
|
// false
|
||||||
setLogin(token);
|
// );
|
||||||
if (!nextScreen) {
|
if (this.nextScreen == null) {
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
} else {
|
} else {
|
||||||
navigation.replace(nextScreen);
|
navigation.replace(this.nextScreen);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
/**
|
||||||
<LinearGradient
|
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
|
||||||
style={GENERAL_STYLES.flex}
|
*/
|
||||||
colors={['#9e0d18', '#530209']}
|
handleNavigationParams() {
|
||||||
start={{ x: 0, y: 0.1 }}
|
this.nextScreen = this.props.route.params?.nextScreen;
|
||||||
end={{ x: 0.1, y: 1 }}
|
}
|
||||||
>
|
|
||||||
<KeyboardAvoidingView
|
/**
|
||||||
behavior={'height'}
|
* Checks if the entered email is valid (matches the regex)
|
||||||
contentContainerStyle={GENERAL_STYLES.flex}
|
*
|
||||||
|
* @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 (
|
||||||
|
<LinearGradient
|
||||||
style={GENERAL_STYLES.flex}
|
style={GENERAL_STYLES.flex}
|
||||||
enabled={true}
|
colors={['#9e0d18', '#530209']}
|
||||||
keyboardVerticalOffset={100}
|
start={{ x: 0, y: 0.1 }}
|
||||||
|
end={{ x: 0.1, y: 1 }}
|
||||||
>
|
>
|
||||||
<CollapsibleScrollView headerColors={'transparent'}>
|
<KeyboardAvoidingView
|
||||||
<View style={GENERAL_STYLES.flex}>
|
behavior={'height'}
|
||||||
<LoginForm
|
contentContainerStyle={GENERAL_STYLES.flex}
|
||||||
loading={loading}
|
style={GENERAL_STYLES.flex}
|
||||||
onSubmit={onSubmit}
|
enabled={true}
|
||||||
onResetPasswordPress={onResetPasswordClick}
|
keyboardVerticalOffset={100}
|
||||||
onHelpPress={showMascotDialog}
|
>
|
||||||
|
<CollapsibleScrollView headerColors={'transparent'}>
|
||||||
|
<View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
|
||||||
|
<MascotPopup
|
||||||
|
visible={mascotDialogVisible}
|
||||||
|
title={i18n.t('screens.login.mascotDialog.title')}
|
||||||
|
message={i18n.t('screens.login.mascotDialog.message')}
|
||||||
|
icon={'help'}
|
||||||
|
buttons={{
|
||||||
|
cancel: {
|
||||||
|
message: i18n.t('screens.login.mascotDialog.button'),
|
||||||
|
icon: 'check',
|
||||||
|
onPress: this.hideMascotDialog,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
emotion={MASCOT_STYLE.NORMAL}
|
||||||
/>
|
/>
|
||||||
</View>
|
<ErrorDialog
|
||||||
<MascotPopup
|
visible={dialogVisible}
|
||||||
visible={mascotDialogVisible}
|
onDismiss={this.hideErrorDialog}
|
||||||
title={i18n.t('screens.login.mascotDialog.title')}
|
status={dialogError.status}
|
||||||
message={i18n.t('screens.login.mascotDialog.message')}
|
code={dialogError.code}
|
||||||
icon={'help'}
|
/>
|
||||||
buttons={{
|
</CollapsibleScrollView>
|
||||||
cancel: {
|
</KeyboardAvoidingView>
|
||||||
message: i18n.t('screens.login.mascotDialog.button'),
|
</LinearGradient>
|
||||||
icon: 'check',
|
);
|
||||||
onPress: hideMascotDialog,
|
}
|
||||||
},
|
|
||||||
}}
|
|
||||||
emotion={MASCOT_STYLE.NORMAL}
|
|
||||||
/>
|
|
||||||
<ErrorDialog
|
|
||||||
visible={
|
|
||||||
currentError.status !== REQUEST_STATUS.SUCCESS ||
|
|
||||||
currentError.code !== undefined
|
|
||||||
}
|
|
||||||
onDismiss={hideErrorDialog}
|
|
||||||
status={currentError.status}
|
|
||||||
code={currentError.code}
|
|
||||||
/>
|
|
||||||
</CollapsibleScrollView>
|
|
||||||
</KeyboardAvoidingView>
|
|
||||||
</LinearGradient>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LoginScreen;
|
export default withTheme(LoginScreen);
|
||||||
|
|
|
@ -17,29 +17,52 @@
|
||||||
* 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 React, { useLayoutEffect, useState } from 'react';
|
import * as React from 'react';
|
||||||
import { View } from 'react-native';
|
import { FlatList, StyleSheet, 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 ProfileWelcomeCard from '../../components/Amicale/Profile/ProfileWelcomeCard';
|
import ConnectionManager from '../../managers/ConnectionManager';
|
||||||
import ProfilePersonalCard from '../../components/Amicale/Profile/ProfilePersonalCard';
|
import {
|
||||||
import ProfileClubCard from '../../components/Amicale/Profile/ProfileClubCard';
|
getAmicaleServices,
|
||||||
import ProfileMembershipCard from '../../components/Amicale/Profile/ProfileMembershipCard';
|
ServiceItemType,
|
||||||
import { useNavigation } from '@react-navigation/core';
|
SERVICES_KEY,
|
||||||
import { useAuthenticatedRequest } from '../../context/loginContext';
|
} from '../../utils/Services';
|
||||||
|
|
||||||
export type ProfileClubType = {
|
type PropsType = {
|
||||||
|
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;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ProfileDataType = {
|
type ProfileDataType = {
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
@ -48,68 +71,87 @@ export type ProfileDataType = {
|
||||||
branch: string;
|
branch: string;
|
||||||
link: string;
|
link: string;
|
||||||
validity: boolean;
|
validity: boolean;
|
||||||
clubs: Array<ProfileClubType>;
|
clubs: Array<ClubType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ProfileScreen() {
|
const styles = StyleSheet.create({
|
||||||
const navigation = useNavigation();
|
card: {
|
||||||
const [dialogVisible, setDialogVisible] = useState(false);
|
margin: 10,
|
||||||
const request = useAuthenticatedRequest<ProfileDataType>('user/profile');
|
},
|
||||||
|
icon: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
editButton: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
mascot: {
|
||||||
|
width: 60,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
class ProfileScreen extends React.Component<PropsType, StateType> {
|
||||||
const getHeaderButton = () => (
|
data: ProfileDataType | undefined;
|
||||||
<MaterialHeaderButtons>
|
|
||||||
<Item
|
flatListData: Array<{ id: string }>;
|
||||||
title={'logout'}
|
|
||||||
iconName={'logout'}
|
amicaleDataset: Array<ServiceItemType>;
|
||||||
onPress={showDisconnectDialog}
|
|
||||||
/>
|
constructor(props: PropsType) {
|
||||||
</MaterialHeaderButtons>
|
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({
|
navigation.setOptions({
|
||||||
headerRight: getHeaderButton,
|
headerRight: this.getHeaderButton,
|
||||||
});
|
});
|
||||||
}, [navigation]);
|
}
|
||||||
|
|
||||||
const getScreen = (data: ProfileDataType | undefined) => {
|
/**
|
||||||
|
* Gets the logout header button
|
||||||
|
*
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
getHeaderButton = () => (
|
||||||
|
<MaterialHeaderButtons>
|
||||||
|
<Item
|
||||||
|
title="logout"
|
||||||
|
iconName="logout"
|
||||||
|
onPress={this.showDisconnectDialog}
|
||||||
|
/>
|
||||||
|
</MaterialHeaderButtons>
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
||||||
const flatListData: Array<{
|
this.data = data;
|
||||||
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 renderItem={getRenderItem} data={flatListData} />
|
<CollapsibleFlatList
|
||||||
|
renderItem={this.getRenderItem}
|
||||||
|
data={this.flatListData}
|
||||||
|
/>
|
||||||
<LogoutDialog
|
<LogoutDialog
|
||||||
visible={dialogVisible}
|
visible={dialogVisible}
|
||||||
onDismiss={hideDisconnectDialog}
|
onDismiss={this.hideDisconnectDialog}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -118,17 +160,346 @@ function ProfileScreen() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRenderItem = ({
|
getRenderItem = ({ item }: { item: { id: string } }) => {
|
||||||
item,
|
switch (item.id) {
|
||||||
}: {
|
case '0':
|
||||||
item: { id: string; render: () => React.ReactElement };
|
return this.getWelcomeCard();
|
||||||
}) => item.render();
|
case '1':
|
||||||
|
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
|
||||||
|
*
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
getServicesList() {
|
||||||
|
return <CardList dataset={this.amicaleDataset} isHorizontal />;
|
||||||
|
}
|
||||||
|
|
||||||
const hideDisconnectDialog = () => setDialogVisible(false);
|
/**
|
||||||
|
* 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <RequestScreen request={request} render={getScreen} />;
|
/**
|
||||||
|
* 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 ProfileScreen;
|
export default withTheme(ProfileScreen);
|
||||||
|
|
|
@ -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 React, { useRef, useState } from 'react';
|
import * as React from 'react';
|
||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import i18n from 'i18n-js';
|
import i18n from 'i18n-js';
|
||||||
import { 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,13 +65,6 @@ 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",
|
||||||
|
@ -120,6 +113,13 @@ type FlatlistType = {
|
||||||
// ],
|
// ],
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
type PropsType = {};
|
||||||
|
|
||||||
|
type StateType = {
|
||||||
|
hasVoted: boolean;
|
||||||
|
mascotDialogVisible: boolean | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
button: {
|
button: {
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
|
@ -131,19 +131,38 @@ const styles = StyleSheet.create({
|
||||||
/**
|
/**
|
||||||
* Screen displaying vote information and controls
|
* Screen displaying vote information and controls
|
||||||
*/
|
*/
|
||||||
export default function VoteScreen() {
|
export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||||
const [hasVoted, setHasVoted] = useState(false);
|
teams: Array<VoteTeamType>;
|
||||||
const [mascotDialogVisible, setMascotDialogVisible] = useState<
|
|
||||||
undefined | boolean
|
|
||||||
>(undefined);
|
|
||||||
|
|
||||||
const datesRequest =
|
hasVoted: boolean;
|
||||||
useAuthenticatedRequest<VoteDatesStringType>('elections/dates');
|
|
||||||
const teamsRequest =
|
datesString: undefined | VoteDatesStringType;
|
||||||
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.
|
||||||
*
|
*
|
||||||
|
@ -154,26 +173,22 @@ export default function VoteScreen() {
|
||||||
* @param dateString The string representation of the wanted date
|
* @param dateString The string representation of the wanted date
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
const getDateString = (date: Date, dateString: string) => {
|
getDateString(date: Date, dateString: string): string {
|
||||||
if (today.getDate() === date.getDate()) {
|
if (this.today.getDate() === date.getDate()) {
|
||||||
const str = getTimeOnlyString(dateString);
|
const str = getTimeOnlyString(dateString);
|
||||||
return str != null ? str : '';
|
return str != null ? str : '';
|
||||||
}
|
}
|
||||||
return dateString;
|
return dateString;
|
||||||
};
|
}
|
||||||
|
|
||||||
const getMainRenderItem = ({
|
getMainRenderItem = ({ item }: { item: { key: string } }) => {
|
||||||
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={showMascotDialog}
|
onPress={this.showMascotDialog}
|
||||||
style={styles.button}
|
style={styles.button}
|
||||||
>
|
>
|
||||||
{i18n.t('screens.vote.mascotDialog.title')}
|
{i18n.t('screens.vote.mascotDialog.title')}
|
||||||
|
@ -181,14 +196,10 @@ export default function VoteScreen() {
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (item.data) {
|
return this.getContent();
|
||||||
return getContent(item.data);
|
|
||||||
} else {
|
|
||||||
return <View />;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createDataset = (
|
createDataset = (
|
||||||
data: ResponseType | undefined,
|
data: ResponseType | undefined,
|
||||||
_loading: boolean,
|
_loading: boolean,
|
||||||
_lastRefreshDate: Date | undefined,
|
_lastRefreshDate: Date | undefined,
|
||||||
|
@ -196,158 +207,157 @@ export default function VoteScreen() {
|
||||||
) => {
|
) => {
|
||||||
// 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 = {
|
|
||||||
teams: [],
|
if (dates && dates.date_begin == null) {
|
||||||
hasVoted: false,
|
this.datesString = undefined;
|
||||||
};
|
} else {
|
||||||
if (dates && dates.date_begin != null) {
|
this.datesString = dates;
|
||||||
flatlistData.datesString = dates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (teams) {
|
if (teams) {
|
||||||
flatlistData.teams = teams.teams;
|
this.teams = teams.teams;
|
||||||
flatlistData.hasVoted = teams.has_voted;
|
this.hasVoted = teams.has_voted;
|
||||||
}
|
}
|
||||||
flatlistData.dates = generateDateObject(flatlistData.datesString);
|
|
||||||
|
this.generateDateObject();
|
||||||
}
|
}
|
||||||
return mainFlatListData;
|
return this.mainFlatListData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getContent = (data: FlatlistType) => {
|
getContent() {
|
||||||
const { dates } = data;
|
const { state } = this;
|
||||||
if (!isVoteStarted(dates)) {
|
if (!this.isVoteStarted()) {
|
||||||
return getTeaseVoteCard(data);
|
return this.getTeaseVoteCard();
|
||||||
}
|
}
|
||||||
if (isVoteRunning(dates) && !data.hasVoted && !hasVoted) {
|
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
|
||||||
return getVoteCard(data);
|
return this.getVoteCard();
|
||||||
}
|
}
|
||||||
if (!isResultStarted(dates)) {
|
if (!this.isResultStarted()) {
|
||||||
return getWaitVoteCard(data);
|
return this.getWaitVoteCard();
|
||||||
}
|
}
|
||||||
if (isResultRunning(dates)) {
|
if (this.isResultRunning()) {
|
||||||
return getVoteResultCard(data);
|
return this.getVoteResultCard();
|
||||||
}
|
}
|
||||||
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
|
||||||
*/
|
*/
|
||||||
const getVoteCard = (data: FlatlistType) => {
|
getVoteCard() {
|
||||||
return (
|
return (
|
||||||
<VoteSelect
|
<VoteSelect
|
||||||
teams={data.teams}
|
teams={this.teams}
|
||||||
onVoteSuccess={onVoteSuccess}
|
onVoteSuccess={this.onVoteSuccess}
|
||||||
onVoteError={() => {
|
onVoteError={this.refreshData}
|
||||||
if (refresh.current) {
|
|
||||||
refresh.current();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Votes have ended, results can be displayed
|
* Votes have ended, results can be displayed
|
||||||
*/
|
*/
|
||||||
const getVoteResultCard = (data: FlatlistType) => {
|
getVoteResultCard() {
|
||||||
if (data.dates != null && data.datesString != null) {
|
if (this.dates != null && this.datesString != null) {
|
||||||
return (
|
return (
|
||||||
<VoteResults
|
<VoteResults
|
||||||
teams={data.teams}
|
teams={this.teams}
|
||||||
dateEnd={getDateString(
|
dateEnd={this.getDateString(
|
||||||
data.dates.date_result_end,
|
this.dates.date_result_end,
|
||||||
data.datesString.date_result_end
|
this.datesString.date_result_end
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vote will open shortly
|
* Vote will open shortly
|
||||||
*/
|
*/
|
||||||
const getTeaseVoteCard = (data: FlatlistType) => {
|
getTeaseVoteCard() {
|
||||||
if (data.dates != null && data.datesString != null) {
|
if (this.dates != null && this.datesString != null) {
|
||||||
return (
|
return (
|
||||||
<VoteTease
|
<VoteTease
|
||||||
startDate={getDateString(
|
startDate={this.getDateString(
|
||||||
data.dates.date_begin,
|
this.dates.date_begin,
|
||||||
data.datesString.date_begin
|
this.datesString.date_begin
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <VoteNotAvailable />;
|
return <VoteNotAvailable />;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Votes have ended, or user has voted waiting for results
|
* Votes have ended, or user has voted waiting for results
|
||||||
*/
|
*/
|
||||||
const getWaitVoteCard = (data: FlatlistType) => {
|
getWaitVoteCard() {
|
||||||
|
const { state } = this;
|
||||||
let startDate = null;
|
let startDate = null;
|
||||||
if (
|
if (
|
||||||
data.dates != null &&
|
this.dates != null &&
|
||||||
data.datesString != null &&
|
this.datesString != null &&
|
||||||
data.dates.date_result_begin != null
|
this.dates.date_result_begin != null
|
||||||
) {
|
) {
|
||||||
startDate = getDateString(
|
startDate = this.getDateString(
|
||||||
data.dates.date_result_begin,
|
this.dates.date_result_begin,
|
||||||
data.datesString.date_result_begin
|
this.datesString.date_result_begin
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<VoteWait
|
<VoteWait
|
||||||
startDate={startDate}
|
startDate={startDate}
|
||||||
hasVoted={data.hasVoted}
|
hasVoted={this.hasVoted || state.hasVoted}
|
||||||
justVoted={hasVoted}
|
justVoted={state.hasVoted}
|
||||||
isVoteRunning={isVoteRunning()}
|
isVoteRunning={this.isVoteRunning()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showMascotDialog = () => {
|
||||||
|
this.setState({ mascotDialogVisible: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const showMascotDialog = () => setMascotDialogVisible(true);
|
hideMascotDialog = () => {
|
||||||
|
this.setState({ mascotDialogVisible: false });
|
||||||
const hideMascotDialog = () => setMascotDialogVisible(false);
|
|
||||||
|
|
||||||
const isVoteStarted = (dates?: VoteDatesObjectType) => {
|
|
||||||
return dates != null && today > dates.date_begin;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isResultRunning = (dates?: VoteDatesObjectType) => {
|
isVoteStarted(): boolean {
|
||||||
|
return this.dates != null && this.today > this.dates.date_begin;
|
||||||
|
}
|
||||||
|
|
||||||
|
isResultRunning(): boolean {
|
||||||
return (
|
return (
|
||||||
dates != null &&
|
this.dates != null &&
|
||||||
today > dates.date_result_begin &&
|
this.today > this.dates.date_result_begin &&
|
||||||
today < dates.date_result_end
|
this.today < this.dates.date_result_end
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const isResultStarted = (dates?: VoteDatesObjectType) => {
|
isResultStarted(): boolean {
|
||||||
return dates != null && today > dates.date_result_begin;
|
return this.dates != null && this.today > this.dates.date_result_begin;
|
||||||
};
|
}
|
||||||
|
|
||||||
const isVoteRunning = (dates?: VoteDatesObjectType) => {
|
isVoteRunning(): boolean {
|
||||||
return dates != null && today > dates.date_begin && today < dates.date_end;
|
return (
|
||||||
};
|
this.dates != null &&
|
||||||
|
this.today > this.dates.date_begin &&
|
||||||
|
this.today < this.dates.date_end
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the objects containing string and Date representations of key vote dates
|
* Generates the objects containing string and Date representations of key vote dates
|
||||||
*/
|
*/
|
||||||
const generateDateObject = (
|
generateDateObject() {
|
||||||
strings?: VoteDatesStringType
|
const strings = this.datesString;
|
||||||
): VoteDatesObjectType | undefined => {
|
if (strings != null) {
|
||||||
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);
|
||||||
|
@ -358,25 +368,27 @@ export default function VoteScreen() {
|
||||||
dateResultBegin != null &&
|
dateResultBegin != null &&
|
||||||
dateResultEnd != null
|
dateResultEnd != null
|
||||||
) {
|
) {
|
||||||
return {
|
this.dates = {
|
||||||
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 {
|
||||||
return undefined;
|
this.dates = undefined;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
this.dates = undefined;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const request = () => {
|
request = () => {
|
||||||
return new Promise((resolve: (data: ResponseType) => void) => {
|
return new Promise((resolve: (data: ResponseType) => void) => {
|
||||||
datesRequest()
|
ConnectionManager.getInstance()
|
||||||
|
.authenticatedRequest<VoteDatesStringType>('elections/dates')
|
||||||
.then((datesData) => {
|
.then((datesData) => {
|
||||||
teamsRequest()
|
ConnectionManager.getInstance()
|
||||||
|
.authenticatedRequest<TeamResponseType>('elections/teams')
|
||||||
.then((teamsData) => {
|
.then((teamsData) => {
|
||||||
resolve({
|
resolve({
|
||||||
dates: datesData,
|
dates: datesData,
|
||||||
|
@ -393,28 +405,38 @@ export default function VoteScreen() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
/**
|
||||||
<View style={GENERAL_STYLES.flex}>
|
* Renders the authenticated screen.
|
||||||
<WebSectionList
|
*
|
||||||
request={request}
|
* Teams and dates are not mandatory to allow showing the information box even if api requests fail
|
||||||
createDataset={createDataset}
|
*
|
||||||
extraData={hasVoted.toString()}
|
* @returns {*}
|
||||||
renderItem={getMainRenderItem}
|
*/
|
||||||
/>
|
render() {
|
||||||
<MascotPopup
|
const { state } = this;
|
||||||
visible={mascotDialogVisible}
|
return (
|
||||||
title={i18n.t('screens.vote.mascotDialog.title')}
|
<View style={GENERAL_STYLES.flex}>
|
||||||
message={i18n.t('screens.vote.mascotDialog.message')}
|
<WebSectionList
|
||||||
icon="vote"
|
request={this.request}
|
||||||
buttons={{
|
createDataset={this.createDataset}
|
||||||
cancel: {
|
extraData={state.hasVoted.toString()}
|
||||||
message: i18n.t('screens.vote.mascotDialog.button'),
|
renderItem={this.getMainRenderItem}
|
||||||
icon: 'check',
|
/>
|
||||||
onPress: hideMascotDialog,
|
<MascotPopup
|
||||||
},
|
visible={state.mascotDialogVisible}
|
||||||
}}
|
title={i18n.t('screens.vote.mascotDialog.title')}
|
||||||
emotion={MASCOT_STYLE.CUTE}
|
message={i18n.t('screens.vote.mascotDialog.message')}
|
||||||
/>
|
icon="vote"
|
||||||
</View>
|
buttons={{
|
||||||
);
|
cancel: {
|
||||||
|
message: i18n.t('screens.vote.mascotDialog.button'),
|
||||||
|
icon: 'check',
|
||||||
|
onPress: this.hideMascotDialog,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
emotion={MASCOT_STYLE.CUTE}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -285,9 +285,8 @@ 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]
|
const gridSize = this.nextPieces[i].getCurrentShape().getCurrentShape()[0]
|
||||||
.getCurrentShape()
|
.length;
|
||||||
.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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,8 +65,9 @@ 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> =
|
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates(
|
||||||
this.currentShape.getCellsCoordinates(true);
|
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,
|
||||||
|
@ -105,8 +106,9 @@ 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> =
|
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates(
|
||||||
this.currentShape.getCellsCoordinates(true);
|
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 ||
|
||||||
|
|
|
@ -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 { StackScreenProps } from '@react-navigation/stack';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import MaterialHeaderButtons, {
|
import MaterialHeaderButtons, {
|
||||||
Item,
|
Item,
|
||||||
} from '../../components/Overrides/CustomHeaderButton';
|
} from '../../components/Overrides/CustomHeaderButton';
|
||||||
|
@ -33,15 +33,11 @@ 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 = StackScreenProps<
|
type PropsType = {
|
||||||
MainStackParamsList,
|
navigation: StackNavigationProp<any>;
|
||||||
MainRoutes.FeedInformation
|
route: { params: { data: FeedItemType; date: string } };
|
||||||
>;
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
|
|
@ -46,6 +46,7 @@ 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';
|
||||||
|
@ -58,7 +59,6 @@ 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,7 +146,9 @@ 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 = useLoginState();
|
const [isLoggedIn, setIsLoggedIn] = useState(
|
||||||
|
ConnectionManager.getInstance().isLoggedIn()
|
||||||
|
);
|
||||||
const { currentDashboard } = useCurrentDashboard();
|
const { currentDashboard } = useCurrentDashboard();
|
||||||
|
|
||||||
let homeDashboard: FullDashboardType | null = null;
|
let homeDashboard: FullDashboardType | null = null;
|
||||||
|
@ -154,7 +156,7 @@ function HomeScreen(props: Props) {
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const getHeaderButton = () => {
|
const getHeaderButton = () => {
|
||||||
let onPressLog = () =>
|
let onPressLog = () =>
|
||||||
navigation.navigate(MainRoutes.Login, { nextScreen: 'profile' });
|
navigation.navigate('login', { nextScreen: 'profile' });
|
||||||
let logIcon = 'login';
|
let logIcon = 'login';
|
||||||
let logColor = theme.colors.primary;
|
let logColor = theme.colors.primary;
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
|
@ -190,15 +192,20 @@ 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.route != null) {
|
if (route.params.nextScreen != null) {
|
||||||
navigation.navigate(route.params.route, route.params.data);
|
navigation.navigate(route.params.nextScreen, 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])
|
||||||
);
|
);
|
||||||
|
@ -328,7 +335,7 @@ function HomeScreen(props: Props) {
|
||||||
|
|
||||||
const hideDisconnectDialog = () => setDialogVisible(false);
|
const hideDisconnectDialog = () => setDialogVisible(false);
|
||||||
|
|
||||||
const openScanner = () => navigation.navigate(MainRoutes.Scanner);
|
const openScanner = () => navigation.navigate('scanner');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the dataset to be used in the FlatList
|
* Creates the dataset to be used in the FlatList
|
||||||
|
|
|
@ -26,6 +26,7 @@ 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';
|
||||||
|
@ -222,6 +223,7 @@ 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()}
|
||||||
|
|
|
@ -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 = {
|
||||||
defaultData?: ParsedUrlDataType;
|
defaultHomeRoute?: string;
|
||||||
|
defaultHomeData?: { [key: string]: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
function MainApp(props: Props, ref?: Ref<NavigationContainerRef<any>>) {
|
function MainApp(props: Props, ref?: Ref<NavigationContainerRef>) {
|
||||||
const darkTheme = useDarkTheme();
|
const darkTheme = useDarkTheme();
|
||||||
const theme = darkTheme ? CustomDarkTheme : CustomWhiteTheme;
|
const theme = darkTheme ? CustomDarkTheme : CustomWhiteTheme;
|
||||||
|
|
||||||
|
@ -44,7 +44,10 @@ function MainApp(props: Props, ref?: Ref<NavigationContainerRef<any>>) {
|
||||||
>
|
>
|
||||||
<SafeAreaView style={GENERAL_STYLES.flex}>
|
<SafeAreaView style={GENERAL_STYLES.flex}>
|
||||||
<NavigationContainer theme={theme} ref={ref}>
|
<NavigationContainer theme={theme} ref={ref}>
|
||||||
<MainNavigator defaultData={props.defaultData} />
|
<MainNavigator
|
||||||
|
defaultHomeRoute={props.defaultHomeRoute}
|
||||||
|
defaultHomeData={props.defaultHomeData}
|
||||||
|
/>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -23,14 +23,11 @@ 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 {
|
import { MainStackParamsList } from '../../navigation/MainNavigator';
|
||||||
MainRoutes,
|
|
||||||
MainStackParamsList,
|
|
||||||
} from '../../navigation/MainNavigator';
|
|
||||||
|
|
||||||
type ImageGalleryScreenNavigationProp = StackScreenProps<
|
type ImageGalleryScreenNavigationProp = StackScreenProps<
|
||||||
MainStackParamsList,
|
MainStackParamsList,
|
||||||
MainRoutes.Gallery
|
'gallery'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type Props = ImageGalleryScreenNavigationProp & {
|
type Props = ImageGalleryScreenNavigationProp & {
|
||||||
|
|
|
@ -32,7 +32,6 @@ 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: {
|
||||||
|
@ -64,10 +63,12 @@ const styles = StyleSheet.create({
|
||||||
*/
|
*/
|
||||||
function DashboardEditScreen() {
|
function DashboardEditScreen() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const isLoggedIn = useLoginState();
|
|
||||||
|
|
||||||
const { currentDashboard, currentDashboardIdList, updateCurrentDashboard } =
|
const {
|
||||||
useCurrentDashboard();
|
currentDashboard,
|
||||||
|
currentDashboardIdList,
|
||||||
|
updateCurrentDashboard,
|
||||||
|
} = useCurrentDashboard();
|
||||||
const initialDashboard = useRef(currentDashboardIdList);
|
const initialDashboard = useRef(currentDashboardIdList);
|
||||||
const [activeItem, setActiveItem] = useState(0);
|
const [activeItem, setActiveItem] = useState(0);
|
||||||
|
|
||||||
|
@ -149,8 +150,7 @@ function DashboardEditScreen() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleFlatList
|
<CollapsibleFlatList
|
||||||
//@ts-ignore
|
data={getCategories(navigation.navigate)}
|
||||||
data={getCategories(navigation.navigate, isLoggedIn)}
|
|
||||||
renderItem={getRenderItem}
|
renderItem={getRenderItem}
|
||||||
ListHeaderComponent={getListHeader()}
|
ListHeaderComponent={getListHeader()}
|
||||||
style={{}}
|
style={{}}
|
||||||
|
|
|
@ -44,7 +44,6 @@ 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: {
|
||||||
|
@ -205,7 +204,7 @@ function SettingsScreen() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getNavigateItem = (
|
const getNavigateItem = (
|
||||||
route: MainRoutes,
|
route: string,
|
||||||
icon: string,
|
icon: string,
|
||||||
title: string,
|
title: string,
|
||||||
subtitle: string,
|
subtitle: string,
|
||||||
|
@ -284,7 +283,7 @@ function SettingsScreen() {
|
||||||
/>
|
/>
|
||||||
{getStartScreenPicker()}
|
{getStartScreenPicker()}
|
||||||
{getNavigateItem(
|
{getNavigateItem(
|
||||||
MainRoutes.DashboardEdit,
|
'dashboard-edit',
|
||||||
'view-dashboard',
|
'view-dashboard',
|
||||||
i18n.t('screens.settings.dashboard'),
|
i18n.t('screens.settings.dashboard'),
|
||||||
i18n.t('screens.settings.dashboardSub')
|
i18n.t('screens.settings.dashboardSub')
|
||||||
|
@ -329,21 +328,21 @@ function SettingsScreen() {
|
||||||
<List.Section>
|
<List.Section>
|
||||||
{isDebugUnlocked
|
{isDebugUnlocked
|
||||||
? getNavigateItem(
|
? getNavigateItem(
|
||||||
MainRoutes.Debug,
|
'debug',
|
||||||
'bug-check',
|
'bug-check',
|
||||||
i18n.t('screens.debug.title'),
|
i18n.t('screens.debug.title'),
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
{getNavigateItem(
|
{getNavigateItem(
|
||||||
MainRoutes.About,
|
'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(
|
||||||
MainRoutes.Feedback,
|
'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')
|
||||||
|
|
|
@ -81,25 +81,26 @@ function GroupSelectionScreen() {
|
||||||
const favoriteGroups = getFavoriteGroups();
|
const favoriteGroups = getFavoriteGroups();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const getSearchBar = () => {
|
|
||||||
return (
|
|
||||||
// @ts-ignore
|
|
||||||
<Searchbar
|
|
||||||
placeholder={i18n.t('screens.proximo.search')}
|
|
||||||
onChangeText={setCurrentSearchString}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerTitle: getSearchBar,
|
headerTitle: getSearchBar,
|
||||||
headerBackTitleVisible: false,
|
headerBackTitleVisible: false,
|
||||||
headerTitleContainerStyle:
|
headerTitleContainerStyle:
|
||||||
Platform.OS === 'ios'
|
Platform.OS === 'ios'
|
||||||
? { marginHorizontal: 0, width: '70%' }
|
? { marginHorizontal: 0, width: '70%' }
|
||||||
: { width: '100%' },
|
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||||
});
|
});
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
|
const getSearchBar = () => {
|
||||||
|
return (
|
||||||
|
// @ts-ignore
|
||||||
|
<Searchbar
|
||||||
|
placeholder={i18n.t('screens.proximo.search')}
|
||||||
|
onChangeText={setCurrentSearchString}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a render item for the given article
|
* Gets a render item for the given article
|
||||||
*
|
*
|
||||||
|
|
|
@ -42,7 +42,6 @@ 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: {
|
||||||
|
@ -106,7 +105,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(MainRoutes.Settings);
|
const onGoToSettings = () => navigation.navigate('settings');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends a FullCalendar action to the web page inside the webview.
|
* Sends a FullCalendar action to the web page inside the webview.
|
||||||
|
|
|
@ -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 { StackScreenProps } from '@react-navigation/stack';
|
import { StackNavigationProp } 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,15 +33,11 @@ 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 = StackScreenProps<
|
type PropsType = {
|
||||||
MainStackParamsList,
|
navigation: StackNavigationProp<any>;
|
||||||
MainRoutes.PlanningInformation
|
route: { params: { data: PlanningEventType; id: number; eventId: number } };
|
||||||
>;
|
};
|
||||||
|
|
||||||
type StateType = {
|
type StateType = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
@ -82,7 +78,7 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
|
||||||
constructor(props: PropsType) {
|
constructor(props: PropsType) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
if (props.route.params.type === 'full') {
|
if (props.route.params.data != null) {
|
||||||
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;
|
||||||
|
|
|
@ -36,7 +36,6 @@ 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: [
|
||||||
|
@ -77,7 +76,6 @@ 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 = {
|
||||||
|
@ -218,7 +216,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(MainRoutes.PlanningInformation, {
|
navigation.navigate('planning-information', {
|
||||||
data: item,
|
data: item,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
|
|
||||||
import React, { useLayoutEffect, useRef, useState } from 'react';
|
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Linking,
|
|
||||||
SectionListData,
|
SectionListData,
|
||||||
SectionListRenderItemInfo,
|
SectionListRenderItemInfo,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
|
@ -52,7 +51,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 ProxiwashListHeader from '../../components/Lists/Proxiwash/ProxiwashListHeader';
|
import ProximoListHeader from '../../components/Lists/Proximo/ProximoListHeader';
|
||||||
import {
|
import {
|
||||||
getPreferenceNumber,
|
getPreferenceNumber,
|
||||||
getPreferenceObject,
|
getPreferenceObject,
|
||||||
|
@ -61,7 +60,6 @@ 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;
|
||||||
|
@ -77,13 +75,7 @@ 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>;
|
||||||
};
|
};
|
||||||
|
@ -107,8 +99,10 @@ function ProxiwashScreen() {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { preferences, updatePreferences } = useProxiwashPreferences();
|
const { preferences, updatePreferences } = useProxiwashPreferences();
|
||||||
const [modalCurrentDisplayItem, setModalCurrentDisplayItem] =
|
const [
|
||||||
useState<React.ReactElement | null>(null);
|
modalCurrentDisplayItem,
|
||||||
|
setModalCurrentDisplayItem,
|
||||||
|
] = useState<React.ReactElement | null>(null);
|
||||||
const reminder = getPreferenceNumber(
|
const reminder = getPreferenceNumber(
|
||||||
ProxiwashPreferenceKeys.proxiwashNotifications,
|
ProxiwashPreferenceKeys.proxiwashNotifications,
|
||||||
preferences
|
preferences
|
||||||
|
@ -161,22 +155,15 @@ 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(MainRoutes.ProxiwashAbout)}
|
onPress={() => navigation.navigate('proxiwash-about')}
|
||||||
/>
|
/>
|
||||||
</MaterialHeaderButtons>
|
</MaterialHeaderButtons>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}, [navigation, selectedWash]);
|
}, [navigation]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback used when the user clicks on enable notifications for a machine
|
* Callback used when the user clicks on enable notifications for a machine
|
||||||
|
@ -451,11 +438,7 @@ function ProxiwashScreen() {
|
||||||
) => {
|
) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
return (
|
return (
|
||||||
<ProxiwashListHeader
|
<ProximoListHeader date={lastRefreshDate} selectedWash={selectedWash} />
|
||||||
date={lastRefreshDate}
|
|
||||||
selectedWash={selectedWash}
|
|
||||||
info={data?.info}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
|
@ -495,7 +478,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(MainRoutes.Settings),
|
onPress: () => navigation.navigate('settings'),
|
||||||
},
|
},
|
||||||
cancel: {
|
cancel: {
|
||||||
message: i18n.t('screens.proxiwash.mascotDialog.cancel'),
|
message: i18n.t('screens.proxiwash.mascotDialog.cancel'),
|
||||||
|
|
|
@ -120,7 +120,6 @@ 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);
|
||||||
|
@ -131,70 +130,6 @@ 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,
|
||||||
|
@ -202,9 +137,21 @@ function ProximoListScreen(props: Props) {
|
||||||
headerTitleContainerStyle:
|
headerTitleContainerStyle:
|
||||||
Platform.OS === 'ios'
|
Platform.OS === 'ios'
|
||||||
? { marginHorizontal: 0, width: '70%' }
|
? { marginHorizontal: 0, width: '70%' }
|
||||||
: { width: '100%' },
|
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||||
});
|
});
|
||||||
}, [navigation, currentSortMode, navParams.shouldFocusSearchBar]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [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.
|
||||||
|
@ -219,6 +166,19 @@ 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
|
||||||
*
|
*
|
||||||
|
@ -237,6 +197,35 @@ 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
|
||||||
*
|
*
|
||||||
|
@ -273,6 +262,42 @@ 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
|
||||||
*
|
*
|
||||||
|
@ -316,8 +341,8 @@ function ProximoListScreen(props: Props) {
|
||||||
data: data
|
data: data
|
||||||
.filter(
|
.filter(
|
||||||
(d) =>
|
(d) =>
|
||||||
navParams.category === -1 ||
|
props.route.params.category === -1 ||
|
||||||
navParams.category === d.category_id
|
props.route.params.category === d.category_id
|
||||||
)
|
)
|
||||||
.sort(sortModes[currentSortMode]),
|
.sort(sortModes[currentSortMode]),
|
||||||
keyExtractor: keyExtractor,
|
keyExtractor: keyExtractor,
|
||||||
|
|
|
@ -32,7 +32,6 @@ 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;
|
||||||
|
|
||||||
|
@ -123,10 +122,10 @@ function ProximoMainScreen() {
|
||||||
shouldFocusSearchBar: true,
|
shouldFocusSearchBar: true,
|
||||||
category: -1,
|
category: -1,
|
||||||
};
|
};
|
||||||
navigation.navigate(MainRoutes.ProximoList, searchScreenData);
|
navigation.navigate('proximo-list', searchScreenData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPressAboutBtn = () => navigation.navigate(MainRoutes.ProximoAbout);
|
const onPressAboutBtn = () => navigation.navigate('proximo-about');
|
||||||
|
|
||||||
const getHeaderButtons = () => {
|
const getHeaderButtons = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -171,8 +170,7 @@ function ProximoMainScreen() {
|
||||||
? i18n.t('screens.proximo.articles')
|
? i18n.t('screens.proximo.articles')
|
||||||
: i18n.t('screens.proximo.article')
|
: i18n.t('screens.proximo.article')
|
||||||
}`;
|
}`;
|
||||||
const onPress = () =>
|
const onPress = () => navigation.navigate('proximo-list', dataToSend);
|
||||||
navigation.navigate(MainRoutes.ProximoList, dataToSend);
|
|
||||||
if (article_number > 0) {
|
if (article_number > 0) {
|
||||||
return (
|
return (
|
||||||
<List.Item
|
<List.Item
|
||||||
|
|
|
@ -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 React, { useLayoutEffect } from 'react';
|
import * as React from 'react';
|
||||||
import { Image, StyleSheet, View } from 'react-native';
|
import { Image, StyleSheet, View } from 'react-native';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
|
@ -25,9 +25,10 @@ import {
|
||||||
Divider,
|
Divider,
|
||||||
List,
|
List,
|
||||||
TouchableRipple,
|
TouchableRipple,
|
||||||
useTheme,
|
withTheme,
|
||||||
} 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,
|
||||||
|
@ -40,9 +41,11 @@ import {
|
||||||
ServiceCategoryType,
|
ServiceCategoryType,
|
||||||
SERVICES_CATEGORIES_KEY,
|
SERVICES_CATEGORIES_KEY,
|
||||||
} from '../../utils/Services';
|
} from '../../utils/Services';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
|
||||||
import { useLoginState } from '../../context/loginContext';
|
type PropsType = {
|
||||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
navigation: StackNavigationProp<any>;
|
||||||
|
theme: ReactNativePaper.Theme;
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
|
@ -58,30 +61,37 @@ const styles = StyleSheet.create({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function ServicesScreen() {
|
class ServicesScreen extends React.Component<PropsType> {
|
||||||
const navigation = useNavigation();
|
finalDataset: Array<ServiceCategoryType>;
|
||||||
const theme = useTheme();
|
|
||||||
const isLoggedIn = useLoginState();
|
|
||||||
|
|
||||||
//@ts-ignore
|
constructor(props: PropsType) {
|
||||||
const finalDataset = getCategories(navigation.navigate, isLoggedIn, [
|
super(props);
|
||||||
SERVICES_CATEGORIES_KEY.SPECIAL,
|
this.finalDataset = getCategories(props.navigation.navigate, [
|
||||||
]);
|
SERVICES_CATEGORIES_KEY.SPECIAL,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
componentDidMount() {
|
||||||
const getAboutButton = () => (
|
const { props } = this;
|
||||||
<MaterialHeaderButtons>
|
props.navigation.setOptions({
|
||||||
<Item
|
headerRight: this.getAboutButton,
|
||||||
title="information"
|
|
||||||
iconName="information"
|
|
||||||
onPress={() => navigation.navigate(MainRoutes.AmicaleContact)}
|
|
||||||
/>
|
|
||||||
</MaterialHeaderButtons>
|
|
||||||
);
|
|
||||||
navigation.setOptions({
|
|
||||||
headerRight: getAboutButton,
|
|
||||||
});
|
});
|
||||||
}, [navigation]);
|
}
|
||||||
|
|
||||||
|
getAboutButton = () => (
|
||||||
|
<MaterialHeaderButtons>
|
||||||
|
<Item
|
||||||
|
title="information"
|
||||||
|
iconName="information"
|
||||||
|
onPress={this.onAboutPress}
|
||||||
|
/>
|
||||||
|
</MaterialHeaderButtons>
|
||||||
|
);
|
||||||
|
|
||||||
|
onAboutPress = () => {
|
||||||
|
const { props } = this;
|
||||||
|
props.navigation.navigate('amicale-contact');
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the list title image for the list.
|
* Gets the list title image for the list.
|
||||||
|
@ -92,7 +102,8 @@ function ServicesScreen() {
|
||||||
* @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 {*}
|
||||||
*/
|
*/
|
||||||
const getListTitleImage = (source: string | number) => {
|
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} />;
|
||||||
}
|
}
|
||||||
|
@ -100,11 +111,11 @@ function ServicesScreen() {
|
||||||
<Avatar.Icon
|
<Avatar.Icon
|
||||||
size={48}
|
size={48}
|
||||||
icon={source}
|
icon={source}
|
||||||
color={theme.colors.primary}
|
color={props.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
|
||||||
|
@ -112,19 +123,20 @@ function ServicesScreen() {
|
||||||
* @param item
|
* @param item
|
||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
const getRenderItem = ({ item }: { item: ServiceCategoryType }) => {
|
getRenderItem = ({ item }: { item: ServiceCategoryType }) => {
|
||||||
|
const { props } = this;
|
||||||
return (
|
return (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
onPress={() =>
|
onPress={() => {
|
||||||
navigation.navigate(MainRoutes.ServicesSection, { data: item })
|
props.navigation.navigate('services-section', { data: item });
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Card.Title
|
<Card.Title
|
||||||
title={item.title}
|
title={item.title}
|
||||||
subtitle={item.subtitle}
|
subtitle={item.subtitle}
|
||||||
left={() => getListTitleImage(item.image)}
|
left={() => this.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 />
|
||||||
|
@ -133,31 +145,33 @@ function ServicesScreen() {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyExtractor = (item: ServiceCategoryType): string => item.title;
|
keyExtractor = (item: ServiceCategoryType): string => item.title;
|
||||||
|
|
||||||
return (
|
render() {
|
||||||
<View>
|
return (
|
||||||
<CollapsibleFlatList
|
<View>
|
||||||
data={finalDataset}
|
<CollapsibleFlatList
|
||||||
renderItem={getRenderItem}
|
data={this.finalDataset}
|
||||||
keyExtractor={keyExtractor}
|
renderItem={this.getRenderItem}
|
||||||
ItemSeparatorComponent={() => <Divider />}
|
keyExtractor={this.keyExtractor}
|
||||||
hasTab
|
ItemSeparatorComponent={() => <Divider />}
|
||||||
/>
|
hasTab
|
||||||
<MascotPopup
|
/>
|
||||||
title={i18n.t('screens.services.mascotDialog.title')}
|
<MascotPopup
|
||||||
message={i18n.t('screens.services.mascotDialog.message')}
|
title={i18n.t('screens.services.mascotDialog.title')}
|
||||||
icon="cloud-question"
|
message={i18n.t('screens.services.mascotDialog.message')}
|
||||||
buttons={{
|
icon="cloud-question"
|
||||||
cancel: {
|
buttons={{
|
||||||
message: i18n.t('screens.services.mascotDialog.button'),
|
cancel: {
|
||||||
icon: 'check',
|
message: i18n.t('screens.services.mascotDialog.button'),
|
||||||
},
|
icon: 'check',
|
||||||
}}
|
},
|
||||||
emotion={MASCOT_STYLE.WINK}
|
}}
|
||||||
/>
|
emotion={MASCOT_STYLE.WINK}
|
||||||
</View>
|
/>
|
||||||
);
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ServicesScreen;
|
export default withTheme(ServicesScreen);
|
||||||
|
|
|
@ -18,19 +18,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 { StackScreenProps } from '@react-navigation/stack';
|
import { StackNavigationProp } 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 = StackScreenProps<
|
type PropsType = {
|
||||||
MainStackParamsList,
|
navigation: StackNavigationProp<any>;
|
||||||
MainRoutes.ServicesSection
|
route: { params: { data: ServiceCategoryType | null } };
|
||||||
>;
|
collapsibleStack: Collapsible;
|
||||||
|
};
|
||||||
|
|
||||||
class ServicesSectionScreen extends React.Component<PropsType> {
|
class ServicesSectionScreen extends React.Component<PropsType> {
|
||||||
finalDataset: null | ServiceCategoryType;
|
finalDataset: null | ServiceCategoryType;
|
||||||
|
|
|
@ -18,16 +18,15 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { StackScreenProps } from '@react-navigation/stack';
|
import { StackNavigationProp } 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 = StackScreenProps<MainStackParamsList, MainRoutes.Website>;
|
type Props = {
|
||||||
|
navigation: StackNavigationProp<any>;
|
||||||
|
route: { params: { host: string; path: string | null; title: string } };
|
||||||
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
url: string;
|
url: string;
|
||||||
|
|
|
@ -19,107 +19,56 @@
|
||||||
|
|
||||||
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, {
|
import PushNotification from 'react-native-push-notification';
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
PushNotification.createChannel(
|
||||||
* Clean channels before creating a new one
|
{
|
||||||
*/
|
channelId: 'reminders', // (required)
|
||||||
function cleanChannels() {
|
channelName: 'Reminders', // (required)
|
||||||
PushNotification.getChannels((idList) => {
|
channelDescription: 'Get laundry reminders', // (optional) default: undefined.
|
||||||
idList.forEach((i) => {
|
playSound: true, // (optional) default: true
|
||||||
if (i !== channelId) {
|
soundName: 'default', // (optional) See `soundName` parameter of `localNotification` function
|
||||||
PushNotification.deleteChannel(i);
|
importance: 4, // (optional) default: 4. Int value of the Android notification importance
|
||||||
}
|
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.
|
||||||
}
|
);
|
||||||
|
|
||||||
export function setupNotifications() {
|
PushNotification.configure({
|
||||||
cleanChannels();
|
// (required) Called when a remote is received or opened, or local notification is opened
|
||||||
PushNotification.channelExists(channelId, (exists) => {
|
onNotification: function (notification) {
|
||||||
if (!exists) {
|
console.log('NOTIFICATION:', notification);
|
||||||
PushNotification.createChannel(
|
|
||||||
{
|
// process the notification
|
||||||
channelId: channelId, // (required)
|
|
||||||
channelName: i18n.t('screens.proxiwash.notifications.channel.title'), // (required)
|
|
||||||
channelDescription: i18n.t(
|
|
||||||
'screens.proxiwash.notifications.channel.description'
|
|
||||||
), // (optional) default: undefined.
|
|
||||||
playSound: true, // (optional) default: true
|
|
||||||
soundName: 'default', // (optional) See `soundName` parameter of `localNotification` function
|
|
||||||
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.
|
|
||||||
},
|
|
||||||
(created) => console.log(`createChannel returned '${created}'`) // (optional) callback returns whether the channel was created, false means it already existed.
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
||||||
onNotification: function (notification) {
|
notification.finish(PushNotificationIOS.FetchResult.NoData);
|
||||||
console.log('NOTIFICATION:', notification);
|
},
|
||||||
|
|
||||||
// process the notification
|
// IOS ONLY (optional): default: all - Permissions to register.
|
||||||
|
permissions: {
|
||||||
|
alert: true,
|
||||||
|
badge: true,
|
||||||
|
sound: true,
|
||||||
|
},
|
||||||
|
|
||||||
// (required) Called when a remote is received or opened, or local notification is opened
|
// Should the initial notification be popped automatically
|
||||||
notification.finish(PushNotificationIOS.FetchResult.NoData);
|
// default: true
|
||||||
},
|
popInitialNotification: true,
|
||||||
|
|
||||||
// IOS ONLY (optional): default: all - Permissions to register.
|
/**
|
||||||
permissions: {
|
* (optional) default: true
|
||||||
alert: true,
|
* - Specified if permissions (ios) and token (android and ios) will requested or not,
|
||||||
badge: true,
|
* - if not, you must call PushNotificationsHandler.requestPermissions() later
|
||||||
sound: true,
|
* - if you are not using remote notification or do not have Firebase installed, use this:
|
||||||
},
|
* requestPermissions: Platform.OS === 'ios'
|
||||||
|
*/
|
||||||
// Should the initial notification be popped automatically
|
requestPermissions: Platform.OS === 'ios',
|
||||||
// default: true
|
});
|
||||||
popInitialNotification: true,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* (optional) default: true
|
|
||||||
* - Specified if permissions (ios) and token (android and ios) will requested or not,
|
|
||||||
* - if not, you must call PushNotificationsHandler.requestPermissions() later
|
|
||||||
* - if you are not using remote notification or do not have Firebase installed, use this:
|
|
||||||
* 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.
|
||||||
|
@ -135,7 +84,6 @@ 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,
|
||||||
}),
|
}),
|
||||||
|
@ -148,7 +96,6 @@ 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,
|
||||||
|
@ -177,8 +124,8 @@ export function setupMachineNotification(
|
||||||
if (isEnabled && endDate) {
|
if (isEnabled && endDate) {
|
||||||
createNotifications(machineID, endDate, reminder);
|
createNotifications(machineID, endDate, reminder);
|
||||||
} else {
|
} else {
|
||||||
PushNotification.cancelLocalNotification(machineID);
|
PushNotification.cancelLocalNotifications({ id: machineID });
|
||||||
const reminderId = reminderIdFactor * parseInt(machineID, 10);
|
const reminderId = reminderIdFactor * parseInt(machineID, 10);
|
||||||
PushNotification.cancelLocalNotification(reminderId.toString());
|
PushNotification.cancelLocalNotifications({ id: reminderId.toString() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(numberOfMonths: number): {
|
export function generateEmptyCalendar(
|
||||||
[key: string]: Array<PlanningEventType>;
|
numberOfMonths: number
|
||||||
} {
|
): { [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> } = {};
|
||||||
|
|
|
@ -86,21 +86,8 @@ 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: MainRoutes, params?: { [key: string]: any }) => void,
|
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||||
isLoggedIn: boolean,
|
|
||||||
excludedItems?: Array<string>
|
excludedItems?: Array<string>
|
||||||
): Array<ServiceItemType> {
|
): Array<ServiceItemType> {
|
||||||
const amicaleDataset = [
|
const amicaleDataset = [
|
||||||
|
@ -109,21 +96,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: getAmicaleOnPress(MainRoutes.ClubList, onPress, isLoggedIn),
|
onPress: () => onPress(MainRoutes.ClubList),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
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: getAmicaleOnPress(MainRoutes.Profile, onPress, isLoggedIn),
|
onPress: () => onPress(MainRoutes.Profile),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: SERVICES_KEY.EQUIPMENT,
|
key: SERVICES_KEY.EQUIPMENT,
|
||||||
title: i18n.t('screens.equipment.title'),
|
title: i18n.t('screens.equipment.title'),
|
||||||
subtitle: i18n.t('screens.services.descriptions.equipment'),
|
subtitle: i18n.t('screens.services.descriptions.equipment'),
|
||||||
image: Urls.images.equipment,
|
image: Urls.images.equipment,
|
||||||
onPress: getAmicaleOnPress(MainRoutes.EquipmentList, onPress, isLoggedIn),
|
onPress: () => onPress(MainRoutes.EquipmentList),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: SERVICES_KEY.AMICALE_WEBSITE,
|
key: SERVICES_KEY.AMICALE_WEBSITE,
|
||||||
|
@ -148,7 +135,7 @@ export function getAmicaleServices(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStudentServices(
|
export function getStudentServices(
|
||||||
onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
|
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||||
excludedItems?: Array<string>
|
excludedItems?: Array<string>
|
||||||
): Array<ServiceItemType> {
|
): Array<ServiceItemType> {
|
||||||
const studentsDataset = [
|
const studentsDataset = [
|
||||||
|
@ -201,7 +188,7 @@ export function getStudentServices(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getINSAServices(
|
export function getINSAServices(
|
||||||
onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
|
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||||
excludedItems?: Array<string>
|
excludedItems?: Array<string>
|
||||||
): Array<ServiceItemType> {
|
): Array<ServiceItemType> {
|
||||||
const insaDataset = [
|
const insaDataset = [
|
||||||
|
@ -274,10 +261,7 @@ export function getINSAServices(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSpecialServices(
|
export function getSpecialServices(
|
||||||
onPress: (
|
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||||
route: MainRoutes | TabRoutes,
|
|
||||||
params?: { [key: string]: any }
|
|
||||||
) => void,
|
|
||||||
excludedItems?: Array<string>
|
excludedItems?: Array<string>
|
||||||
): Array<ServiceItemType> {
|
): Array<ServiceItemType> {
|
||||||
const specialDataset = [
|
const specialDataset = [
|
||||||
|
@ -304,11 +288,7 @@ export function getSpecialServices(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCategories(
|
export function getCategories(
|
||||||
onPress: (
|
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||||
route: MainRoutes | TabRoutes,
|
|
||||||
params?: { [key: string]: any }
|
|
||||||
) => void,
|
|
||||||
isLoggedIn: boolean,
|
|
||||||
excludedItems?: Array<string>
|
excludedItems?: Array<string>
|
||||||
): Array<ServiceCategoryType> {
|
): Array<ServiceCategoryType> {
|
||||||
const categoriesDataset = [
|
const categoriesDataset = [
|
||||||
|
@ -317,7 +297,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, isLoggedIn),
|
content: getAmicaleServices(onPress),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: SERVICES_CATEGORIES_KEY.STUDENTS,
|
key: SERVICES_CATEGORIES_KEY.STUDENTS,
|
||||||
|
|
|
@ -101,13 +101,13 @@ export const CustomWhiteTheme: ReactNativePaper.Theme = {
|
||||||
// Tetris
|
// Tetris
|
||||||
tetrisBackground: '#f0f0f0',
|
tetrisBackground: '#f0f0f0',
|
||||||
tetrisScore: '#e2bd33',
|
tetrisScore: '#e2bd33',
|
||||||
tetrisI: '#be1522',
|
tetrisI: '#3cd9e6',
|
||||||
tetrisO: '#EB6C1F',
|
tetrisO: '#ffdd00',
|
||||||
tetrisT: '#5cb85c',
|
tetrisT: '#a716e5',
|
||||||
tetrisS: '#5294E2',
|
tetrisS: '#09c528',
|
||||||
tetrisZ: '#dede00',
|
tetrisZ: '#ff0009',
|
||||||
tetrisJ: '#69009d',
|
tetrisJ: '#2a67e3',
|
||||||
tetrisL: '#553716',
|
tetrisL: '#da742d',
|
||||||
|
|
||||||
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: '#be1522',
|
tetrisI: '#30b3be',
|
||||||
tetrisO: '#EB6C1F',
|
tetrisO: '#c1a700',
|
||||||
tetrisT: '#5cb85c',
|
tetrisT: '#9114c7',
|
||||||
tetrisS: '#5294E2',
|
tetrisS: '#08a121',
|
||||||
tetrisZ: '#dede00',
|
tetrisZ: '#b50008',
|
||||||
tetrisJ: '#69009d',
|
tetrisJ: '#0f37b9',
|
||||||
tetrisL: '#553716',
|
tetrisL: '#b96226',
|
||||||
|
|
||||||
gameGold: '#ffd610',
|
gameGold: '#ffd610',
|
||||||
gameSilver: '#7b7b7b',
|
gameSilver: '#7b7b7b',
|
||||||
|
|
|
@ -18,15 +18,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Linking } from 'react-native';
|
import { Linking } from 'react-native';
|
||||||
import {
|
|
||||||
ClubInformationScreenParams,
|
|
||||||
MainRoutes,
|
|
||||||
PlanningInformationScreenParams,
|
|
||||||
} from '../navigation/MainNavigator';
|
|
||||||
|
|
||||||
export type ParsedUrlDataType = {
|
export type ParsedUrlDataType = {
|
||||||
route: MainRoutes.ClubInformation | MainRoutes.PlanningInformation;
|
route: string;
|
||||||
data: ClubInformationScreenParams | PlanningInformationScreenParams;
|
data: { [key: string]: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ParsedUrlCallbackType = (parsedData: ParsedUrlDataType) => void;
|
export type ParsedUrlCallbackType = (parsedData: ParsedUrlDataType) => void;
|
||||||
|
@ -46,6 +41,10 @@ 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;
|
||||||
|
@ -153,11 +152,8 @@ 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: MainRoutes.ClubInformation,
|
route: URLHandler.CLUB_INFO_ROUTE,
|
||||||
data: {
|
data: { clubId: id.toString() },
|
||||||
type: 'id',
|
|
||||||
clubId: id,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,11 +173,8 @@ 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: MainRoutes.PlanningInformation,
|
route: URLHandler.EVENT_INFO_ROUTE,
|
||||||
data: {
|
data: { eventId: id.toString() },
|
||||||
type: 'id',
|
|
||||||
eventId: id,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,8 +80,7 @@ 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) => {
|
||||||
|
@ -89,9 +88,7 @@ export async function apiRequest<T>(
|
||||||
if (params != null) {
|
if (params != null) {
|
||||||
requestParams = { ...params };
|
requestParams = { ...params };
|
||||||
}
|
}
|
||||||
if (token) {
|
console.log(Urls.amicale.api + path);
|
||||||
requestParams = { ...requestParams, token: token };
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(Urls.amicale.api + path, {
|
fetch(Urls.amicale.api + path, {
|
||||||
method,
|
method,
|
||||||
|
@ -104,9 +101,11 @@ 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((data): ApiResponseType<T> => {
|
return response.json().then(
|
||||||
return { status: status, error: data.error, data: data.data };
|
(data): ApiResponseType<T> => {
|
||||||
});
|
return { status: status, error: data.error, data: data.data };
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return { status: status };
|
return { status: status };
|
||||||
}
|
}
|
||||||
|
@ -136,33 +135,6 @@ 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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -110,17 +110,13 @@ export const defaultPreferences: { [key in GeneralPreferenceKeys]: string } = {
|
||||||
export function isValidGeneralPreferenceKey(
|
export function isValidGeneralPreferenceKey(
|
||||||
key: string
|
key: string
|
||||||
): key is GeneralPreferenceKeys {
|
): key is GeneralPreferenceKeys {
|
||||||
return Object.values(GeneralPreferenceKeys).includes(
|
return key in Object.values(GeneralPreferenceKeys);
|
||||||
key as GeneralPreferenceKeys
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidMascotPreferenceKey(
|
export function isValidMascotPreferenceKey(
|
||||||
key: string
|
key: string
|
||||||
): key is MascotPreferenceKeys {
|
): key is MascotPreferenceKeys {
|
||||||
return Object.values(MascotPreferenceKeys).includes(
|
return key in Object.values(MascotPreferenceKeys);
|
||||||
key as MascotPreferenceKeys
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
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;
|
|
||||||
};
|
|
|
@ -30,34 +30,33 @@
|
||||||
// "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'. */
|
||||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
"resolveJsonModule": true, /* Allow import of JSON files */
|
"resolveJsonModule": true, /* Allow import of JSON files */
|
||||||
/* Source Map Options */
|
/* Source Map Options */
|
||||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
// "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"
|
||||||
|
|
Loading…
Reference in a new issue