Compare commits
35 commits
functional
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
aaf72d9122 | ||
|
1a696f0628 | ||
|
9acfbf00df | ||
|
9efd40e48c | ||
|
de8820eada | ||
|
dc944060e1 | ||
|
2c11addf40 | ||
|
8bacddc7b5 | ||
|
53ec2bb578 | ||
|
764296708c | ||
|
d3e94ac9b3 | ||
|
7c38ec0bdb | ||
|
cb3af52483 | ||
|
d5c6aa6b48 | ||
|
6104b88815 | ||
|
26f6518270 | ||
|
3b2776542a | ||
|
76f13f04d5 | ||
|
67b5a5fb4f | ||
|
c75b90d254 | ||
|
b9c99bf269 | ||
7f763dcbcb | |||
|
1f930223c4 | ||
|
ba62e5d3ec | ||
|
b127cca068 | ||
|
06dc9966ec | ||
|
53b3f00005 | ||
|
20aed5cc80 | ||
|
0be3a53747 | ||
|
bdffd01df4 | ||
|
c500ae05e6 | ||
|
b289a85b8a | ||
|
ffa4cfa376 | ||
|
541c002558 | ||
|
44aa52b3aa |
82 changed files with 28485 additions and 4279 deletions
47
App.tsx
47
App.tsx
|
@ -21,7 +21,6 @@ import React from 'react';
|
|||
import { LogBox, Platform } from 'react-native';
|
||||
import { setSafeBounceHeight } from 'react-navigation-collapsible';
|
||||
import SplashScreen from 'react-native-splash-screen';
|
||||
import ConnectionManager from './src/managers/ConnectionManager';
|
||||
import type { ParsedUrlDataType } from './src/utils/URLHandler';
|
||||
import URLHandler from './src/utils/URLHandler';
|
||||
import initLocales from './src/utils/Locales';
|
||||
|
@ -48,15 +47,17 @@ import {
|
|||
ProxiwashPreferencesProvider,
|
||||
} from './src/components/providers/PreferencesProvider';
|
||||
import MainApp from './src/screens/MainApp';
|
||||
import LoginProvider from './src/components/providers/LoginProvider';
|
||||
import { retrieveLoginToken } from './src/utils/loginToken';
|
||||
import { setupNotifications } from './src/utils/Notifications';
|
||||
import { TabRoutes } from './src/navigation/TabNavigator';
|
||||
|
||||
// Native optimizations https://reactnavigation.org/docs/react-native-screens
|
||||
// Crashes app when navigating away from webview on android 9+
|
||||
// enableScreens(true);
|
||||
initLocales();
|
||||
setupNotifications();
|
||||
|
||||
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',
|
||||
'`new NativeEventEmitter()` was called with a non-null argument',
|
||||
]);
|
||||
|
||||
type StateType = {
|
||||
|
@ -67,14 +68,13 @@ type StateType = {
|
|||
proxiwash: ProxiwashPreferencesType;
|
||||
mascot: MascotPreferencesType;
|
||||
};
|
||||
loginToken?: string;
|
||||
};
|
||||
|
||||
export default class App extends React.Component<{}, StateType> {
|
||||
navigatorRef: { current: null | NavigationContainerRef };
|
||||
navigatorRef: { current: null | NavigationContainerRef<any> };
|
||||
|
||||
defaultHomeRoute: string | undefined;
|
||||
|
||||
defaultHomeData: { [key: string]: string } | undefined;
|
||||
defaultData?: ParsedUrlDataType;
|
||||
|
||||
urlHandler: URLHandler;
|
||||
|
||||
|
@ -88,11 +88,10 @@ export default class App extends React.Component<{}, StateType> {
|
|||
proxiwash: defaultProxiwashPreferences,
|
||||
mascot: defaultMascotPreferences,
|
||||
},
|
||||
loginToken: undefined,
|
||||
};
|
||||
initLocales();
|
||||
this.navigatorRef = React.createRef();
|
||||
this.defaultHomeRoute = undefined;
|
||||
this.defaultHomeData = undefined;
|
||||
this.defaultData = undefined;
|
||||
this.urlHandler = new URLHandler(this.onInitialURLParsed, this.onDetectURL);
|
||||
this.urlHandler.listen();
|
||||
setSafeBounceHeight(Platform.OS === 'ios' ? 100 : 20);
|
||||
|
@ -106,8 +105,7 @@ export default class App extends React.Component<{}, StateType> {
|
|||
* @param parsedData The data parsed from the url
|
||||
*/
|
||||
onInitialURLParsed = (parsedData: ParsedUrlDataType) => {
|
||||
this.defaultHomeRoute = parsedData.route;
|
||||
this.defaultHomeData = parsedData.data;
|
||||
this.defaultData = parsedData;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -120,9 +118,9 @@ export default class App extends React.Component<{}, StateType> {
|
|||
// Navigate to nested navigator and pass data to the index screen
|
||||
const nav = this.navigatorRef.current;
|
||||
if (nav != null) {
|
||||
nav.navigate('home', {
|
||||
screen: 'index',
|
||||
params: { nextScreen: parsedData.route, data: parsedData.data },
|
||||
nav.navigate(TabRoutes.Home, {
|
||||
nextScreen: parsedData.route,
|
||||
data: parsedData.data,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -136,10 +134,11 @@ export default class App extends React.Component<{}, StateType> {
|
|||
| PlanexPreferencesType
|
||||
| ProxiwashPreferencesType
|
||||
| MascotPreferencesType
|
||||
| void
|
||||
| string
|
||||
| undefined
|
||||
>
|
||||
) => {
|
||||
const [general, planex, proxiwash, mascot] = values;
|
||||
const [general, planex, proxiwash, mascot, token] = values;
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
initialPreferences: {
|
||||
|
@ -148,6 +147,7 @@ export default class App extends React.Component<{}, StateType> {
|
|||
proxiwash: proxiwash as ProxiwashPreferencesType,
|
||||
mascot: mascot as MascotPreferencesType,
|
||||
},
|
||||
loginToken: token as string | undefined,
|
||||
});
|
||||
SplashScreen.hide();
|
||||
};
|
||||
|
@ -175,7 +175,7 @@ export default class App extends React.Component<{}, StateType> {
|
|||
Object.values(MascotPreferenceKeys),
|
||||
defaultMascotPreferences
|
||||
),
|
||||
ConnectionManager.getInstance().recoverLogin(),
|
||||
retrieveLoginToken(),
|
||||
])
|
||||
.then(this.onLoadFinished)
|
||||
.catch(this.onLoadFinished);
|
||||
|
@ -202,11 +202,12 @@ export default class App extends React.Component<{}, StateType> {
|
|||
<MascotPreferencesProvider
|
||||
initialPreferences={this.state.initialPreferences.mascot}
|
||||
>
|
||||
<LoginProvider initialToken={this.state.loginToken}>
|
||||
<MainApp
|
||||
ref={this.navigatorRef}
|
||||
defaultHomeData={this.defaultHomeData}
|
||||
defaultHomeRoute={this.defaultHomeRoute}
|
||||
defaultData={this.defaultData}
|
||||
/>
|
||||
</LoginProvider>
|
||||
</MascotPreferencesProvider>
|
||||
</ProxiwashPreferencesProvider>
|
||||
</PlanexPreferencesProvider>
|
||||
|
|
|
@ -141,17 +141,12 @@ android {
|
|||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'fr.amicaleinsat.application'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 45
|
||||
versionName "4.1.0"
|
||||
versionCode 49
|
||||
versionName "5.0.0-3"
|
||||
missingDimensionStrategy 'react-native-camera', 'general'
|
||||
}
|
||||
splits {
|
||||
|
@ -238,7 +233,7 @@ dependencies {
|
|||
// Run this once to be able to run the application with BUCK
|
||||
// puts all compile dependencies into folder libs for BUCK to use
|
||||
task copyDownloadableDepsToLibs(type: Copy) {
|
||||
from configurations.compile
|
||||
from configurations.implementation
|
||||
into 'libs'
|
||||
}
|
||||
|
||||
|
|
|
@ -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) -->
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_foreground"
|
||||
android:value="false"/>
|
||||
Change the resource name to your App's accent color - or any other color you want
|
||||
<!-- Change the resource name to your App's accent color - or any other color you want -->
|
||||
<meta-data android:name="com.dieam.reactnativepushnotification.notification_color"
|
||||
android:resource="@color/colorPrimary"/>
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
|
||||
buildscript {
|
||||
ext {
|
||||
buildToolsVersion = "29.0.3"
|
||||
buildToolsVersion = "30.0.2"
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 29
|
||||
targetSdkVersion = 29
|
||||
compileSdkVersion = 30
|
||||
targetSdkVersion = 30
|
||||
ndkVersion = "20.1.5948944"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:4.1.0")
|
||||
classpath("com.android.tools.build:gradle:4.2.1")
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -22,6 +22,7 @@ buildscript {
|
|||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
mavenLocal()
|
||||
maven {
|
||||
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
|
||||
|
@ -36,7 +37,6 @@ allprojects {
|
|||
url "$rootDir/../node_modules/expo-camera/android/maven"
|
||||
}
|
||||
google()
|
||||
jcenter()
|
||||
maven { url 'https://www.jitpack.io' }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,4 +24,8 @@ android.useAndroidX=true
|
|||
# Automatically convert third-party libraries to use AndroidX
|
||||
android.enableJetifier=true
|
||||
# Version of flipper SDK to use with React Native
|
||||
FLIPPER_VERSION=0.75.1
|
||||
FLIPPER_VERSION=0.93.0
|
||||
# Increase Java heap size for compilation
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -126,6 +126,7 @@
|
|||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle Expo Assets */,
|
||||
58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */,
|
||||
2C1F7D7FCACF5494D140CFB7 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -199,6 +200,24 @@
|
|||
shellPath = /bin/sh;
|
||||
shellScript = "../node_modules/react-native/scripts/react-native-xcode.sh\n";
|
||||
};
|
||||
2C1F7D7FCACF5494D140CFB7 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Campus/Pods-Campus-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/hermes.framework/hermes",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Campus/Pods-Campus-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
58CDB7AB66969EE82AA3E3B0 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -388,6 +407,7 @@
|
|||
COPY_PHASE_STRIP = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
|
@ -403,7 +423,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application;
|
||||
|
@ -444,6 +464,7 @@
|
|||
COPY_PHASE_STRIP = YES;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "arm64 i386";
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
|
@ -452,7 +473,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = fr.amicaleinsat.application;
|
||||
PRODUCT_NAME = application;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<string>5.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
@ -30,25 +30,25 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<string>4</string>
|
||||
<key>FacebookAdvertiserIDCollectionEnabled</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>FacebookAutoInitEnabled</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>FacebookAutoLogAppEventsEnabled</key>
|
||||
<false/>
|
||||
<false />
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<true />
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
@ -65,7 +65,7 @@
|
|||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<true />
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
@ -74,6 +74,6 @@
|
|||
<key>UIUserInterfaceStyle</key>
|
||||
<string>Automatic</string>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<false />
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require_relative '../node_modules/react-native/scripts/react_native_pods'
|
||||
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'
|
||||
|
||||
platform :ios, '10.0'
|
||||
platform :ios, '11.0'
|
||||
|
||||
target 'Campus' do
|
||||
config = use_native_modules!
|
||||
|
@ -9,7 +9,7 @@ target 'Campus' do
|
|||
use_react_native!(
|
||||
:path => config[:reactNativePath],
|
||||
# to enable hermes on iOS, change `false` to `true` and then install pods
|
||||
:hermes_enabled => false
|
||||
:hermes_enabled => true
|
||||
)
|
||||
|
||||
|
||||
|
@ -28,5 +28,4 @@ target 'Campus' do
|
|||
post_install do |installer|
|
||||
react_native_post_install(installer)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -47,34 +47,38 @@
|
|||
"paymentTab": "Payment",
|
||||
"tariffs": "Tariffs",
|
||||
"paymentMethods": "Payment Methods",
|
||||
"washerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the command central, then press the START button on the machine.\n\nWhen the program is finished, the screen indicates 'Programme terminé', press the yellow button to open the lid and retrieve your laundry.",
|
||||
"washerProcedure": "Put your laundry in the tumble without tamping it and by respecting weight limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the central command, then press the START button on the machine.\n\nWhen the program is finished, the screen indicates 'Programme terminé', press the yellow button to open the lid and retrieve your laundry.",
|
||||
"washerTips": "Program 'blanc/couleur': 6kg of dry laundry (cotton linen, linen, underwear, sheets, jeans, towels).\n\nProgram 'non repassable': 3,5 kg of dry laundry (synthetic fibre linen, cotton and polyester mixed).\n\nProgram 'fin 30°C': 2,5 kg of dry laundry (delicate linen in synthetic fibres).\n\nProgram 'laine 30°C': 2,5 kg of dry laundry (wool textiles).",
|
||||
"dryerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the command central, then press the START button on the machine.",
|
||||
"dryerTips": "The advised dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.",
|
||||
"dryerProcedure": "Put your laundry in the tumble without tamping it and by respecting charge limits.\n\nClose the machine's door.\n\nChoose a program using one of the four favorite program buttons.\n\nPay to the central command , then press the START button on the machine.",
|
||||
"dryerTips": "The recommended dryer length is 35 minutes for 14 kg of laundry. You can choose a shorter length if the dryer is not fully charged.",
|
||||
"procedure": "Procedure",
|
||||
"tips": "Tips",
|
||||
"numAvailable": "available",
|
||||
"numAvailablePlural": "available",
|
||||
"errors": {
|
||||
"title": "Proxiwash message",
|
||||
"button": "More info"
|
||||
},
|
||||
"washinsa": {
|
||||
"title": "INSA laundromat",
|
||||
"subtitle": "Your favorite laundromat!!",
|
||||
"description": "This is the washing service for INSA's residences (We don't mind if you do not live on the campus and do your laundry here). The room is right next to the R2, with 3 dryers and 9 washers. It is open 7d/7 24h/24! You can bring your own detergent, use the one given on site or buy it at the Proximo (cheaper than the one given by the machines).",
|
||||
"tariff": "Washers 6kg: 3€ the washer + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.",
|
||||
"paymentMethods": "Cash up until 10€.\nCredit Card also accepted."
|
||||
"tariff": "Washers 6kg: 3€ per run + 0.80€ with detergent.\nDryers 14kg: 0.35€ for 5min of dryer usage.",
|
||||
"paymentMethods": "Cash up to 10€.\nCredit Cards also accepted."
|
||||
},
|
||||
"tripodeB": {
|
||||
"title": "Tripode B laundromat",
|
||||
"subtitle": "For those who live near the metro.",
|
||||
"description": "This is the washing service for Tripode B and C residences, as well as Thalès and Pythagore. The room is at the foot of Tripod B in front of the Pythagore residence, with 2 dryers and 6 washers. It is open 7d/7 from 7am to 11pm. In addition to the 6kg washers there is one 10kg washer.",
|
||||
"tariff": "Washers 6kg: 2.60€ 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 Card accepted."
|
||||
"tariff": "Washers 6kg: 2.60€ per run + 0.90€ with detergent.\nWashers 10kg: 4.90€ per run + 1.50€ with detergent.\nDryers 14kg: 0.40€ for 5min of dryer usage.",
|
||||
"paymentMethods": "Credit Cards accepted."
|
||||
},
|
||||
"modal": {
|
||||
"enableNotifications": "Notify me",
|
||||
"disableNotifications": "Stop notifications",
|
||||
"cancel": "Cancel",
|
||||
"finished": "This machine is finished. If you started it, you can get back your laundry.",
|
||||
"ready": "This machine is empty and ready to use.",
|
||||
"finished": "This machine is finished. If you started it, you can pick up your laundry.",
|
||||
"ready": "This machine is empty and ready for use.",
|
||||
"running": "This machine has been started at %{start} and will end at %{end}.\n\nRemaining time: %{remaining} min.\nProgram: %{program}",
|
||||
"runningNotStarted": "This machine is ready but not started. Please make sure you pressed the start button.",
|
||||
"broken": "This machine is out of order and cannot be used. Thank you for your comprehension.",
|
||||
|
@ -93,14 +97,18 @@
|
|||
"unknown": "UNKNOWN"
|
||||
},
|
||||
"notifications": {
|
||||
"channel": {
|
||||
"title": "Laundry reminders",
|
||||
"description": "Get reminders for watched washers/dryers"
|
||||
},
|
||||
"machineFinishedTitle": "Laundry Ready",
|
||||
"machineFinishedBody": "The machine n°{{number}} is finished and your laundry is ready to pickup",
|
||||
"machineFinishedBody": "Machine n°{{number}} is finished and your laundry is ready for pickup",
|
||||
"machineRunningTitle": "Laundry running: {{time}} minutes left",
|
||||
"machineRunningBody": "The machine n°{{number}} is still running"
|
||||
"machineRunningBody": "Machine n°{{number}} is still running"
|
||||
},
|
||||
"mascotDialog": {
|
||||
"title": "Small tips",
|
||||
"message": "No need for queues anymore, you will be notified when machines are ready !\n\nIf you have your head in the clouds, you can turn on notifications for your machine by clicking on it.\n\nIf you live off campus we have other laundromat available, check the settings !!!!",
|
||||
"message": "No need for queues anymore, you will be notified when machines are ready !\n\nIf you have your head in the clouds, you can turn on notifications for your machine by clicking on it.\n\nIf you live off campus we have another available laundromat, check the settings !!!!",
|
||||
"ok": "Settings",
|
||||
"cancel": "Later"
|
||||
}
|
||||
|
@ -137,12 +145,12 @@
|
|||
},
|
||||
"planex": {
|
||||
"title": "Planex",
|
||||
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button bellow.",
|
||||
"noGroupSelected": "No group selected. Please select your group using the big beautiful red button below.",
|
||||
"favorites": {
|
||||
"title": "Favorites",
|
||||
"empty": {
|
||||
"title": "No favorites",
|
||||
"subtitle": "Clic on the star next to a group to add it to the favorites"
|
||||
"subtitle": "Click on the star next to a group to add it to the favorites"
|
||||
}
|
||||
},
|
||||
"mascotDialog": {
|
||||
|
@ -156,7 +164,7 @@
|
|||
"amicaleAbout": {
|
||||
"title": "A question ?",
|
||||
"subtitle": "Ask the Amicale",
|
||||
"message": "You want to revive a club?\nYou want to start a new project?\nHere are al the contacts you need! Do not hesitate to write a mail or send a message to the Amicale's Facebook page!",
|
||||
"message": "Want to revive a club?\nWant to start a new project?\nHere are all the contacts you need! Don't hesitate to write a mail or send a message to the Amicale's Facebook page!",
|
||||
"roles": {
|
||||
"interSchools": "Inter Schools",
|
||||
"culture": "Culture",
|
||||
|
@ -181,8 +189,8 @@
|
|||
"sortPrice": "Price",
|
||||
"sortPriceReverse": "Price (reverse)",
|
||||
"inStock": "in stock",
|
||||
"description": "The Proximo is your small grocery store maintained by students directly on the campus. Open every day from 18h30 to 19h30, we welcome you when you are short on pastas or sodas ! Different products for different problems, everything at cost price. You can pay by Lydia or cash.",
|
||||
"openingHours": "Openning Hours",
|
||||
"description": "The Proximo is your small grocery store held by students directly on campus. Open every day from 18h30 to 19h30, we welcome you when you are short on pasta or soda ! Different products for different problems, everything is sold at cost. You can pay with Lydia or cash.",
|
||||
"openingHours": "Opening Hours",
|
||||
"paymentMethods": "Payment Methods",
|
||||
"paymentMethodsDescription": "Cash or Lydia",
|
||||
"search": "Search",
|
||||
|
@ -212,7 +220,7 @@
|
|||
"resetPassword": "Forgot Password",
|
||||
"mascotDialog": {
|
||||
"title": "An account?",
|
||||
"message": "An Amicale account allows you to take part in several activities around campus. You can join a club, or even create your own!\n\nLogging into your Amicale account on the app will allow you to see all available clubs on the campus, vote for the upcoming elections, and more to come!\n\nNo Account? Go to the Amicale's building during open hours to create one.",
|
||||
"message": "An Amicale account allows you to take part in several activities around campus. You can join a club, or even create your own!\n\nLogging into your Amicale account on the app will allow you to see all available clubs on the campus, vote for the upcoming elections, and more to come!\n\nNo Account? Go to the Amicale's building during opening hours to create one.",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
|
@ -230,8 +238,8 @@
|
|||
"membershipPayed": "Payed",
|
||||
"membershipNotPayed": "Not payed",
|
||||
"welcomeTitle": "Welcome %{name}!",
|
||||
"welcomeDescription": "This is your Amicale INSA Toulouse personal space. Bellow are the current services you can access thanks to your account. Feels empty? You're right and we plan on fixing that, so stay tuned!",
|
||||
"welcomeFeedback": "We plan on doing more! If you have any suggestions or found bugs, please tell us by clicking the button bellow."
|
||||
"welcomeDescription": "This is your Amicale INSA Toulouse personal space. Below are the services you can currently access thanks to your account. Feels empty? You're right and we plan on fixing that, so stay tuned!",
|
||||
"welcomeFeedback": "We plan on doing more! If you have any suggestions or found bugs, please tell us by clicking the button below."
|
||||
},
|
||||
"clubs": {
|
||||
"title": "Clubs",
|
||||
|
@ -245,10 +253,10 @@
|
|||
"amicaleContact": "Contact the Amicale",
|
||||
"invalidClub": "Could not find the club. Please make sure the club you are trying to access is valid.",
|
||||
"about": {
|
||||
"text": "The clubs, making the campus live, with more than sixty clubs offering various activities! From the philosophy club to the PABI (Production Artisanale de Bière Insaienne), without forgetting the multiple music and dance clubs, you will surely find an activity that suits you!",
|
||||
"text": "The clubs keep the campus alive, with more than sixty clubs offering various activities! From the philosophy club to the PABI (Production Artisanale de Bière Insalienne), without forgetting the multiple music and dance clubs, you will surely find an activity that suits you!",
|
||||
"title": "A question ?",
|
||||
"subtitle": "Ask the Amicale",
|
||||
"message": "You have a question concerning the clubs?\nYou want to revive or create a club?\nContact the Amicale at the following address:"
|
||||
"message": "Do you have a question regarding clubs?\nWant to revive or create a club?\nContact the Amicale at the following address:"
|
||||
}
|
||||
},
|
||||
"vote": {
|
||||
|
@ -257,14 +265,14 @@
|
|||
"select": {
|
||||
"title": "Elections open",
|
||||
"subtitle": "Vote now!",
|
||||
"sendButton": "Send Vote",
|
||||
"dialogTitle": "Send Vote?",
|
||||
"dialogTitleLoading": "Sending vote...",
|
||||
"dialogMessage": "Are you sure you want to send your vote? You will not be able to change it."
|
||||
"sendButton": "Cast Vote",
|
||||
"dialogTitle": "Cast Vote?",
|
||||
"dialogTitleLoading": "Casting vote...",
|
||||
"dialogMessage": "Are you sure you want to cast your vote? You will not be able to change it."
|
||||
},
|
||||
"tease": {
|
||||
"title": "Elections incoming",
|
||||
"subtitle": "Be ready to vote!",
|
||||
"subtitle": "Get ready to vote!",
|
||||
"message": "Vote start:"
|
||||
},
|
||||
"wait": {
|
||||
|
@ -284,7 +292,7 @@
|
|||
},
|
||||
"mascotDialog": {
|
||||
"title": "Why vote?",
|
||||
"message": "The Amicale's elections is the right moment for you to choose the next team, which will handle different projects on the campus, help organizing your favorite events, animate the campus life during the whole year, and relay your ideas to the administration, so that your campus life is the most enjoyable possible!\nYour turn to make a change!\uD83D\uDE09\n\nNote: If there is only one list, it is still important to vote to show your support, so that the administration knows the current list is supported by students. It is always a plus when taking difficult decisions! \uD83D\uDE09",
|
||||
"message": "The Amicale's elections are the right moment for you to choose the next team, which will handle different projects on the campus, help organizing your favorite events, animate the campus life during the whole year, and relay your ideas to the administration, so that your campus life is the most enjoyable possible!\nYour turn to make a change!\uD83D\uDE09\n\nNote: If there is only one list, it is still important to vote to show your support, so that the administration knows the current list is supported by students. It is always a plus when taking difficult decisions! \uD83D\uDE09",
|
||||
"button": "Ok"
|
||||
}
|
||||
},
|
||||
|
@ -309,7 +317,7 @@
|
|||
"bookingConfirmedMessage": "Do not forget to come by the Amicale to give your bail in exchange of the equipment.",
|
||||
"mascotDialog": {
|
||||
"title": "How does it work ?",
|
||||
"message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, click the equipment of your choice in the list bellow, enter your lend dates, then come around the Amicale to claim it and give your bail.",
|
||||
"message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, select the equipment of your choice in the list below, enter your lend dates, then come around the Amicale to claim it and give your bail.",
|
||||
"button": "Ok"
|
||||
}
|
||||
},
|
||||
|
@ -329,7 +337,7 @@
|
|||
},
|
||||
"mascotDialog": {
|
||||
"title": "Scano...what?",
|
||||
"message": "Scanotron 3000 allows you to scan Campus QR codes, created by clubs or event managers, to get more detailed info!\n\nThe camera will never be used for any other purposes.",
|
||||
"message": "Scanotron 3000 allows you to scan Campus QR codes, created by clubs or event managers, to get more detailed info!\n\nThe camera will never be used for any other purpose.",
|
||||
"button": "OK"
|
||||
}
|
||||
},
|
||||
|
@ -340,11 +348,11 @@
|
|||
"nightModeSubOn": "Your eyes are at peace",
|
||||
"nightModeSubOff": "Your eyes are burning",
|
||||
"nightModeAuto": "Follow system dark mode",
|
||||
"nightModeAutoSub": "Follows the mode chosen by your system",
|
||||
"nightModeAutoSub": "Follows the mode set by your system",
|
||||
"startScreen": "Start Screen",
|
||||
"startScreenSub": "Select which screen to start the app on",
|
||||
"dashboard": "Dashboard",
|
||||
"dashboardSub": "Edit what services to display on the dashboard",
|
||||
"dashboardSub": "Edit which services to display on the dashboard",
|
||||
"proxiwashNotifReminder": "Machine running reminder",
|
||||
"proxiwashNotifReminderSub": "How many minutes before",
|
||||
"proxiwashChangeWash": "Laundromat selection",
|
||||
|
@ -352,7 +360,7 @@
|
|||
"information": "Information",
|
||||
"dashboardEdit": {
|
||||
"title": "Edit dashboard",
|
||||
"message": "The five items above represent your dashboard.\nYou can replace one of its services by selecting it, and then by clicking on the desired new service in the list bellow.",
|
||||
"message": "The five items above represent your dashboard.\nYou can replace one of its services by selecting it, and then by clicking on the desired new service in the list below.",
|
||||
"undo": "Undo changes"
|
||||
}
|
||||
},
|
||||
|
@ -375,7 +383,7 @@
|
|||
"docjyj": "Student in 2MIC FAS (2020). He added some new features and fixed some bugs.",
|
||||
"yohan": "Student in 4IR (2020). He helped to fix bugs and gave some ideas.",
|
||||
"beranger": "Student in 4AE (2020) and president of the Amicale when the app was created. The app was his idea. He helped a lot to find bugs, new features and communication.",
|
||||
"celine": "Student in 4GPE (2020). Without her, everything would be less cute. She helped to write the text, for communication, and also to create the mascot 🦊.",
|
||||
"celine": "Student in 4GPE (2020). Without her, everything wouldn't be as cute. She helped to write the text, for communication, and also to create the mascot 🦊.",
|
||||
"damien": "Student in 4IR (2020) and creator of the 2020 version of the Amicale's website. Thanks to his help, integrating Amicale's services into the app was child's play.",
|
||||
"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."
|
||||
|
@ -385,10 +393,10 @@
|
|||
"title": "Contribute",
|
||||
"feedback": "Contact the dev",
|
||||
"feedbackSubtitle": "A student like you!",
|
||||
"feedbackDescription": "Feedback or bugs, you are always welcome.\nChoose your preferred way from the buttons bellow.",
|
||||
"feedbackDescription": "Feedback or bugs, you are always welcome.\nChoose your preferred way from the buttons below.",
|
||||
"contribute": "Contribute to the project",
|
||||
"contributeSubtitle": "With a possible \"implication citoyenne\"!",
|
||||
"contributeDescription": "Everyone can help: communication, design or coding! You are free to contribute as you like.\nYou can find bellow a link to Trello for project organization, and a link to the source code on GitEtud.",
|
||||
"contributeDescription": "Everyone can help: communication, design or coding! You are free to contribute as you like.\nYou can find below a link to Trello for project organization, and a link to the source code on GitEtud.",
|
||||
"homeButtonTitle": "Contribute to the project",
|
||||
"homeButtonSubtitle": "Your help is important"
|
||||
},
|
||||
|
@ -426,11 +434,11 @@
|
|||
"intro": {
|
||||
"slideMain": {
|
||||
"title": "Welcome to CAMPUS!",
|
||||
"text": "The students app of the INSA Toulouse! Read along to see everything you can do."
|
||||
"text": "INSA Toulouse's student app! Read along to see everything you can do."
|
||||
},
|
||||
"slidePlanex": {
|
||||
"title": "Prettier Planex",
|
||||
"text": "Lookup your and your friends timetable with a mobile friendly Planex!"
|
||||
"text": "Lookup your friends' and your own timetables with a mobile friendly Planex!"
|
||||
},
|
||||
"slideEvents": {
|
||||
"title": "Events",
|
||||
|
@ -438,7 +446,7 @@
|
|||
},
|
||||
"slideServices": {
|
||||
"title": "And even more!",
|
||||
"text": "You can do much more with CAMPUS, but I can't explain everything here. Explore the app to find out!"
|
||||
"text": "You can do much more with CAMPUS, but I can't explain everything here. Explore the app to find out for yourself!"
|
||||
},
|
||||
"slideDone": {
|
||||
"title": "Contribute to the project!",
|
||||
|
|
|
@ -55,6 +55,10 @@
|
|||
"tips": "Conseils",
|
||||
"numAvailable": "disponible",
|
||||
"numAvailablePlural": "disponibles",
|
||||
"errors": {
|
||||
"title": "Message laverie",
|
||||
"button": "En savoir plus"
|
||||
},
|
||||
"washinsa": {
|
||||
"title": "Laverie INSA",
|
||||
"subtitle": "Ta laverie préférée !!",
|
||||
|
@ -93,6 +97,10 @@
|
|||
"unknown": "INCONNU"
|
||||
},
|
||||
"notifications": {
|
||||
"channel": {
|
||||
"title": "Rappels laverie",
|
||||
"description": "Recevoir des rappels pour les machines demandées"
|
||||
},
|
||||
"machineFinishedTitle": "Linge prêt",
|
||||
"machineFinishedBody": "La machine n°{{number}} est terminée et ton linge est prêt à être récupéré",
|
||||
"machineRunningTitle": "Machine en cours: {{time}} minutes restantes",
|
||||
|
|
27482
package-lock.json
generated
27482
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",
|
||||
"version": "4.1.0",
|
||||
"version": "5.0.0-3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
@ -14,75 +14,77 @@
|
|||
"lint-fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
|
||||
"full-check": "npm run typescript && npm run lint && npm run test",
|
||||
"pod": "cd ios && pod install && cd ..",
|
||||
"bundle": "npm run full-check && cd android && ./gradlew bundleRelease",
|
||||
"bundle": "cd android && ./gradlew bundleRelease",
|
||||
"clean": "react-native-clean-project",
|
||||
"postversion": "react-native-version"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nartc/react-native-barcode-mask": "1.2.0",
|
||||
"@react-native-async-storage/async-storage": "^1.15.4",
|
||||
"@react-native-async-storage/async-storage": "1.15.7",
|
||||
"@react-native-community/masked-view": "0.1.11",
|
||||
"@react-native-community/push-notification-ios": "1.8.0",
|
||||
"@react-native-community/slider": "3.0.3",
|
||||
"@react-navigation/bottom-tabs": "5.11.10",
|
||||
"@react-navigation/native": "5.9.4",
|
||||
"@react-navigation/stack": "5.14.4",
|
||||
"@react-native-community/push-notification-ios": "1.10.1",
|
||||
"@react-native-community/slider": "4.1.6",
|
||||
"@react-navigation/bottom-tabs": "6.0.5",
|
||||
"@react-navigation/native": "6.0.2",
|
||||
"@react-navigation/stack": "6.0.7",
|
||||
"i18n-js": "3.8.0",
|
||||
"moment": "^2.29.1",
|
||||
"react": "17.0.1",
|
||||
"react-native": "0.64.1",
|
||||
"moment": "2.29.1",
|
||||
"react": "17.0.2",
|
||||
"react-native": "0.65.1",
|
||||
"react-native-animatable": "1.3.3",
|
||||
"react-native-app-intro-slider": "4.0.4",
|
||||
"react-native-appearance": "0.3.4",
|
||||
"react-native-autolink": "4.0.0",
|
||||
"react-native-calendars": "1.1260.0",
|
||||
"react-native-camera": "3.43.6",
|
||||
"react-native-calendars": "1.1266.0",
|
||||
"react-native-camera": "4.1.1",
|
||||
"react-native-collapsible": "1.6.0",
|
||||
"react-native-gesture-handler": "1.10.3",
|
||||
"react-native-image-zoom-viewer": "3.0.1",
|
||||
"react-native-keychain": "4.0.5",
|
||||
"react-native-linear-gradient": "2.5.6",
|
||||
"react-native-localize": "2.0.3",
|
||||
"react-native-localize": "2.1.4",
|
||||
"react-native-modalize": "2.0.8",
|
||||
"react-native-paper": "4.8.1",
|
||||
"react-native-permissions": "3.0.3",
|
||||
"react-native-push-notification": "7.3.0",
|
||||
"react-native-paper": "4.9.2",
|
||||
"react-native-permissions": "3.0.5",
|
||||
"react-native-push-notification": "8.1.0",
|
||||
"react-native-reanimated": "1.13.2",
|
||||
"react-native-render-html": "5.1.0",
|
||||
"react-native-safe-area-context": "3.2.0",
|
||||
"react-native-screens": "3.1.1",
|
||||
"react-native-render-html": "6.1.0",
|
||||
"react-native-safe-area-context": "3.3.2",
|
||||
"react-native-screens": "3.7.0",
|
||||
"react-native-splash-screen": "3.2.0",
|
||||
"react-native-timeago": "^0.5.0",
|
||||
"react-native-timeago": "0.5.0",
|
||||
"react-native-vector-icons": "8.1.0",
|
||||
"react-native-webview": "11.4.3",
|
||||
"react-navigation-collapsible": "5.9.1",
|
||||
"react-navigation-header-buttons": "7.0.1"
|
||||
"react-native-webview": "11.13.0",
|
||||
"react-navigation-collapsible": "6.0.0",
|
||||
"react-navigation-header-buttons": "9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.12.9",
|
||||
"@babel/runtime": "7.12.5",
|
||||
"@react-native-community/eslint-config": "2.0.0",
|
||||
"@types/i18n-js": "3.8.0",
|
||||
"@types/jest": "26.0.23",
|
||||
"@react-native-community/eslint-config": "3.0.1",
|
||||
"@types/i18n-js": "3.8.2",
|
||||
"@types/jest": "26.0.24",
|
||||
"@types/react": "17.0.3",
|
||||
"@types/react-native": "0.64.4",
|
||||
"@types/react-native-calendars": "1.20.10",
|
||||
"@types/react-native-push-notification": "^7.2.0",
|
||||
"@types/react-native-vector-icons": "6.4.6",
|
||||
"@types/react-native": "0.65.0",
|
||||
"@types/react-native-calendars": "1.1264.2",
|
||||
"@types/react-native-push-notification": "7.3.2",
|
||||
"@types/react-native-vector-icons": "6.4.8",
|
||||
"@types/react-test-renderer": "17.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.22.1",
|
||||
"@typescript-eslint/parser": "4.22.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.31.0",
|
||||
"@typescript-eslint/parser": "4.31.0",
|
||||
"babel-jest": "26.6.3",
|
||||
"eslint": "7.25.0",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-prettier": "8.3.0",
|
||||
"jest": "26.6.3",
|
||||
"jest-extended": "0.11.5",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"metro-react-native-babel-preset": "0.64.0",
|
||||
"prettier": "2.2.1",
|
||||
"react-native-clean-project": "^3.6.3",
|
||||
"metro-react-native-babel-preset": "0.66.0",
|
||||
"prettier": "2.4.0",
|
||||
"react-native-clean-project": "3.6.7",
|
||||
"react-native-codegen": "0.0.7",
|
||||
"react-native-version": "4.0.0",
|
||||
"react-test-renderer": "17.0.1",
|
||||
"typescript": "4.2.4"
|
||||
"react-test-renderer": "17.0.2",
|
||||
"typescript": "4.4.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
|
|
231
src/components/Amicale/Login/LoginForm.tsx
Normal file
231
src/components/Amicale/Login/LoginForm.tsx
Normal file
|
@ -0,0 +1,231 @@
|
|||
import React, { useRef, useState } from 'react';
|
||||
import {
|
||||
Image,
|
||||
StyleSheet,
|
||||
View,
|
||||
TextInput as RNTextInput,
|
||||
} from 'react-native';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
HelperText,
|
||||
TextInput,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
onSubmit: (email: string, password: string) => void;
|
||||
onHelpPress: () => void;
|
||||
onResetPasswordPress: () => void;
|
||||
};
|
||||
|
||||
const ICON_AMICALE = require('../../../../assets/amicale.png');
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
header: {
|
||||
fontSize: 36,
|
||||
marginBottom: 48,
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
buttonContainer: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
lockButton: {
|
||||
marginRight: 'auto',
|
||||
marginBottom: 20,
|
||||
},
|
||||
sendButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
const emailRegex = /^.+@.+\..+$/;
|
||||
|
||||
/**
|
||||
* Checks if the entered email is valid (matches the regex)
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isEmailValid(email: string): boolean {
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has entered a password
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPasswordValid(password: string): boolean {
|
||||
return password !== '';
|
||||
}
|
||||
|
||||
export default function LoginForm(props: Props) {
|
||||
const theme = useTheme();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isEmailValidated, setIsEmailValidated] = useState(false);
|
||||
const [isPasswordValidated, setIsPasswordValidated] = useState(false);
|
||||
const passwordRef = useRef<RNTextInput>(null);
|
||||
/**
|
||||
* Checks if we should tell the user his email is invalid.
|
||||
* We should only show this if his email is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
const shouldShowEmailError = () => {
|
||||
return isEmailValidated && !isEmailValid(email);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if we should tell the user his password is invalid.
|
||||
* We should only show this if his password is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
const shouldShowPasswordError = () => {
|
||||
return isPasswordValidated && !isPasswordValid(password);
|
||||
};
|
||||
|
||||
const onEmailSubmit = () => {
|
||||
if (passwordRef.current) {
|
||||
passwordRef.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his email is ready to be validated
|
||||
*/
|
||||
const validateEmail = () => setIsEmailValidated(true);
|
||||
|
||||
/**
|
||||
* The user has unfocused the input, his password is ready to be validated
|
||||
*/
|
||||
const validatePassword = () => setIsPasswordValidated(true);
|
||||
|
||||
const onEmailChange = (value: string) => {
|
||||
if (isEmailValidated) {
|
||||
setIsEmailValidated(false);
|
||||
}
|
||||
setEmail(value);
|
||||
};
|
||||
|
||||
const onPasswordChange = (value: string) => {
|
||||
if (isPasswordValidated) {
|
||||
setIsPasswordValidated(false);
|
||||
}
|
||||
setPassword(value);
|
||||
};
|
||||
|
||||
const shouldEnableLogin = () => {
|
||||
return isEmailValid(email) && isPasswordValid(password) && !props.loading;
|
||||
};
|
||||
|
||||
const onSubmit = () => {
|
||||
if (shouldEnableLogin()) {
|
||||
props.onSubmit(email, password);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.login.title')}
|
||||
titleStyle={styles.text}
|
||||
subtitle={i18n.t('screens.login.subtitle')}
|
||||
subtitleStyle={styles.text}
|
||||
left={({ size }) => (
|
||||
<Image
|
||||
source={ICON_AMICALE}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<View>
|
||||
<TextInput
|
||||
label={i18n.t('screens.login.email')}
|
||||
mode={'outlined'}
|
||||
value={email}
|
||||
onChangeText={onEmailChange}
|
||||
onBlur={validateEmail}
|
||||
onSubmitEditing={onEmailSubmit}
|
||||
error={shouldShowEmailError()}
|
||||
textContentType={'emailAddress'}
|
||||
autoCapitalize={'none'}
|
||||
autoCompleteType={'email'}
|
||||
autoCorrect={false}
|
||||
keyboardType={'email-address'}
|
||||
returnKeyType={'next'}
|
||||
secureTextEntry={false}
|
||||
/>
|
||||
<HelperText type={'error'} visible={shouldShowEmailError()}>
|
||||
{i18n.t('screens.login.emailError')}
|
||||
</HelperText>
|
||||
<TextInput
|
||||
ref={passwordRef}
|
||||
label={i18n.t('screens.login.password')}
|
||||
mode={'outlined'}
|
||||
value={password}
|
||||
onChangeText={onPasswordChange}
|
||||
onBlur={validatePassword}
|
||||
onSubmitEditing={onSubmit}
|
||||
error={shouldShowPasswordError()}
|
||||
textContentType={'password'}
|
||||
autoCapitalize={'none'}
|
||||
autoCompleteType={'password'}
|
||||
autoCorrect={false}
|
||||
keyboardType={'default'}
|
||||
returnKeyType={'done'}
|
||||
secureTextEntry={true}
|
||||
/>
|
||||
<HelperText type={'error'} visible={shouldShowPasswordError()}>
|
||||
{i18n.t('screens.login.passwordError')}
|
||||
</HelperText>
|
||||
</View>
|
||||
<Card.Actions style={styles.buttonContainer}>
|
||||
<Button
|
||||
icon="lock-question"
|
||||
mode="contained"
|
||||
onPress={props.onResetPasswordPress}
|
||||
color={theme.colors.warning}
|
||||
style={styles.lockButton}
|
||||
>
|
||||
{i18n.t('screens.login.resetPassword')}
|
||||
</Button>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
disabled={!shouldEnableLogin()}
|
||||
loading={props.loading}
|
||||
onPress={onSubmit}
|
||||
style={styles.sendButton}
|
||||
>
|
||||
{i18n.t('screens.login.title')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="help-circle"
|
||||
mode="contained"
|
||||
onPress={props.onHelpPress}
|
||||
style={GENERAL_STYLES.centerHorizontal}
|
||||
>
|
||||
{i18n.t('screens.login.mascotDialog.title')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</View>
|
||||
);
|
||||
}
|
|
@ -20,8 +20,7 @@
|
|||
import * as React from 'react';
|
||||
import i18n from 'i18n-js';
|
||||
import LoadingConfirmDialog from '../Dialogs/LoadingConfirmDialog';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useLogout } from '../../utils/logout';
|
||||
|
||||
type PropsType = {
|
||||
visible: boolean;
|
||||
|
@ -29,20 +28,14 @@ type PropsType = {
|
|||
};
|
||||
|
||||
function LogoutDialog(props: PropsType) {
|
||||
const navigation = useNavigation();
|
||||
const onLogout = useLogout();
|
||||
// Use a loading dialog as it can take some time to update the context
|
||||
const onClickAccept = async (): Promise<void> => {
|
||||
return new Promise((resolve: () => void) => {
|
||||
ConnectionManager.getInstance()
|
||||
.disconnect()
|
||||
.then(() => {
|
||||
navigation.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'main' }],
|
||||
});
|
||||
onLogout();
|
||||
props.onDismiss();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
104
src/components/Amicale/Profile/ProfileClubCard.tsx
Normal file
104
src/components/Amicale/Profile/ProfileClubCard.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import React from 'react';
|
||||
import { Card, Avatar, Divider, useTheme, List } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { FlatList, StyleSheet } from 'react-native';
|
||||
import { ProfileClubType } from '../../../screens/Amicale/ProfileScreen';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
clubs?: Array<ProfileClubType>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default function ProfileClubCard(props: Props) {
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation();
|
||||
|
||||
const clubKeyExtractor = (item: ProfileClubType) => item.name;
|
||||
|
||||
const getClubListItem = ({ item }: { item: ProfileClubType }) => {
|
||||
const onPress = () =>
|
||||
navigation.navigate(MainRoutes.ClubInformation, {
|
||||
type: 'id',
|
||||
clubId: item.id,
|
||||
});
|
||||
let description = i18n.t('screens.profile.isMember');
|
||||
let icon = (leftProps: {
|
||||
color: string;
|
||||
style: {
|
||||
marginLeft: number;
|
||||
marginRight: number;
|
||||
marginVertical?: number;
|
||||
};
|
||||
}) => (
|
||||
<List.Icon
|
||||
color={leftProps.color}
|
||||
style={leftProps.style}
|
||||
icon="chevron-right"
|
||||
/>
|
||||
);
|
||||
if (item.is_manager) {
|
||||
description = i18n.t('screens.profile.isManager');
|
||||
icon = (leftProps) => (
|
||||
<List.Icon
|
||||
style={leftProps.style}
|
||||
icon="star"
|
||||
color={theme.colors.primary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<List.Item
|
||||
title={item.name}
|
||||
description={description}
|
||||
left={icon}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function getClubList(list: Array<ProfileClubType> | undefined) {
|
||||
if (!list) {
|
||||
return null;
|
||||
}
|
||||
|
||||
list.sort((a) => (a.is_manager ? -1 : 1));
|
||||
return (
|
||||
<FlatList
|
||||
renderItem={getClubListItem}
|
||||
keyExtractor={clubKeyExtractor}
|
||||
data={list}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.clubs')}
|
||||
subtitle={i18n.t('screens.profile.clubsSubtitle')}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon
|
||||
size={iconProps.size}
|
||||
icon="account-group"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
{getClubList(props.clubs)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
56
src/components/Amicale/Profile/ProfileMembershipCard.tsx
Normal file
56
src/components/Amicale/Profile/ProfileMembershipCard.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import { Avatar, Card, List, useTheme } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
valid?: boolean;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export default function ProfileMembershipCard(props: Props) {
|
||||
const theme = useTheme();
|
||||
const state = props.valid === true;
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.membership')}
|
||||
subtitle={i18n.t('screens.profile.membershipSubtitle')}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon
|
||||
size={iconProps.size}
|
||||
icon="credit-card"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<List.Section>
|
||||
<List.Item
|
||||
title={
|
||||
state
|
||||
? i18n.t('screens.profile.membershipPayed')
|
||||
: i18n.t('screens.profile.membershipNotPayed')
|
||||
}
|
||||
left={(leftProps) => (
|
||||
<List.Icon
|
||||
style={leftProps.style}
|
||||
color={state ? theme.colors.success : theme.colors.danger}
|
||||
icon={state ? 'check' : 'close'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</List.Section>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
111
src/components/Amicale/Profile/ProfilePersonalCard.tsx
Normal file
111
src/components/Amicale/Profile/ProfilePersonalCard.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { useNavigation } from '@react-navigation/core';
|
||||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
List,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import Urls from '../../../constants/Urls';
|
||||
import { ProfileDataType } from '../../../screens/Amicale/ProfileScreen';
|
||||
import i18n from 'i18n-js';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
profile?: ProfileDataType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
editButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
mascot: {
|
||||
width: 60,
|
||||
},
|
||||
title: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
|
||||
function getFieldValue(field?: string): string {
|
||||
return field ? field : i18n.t('screens.profile.noData');
|
||||
}
|
||||
|
||||
export default function ProfilePersonalCard(props: Props) {
|
||||
const { profile } = props;
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation();
|
||||
|
||||
function getPersonalListItem(field: string | undefined, icon: string) {
|
||||
const title = field != null ? getFieldValue(field) : ':(';
|
||||
const subtitle = field != null ? '' : getFieldValue(field);
|
||||
return (
|
||||
<List.Item
|
||||
title={title}
|
||||
description={subtitle}
|
||||
left={(leftProps) => (
|
||||
<List.Icon
|
||||
style={leftProps.style}
|
||||
icon={icon}
|
||||
color={field != null ? leftProps.color : theme.colors.textDisabled}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={`${profile?.first_name} ${profile?.last_name}`}
|
||||
subtitle={profile?.email}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon
|
||||
size={iconProps.size}
|
||||
icon="account"
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
<List.Section>
|
||||
<List.Subheader>
|
||||
{i18n.t('screens.profile.personalInformation')}
|
||||
</List.Subheader>
|
||||
{getPersonalListItem(profile?.birthday, 'cake-variant')}
|
||||
{getPersonalListItem(profile?.phone, 'phone')}
|
||||
{getPersonalListItem(profile?.email, 'email')}
|
||||
{getPersonalListItem(profile?.branch, 'school')}
|
||||
</List.Section>
|
||||
<Divider />
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="account-edit"
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
navigation.navigate(MainRoutes.Website, {
|
||||
host: Urls.websites.amicale,
|
||||
path: profile?.link,
|
||||
title: i18n.t('screens.websites.amicale'),
|
||||
});
|
||||
}}
|
||||
style={styles.editButton}
|
||||
>
|
||||
{i18n.t('screens.profile.editInformation')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
84
src/components/Amicale/Profile/ProfileWelcomeCard.tsx
Normal file
84
src/components/Amicale/Profile/ProfileWelcomeCard.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { useNavigation } from '@react-navigation/core';
|
||||
import React from 'react';
|
||||
import { Button, Card, Divider, Paragraph } from 'react-native-paper';
|
||||
import Mascot, { MASCOT_STYLE } from '../../Mascot/Mascot';
|
||||
import i18n from 'i18n-js';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import CardList from '../../Lists/CardList/CardList';
|
||||
import { getAmicaleServices, SERVICES_KEY } from '../../../utils/Services';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
firstname?: string;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
editButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
mascot: {
|
||||
width: 60,
|
||||
},
|
||||
title: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
|
||||
function ProfileWelcomeCard(props: Props) {
|
||||
const navigation = useNavigation();
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.profile.welcomeTitle', {
|
||||
name: props.firstname,
|
||||
})}
|
||||
left={() => (
|
||||
<Mascot
|
||||
style={styles.mascot}
|
||||
emotion={MASCOT_STYLE.COOL}
|
||||
animated
|
||||
entryAnimation={{
|
||||
animation: 'bounceIn',
|
||||
duration: 1000,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
titleStyle={styles.title}
|
||||
/>
|
||||
<Card.Content>
|
||||
<Divider />
|
||||
<Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
|
||||
<CardList
|
||||
dataset={getAmicaleServices(
|
||||
(route) => navigation.navigate(route),
|
||||
true,
|
||||
[SERVICES_KEY.PROFILE]
|
||||
)}
|
||||
isHorizontal={true}
|
||||
/>
|
||||
<Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
|
||||
<Divider />
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="bug"
|
||||
mode="contained"
|
||||
onPress={() => {
|
||||
navigation.navigate(MainRoutes.Feedback);
|
||||
}}
|
||||
style={styles.editButton}
|
||||
>
|
||||
{i18n.t('screens.feedback.homeButtonTitle')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(
|
||||
ProfileWelcomeCard,
|
||||
(pp, np) => pp.firstname === np.firstname
|
||||
);
|
|
@ -17,30 +17,23 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Avatar, Button, Card, RadioButton } from 'react-native-paper';
|
||||
import { FlatList, StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import LoadingConfirmDialog from '../../Dialogs/LoadingConfirmDialog';
|
||||
import ErrorDialog from '../../Dialogs/ErrorDialog';
|
||||
import type { VoteTeamType } from '../../../screens/Amicale/VoteScreen';
|
||||
import { ApiRejectType } from '../../../utils/WebData';
|
||||
import { REQUEST_STATUS } from '../../../utils/Requests';
|
||||
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||
|
||||
type PropsType = {
|
||||
type Props = {
|
||||
teams: Array<VoteTeamType>;
|
||||
onVoteSuccess: () => void;
|
||||
onVoteError: () => void;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
selectedTeam: string;
|
||||
voteDialogVisible: boolean;
|
||||
errorDialogVisible: boolean;
|
||||
currentError: ApiRejectType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
|
@ -50,68 +43,47 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
export default class VoteSelect extends React.PureComponent<
|
||||
PropsType,
|
||||
StateType
|
||||
> {
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedTeam: 'none',
|
||||
voteDialogVisible: false,
|
||||
errorDialogVisible: false,
|
||||
currentError: { status: REQUEST_STATUS.SUCCESS },
|
||||
};
|
||||
}
|
||||
function VoteSelect(props: Props) {
|
||||
const [selectedTeam, setSelectedTeam] = useState('none');
|
||||
const [voteDialogVisible, setVoteDialogVisible] = useState(false);
|
||||
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
||||
status: REQUEST_STATUS.SUCCESS,
|
||||
});
|
||||
const request = useAuthenticatedRequest('elections/vote', {
|
||||
team: parseInt(selectedTeam, 10),
|
||||
});
|
||||
|
||||
onVoteSelectionChange = (teamName: string): void =>
|
||||
this.setState({ selectedTeam: teamName });
|
||||
const voteKeyExtractor = (item: VoteTeamType) => item.id.toString();
|
||||
|
||||
voteKeyExtractor = (item: VoteTeamType): string => item.id.toString();
|
||||
|
||||
voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
||||
const voteRenderItem = ({ item }: { item: VoteTeamType }) => (
|
||||
<RadioButton.Item label={item.name} value={item.id.toString()} />
|
||||
);
|
||||
|
||||
showVoteDialog = (): void => this.setState({ voteDialogVisible: true });
|
||||
const showVoteDialog = () => setVoteDialogVisible(true);
|
||||
|
||||
onVoteDialogDismiss = (): void => this.setState({ voteDialogVisible: false });
|
||||
const onVoteDialogDismiss = () => setVoteDialogVisible(false);
|
||||
|
||||
onVoteDialogAccept = async (): Promise<void> => {
|
||||
const onVoteDialogAccept = async (): Promise<void> => {
|
||||
return new Promise((resolve: () => void) => {
|
||||
const { state } = this;
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest('elections/vote', {
|
||||
team: parseInt(state.selectedTeam, 10),
|
||||
})
|
||||
request()
|
||||
.then(() => {
|
||||
this.onVoteDialogDismiss();
|
||||
const { props } = this;
|
||||
onVoteDialogDismiss();
|
||||
props.onVoteSuccess();
|
||||
resolve();
|
||||
})
|
||||
.catch((error: ApiRejectType) => {
|
||||
this.onVoteDialogDismiss();
|
||||
this.showErrorDialog(error);
|
||||
onVoteDialogDismiss();
|
||||
setCurrentError(error);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
showErrorDialog = (error: ApiRejectType): void =>
|
||||
this.setState({
|
||||
errorDialogVisible: true,
|
||||
currentError: error,
|
||||
});
|
||||
|
||||
onErrorDialogDismiss = () => {
|
||||
this.setState({ errorDialogVisible: false });
|
||||
const { props } = this;
|
||||
const onErrorDialogDismiss = () => {
|
||||
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||
props.onVoteError();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state, props } = this;
|
||||
return (
|
||||
<View>
|
||||
<Card style={styles.card}>
|
||||
|
@ -124,44 +96,48 @@ export default class VoteSelect extends React.PureComponent<
|
|||
/>
|
||||
<Card.Content>
|
||||
<RadioButton.Group
|
||||
onValueChange={this.onVoteSelectionChange}
|
||||
value={state.selectedTeam}
|
||||
onValueChange={setSelectedTeam}
|
||||
value={selectedTeam}
|
||||
>
|
||||
<FlatList
|
||||
data={props.teams}
|
||||
keyExtractor={this.voteKeyExtractor}
|
||||
extraData={state.selectedTeam}
|
||||
renderItem={this.voteRenderItem}
|
||||
keyExtractor={voteKeyExtractor}
|
||||
extraData={selectedTeam}
|
||||
renderItem={voteRenderItem}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</Card.Content>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
icon="send"
|
||||
mode="contained"
|
||||
onPress={this.showVoteDialog}
|
||||
icon={'send'}
|
||||
mode={'contained'}
|
||||
onPress={showVoteDialog}
|
||||
style={styles.button}
|
||||
disabled={state.selectedTeam === 'none'}
|
||||
disabled={selectedTeam === 'none'}
|
||||
>
|
||||
{i18n.t('screens.vote.select.sendButton')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
<LoadingConfirmDialog
|
||||
visible={state.voteDialogVisible}
|
||||
onDismiss={this.onVoteDialogDismiss}
|
||||
onAccept={this.onVoteDialogAccept}
|
||||
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={state.errorDialogVisible}
|
||||
onDismiss={this.onErrorDialogDismiss}
|
||||
status={state.currentError.status}
|
||||
code={state.currentError.code}
|
||||
visible={
|
||||
currentError.status !== REQUEST_STATUS.SUCCESS ||
|
||||
currentError.code !== undefined
|
||||
}
|
||||
onDismiss={onErrorDialogDismiss}
|
||||
status={currentError.status}
|
||||
code={currentError.code}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default VoteSelect;
|
||||
|
|
|
@ -24,6 +24,7 @@ import * as Animatable from 'react-native-animatable';
|
|||
import { TAB_BAR_HEIGHT } from '../Tabbar/CustomTabBar';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { useCollapsible } from '../../context/CollapsibleContext';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
onPress: (action: string, data?: string) => void;
|
||||
|
@ -138,7 +139,7 @@ function PlanexBottomBar(props: Props) {
|
|||
>
|
||||
<FAB
|
||||
icon={'account-clock'}
|
||||
onPress={() => navigation.navigate('group-select')}
|
||||
onPress={() => navigation.navigate(MainRoutes.GroupSelect)}
|
||||
/>
|
||||
</Animatable.View>
|
||||
</View>
|
||||
|
|
|
@ -72,11 +72,8 @@ function CollapsibleComponent(props: Props) {
|
|||
}, [collapsible, setCollapsible])
|
||||
);
|
||||
|
||||
const {
|
||||
containerPaddingTop,
|
||||
scrollIndicatorInsetTop,
|
||||
onScrollWithListener,
|
||||
} = collapsible;
|
||||
const { containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener } =
|
||||
collapsible;
|
||||
|
||||
const paddingBottom = props.hasTab ? TAB_BAR_HEIGHT : 0;
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { List } from 'react-native-paper';
|
|||
import { StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
item: {
|
||||
|
@ -53,7 +54,7 @@ function ActionsDashBoardItem() {
|
|||
icon="chevron-right"
|
||||
/>
|
||||
)}
|
||||
onPress={(): void => navigation.navigate('feedback')}
|
||||
onPress={(): void => navigation.navigate(MainRoutes.Feedback)}
|
||||
style={styles.item}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -30,6 +30,7 @@ import type { NewsSourceType } from '../../constants/NewsSourcesConstants';
|
|||
import ImageGalleryButton from '../Media/ImageGalleryButton';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
item: FeedItemType;
|
||||
|
@ -67,7 +68,7 @@ const styles = StyleSheet.create({
|
|||
function FeedItem(props: PropsType) {
|
||||
const navigation = useNavigation();
|
||||
const onPress = () => {
|
||||
navigation.navigate('feed-information', {
|
||||
navigation.navigate(MainRoutes.FeedInformation, {
|
||||
data: item,
|
||||
date: getFormattedDate(props.item.time),
|
||||
});
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
import * as React from 'react';
|
||||
import { Avatar, List, useTheme } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import type { DeviceType } from '../../../screens/Amicale/Equipment/EquipmentListScreen';
|
||||
import {
|
||||
getFirstEquipmentAvailability,
|
||||
|
@ -29,9 +28,10 @@ import {
|
|||
} from '../../../utils/EquipmentBooking';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
userDeviceRentDates: [string, string] | null;
|
||||
item: DeviceType;
|
||||
height: number;
|
||||
|
@ -48,7 +48,8 @@ const styles = StyleSheet.create({
|
|||
|
||||
function EquipmentListItem(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
const { item, userDeviceRentDates, navigation, height } = props;
|
||||
const navigation = useNavigation();
|
||||
const { item, userDeviceRentDates, height } = props;
|
||||
const isRented = userDeviceRentDates != null;
|
||||
const isAvailable = isEquipmentAvailable(item);
|
||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||
|
@ -56,14 +57,14 @@ function EquipmentListItem(props: PropsType) {
|
|||
let onPress;
|
||||
if (isRented) {
|
||||
onPress = () => {
|
||||
navigation.navigate('equipment-confirm', {
|
||||
navigation.navigate(MainRoutes.EquipmentConfirm, {
|
||||
item,
|
||||
dates: userDeviceRentDates,
|
||||
});
|
||||
};
|
||||
} else {
|
||||
onPress = () => {
|
||||
navigation.navigate('equipment-rent', { item });
|
||||
navigation.navigate(MainRoutes.EquipmentRent, { item });
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Avatar, Button, Card, Text } from 'react-native-paper';
|
||||
import TimeAgo from 'react-native-timeago';
|
||||
import i18n from 'i18n-js';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
|
||||
|
||||
let moment = require('moment'); //load moment module to set local language
|
||||
require('moment/locale/fr'); // import moment local language file during the application build
|
||||
moment.locale('fr');
|
||||
|
||||
type Props = {
|
||||
date?: Date;
|
||||
selectedWash: 'tripodeB' | 'washinsa';
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginHorizontal: 5,
|
||||
},
|
||||
actions: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
function ProximoListHeader(props: Props) {
|
||||
const navigation = useNavigation();
|
||||
const { date, selectedWash } = props;
|
||||
let title = i18n.t('screens.proxiwash.washinsa.title');
|
||||
let icon = ProxiwashConstants.washinsa.icon;
|
||||
if (selectedWash === 'tripodeB') {
|
||||
title = i18n.t('screens.proxiwash.tripodeB.title');
|
||||
icon = ProxiwashConstants.tripodeB.icon;
|
||||
}
|
||||
return (
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={title}
|
||||
subtitle={
|
||||
date ? (
|
||||
<Text>
|
||||
{i18n.t('screens.proxiwash.updated')}
|
||||
<TimeAgo time={date} interval={2000} />
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
left={(iconProps) => <Avatar.Icon icon={icon} size={iconProps.size} />}
|
||||
/>
|
||||
<Card.Actions style={styles.actions}>
|
||||
<Button
|
||||
mode={'contained'}
|
||||
onPress={() => navigation.navigate(MainRoutes.Settings)}
|
||||
icon={'swap-horizontal'}
|
||||
>
|
||||
{i18n.t('screens.proxiwash.switch')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProximoListHeader;
|
129
src/components/Lists/Proxiwash/ProxiwashListHeader.tsx
Normal file
129
src/components/Lists/Proxiwash/ProxiwashListHeader.tsx
Normal file
|
@ -0,0 +1,129 @@
|
|||
import React from 'react';
|
||||
import { Linking, StyleSheet } from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Paragraph,
|
||||
Text,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import TimeAgo from 'react-native-timeago';
|
||||
import i18n from 'i18n-js';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
import ProxiwashConstants from '../../../constants/ProxiwashConstants';
|
||||
import { ProxiwashInfoType } from '../../../screens/Proxiwash/ProxiwashScreen';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
|
||||
let moment = require('moment'); //load moment module to set local language
|
||||
require('moment/locale/fr'); // import moment local language file during the application build
|
||||
moment.locale('fr');
|
||||
|
||||
type Props = {
|
||||
date?: Date;
|
||||
selectedWash: 'tripodeB' | 'washinsa';
|
||||
info?: ProxiwashInfoType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginHorizontal: 5,
|
||||
},
|
||||
messageCard: {
|
||||
marginTop: 50,
|
||||
marginBottom: 10,
|
||||
},
|
||||
actions: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
function ProxiwashListHeader(props: Props) {
|
||||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
const { date, selectedWash } = props;
|
||||
let title = i18n.t('screens.proxiwash.washinsa.title');
|
||||
let icon = ProxiwashConstants.washinsa.icon;
|
||||
if (selectedWash === 'tripodeB') {
|
||||
title = i18n.t('screens.proxiwash.tripodeB.title');
|
||||
icon = ProxiwashConstants.tripodeB.icon;
|
||||
}
|
||||
const message = props.info?.message;
|
||||
return (
|
||||
<>
|
||||
<Card style={styles.card}>
|
||||
<Card.Title
|
||||
title={title}
|
||||
subtitle={
|
||||
date ? (
|
||||
<Text>
|
||||
{i18n.t('screens.proxiwash.updated')}
|
||||
<TimeAgo time={date} interval={2000} />
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon icon={icon} size={iconProps.size} />
|
||||
)}
|
||||
/>
|
||||
<Card.Actions style={styles.actions}>
|
||||
<Button
|
||||
mode={'contained'}
|
||||
onPress={() => navigation.navigate(MainRoutes.Settings)}
|
||||
icon={'swap-horizontal'}
|
||||
>
|
||||
{i18n.t('screens.proxiwash.switch')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
{message ? (
|
||||
<Card
|
||||
style={{
|
||||
...styles.card,
|
||||
...styles.messageCard,
|
||||
}}
|
||||
>
|
||||
<Animatable.View
|
||||
useNativeDriver={false}
|
||||
animation={'flash'}
|
||||
iterationCount={'infinite'}
|
||||
duration={2000}
|
||||
>
|
||||
<Card.Title
|
||||
title={i18n.t('screens.proxiwash.errors.title')}
|
||||
titleStyle={{
|
||||
color: theme.colors.primary,
|
||||
}}
|
||||
left={(iconProps) => (
|
||||
<Avatar.Icon icon={'alert'} size={iconProps.size} />
|
||||
)}
|
||||
/>
|
||||
</Animatable.View>
|
||||
<Card.Content>
|
||||
<Paragraph
|
||||
style={{
|
||||
color: theme.colors.warning,
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Paragraph>
|
||||
</Card.Content>
|
||||
<Card.Actions style={styles.actions}>
|
||||
<Button
|
||||
mode={'contained'}
|
||||
onPress={() =>
|
||||
Linking.openURL(ProxiwashConstants[selectedWash].webPageUrl)
|
||||
}
|
||||
icon={'open-in-new'}
|
||||
>
|
||||
{i18n.t('screens.proxiwash.errors.button')}
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProxiwashListHeader;
|
|
@ -22,6 +22,7 @@ import { TouchableRipple } from 'react-native-paper';
|
|||
import { Image } from 'react-native-animatable';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { StyleSheet, ViewStyle } from 'react-native';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
images: Array<{ url: string }>;
|
||||
|
@ -39,7 +40,7 @@ function ImageGalleryButton(props: PropsType) {
|
|||
const navigation = useNavigation();
|
||||
|
||||
const onPress = () => {
|
||||
navigation.navigate('gallery', { images: props.images });
|
||||
navigation.navigate(MainRoutes.Gallery, { images: props.images });
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -18,9 +18,13 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Text } from 'react-native-paper';
|
||||
import HTML from 'react-native-render-html';
|
||||
import { GestureResponderEvent, Linking } from 'react-native';
|
||||
import { Text, useTheme } from 'react-native-paper';
|
||||
import HTML, {
|
||||
CustomRendererProps,
|
||||
TBlock,
|
||||
TText,
|
||||
} from 'react-native-render-html';
|
||||
import { Dimensions, GestureResponderEvent, Linking } from 'react-native';
|
||||
|
||||
type PropsType = {
|
||||
html: string;
|
||||
|
@ -30,37 +34,54 @@ type PropsType = {
|
|||
* Abstraction layer for Agenda component, using custom configuration
|
||||
*/
|
||||
function CustomHTML(props: PropsType) {
|
||||
const theme = useTheme();
|
||||
const openWebLink = (_event: GestureResponderEvent, link: string) => {
|
||||
Linking.openURL(link);
|
||||
};
|
||||
|
||||
const getBasicText = (
|
||||
_htmlAttribs: any,
|
||||
children: any,
|
||||
_convertedCSSStyles: any,
|
||||
passProps: any
|
||||
) => {
|
||||
return <Text {...passProps}>{children}</Text>;
|
||||
// Why is this so complex?? I just want to replace the default Text element with the one
|
||||
// from react-native-paper
|
||||
// Might need to read the doc a bit more: https://meliorence.github.io/react-native-render-html/
|
||||
// For now this seems to work
|
||||
const getBasicText = (rendererProps: CustomRendererProps<TBlock>) => {
|
||||
let text: TText | undefined;
|
||||
if (rendererProps.tnode.children.length > 0) {
|
||||
const phrasing = rendererProps.tnode.children[0];
|
||||
if (phrasing.children.length > 0) {
|
||||
text = phrasing.children[0] as TText;
|
||||
}
|
||||
}
|
||||
if (text) {
|
||||
return <Text>{text.data}</Text>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getListBullet = () => {
|
||||
return <Text>- </Text>;
|
||||
};
|
||||
|
||||
// Surround description with p to allow text styling if the description is not html
|
||||
return (
|
||||
<HTML
|
||||
html={`<p>${props.html}</p>`}
|
||||
// Surround description with p to allow text styling if the description is not html
|
||||
source={{ html: `<p>${props.html}</p>` }}
|
||||
// Use Paper Text instead of React
|
||||
renderers={{
|
||||
p: getBasicText,
|
||||
li: getBasicText,
|
||||
}}
|
||||
listsPrefixesRenderers={{
|
||||
ul: getListBullet,
|
||||
// Sometimes we have images inside the text, just ignore them
|
||||
ignoredDomTags={['img']}
|
||||
// Ignore text color
|
||||
ignoredStyles={['color', 'backgroundColor']}
|
||||
contentWidth={Dimensions.get('window').width - 50}
|
||||
renderersProps={{
|
||||
a: {
|
||||
onPress: openWebLink,
|
||||
},
|
||||
ul: {
|
||||
markerTextStyle: {
|
||||
color: theme.colors.text,
|
||||
},
|
||||
},
|
||||
}}
|
||||
ignoredTags={['img']}
|
||||
ignoredStyles={['color', 'background-color']}
|
||||
onLinkPress={openWebLink}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -127,6 +127,7 @@ function PlanexWebview(props: Props) {
|
|||
onMessage={props.onMessage}
|
||||
showAdvancedControls={false}
|
||||
showControls={props.currentGroup !== undefined}
|
||||
incognito={true}
|
||||
/>
|
||||
{!props.currentGroup ? (
|
||||
<ErrorView
|
||||
|
|
|
@ -11,7 +11,7 @@ import i18n from 'i18n-js';
|
|||
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import { useLogout } from '../../utils/logout';
|
||||
|
||||
export type RequestScreenProps<T> = {
|
||||
request: () => Promise<T>;
|
||||
|
@ -44,17 +44,12 @@ type Props<T> = RequestScreenProps<T>;
|
|||
const MIN_REFRESH_TIME = 3 * 1000;
|
||||
|
||||
export default function RequestScreen<T>(props: Props<T>) {
|
||||
const onLogout = useLogout();
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const route = useRoute();
|
||||
const refreshInterval = useRef<number>();
|
||||
const [
|
||||
loading,
|
||||
lastRefreshDate,
|
||||
status,
|
||||
code,
|
||||
data,
|
||||
refreshData,
|
||||
] = useRequestLogic<T>(
|
||||
const [loading, lastRefreshDate, status, code, data, refreshData] =
|
||||
useRequestLogic<T>(
|
||||
props.request,
|
||||
props.cache,
|
||||
props.onCacheUpdate,
|
||||
|
@ -76,7 +71,8 @@ export default function RequestScreen<T>(props: Props<T>) {
|
|||
if (props.refresh !== lastRefresh.current) {
|
||||
lastRefresh.current = props.refresh === true;
|
||||
}
|
||||
}, [props, loading, refreshData]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props, loading]);
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
|
@ -94,7 +90,8 @@ export default function RequestScreen<T>(props: Props<T>) {
|
|||
clearInterval(refreshInterval.current);
|
||||
}
|
||||
};
|
||||
}, [props.cache, props.refreshOnFocus, props.autoRefreshTime, refreshData])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.cache, props.refreshOnFocus, props.autoRefreshTime])
|
||||
);
|
||||
|
||||
const isErrorCritical = (e: API_REQUEST_CODES | undefined) => {
|
||||
|
@ -103,13 +100,10 @@ export default function RequestScreen<T>(props: Props<T>) {
|
|||
|
||||
useEffect(() => {
|
||||
if (isErrorCritical(code)) {
|
||||
ConnectionManager.getInstance()
|
||||
.disconnect()
|
||||
.then(() => {
|
||||
onLogout();
|
||||
navigation.replace(MainRoutes.Login, { nextScreen: route.name });
|
||||
});
|
||||
}
|
||||
}, [code, navigation, route]);
|
||||
}, [code, navigation, route, onLogout]);
|
||||
|
||||
if (data === undefined && loading && props.showLoading !== false) {
|
||||
return <BasicLoadingScreen />;
|
||||
|
|
|
@ -141,11 +141,15 @@ function WebSectionList<ItemT, RawData>(props: Props<ItemT, RawData>) {
|
|||
<ErrorView
|
||||
status={status}
|
||||
code={code}
|
||||
button={{
|
||||
button={
|
||||
code !== API_REQUEST_CODES.BAD_TOKEN
|
||||
? {
|
||||
icon: 'refresh',
|
||||
text: i18n.t('general.retry'),
|
||||
onPress: () => refreshData(),
|
||||
}}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ type Props = {
|
|||
customPaddingFunction?: null | ((padding: number) => string);
|
||||
showAdvancedControls?: boolean;
|
||||
showControls?: boolean;
|
||||
incognito?: boolean;
|
||||
};
|
||||
|
||||
const AnimatedWebView = Animated.createAnimatedComponent(WebView);
|
||||
|
@ -272,6 +273,7 @@ function WebViewScreen(props: Props) {
|
|||
onLoad={() => injectJavaScript(getJavascriptPadding(containerPaddingTop))}
|
||||
// Animations
|
||||
onScroll={onScrollWithListener(onScroll)}
|
||||
incognito={props.incognito}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import { useCollapsible } from '../../context/CollapsibleContext';
|
|||
export const TAB_BAR_HEIGHT = 50;
|
||||
|
||||
function CustomTabBar(
|
||||
props: BottomTabBarProps<any> & {
|
||||
props: BottomTabBarProps & {
|
||||
icons: {
|
||||
[key: string]: {
|
||||
normal: string;
|
||||
|
@ -94,10 +94,7 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
function areEqual(
|
||||
prevProps: BottomTabBarProps<any>,
|
||||
nextProps: BottomTabBarProps<any>
|
||||
) {
|
||||
function areEqual(prevProps: BottomTabBarProps, nextProps: BottomTabBarProps) {
|
||||
return prevProps.state.index === nextProps.state.index;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,10 +17,8 @@ export default function CollapsibleProvider(props: Props) {
|
|||
}));
|
||||
};
|
||||
|
||||
const [
|
||||
currentCollapsible,
|
||||
setCurrentCollapsible,
|
||||
] = useState<CollapsibleContextType>({
|
||||
const [currentCollapsible, setCurrentCollapsible] =
|
||||
useState<CollapsibleContextType>({
|
||||
collapsible: undefined,
|
||||
setCollapsible: setCollapsible,
|
||||
});
|
||||
|
|
27
src/components/providers/LoginProvider.tsx
Normal file
27
src/components/providers/LoginProvider.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import React, { useState } from 'react';
|
||||
import { LoginContext, LoginContextType } from '../../context/loginContext';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactChild;
|
||||
initialToken: string | undefined;
|
||||
};
|
||||
|
||||
export default function LoginProvider(props: Props) {
|
||||
const setLogin = (token: string | undefined) => {
|
||||
setLoginState((prevState) => ({
|
||||
...prevState,
|
||||
token,
|
||||
}));
|
||||
};
|
||||
|
||||
const [loginState, setLoginState] = useState<LoginContextType>({
|
||||
token: props.initialToken,
|
||||
setLogin: setLogin,
|
||||
});
|
||||
|
||||
return (
|
||||
<LoginContext.Provider value={loginState}>
|
||||
{props.children}
|
||||
</LoginContext.Provider>
|
||||
);
|
||||
}
|
|
@ -48,6 +48,7 @@ export default {
|
|||
paymentMethods: 'screens.proxiwash.washinsa.paymentMethods',
|
||||
icon: 'school-outline',
|
||||
url: Urls.app.api + 'washinsa/washinsa_data.json',
|
||||
webPageUrl: Urls.proxiwash.washinsa,
|
||||
},
|
||||
tripodeB: {
|
||||
id: 'tripodeB',
|
||||
|
@ -58,5 +59,6 @@ export default {
|
|||
paymentMethods: 'screens.proxiwash.tripodeB.paymentMethods',
|
||||
icon: 'domain',
|
||||
url: Urls.app.api + 'washinsa/tripode_b_data.json',
|
||||
webPageUrl: Urls.proxiwash.tripodeB,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -30,6 +30,8 @@ const PROXIMO_ENDPOINT = STUDENT_SERVER + '~proximo/v2/api/';
|
|||
const PROXIMO_IMAGES_ENDPOINT =
|
||||
STUDENT_SERVER + '~proximo/api_proximo/storage/app/public/';
|
||||
const APP_IMAGES_ENDPOINT = STUDENT_SERVER + '~amicale_app/images/';
|
||||
const PROXIWASH_ENDPOINT =
|
||||
'https://www.proxiwash.com/weblaverie/ma-laverie-2?s=';
|
||||
|
||||
export default {
|
||||
amicale: {
|
||||
|
@ -48,6 +50,10 @@ export default {
|
|||
images: PROXIMO_IMAGES_ENDPOINT + 'img/',
|
||||
icons: PROXIMO_IMAGES_ENDPOINT + 'icon/',
|
||||
},
|
||||
proxiwash: {
|
||||
washinsa: PROXIWASH_ENDPOINT + 'cf4f39',
|
||||
tripodeB: PROXIWASH_ENDPOINT + 'b310b7',
|
||||
},
|
||||
planex: {
|
||||
planning: PLANEX_SERVER,
|
||||
groups: PLANEX_SERVER + 'wsAdeGrp.php?projectId=1',
|
||||
|
|
46
src/context/loginContext.ts
Normal file
46
src/context/loginContext.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { apiRequest } from '../utils/WebData';
|
||||
|
||||
export type LoginContextType = {
|
||||
token: string | undefined;
|
||||
setLogin: (token: string | undefined) => void;
|
||||
};
|
||||
|
||||
export const LoginContext = React.createContext<LoginContextType>({
|
||||
token: undefined,
|
||||
setLogin: () => undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* Hook used to retrieve the user token and puid.
|
||||
* @returns Login context with token and puid to undefined if user is not logged in
|
||||
*/
|
||||
export function useLogin() {
|
||||
return useContext(LoginContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is connected
|
||||
* @returns True if the user is connected
|
||||
*/
|
||||
export function useLoginState() {
|
||||
const { token } = useLogin();
|
||||
return token !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current user token.
|
||||
* @returns The token, or empty string if the user is not logged in
|
||||
*/
|
||||
export function useLoginToken() {
|
||||
const { token } = useLogin();
|
||||
return token ? token : '';
|
||||
}
|
||||
|
||||
export function useAuthenticatedRequest<T>(
|
||||
path: string,
|
||||
params?: { [key: string]: any }
|
||||
) {
|
||||
const token = useLoginToken();
|
||||
return () => apiRequest<T>(path, 'POST', params, token);
|
||||
}
|
|
@ -25,6 +25,7 @@ import {
|
|||
getSpecialServices,
|
||||
getStudentServices,
|
||||
} from '../utils/Services';
|
||||
import { useLoginState } from './loginContext';
|
||||
|
||||
const colorScheme = Appearance.getColorScheme();
|
||||
|
||||
|
@ -135,6 +136,7 @@ export function useDarkTheme() {
|
|||
export function useCurrentDashboard() {
|
||||
const { preferences, updatePreferences } = usePreferences();
|
||||
const navigation = useNavigation();
|
||||
const isLoggedIn = useLoginState();
|
||||
const dashboardIdList = getPreferenceObject(
|
||||
GeneralPreferenceKeys.dashboardItems,
|
||||
preferences
|
||||
|
@ -145,10 +147,10 @@ export function useCurrentDashboard() {
|
|||
};
|
||||
|
||||
const allDatasets = [
|
||||
...getAmicaleServices(navigation.navigate),
|
||||
...getStudentServices(navigation.navigate),
|
||||
...getINSAServices(navigation.navigate),
|
||||
...getSpecialServices(navigation.navigate),
|
||||
...getAmicaleServices((route) => navigation.navigate(route), isLoggedIn),
|
||||
...getStudentServices((route) => navigation.navigate(route)),
|
||||
...getINSAServices((route) => navigation.navigate(route)),
|
||||
...getSpecialServices((route) => navigation.navigate(route)),
|
||||
];
|
||||
return {
|
||||
currentDashboard: allDatasets.filter((item) =>
|
||||
|
|
|
@ -1,205 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2019 - 2020 Arnaud Vergnet.
|
||||
*
|
||||
* This file is part of Campus INSAT.
|
||||
*
|
||||
* Campus INSAT is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Campus INSAT is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as Keychain from 'react-native-keychain';
|
||||
import { REQUEST_STATUS } from '../utils/Requests';
|
||||
import type { ApiDataLoginType, ApiRejectType } from '../utils/WebData';
|
||||
import { apiRequest } from '../utils/WebData';
|
||||
|
||||
/**
|
||||
* champ: error
|
||||
*
|
||||
* 0 : SUCCESS -> pas d'erreurs
|
||||
* 1 : BAD_CREDENTIALS -> email ou mdp invalide
|
||||
* 2 : BAD_TOKEN -> session expirée
|
||||
* 3 : NO_CONSENT
|
||||
* 403 : FORBIDDEN -> accès a la ressource interdit
|
||||
* 500 : SERVER_ERROR -> pb coté serveur
|
||||
*/
|
||||
|
||||
const AUTH_PATH = 'password';
|
||||
|
||||
export default class ConnectionManager {
|
||||
static instance: ConnectionManager | null = null;
|
||||
|
||||
private token: string | null;
|
||||
|
||||
constructor() {
|
||||
this.token = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this class instance or create one if none is found
|
||||
*
|
||||
* @returns {ConnectionManager}
|
||||
*/
|
||||
static getInstance(): ConnectionManager {
|
||||
if (ConnectionManager.instance == null) {
|
||||
ConnectionManager.instance = new ConnectionManager();
|
||||
}
|
||||
return ConnectionManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current token
|
||||
*
|
||||
* @returns {string | null}
|
||||
*/
|
||||
getToken(): string | null {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to recover login token from the secure keychain
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async recoverLogin(): Promise<void> {
|
||||
return new Promise((resolve: () => void) => {
|
||||
const token = this.getToken();
|
||||
if (token != null) {
|
||||
resolve();
|
||||
} else {
|
||||
Keychain.getGenericPassword()
|
||||
.then((data: Keychain.UserCredentials | false) => {
|
||||
if (data && data.password != null) {
|
||||
this.token = data.password;
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch(() => resolve());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has a valid token
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isLoggedIn(): boolean {
|
||||
return this.getToken() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the login token in the secure keychain
|
||||
*
|
||||
* @param email
|
||||
* @param token
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async saveLogin(_email: string, token: string): Promise<void> {
|
||||
return new Promise((resolve: () => void, reject: () => void) => {
|
||||
Keychain.setGenericPassword('token', token)
|
||||
.then(() => {
|
||||
this.token = token;
|
||||
resolve();
|
||||
})
|
||||
.catch((): void => reject());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the login token from the keychain
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
return new Promise((resolve: () => void, reject: () => void) => {
|
||||
Keychain.resetGenericPassword()
|
||||
.then(() => {
|
||||
this.token = null;
|
||||
resolve();
|
||||
})
|
||||
.catch((): void => reject());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the given login and password to the api.
|
||||
* If the combination is valid, the login token is received and saved in the secure keychain.
|
||||
* If not, the promise is rejected with the corresponding error code.
|
||||
*
|
||||
* @param email
|
||||
* @param password
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async connect(email: string, password: string): Promise<void> {
|
||||
return new Promise(
|
||||
(resolve: () => void, reject: (error: ApiRejectType) => void) => {
|
||||
const data = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
apiRequest<ApiDataLoginType>(AUTH_PATH, 'POST', data)
|
||||
.then((response: ApiDataLoginType) => {
|
||||
if (response.token != null) {
|
||||
this.saveLogin(email, response.token)
|
||||
.then(() => resolve())
|
||||
.catch(() =>
|
||||
reject({
|
||||
status: REQUEST_STATUS.TOKEN_SAVE,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
reject({
|
||||
status: REQUEST_STATUS.SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an authenticated request with the login token to the API
|
||||
*
|
||||
* @param path
|
||||
* @param params
|
||||
* @returns Promise<ApiGenericDataType>
|
||||
*/
|
||||
async authenticatedRequest<T>(
|
||||
path: string,
|
||||
params?: { [key: string]: any }
|
||||
): Promise<T> {
|
||||
return new Promise(
|
||||
(
|
||||
resolve: (response: T) => void,
|
||||
reject: (error: ApiRejectType) => void
|
||||
) => {
|
||||
if (this.getToken() !== null) {
|
||||
const data = {
|
||||
...params,
|
||||
token: this.getToken(),
|
||||
};
|
||||
apiRequest<T>(path, 'POST', data)
|
||||
.then((response: T) => resolve(response))
|
||||
.catch(reject);
|
||||
} else {
|
||||
reject({
|
||||
status: REQUEST_STATUS.TOKEN_RETRIEVE,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -24,7 +24,7 @@ import SettingsScreen from '../screens/Other/Settings/SettingsScreen';
|
|||
import AboutScreen from '../screens/About/AboutScreen';
|
||||
import AboutDependenciesScreen from '../screens/About/AboutDependenciesScreen';
|
||||
import DebugScreen from '../screens/About/DebugScreen';
|
||||
import TabNavigator from './TabNavigator';
|
||||
import TabNavigator, { TabRoutes } from './TabNavigator';
|
||||
import GameMainScreen from '../screens/Game/screens/GameMainScreen';
|
||||
import VoteScreen from '../screens/Amicale/VoteScreen';
|
||||
import LoginScreen from '../screens/Amicale/LoginScreen';
|
||||
|
@ -33,7 +33,10 @@ import ProximoMainScreen from '../screens/Services/Proximo/ProximoMainScreen';
|
|||
import ProximoListScreen from '../screens/Services/Proximo/ProximoListScreen';
|
||||
import ProximoAboutScreen from '../screens/Services/Proximo/ProximoAboutScreen';
|
||||
import ProfileScreen from '../screens/Amicale/ProfileScreen';
|
||||
import ClubListScreen from '../screens/Amicale/Clubs/ClubListScreen';
|
||||
import ClubListScreen, {
|
||||
ClubCategoryType,
|
||||
ClubType,
|
||||
} from '../screens/Amicale/Clubs/ClubListScreen';
|
||||
import ClubAboutScreen from '../screens/Amicale/Clubs/ClubAboutScreen';
|
||||
import ClubDisplayScreen from '../screens/Amicale/Clubs/ClubDisplayScreen';
|
||||
import BugReportScreen from '../screens/Other/FeedbackScreen';
|
||||
|
@ -52,6 +55,18 @@ import {
|
|||
GeneralPreferenceKeys,
|
||||
} from '../utils/asyncStorage';
|
||||
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 {
|
||||
Main = 'main',
|
||||
|
@ -69,6 +84,7 @@ export enum MainRoutes {
|
|||
Proximo = 'proximo',
|
||||
ProximoList = 'proximo-list',
|
||||
ProximoAbout = 'proximo-about',
|
||||
ProxiwashAbout = 'proxiwash-about',
|
||||
Profile = 'profile',
|
||||
ClubList = 'club-list',
|
||||
ClubInformation = 'club-information',
|
||||
|
@ -79,28 +95,72 @@ export enum MainRoutes {
|
|||
Vote = 'vote',
|
||||
Feedback = 'feedback',
|
||||
Website = 'website',
|
||||
PlanningInformation = 'planning-information',
|
||||
Scanner = 'scanner',
|
||||
FeedInformation = 'feed-information',
|
||||
GroupSelect = 'group-select',
|
||||
ServicesSection = 'services-section',
|
||||
AmicaleContact = 'amicale-contact',
|
||||
}
|
||||
|
||||
type DefaultParams = { [key in MainRoutes]: object | undefined };
|
||||
type DefaultParams = { [key in MainRoutes]: object | undefined } & {
|
||||
[key in TabRoutes]: object | undefined;
|
||||
};
|
||||
|
||||
export type FullParamsList = DefaultParams & {
|
||||
'login': { nextScreen: string };
|
||||
'equipment-confirm': {
|
||||
export type MainStackParamsList = DefaultParams & {
|
||||
[MainRoutes.Login]: { nextScreen: string };
|
||||
[MainRoutes.EquipmentConfirm]: {
|
||||
item?: DeviceType;
|
||||
dates: [string, string];
|
||||
};
|
||||
'equipment-rent': { item?: DeviceType };
|
||||
'gallery': { images: Array<{ url: string }> };
|
||||
[MainRoutes.EquipmentRent]: { item?: DeviceType };
|
||||
[MainRoutes.Gallery]: { images: Array<{ url: string }> };
|
||||
[MainRoutes.ProximoList]: {
|
||||
shouldFocusSearchBar: boolean;
|
||||
category: number;
|
||||
};
|
||||
[MainRoutes.ClubInformation]: ClubInformationScreenParams;
|
||||
[MainRoutes.Website]: {
|
||||
host: string;
|
||||
path?: string;
|
||||
title: string;
|
||||
};
|
||||
[MainRoutes.FeedInformation]: {
|
||||
data: FeedItemType;
|
||||
date: string;
|
||||
};
|
||||
[MainRoutes.PlanningInformation]: PlanningInformationScreenParams;
|
||||
[MainRoutes.ServicesSection]: {
|
||||
data: ServiceCategoryType;
|
||||
};
|
||||
};
|
||||
|
||||
// Don't know why but TS is complaining without this
|
||||
// See: https://stackoverflow.com/questions/63652687/interface-does-not-satisfy-the-constraint-recordstring-object-undefined
|
||||
export type MainStackParamsList = FullParamsList &
|
||||
Record<string, object | undefined>;
|
||||
export type ClubInformationScreenParams =
|
||||
| {
|
||||
type: 'full';
|
||||
data: ClubType;
|
||||
categories: Array<ClubCategoryType>;
|
||||
}
|
||||
| {
|
||||
type: 'id';
|
||||
clubId: number;
|
||||
};
|
||||
|
||||
export type PlanningInformationScreenParams =
|
||||
| {
|
||||
type: 'full';
|
||||
data: PlanningEventType;
|
||||
}
|
||||
| {
|
||||
type: 'id';
|
||||
eventId: number;
|
||||
};
|
||||
|
||||
declare global {
|
||||
namespace ReactNavigation {
|
||||
interface RootParamList extends MainStackParamsList {}
|
||||
}
|
||||
}
|
||||
|
||||
const MainStack = createStackNavigator<MainStackParamsList>();
|
||||
|
||||
|
@ -118,6 +178,62 @@ function getIntroScreens() {
|
|||
);
|
||||
}
|
||||
|
||||
function getAmicaleScreens() {
|
||||
return (
|
||||
<>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.Profile}
|
||||
component={ProfileScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.profile.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.ClubList}
|
||||
component={ClubListScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.clubs.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.ClubInformation}
|
||||
component={ClubDisplayScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.clubs.details'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.ClubAbout}
|
||||
component={ClubAboutScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.clubs.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.EquipmentList}
|
||||
component={EquipmentScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.equipment.title'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.EquipmentRent}
|
||||
component={EquipmentLendScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.equipment.book'),
|
||||
}}
|
||||
/>
|
||||
<MainStack.Screen
|
||||
name={MainRoutes.EquipmentConfirm}
|
||||
component={EquipmentConfirmScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.equipment.confirm'),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getRegularScreens(createTabNavigator: () => React.ReactElement) {
|
||||
return (
|
||||
<>
|
||||
|
@ -233,55 +349,6 @@ function getRegularScreens(createTabNavigator: () => React.ReactElement) {
|
|||
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
|
||||
name={MainRoutes.Vote}
|
||||
component={VoteScreen}
|
||||
|
@ -296,32 +363,75 @@ function getRegularScreens(createTabNavigator: () => React.ReactElement) {
|
|||
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: {
|
||||
showIntro: boolean;
|
||||
isloggedIn: boolean;
|
||||
createTabNavigator: () => React.ReactElement;
|
||||
}) {
|
||||
const { showIntro, createTabNavigator } = props;
|
||||
const { showIntro, isloggedIn, createTabNavigator } = props;
|
||||
return (
|
||||
<MainStack.Navigator
|
||||
initialRouteName={showIntro ? MainRoutes.Intro : MainRoutes.Main}
|
||||
headerMode={'screen'}
|
||||
screenOptions={{
|
||||
headerMode: 'float',
|
||||
}}
|
||||
>
|
||||
{showIntro ? getIntroScreens() : getRegularScreens(createTabNavigator)}
|
||||
{isloggedIn ? getAmicaleScreens() : null}
|
||||
</MainStack.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
type PropsType = {
|
||||
defaultHomeRoute?: string;
|
||||
defaultHomeData?: { [key: string]: string };
|
||||
defaultData?: ParsedUrlDataType;
|
||||
};
|
||||
|
||||
function MainNavigator(props: PropsType) {
|
||||
const { preferences } = usePreferences();
|
||||
const isloggedIn = useLoginState();
|
||||
const showIntro = getPreferenceBool(
|
||||
GeneralPreferenceKeys.showIntro,
|
||||
preferences
|
||||
|
@ -330,6 +440,7 @@ function MainNavigator(props: PropsType) {
|
|||
return (
|
||||
<MainStackComponent
|
||||
showIntro={showIntro !== false}
|
||||
isloggedIn={isloggedIn}
|
||||
createTabNavigator={createTabNavigator}
|
||||
/>
|
||||
);
|
||||
|
@ -337,7 +448,5 @@ function MainNavigator(props: PropsType) {
|
|||
|
||||
export default React.memo(
|
||||
MainNavigator,
|
||||
(pp: PropsType, np: PropsType) =>
|
||||
pp.defaultHomeRoute === np.defaultHomeRoute &&
|
||||
pp.defaultHomeData === np.defaultHomeData
|
||||
(pp: PropsType, np: PropsType) => pp.defaultData === np.defaultData
|
||||
);
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
|
||||
import { Title, useTheme } from 'react-native-paper';
|
||||
|
@ -27,24 +26,17 @@ import i18n from 'i18n-js';
|
|||
import { View } from 'react-native-animatable';
|
||||
import HomeScreen from '../screens/Home/HomeScreen';
|
||||
import PlanningScreen from '../screens/Planning/PlanningScreen';
|
||||
import PlanningDisplayScreen from '../screens/Planning/PlanningDisplayScreen';
|
||||
import ProxiwashScreen from '../screens/Proxiwash/ProxiwashScreen';
|
||||
import ProxiwashAboutScreen from '../screens/Proxiwash/ProxiwashAboutScreen';
|
||||
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 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 { usePreferences } from '../context/preferencesContext';
|
||||
import {
|
||||
getPreferenceString,
|
||||
GeneralPreferenceKeys,
|
||||
} from '../utils/asyncStorage';
|
||||
import { ParsedUrlDataType } from '../utils/URLHandler';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
|
@ -60,183 +52,24 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
export enum TabRoutes {
|
||||
Services = 'services',
|
||||
Proxiwash = 'proxiwash',
|
||||
Home = 'home',
|
||||
Planning = 'events',
|
||||
Planex = 'planex',
|
||||
}
|
||||
|
||||
type DefaultParams = { [key in TabRoutes]: object | undefined };
|
||||
|
||||
export type FullParamsList = DefaultParams & {
|
||||
[TabRoutes.Home]: {
|
||||
nextScreen: string;
|
||||
data: Record<string, object | undefined>;
|
||||
};
|
||||
export type TabStackParamsList = DefaultParams & {
|
||||
[TabRoutes.Home]: ParsedUrlDataType;
|
||||
};
|
||||
|
||||
// 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>();
|
||||
|
||||
type PropsType = {
|
||||
defaultHomeRoute?: string;
|
||||
defaultHomeData?: { [key: string]: string };
|
||||
defaultData?: ParsedUrlDataType;
|
||||
};
|
||||
|
||||
const ICONS: {
|
||||
|
@ -278,9 +111,7 @@ function TabNavigator(props: PropsType) {
|
|||
} else {
|
||||
defaultRoute = defaultRoute.toLowerCase();
|
||||
}
|
||||
|
||||
const createHomeStackComponent = () =>
|
||||
HomeStackComponent(props.defaultHomeRoute, props.defaultHomeData);
|
||||
const { colors } = useTheme();
|
||||
|
||||
const LABELS: {
|
||||
[key: string]: string;
|
||||
|
@ -293,35 +124,62 @@ function TabNavigator(props: PropsType) {
|
|||
};
|
||||
return (
|
||||
<Tab.Navigator
|
||||
initialRouteName={defaultRoute}
|
||||
initialRouteName={defaultRoute as TabRoutes}
|
||||
tabBar={(tabProps) => (
|
||||
<CustomTabBar {...tabProps} labels={LABELS} icons={ICONS} />
|
||||
)}
|
||||
>
|
||||
<Tab.Screen
|
||||
name={'services'}
|
||||
component={ServicesStackComponent}
|
||||
name={TabRoutes.Services}
|
||||
component={WebsitesHomeScreen}
|
||||
options={{ title: i18n.t('screens.services.title') }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={'proxiwash'}
|
||||
component={ProxiwashStackComponent}
|
||||
name={TabRoutes.Proxiwash}
|
||||
component={ProxiwashScreen}
|
||||
options={{ title: i18n.t('screens.proxiwash.title') }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={'home'}
|
||||
component={createHomeStackComponent}
|
||||
options={{ title: i18n.t('screens.home.title') }}
|
||||
name={TabRoutes.Home}
|
||||
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={props.defaultData}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={'events'}
|
||||
component={PlanningStackComponent}
|
||||
name={TabRoutes.Planning}
|
||||
component={PlanningScreen}
|
||||
options={{ title: i18n.t('screens.planning.title') }}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={'planex'}
|
||||
component={PlanexStackComponent}
|
||||
options={{ title: i18n.t('screens.planex.title') }}
|
||||
name={TabRoutes.Planex}
|
||||
component={PlanexScreen}
|
||||
options={{
|
||||
title: i18n.t('screens.planex.title'),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
|
@ -329,15 +187,5 @@ function TabNavigator(props: PropsType) {
|
|||
|
||||
export default React.memo(
|
||||
TabNavigator,
|
||||
(pp: PropsType, np: PropsType) =>
|
||||
pp.defaultHomeRoute === np.defaultHomeRoute &&
|
||||
pp.defaultHomeData === np.defaultHomeData
|
||||
(pp: PropsType, np: PropsType) => pp.defaultData === np.defaultData
|
||||
);
|
||||
|
||||
export enum TabRoutes {
|
||||
Services = 'services',
|
||||
Proxiwash = 'proxiwash',
|
||||
Home = 'home',
|
||||
Planning = 'events',
|
||||
Planex = 'planex',
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import OptionsDialog from '../../components/Dialogs/OptionsDialog';
|
|||
import type { OptionsDialogButtonType } from '../../components/Dialogs/OptionsDialog';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
const APP_LOGO = require('../../../assets/android.icon.round.png');
|
||||
|
||||
|
@ -179,7 +180,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
|
|||
{
|
||||
onPressCallback: () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('feedback');
|
||||
navigation.navigate(MainRoutes.Feedback);
|
||||
},
|
||||
icon: 'bug',
|
||||
text: i18n.t('screens.feedback.homeButtonTitle'),
|
||||
|
@ -236,7 +237,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
|
|||
{
|
||||
onPressCallback: () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('dependencies');
|
||||
navigation.navigate(MainRoutes.Dependencies);
|
||||
},
|
||||
icon: 'developer-board',
|
||||
text: i18n.t('screens.about.libs'),
|
||||
|
@ -275,7 +276,7 @@ class AboutScreen extends React.Component<PropsType, StateType> {
|
|||
{
|
||||
onPressCallback: () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('feedback');
|
||||
navigation.navigate(MainRoutes.Feedback);
|
||||
},
|
||||
icon: 'hand-pointing-right',
|
||||
text: i18n.t('screens.about.user.you'),
|
||||
|
|
|
@ -64,10 +64,8 @@ function DebugScreen() {
|
|||
const modalRef = useRef<Modalize>(null);
|
||||
|
||||
const [modalInputValue, setModalInputValue] = useState<string>('');
|
||||
const [
|
||||
modalCurrentDisplayItem,
|
||||
setModalCurrentDisplayItem,
|
||||
] = useState<PreferenceItemType | null>(null);
|
||||
const [modalCurrentDisplayItem, setModalCurrentDisplayItem] =
|
||||
useState<PreferenceItemType | null>(null);
|
||||
|
||||
const currentPreferences: Array<PreferenceItemType> = [];
|
||||
Object.values(GeneralPreferenceKeys).forEach((key) => {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Linking, StyleSheet, View } from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
|
@ -25,29 +25,26 @@ import {
|
|||
Card,
|
||||
Chip,
|
||||
Paragraph,
|
||||
withTheme,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import CustomHTML from '../../../components/Overrides/CustomHTML';
|
||||
import { TAB_BAR_HEIGHT } from '../../../components/Tabbar/CustomTabBar';
|
||||
import type { ClubCategoryType, ClubType } from './ClubListScreen';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
import ImageGalleryButton from '../../../components/Media/ImageGalleryButton';
|
||||
import RequestScreen from '../../../components/Screens/RequestScreen';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import { useFocusEffect } from '@react-navigation/core';
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||
import { StackScreenProps } from '@react-navigation/stack';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
route: {
|
||||
params?: {
|
||||
data?: ClubType;
|
||||
categories?: Array<ClubCategoryType>;
|
||||
clubId?: number;
|
||||
};
|
||||
};
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
type Props = StackScreenProps<MainStackParamsList, MainRoutes.ClubInformation>;
|
||||
|
||||
type ResponseType = ClubType;
|
||||
|
||||
|
@ -89,33 +86,28 @@ const styles = StyleSheet.create({
|
|||
* If called with data and categories navigation parameters, will use those to display the data.
|
||||
* If called with clubId parameter, will fetch the information on the server
|
||||
*/
|
||||
class ClubDisplayScreen extends React.Component<PropsType> {
|
||||
displayData: ClubType | undefined;
|
||||
function ClubDisplayScreen(props: Props) {
|
||||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
|
||||
categories: Array<ClubCategoryType> | null;
|
||||
const [displayData, setDisplayData] = useState<ClubType | undefined>();
|
||||
const [categories, setCategories] = useState<
|
||||
Array<ClubCategoryType> | undefined
|
||||
>();
|
||||
const [clubId, setClubId] = useState<number | undefined>();
|
||||
|
||||
clubId: number;
|
||||
|
||||
shouldFetchData: boolean;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.displayData = undefined;
|
||||
this.categories = null;
|
||||
this.clubId = props.route.params?.clubId ? props.route.params.clubId : 0;
|
||||
this.shouldFetchData = true;
|
||||
|
||||
if (
|
||||
props.route.params &&
|
||||
props.route.params.data &&
|
||||
props.route.params.categories
|
||||
) {
|
||||
this.displayData = props.route.params.data;
|
||||
this.categories = props.route.params.categories;
|
||||
this.clubId = props.route.params.data.id;
|
||||
this.shouldFetchData = false;
|
||||
}
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (props.route.params.type === 'full') {
|
||||
setDisplayData(props.route.params.data);
|
||||
setCategories(props.route.params.categories);
|
||||
setClubId(props.route.params.data.id);
|
||||
} else {
|
||||
const id = props.route.params.clubId;
|
||||
setClubId(id ? id : 0);
|
||||
}
|
||||
}, [props.route.params])
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the name of the category with the given ID
|
||||
|
@ -123,17 +115,17 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
* @param id The category's ID
|
||||
* @returns {string|*}
|
||||
*/
|
||||
getCategoryName(id: number): string {
|
||||
const getCategoryName = (id: number): string => {
|
||||
let categoryName = '';
|
||||
if (this.categories !== null) {
|
||||
this.categories.forEach((item: ClubCategoryType) => {
|
||||
if (categories) {
|
||||
categories.forEach((item: ClubCategoryType) => {
|
||||
if (id === item.id) {
|
||||
categoryName = item.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
return categoryName;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the view for rendering categories
|
||||
|
@ -141,23 +133,23 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
* @param categories The categories to display (max 2)
|
||||
* @returns {null|*}
|
||||
*/
|
||||
getCategoriesRender(categories: Array<number | null>) {
|
||||
if (this.categories == null) {
|
||||
const getCategoriesRender = (c: Array<number | null>) => {
|
||||
if (!categories) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const final: Array<React.ReactNode> = [];
|
||||
categories.forEach((cat: number | null) => {
|
||||
c.forEach((cat: number | null) => {
|
||||
if (cat != null) {
|
||||
final.push(
|
||||
<Chip style={styles.category} key={cat}>
|
||||
{this.getCategoryName(cat)}
|
||||
{getCategoryName(cat)}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
});
|
||||
return <View style={styles.categoryContainer}>{final}</View>;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the view for rendering club managers if any
|
||||
|
@ -166,8 +158,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
* @param email The club contact email
|
||||
* @returns {*}
|
||||
*/
|
||||
getManagersRender(managers: Array<string>, email: string | null) {
|
||||
const { props } = this;
|
||||
const getManagersRender = (managers: Array<string>, email: string | null) => {
|
||||
const managersListView: Array<React.ReactNode> = [];
|
||||
managers.forEach((item: string) => {
|
||||
managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
|
||||
|
@ -191,22 +182,18 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
<Avatar.Icon
|
||||
size={iconProps.size}
|
||||
style={styles.icon}
|
||||
color={
|
||||
hasManagers
|
||||
? props.theme.colors.success
|
||||
: props.theme.colors.primary
|
||||
}
|
||||
color={hasManagers ? theme.colors.success : theme.colors.primary}
|
||||
icon="account-tie"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Card.Content>
|
||||
{managersListView}
|
||||
{ClubDisplayScreen.getEmailButton(email, hasManagers)}
|
||||
{getEmailButton(email, hasManagers)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the email button to contact the club, or the amicale if the club does not have any managers
|
||||
|
@ -215,7 +202,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
* @param hasManagers True if the club has managers
|
||||
* @returns {*}
|
||||
*/
|
||||
static getEmailButton(email: string | null, hasManagers: boolean) {
|
||||
const getEmailButton = (email: string | null, hasManagers: boolean) => {
|
||||
const destinationEmail =
|
||||
email != null && hasManagers ? email : AMICALE_MAIL;
|
||||
const text =
|
||||
|
@ -236,14 +223,14 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
</Button>
|
||||
</Card.Actions>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getScreen = (data: ResponseType | undefined) => {
|
||||
const getScreen = (data: ResponseType | undefined) => {
|
||||
if (data) {
|
||||
this.updateHeaderTitle(data);
|
||||
updateHeaderTitle(data);
|
||||
return (
|
||||
<CollapsibleScrollView style={styles.scroll} hasTab>
|
||||
{this.getCategoriesRender(data.category)}
|
||||
{getCategoriesRender(data.category)}
|
||||
{data.logo !== null ? (
|
||||
<ImageGalleryButton
|
||||
images={[{ url: data.logo }]}
|
||||
|
@ -261,7 +248,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
) : (
|
||||
<View />
|
||||
)}
|
||||
{this.getManagersRender(data.responsibles, data.email)}
|
||||
{getManagersRender(data.responsibles, data.email)}
|
||||
</CollapsibleScrollView>
|
||||
);
|
||||
}
|
||||
|
@ -273,27 +260,22 @@ class ClubDisplayScreen extends React.Component<PropsType> {
|
|||
*
|
||||
* @param data The club data
|
||||
*/
|
||||
updateHeaderTitle(data: ClubType) {
|
||||
const { props } = this;
|
||||
props.navigation.setOptions({ title: data.name });
|
||||
}
|
||||
const updateHeaderTitle = (data: ClubType) => {
|
||||
navigation.setOptions({ title: data.name });
|
||||
};
|
||||
|
||||
const request = useAuthenticatedRequest<ClubType>('clubs/info', {
|
||||
id: clubId,
|
||||
});
|
||||
|
||||
render() {
|
||||
if (this.shouldFetchData) {
|
||||
return (
|
||||
<RequestScreen
|
||||
request={() =>
|
||||
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
||||
'clubs/info',
|
||||
{ id: this.clubId }
|
||||
)
|
||||
}
|
||||
render={this.getScreen}
|
||||
request={request}
|
||||
render={getScreen}
|
||||
cache={displayData}
|
||||
onCacheUpdate={setDisplayData}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return this.getScreen(this.displayData);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ClubDisplayScreen);
|
||||
export default ClubDisplayScreen;
|
||||
|
|
|
@ -17,11 +17,10 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import { Searchbar } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem';
|
||||
import {
|
||||
isItemInCategoryFilter,
|
||||
|
@ -31,8 +30,10 @@ import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader';
|
|||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../../components/Overrides/CustomHeaderButton';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
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 = {
|
||||
id: number;
|
||||
|
@ -49,15 +50,6 @@ export type ClubType = {
|
|||
responsibles: Array<string>;
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
currentlySelectedCategories: Array<number>;
|
||||
currentSearchString: string;
|
||||
};
|
||||
|
||||
type ResponseType = {
|
||||
categories: Array<ClubCategoryType>;
|
||||
clubs: Array<ClubType>;
|
||||
|
@ -65,33 +57,50 @@ type ResponseType = {
|
|||
|
||||
const LIST_ITEM_HEIGHT = 96;
|
||||
|
||||
class ClubListScreen extends React.Component<PropsType, StateType> {
|
||||
categories: Array<ClubCategoryType>;
|
||||
function ClubListScreen() {
|
||||
const navigation = useNavigation();
|
||||
const request = useAuthenticatedRequest<ResponseType>('clubs/list');
|
||||
const [currentlySelectedCategories, setCurrentlySelectedCategories] =
|
||||
useState<Array<number>>([]);
|
||||
const [currentSearchString, setCurrentSearchString] = useState('');
|
||||
const categories = useRef<Array<ClubCategoryType>>([]);
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.categories = [];
|
||||
this.state = {
|
||||
currentlySelectedCategories: [],
|
||||
currentSearchString: '',
|
||||
useLayoutEffect(() => {
|
||||
const getSearchBar = () => {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Searchbar
|
||||
placeholder={i18n.t('screens.proximo.search')}
|
||||
onChangeText={onSearchStringChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the header content
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { props } = this;
|
||||
props.navigation.setOptions({
|
||||
headerTitle: this.getSearchBar,
|
||||
headerRight: this.getHeaderButtons,
|
||||
const getHeaderButtons = () => {
|
||||
return (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="main"
|
||||
iconName="information"
|
||||
onPress={() => navigation.navigate(MainRoutes.ClubAbout)}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
};
|
||||
navigation.setOptions({
|
||||
headerTitle: getSearchBar,
|
||||
headerRight: getHeaderButtons,
|
||||
headerBackTitleVisible: false,
|
||||
headerTitleContainerStyle:
|
||||
Platform.OS === 'ios'
|
||||
? { marginHorizontal: 0, width: '70%' }
|
||||
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||
: { width: '100%' },
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navigation]);
|
||||
|
||||
const onSearchStringChange = (str: string) => {
|
||||
updateFilteredData(str, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used when clicking an article in the list.
|
||||
|
@ -99,61 +108,21 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
*
|
||||
* @param item The article pressed
|
||||
*/
|
||||
onListItemPress(item: ClubType) {
|
||||
const { props } = this;
|
||||
props.navigation.navigate('club-information', {
|
||||
const onListItemPress = (item: ClubType) => {
|
||||
navigation.navigate(MainRoutes.ClubInformation, {
|
||||
type: 'full',
|
||||
data: item,
|
||||
categories: this.categories,
|
||||
categories: categories.current,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used when the search changes
|
||||
*
|
||||
* @param str The new search string
|
||||
*/
|
||||
onSearchStringChange = (str: string) => {
|
||||
this.updateFilteredData(str, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the header search bar
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
getSearchBar = () => {
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Searchbar
|
||||
placeholder={i18n.t('screens.proximo.search')}
|
||||
onChangeText={this.onSearchStringChange}
|
||||
/>
|
||||
);
|
||||
const onChipSelect = (id: number) => {
|
||||
updateFilteredData(null, id);
|
||||
};
|
||||
|
||||
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) => {
|
||||
const createDataset = (data: ResponseType | undefined) => {
|
||||
if (data) {
|
||||
this.categories = data?.categories;
|
||||
categories.current = data.categories;
|
||||
return [{ title: '', data: data.clubs }];
|
||||
} else {
|
||||
return [];
|
||||
|
@ -165,30 +134,23 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getListHeader(data: ResponseType | undefined) {
|
||||
const { state } = this;
|
||||
const getListHeader = (data: ResponseType | undefined) => {
|
||||
if (data) {
|
||||
return (
|
||||
<ClubListHeader
|
||||
categories={this.categories}
|
||||
selectedCategories={state.currentlySelectedCategories}
|
||||
onChipSelect={this.onChipSelect}
|
||||
categories={categories.current}
|
||||
selectedCategories={currentlySelectedCategories}
|
||||
onChipSelect={onChipSelect}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the category object of the given ID
|
||||
*
|
||||
* @param id The ID of the category to find
|
||||
* @returns {*}
|
||||
*/
|
||||
getCategoryOfId = (id: number): ClubCategoryType | null => {
|
||||
const getCategoryOfId = (id: number): ClubCategoryType | null => {
|
||||
let cat = null;
|
||||
this.categories.forEach((item: ClubCategoryType) => {
|
||||
categories.current.forEach((item: ClubCategoryType) => {
|
||||
if (id === item.id) {
|
||||
cat = item;
|
||||
}
|
||||
|
@ -196,14 +158,14 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
return cat;
|
||||
};
|
||||
|
||||
getRenderItem = ({ item }: { item: ClubType }) => {
|
||||
const getRenderItem = ({ item }: { item: ClubType }) => {
|
||||
const onPress = () => {
|
||||
this.onListItemPress(item);
|
||||
onListItemPress(item);
|
||||
};
|
||||
if (this.shouldRenderItem(item)) {
|
||||
if (shouldRenderItem(item)) {
|
||||
return (
|
||||
<ClubListItem
|
||||
categoryTranslator={this.getCategoryOfId}
|
||||
categoryTranslator={getCategoryOfId}
|
||||
item={item}
|
||||
onPress={onPress}
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
|
@ -213,7 +175,7 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
return null;
|
||||
};
|
||||
|
||||
keyExtractor = (item: ClubType): string => item.id.toString();
|
||||
const keyExtractor = (item: ClubType): string => item.id.toString();
|
||||
|
||||
/**
|
||||
* Updates the search string and category filter, saving them to the State.
|
||||
|
@ -224,10 +186,12 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
* @param filterStr The new filter string to use
|
||||
* @param categoryId The category to add/remove from the filter
|
||||
*/
|
||||
updateFilteredData(filterStr: string | null, categoryId: number | null) {
|
||||
const { state } = this;
|
||||
const newCategoriesState = [...state.currentlySelectedCategories];
|
||||
let newStrState = state.currentSearchString;
|
||||
const updateFilteredData = (
|
||||
filterStr: string | null,
|
||||
categoryId: number | null
|
||||
) => {
|
||||
const newCategoriesState = [...currentlySelectedCategories];
|
||||
let newStrState = currentSearchString;
|
||||
if (filterStr !== null) {
|
||||
newStrState = filterStr;
|
||||
}
|
||||
|
@ -240,12 +204,10 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
}
|
||||
if (filterStr !== null || categoryId !== null) {
|
||||
this.setState({
|
||||
currentSearchString: newStrState,
|
||||
currentlySelectedCategories: newCategoriesState,
|
||||
});
|
||||
}
|
||||
setCurrentSearchString(newStrState);
|
||||
setCurrentlySelectedCategories(newCategoriesState);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the given item should be rendered according to current name and category filters
|
||||
|
@ -253,35 +215,28 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
|
|||
* @param item The club to check
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldRenderItem(item: ClubType): boolean {
|
||||
const { state } = this;
|
||||
const shouldRenderItem = (item: ClubType): boolean => {
|
||||
let shouldRender =
|
||||
state.currentlySelectedCategories.length === 0 ||
|
||||
isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
|
||||
currentlySelectedCategories.length === 0 ||
|
||||
isItemInCategoryFilter(currentlySelectedCategories, item.category);
|
||||
if (shouldRender) {
|
||||
shouldRender = stringMatchQuery(item.name, state.currentSearchString);
|
||||
shouldRender = stringMatchQuery(item.name, currentSearchString);
|
||||
}
|
||||
return shouldRender;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WebSectionList
|
||||
request={() =>
|
||||
ConnectionManager.getInstance().authenticatedRequest<ResponseType>(
|
||||
'clubs/list'
|
||||
)
|
||||
}
|
||||
createDataset={this.createDataset}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getRenderItem}
|
||||
renderListHeaderComponent={(data) => this.getListHeader(data)}
|
||||
request={request}
|
||||
createDataset={createDataset}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={getRenderItem}
|
||||
renderListHeaderComponent={getListHeader}
|
||||
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
|
||||
removeClippedSubviews={true}
|
||||
itemHeight={LIST_ITEM_HEIGHT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ClubListScreen;
|
||||
|
|
|
@ -31,12 +31,15 @@ import i18n from 'i18n-js';
|
|||
import { getRelativeDateString } from '../../../utils/EquipmentBooking';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
import { StackScreenProps } from '@react-navigation/stack';
|
||||
import { MainStackParamsList } from '../../../navigation/MainNavigator';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../../navigation/MainNavigator';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
|
||||
type EquipmentConfirmScreenNavigationProp = StackScreenProps<
|
||||
MainStackParamsList,
|
||||
'equipment-confirm'
|
||||
MainRoutes.EquipmentConfirm
|
||||
>;
|
||||
|
||||
type Props = EquipmentConfirmScreenNavigationProp;
|
||||
|
|
|
@ -17,26 +17,17 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Button } from 'react-native-paper';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import i18n from 'i18n-js';
|
||||
import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
|
||||
import MascotPopup from '../../../components/Mascot/MascotPopup';
|
||||
import { MASCOT_STYLE } from '../../../components/Mascot/Mascot';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import { ApiRejectType } from '../../../utils/WebData';
|
||||
import WebSectionList from '../../../components/Screens/WebSectionList';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
mascotDialogVisible: boolean | undefined;
|
||||
};
|
||||
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||
|
||||
export type DeviceType = {
|
||||
id: number;
|
||||
|
@ -67,69 +58,63 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
||||
userRents: null | Array<RentedDeviceType>;
|
||||
function EquipmentListScreen() {
|
||||
const userRents = useRef<undefined | Array<RentedDeviceType>>();
|
||||
const [mascotDialogVisible, setMascotDialogVisible] = useState<
|
||||
undefined | boolean
|
||||
>(undefined);
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.userRents = null;
|
||||
this.state = {
|
||||
mascotDialogVisible: undefined,
|
||||
};
|
||||
}
|
||||
const requestAll =
|
||||
useAuthenticatedRequest<{ devices: Array<DeviceType> }>('location/all');
|
||||
const requestOwn = useAuthenticatedRequest<{
|
||||
locations: Array<RentedDeviceType>;
|
||||
}>('location/my');
|
||||
|
||||
getRenderItem = ({ item }: { item: DeviceType }) => {
|
||||
const { navigation } = this.props;
|
||||
const getRenderItem = ({ item }: { item: DeviceType }) => {
|
||||
return (
|
||||
<EquipmentListItem
|
||||
navigation={navigation}
|
||||
item={item}
|
||||
userDeviceRentDates={this.getUserDeviceRentDates(item)}
|
||||
userDeviceRentDates={getUserDeviceRentDates(item)}
|
||||
height={LIST_ITEM_HEIGHT}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
getUserDeviceRentDates(item: DeviceType): [string, string] | null {
|
||||
const getUserDeviceRentDates = (
|
||||
item: DeviceType
|
||||
): [string, string] | null => {
|
||||
let dates = null;
|
||||
if (this.userRents != null) {
|
||||
this.userRents.forEach((device: RentedDeviceType) => {
|
||||
if (userRents.current) {
|
||||
userRents.current.forEach((device: RentedDeviceType) => {
|
||||
if (item.id === device.device_id) {
|
||||
dates = [device.begin, device.end];
|
||||
}
|
||||
});
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the list header, with explains this screen's purpose
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getListHeader() {
|
||||
const getListHeader = () => {
|
||||
return (
|
||||
<View style={styles.headerContainer}>
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="help-circle"
|
||||
onPress={this.showMascotDialog}
|
||||
onPress={showMascotDialog}
|
||||
style={GENERAL_STYLES.centerHorizontal}
|
||||
>
|
||||
{i18n.t('screens.equipment.mascotDialog.title')}
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
keyExtractor = (item: DeviceType): string => item.id.toString();
|
||||
const keyExtractor = (item: DeviceType): string => item.id.toString();
|
||||
|
||||
createDataset = (data: ResponseType | undefined) => {
|
||||
const createDataset = (data: ResponseType | undefined) => {
|
||||
if (data) {
|
||||
const userRents = data.locations;
|
||||
|
||||
if (userRents) {
|
||||
this.userRents = userRents;
|
||||
if (data.locations) {
|
||||
userRents.current = data.locations;
|
||||
}
|
||||
return [{ title: '', data: data.devices }];
|
||||
} else {
|
||||
|
@ -137,27 +122,19 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
};
|
||||
|
||||
showMascotDialog = () => {
|
||||
this.setState({ mascotDialogVisible: true });
|
||||
};
|
||||
const showMascotDialog = () => setMascotDialogVisible(true);
|
||||
|
||||
hideMascotDialog = () => {
|
||||
this.setState({ mascotDialogVisible: false });
|
||||
};
|
||||
const hideMascotDialog = () => setMascotDialogVisible(false);
|
||||
|
||||
request = () => {
|
||||
const request = () => {
|
||||
return new Promise(
|
||||
(
|
||||
resolve: (data: ResponseType) => void,
|
||||
reject: (error: ApiRejectType) => void
|
||||
) => {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest<{ devices: Array<DeviceType> }>('location/all')
|
||||
requestAll()
|
||||
.then((devicesData) => {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest<{
|
||||
locations: Array<RentedDeviceType>;
|
||||
}>('location/my')
|
||||
requestOwn()
|
||||
.then((rentsData) => {
|
||||
resolve({
|
||||
devices: devicesData.devices,
|
||||
|
@ -175,19 +152,17 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
|||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { state } = this;
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<WebSectionList
|
||||
request={this.request}
|
||||
createDataset={this.createDataset}
|
||||
keyExtractor={this.keyExtractor}
|
||||
renderItem={this.getRenderItem}
|
||||
renderListHeaderComponent={() => this.getListHeader()}
|
||||
request={request}
|
||||
createDataset={createDataset}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={getRenderItem}
|
||||
renderListHeaderComponent={getListHeader}
|
||||
/>
|
||||
<MascotPopup
|
||||
visible={state.mascotDialogVisible}
|
||||
visible={mascotDialogVisible}
|
||||
title={i18n.t('screens.equipment.mascotDialog.title')}
|
||||
message={i18n.t('screens.equipment.mascotDialog.message')}
|
||||
icon="vote"
|
||||
|
@ -195,14 +170,13 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
|
|||
cancel: {
|
||||
message: i18n.t('screens.equipment.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: this.hideMascotDialog,
|
||||
onPress: hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.WINK}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EquipmentListScreen;
|
||||
|
|
|
@ -17,21 +17,20 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Caption,
|
||||
Card,
|
||||
Headline,
|
||||
Subheading,
|
||||
withTheme,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
||||
import { BackHandler, StyleSheet, View } from 'react-native';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import i18n from 'i18n-js';
|
||||
import { CalendarList, PeriodMarking } from 'react-native-calendars';
|
||||
import type { DeviceType } from './EquipmentListScreen';
|
||||
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
|
||||
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
|
||||
import {
|
||||
|
@ -42,34 +41,24 @@ import {
|
|||
getValidRange,
|
||||
isEquipmentAvailable,
|
||||
} from '../../../utils/EquipmentBooking';
|
||||
import ConnectionManager from '../../../managers/ConnectionManager';
|
||||
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
||||
import { MainStackParamsList } from '../../../navigation/MainNavigator';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../../navigation/MainNavigator';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import { ApiRejectType } from '../../../utils/WebData';
|
||||
import { REQUEST_STATUS } from '../../../utils/Requests';
|
||||
import { useFocusEffect } from '@react-navigation/core';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAuthenticatedRequest } from '../../../context/loginContext';
|
||||
|
||||
type EquipmentRentScreenNavigationProp = StackScreenProps<
|
||||
MainStackParamsList,
|
||||
'equipment-rent'
|
||||
>;
|
||||
|
||||
type Props = EquipmentRentScreenNavigationProp & {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
type Props = StackScreenProps<MainStackParamsList, MainRoutes.EquipmentRent>;
|
||||
|
||||
export type MarkedDatesObjectType = {
|
||||
[key: string]: PeriodMarking;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
dialogVisible: boolean;
|
||||
errorDialogVisible: boolean;
|
||||
markedDates: MarkedDatesObjectType;
|
||||
currentError: ApiRejectType;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
marginLeft: 'auto',
|
||||
|
@ -114,98 +103,101 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
class EquipmentRentScreen extends React.Component<Props, StateType> {
|
||||
item: DeviceType | null;
|
||||
function EquipmentRentScreen(props: Props) {
|
||||
const theme = useTheme();
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const [currentError, setCurrentError] = useState<ApiRejectType>({
|
||||
status: REQUEST_STATUS.SUCCESS,
|
||||
});
|
||||
const [markedDates, setMarkedDates] = useState<MarkedDatesObjectType>({});
|
||||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
|
||||
bookedDates: Array<string>;
|
||||
const item = props.route.params.item;
|
||||
|
||||
bookRef: { current: null | (Animatable.View & View) };
|
||||
const bookedDates = useRef<Array<string>>([]);
|
||||
const canBookEquipment = useRef(false);
|
||||
|
||||
canBookEquipment: boolean;
|
||||
const bookRef = useRef<Animatable.View & View>(null);
|
||||
|
||||
lockedDates: {
|
||||
let lockedDates: {
|
||||
[key: string]: PeriodMarking;
|
||||
};
|
||||
} = {};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.item = null;
|
||||
this.lockedDates = {};
|
||||
this.state = {
|
||||
dialogVisible: false,
|
||||
errorDialogVisible: false,
|
||||
markedDates: {},
|
||||
currentError: { status: REQUEST_STATUS.SUCCESS },
|
||||
};
|
||||
this.resetSelection();
|
||||
this.bookRef = React.createRef();
|
||||
this.canBookEquipment = false;
|
||||
this.bookedDates = [];
|
||||
if (props.route.params != null) {
|
||||
if (props.route.params.item != null) {
|
||||
this.item = props.route.params.item;
|
||||
} else {
|
||||
this.item = null;
|
||||
}
|
||||
}
|
||||
const { item } = this;
|
||||
if (item != null) {
|
||||
this.lockedDates = {};
|
||||
if (item) {
|
||||
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),
|
||||
lockedDates = {
|
||||
...lockedDates,
|
||||
...generateMarkedDates(false, theme, range),
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures focus and blur events to hook on android back button
|
||||
*/
|
||||
componentDidMount() {
|
||||
const { navigation } = this.props;
|
||||
navigation.addListener('focus', () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
onBackButtonPressAndroid
|
||||
);
|
||||
});
|
||||
navigation.addListener('blur', () => {
|
||||
return () => {
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.onBackButtonPressAndroid
|
||||
onBackButtonPressAndroid
|
||||
);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides default android back button behaviour to deselect date if any is selected.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
onBackButtonPressAndroid = (): boolean => {
|
||||
if (this.bookedDates.length > 0) {
|
||||
this.resetSelection();
|
||||
this.updateMarkedSelection();
|
||||
const onBackButtonPressAndroid = (): boolean => {
|
||||
if (bookedDates.current.length > 0) {
|
||||
resetSelection();
|
||||
updateMarkedSelection();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
onDialogDismiss = () => {
|
||||
this.setState({ dialogVisible: false });
|
||||
const showDialog = () => setDialogVisible(true);
|
||||
|
||||
const onDialogDismiss = () => setDialogVisible(false);
|
||||
|
||||
const onErrorDialogDismiss = () =>
|
||||
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||
|
||||
const getBookStartDate = (): Date | null => {
|
||||
return bookedDates.current.length > 0
|
||||
? new Date(bookedDates.current[0])
|
||||
: null;
|
||||
};
|
||||
|
||||
onErrorDialogDismiss = () => {
|
||||
this.setState({ errorDialogVisible: false });
|
||||
const getBookEndDate = (): Date | null => {
|
||||
const { length } = bookedDates.current;
|
||||
return length > 0 ? new Date(bookedDates.current[length - 1]) : null;
|
||||
};
|
||||
|
||||
const start = getBookStartDate();
|
||||
const end = getBookEndDate();
|
||||
const request = useAuthenticatedRequest(
|
||||
'location/booking',
|
||||
item && start && end
|
||||
? {
|
||||
device: item.id,
|
||||
begin: getISODate(start),
|
||||
end: getISODate(end),
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
/**
|
||||
* Sends the selected data to the server and waits for a response.
|
||||
* If the request is a success, navigate to the recap screen.
|
||||
|
@ -213,54 +205,37 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
onDialogAccept = (): Promise<void> => {
|
||||
const onDialogAccept = (): Promise<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) {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest('location/booking', {
|
||||
device: item.id,
|
||||
begin: getISODate(start),
|
||||
end: getISODate(end),
|
||||
})
|
||||
request()
|
||||
.then(() => {
|
||||
this.onDialogDismiss();
|
||||
props.navigation.replace('equipment-confirm', {
|
||||
item: this.item,
|
||||
onDialogDismiss();
|
||||
navigation.replace('equipment-confirm', {
|
||||
item: item,
|
||||
dates: [getISODate(start), getISODate(end)],
|
||||
});
|
||||
resolve();
|
||||
})
|
||||
.catch((error: ApiRejectType) => {
|
||||
this.onDialogDismiss();
|
||||
this.showErrorDialog(error);
|
||||
onDialogDismiss();
|
||||
setCurrentError(error);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
this.onDialogDismiss();
|
||||
onDialogDismiss();
|
||||
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.
|
||||
* If both start and end dates are already selected, unselect all.
|
||||
*
|
||||
* @param day The day selected
|
||||
*/
|
||||
selectNewDate = (day: {
|
||||
const selectNewDate = (day: {
|
||||
dateString: string;
|
||||
day: number;
|
||||
month: number;
|
||||
|
@ -268,84 +243,64 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
year: number;
|
||||
}) => {
|
||||
const selected = new Date(day.dateString);
|
||||
const start = this.getBookStartDate();
|
||||
|
||||
if (!this.lockedDates[day.dateString] != null) {
|
||||
if (!lockedDates[day.dateString] != null) {
|
||||
if (start === null) {
|
||||
this.updateSelectionRange(selected, selected);
|
||||
this.enableBooking();
|
||||
updateSelectionRange(selected, selected);
|
||||
enableBooking();
|
||||
} else if (start.getTime() === selected.getTime()) {
|
||||
this.resetSelection();
|
||||
} else if (this.bookedDates.length === 1) {
|
||||
this.updateSelectionRange(start, selected);
|
||||
this.enableBooking();
|
||||
resetSelection();
|
||||
} else if (bookedDates.current.length === 1) {
|
||||
updateSelectionRange(start, selected);
|
||||
enableBooking();
|
||||
} else {
|
||||
this.resetSelection();
|
||||
resetSelection();
|
||||
}
|
||||
this.updateMarkedSelection();
|
||||
updateMarkedSelection();
|
||||
}
|
||||
};
|
||||
|
||||
showErrorDialog = (error: ApiRejectType) => {
|
||||
this.setState({
|
||||
errorDialogVisible: true,
|
||||
currentError: error,
|
||||
});
|
||||
const showBookButton = () => {
|
||||
if (bookRef.current && bookRef.current.fadeInUp) {
|
||||
bookRef.current.fadeInUp(500);
|
||||
}
|
||||
};
|
||||
|
||||
showDialog = () => {
|
||||
this.setState({ dialogVisible: true });
|
||||
const hideBookButton = () => {
|
||||
if (bookRef.current && bookRef.current.fadeOutDown) {
|
||||
bookRef.current.fadeOutDown(500);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the book button by plying a fade animation
|
||||
*/
|
||||
showBookButton() {
|
||||
if (this.bookRef.current && this.bookRef.current.fadeInUp) {
|
||||
this.bookRef.current.fadeInUp(500);
|
||||
}
|
||||
const enableBooking = () => {
|
||||
if (!canBookEquipment.current) {
|
||||
showBookButton();
|
||||
canBookEquipment.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the book button by plying a fade animation
|
||||
*/
|
||||
hideBookButton() {
|
||||
if (this.bookRef.current && this.bookRef.current.fadeOutDown) {
|
||||
this.bookRef.current.fadeOutDown(500);
|
||||
}
|
||||
const resetSelection = () => {
|
||||
if (canBookEquipment.current) {
|
||||
hideBookButton();
|
||||
}
|
||||
canBookEquipment.current = false;
|
||||
bookedDates.current = [];
|
||||
};
|
||||
|
||||
enableBooking() {
|
||||
if (!this.canBookEquipment) {
|
||||
this.showBookButton();
|
||||
this.canBookEquipment = true;
|
||||
}
|
||||
const updateSelectionRange = (s: Date, e: Date) => {
|
||||
if (item) {
|
||||
bookedDates.current = getValidRange(s, e, item);
|
||||
} else {
|
||||
bookedDates.current = [];
|
||||
}
|
||||
};
|
||||
|
||||
resetSelection() {
|
||||
if (this.canBookEquipment) {
|
||||
this.hideBookButton();
|
||||
}
|
||||
this.canBookEquipment = false;
|
||||
this.bookedDates = [];
|
||||
}
|
||||
const updateMarkedSelection = () => {
|
||||
setMarkedDates(generateMarkedDates(true, theme, bookedDates.current));
|
||||
};
|
||||
|
||||
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()) {
|
||||
|
@ -358,7 +313,8 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
date: getRelativeDateString(start),
|
||||
});
|
||||
}
|
||||
if (item != null) {
|
||||
|
||||
if (item) {
|
||||
const isAvailable = isEquipmentAvailable(item);
|
||||
const firstAvailability = getFirstEquipmentAvailability(item);
|
||||
return (
|
||||
|
@ -370,9 +326,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
<View style={styles.titleContainer}>
|
||||
<Headline style={styles.title}>{item.name}</Headline>
|
||||
<Caption style={styles.caption}>
|
||||
(
|
||||
{i18n.t('screens.equipment.bail', { cost: item.caution })}
|
||||
)
|
||||
({i18n.t('screens.equipment.bail', { cost: item.caution })})
|
||||
</Caption>
|
||||
</View>
|
||||
</View>
|
||||
|
@ -380,9 +334,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
<Button
|
||||
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
||||
color={
|
||||
isAvailable
|
||||
? props.theme.colors.success
|
||||
: props.theme.colors.primary
|
||||
isAvailable ? theme.colors.success : theme.colors.primary
|
||||
}
|
||||
mode="text"
|
||||
>
|
||||
|
@ -390,9 +342,7 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
date: getRelativeDateString(firstAvailability),
|
||||
})}
|
||||
</Button>
|
||||
<Subheading style={styles.subtitle}>
|
||||
{subHeadingText}
|
||||
</Subheading>
|
||||
<Subheading style={styles.subtitle}>{subHeadingText}</Subheading>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
<CalendarList
|
||||
|
@ -407,28 +357,28 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
// Enable paging on horizontal, default = false
|
||||
pagingEnabled
|
||||
// Handler which gets executed on day press. Default = undefined
|
||||
onDayPress={this.selectNewDate}
|
||||
onDayPress={selectNewDate}
|
||||
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
||||
firstDay={1}
|
||||
// Hide month navigation arrows.
|
||||
hideArrows={false}
|
||||
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
||||
markingType={'period'}
|
||||
markedDates={{ ...this.lockedDates, ...state.markedDates }}
|
||||
markedDates={{ ...lockedDates, ...markedDates }}
|
||||
theme={{
|
||||
'backgroundColor': props.theme.colors.agendaBackgroundColor,
|
||||
'calendarBackground': props.theme.colors.background,
|
||||
'textSectionTitleColor': props.theme.colors.agendaDayTextColor,
|
||||
'selectedDayBackgroundColor': props.theme.colors.primary,
|
||||
'backgroundColor': theme.colors.agendaBackgroundColor,
|
||||
'calendarBackground': theme.colors.background,
|
||||
'textSectionTitleColor': theme.colors.agendaDayTextColor,
|
||||
'selectedDayBackgroundColor': theme.colors.primary,
|
||||
'selectedDayTextColor': '#ffffff',
|
||||
'todayTextColor': props.theme.colors.text,
|
||||
'dayTextColor': props.theme.colors.text,
|
||||
'textDisabledColor': props.theme.colors.agendaDayTextColor,
|
||||
'dotColor': props.theme.colors.primary,
|
||||
'todayTextColor': theme.colors.text,
|
||||
'dayTextColor': theme.colors.text,
|
||||
'textDisabledColor': theme.colors.agendaDayTextColor,
|
||||
'dotColor': theme.colors.primary,
|
||||
'selectedDotColor': '#ffffff',
|
||||
'arrowColor': props.theme.colors.primary,
|
||||
'monthTextColor': props.theme.colors.text,
|
||||
'indicatorColor': props.theme.colors.primary,
|
||||
'arrowColor': theme.colors.primary,
|
||||
'monthTextColor': theme.colors.text,
|
||||
'indicatorColor': theme.colors.primary,
|
||||
'textDayFontFamily': 'monospace',
|
||||
'textMonthFontFamily': 'monospace',
|
||||
'textDayHeaderFontFamily': 'monospace',
|
||||
|
@ -451,29 +401,32 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
/>
|
||||
</CollapsibleScrollView>
|
||||
<LoadingConfirmDialog
|
||||
visible={state.dialogVisible}
|
||||
onDismiss={this.onDialogDismiss}
|
||||
onAccept={this.onDialogAccept}
|
||||
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
|
||||
visible={state.errorDialogVisible}
|
||||
onDismiss={this.onErrorDialogDismiss}
|
||||
status={state.currentError.status}
|
||||
code={state.currentError.code}
|
||||
visible={
|
||||
currentError.status !== REQUEST_STATUS.SUCCESS ||
|
||||
currentError.code !== undefined
|
||||
}
|
||||
onDismiss={onErrorDialogDismiss}
|
||||
status={currentError.status}
|
||||
code={currentError.code}
|
||||
/>
|
||||
<Animatable.View
|
||||
ref={this.bookRef}
|
||||
ref={bookRef}
|
||||
useNativeDriver
|
||||
style={styles.buttonContainer}
|
||||
>
|
||||
<Button
|
||||
icon="bookmark-check"
|
||||
mode="contained"
|
||||
onPress={this.showDialog}
|
||||
onPress={showDialog}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t('screens.equipment.bookButton')}
|
||||
|
@ -483,7 +436,6 @@ class EquipmentRentScreen extends React.Component<Props, StateType> {
|
|||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(EquipmentRentScreen);
|
||||
export default EquipmentRentScreen;
|
||||
|
|
|
@ -17,158 +17,59 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Image, KeyboardAvoidingView, StyleSheet, View } from 'react-native';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
HelperText,
|
||||
TextInput,
|
||||
withTheme,
|
||||
} from 'react-native-paper';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { KeyboardAvoidingView, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import ErrorDialog from '../../components/Dialogs/ErrorDialog';
|
||||
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
|
||||
import { MainStackParamsList } from '../../navigation/MainNavigator';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../navigation/MainNavigator';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
import { ApiRejectType } from '../../utils/WebData';
|
||||
import { ApiRejectType, connectToAmicale } from '../../utils/WebData';
|
||||
import { REQUEST_STATUS } from '../../utils/Requests';
|
||||
import LoginForm from '../../components/Amicale/Login/LoginForm';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import { TabRoutes } from '../../navigation/TabNavigator';
|
||||
import { useShouldShowMascot } from '../../context/preferencesContext';
|
||||
import { useLogin } from '../../context/loginContext';
|
||||
import { saveLoginToken } from '../../utils/loginToken';
|
||||
|
||||
type LoginScreenNavigationProp = StackScreenProps<MainStackParamsList, 'login'>;
|
||||
type Props = StackScreenProps<MainStackParamsList, MainRoutes.Login>;
|
||||
|
||||
type Props = LoginScreenNavigationProp & {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
function LoginScreen(props: Props) {
|
||||
const navigation = useNavigation<StackNavigationProp<any>>();
|
||||
const { setLogin } = useLogin();
|
||||
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);
|
||||
|
||||
type StateType = {
|
||||
email: string;
|
||||
password: string;
|
||||
isEmailValidated: boolean;
|
||||
isPasswordValidated: boolean;
|
||||
loading: boolean;
|
||||
dialogVisible: boolean;
|
||||
dialogError: ApiRejectType;
|
||||
mascotDialogVisible: boolean | undefined;
|
||||
};
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setNextScreen(props.route.params?.nextScreen);
|
||||
}, [props.route.params])
|
||||
);
|
||||
|
||||
const ICON_AMICALE = require('../../../assets/amicale.png');
|
||||
|
||||
const emailRegex = /^.+@.+\..+$/;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginTop: 'auto',
|
||||
marginBottom: 'auto',
|
||||
},
|
||||
header: {
|
||||
fontSize: 36,
|
||||
marginBottom: 48,
|
||||
},
|
||||
text: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
buttonContainer: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
lockButton: {
|
||||
marginRight: 'auto',
|
||||
marginBottom: 20,
|
||||
},
|
||||
sendButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
});
|
||||
|
||||
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', {
|
||||
const onResetPasswordClick = () => {
|
||||
navigation.navigate(MainRoutes.Website, {
|
||||
host: Urls.websites.amicale,
|
||||
path: Urls.amicale.resetPassword,
|
||||
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.
|
||||
*
|
||||
|
@ -176,253 +77,39 @@ class LoginScreen extends React.Component<Props, StateType> {
|
|||
* then makes the login request and enters a loading state until the request finishes
|
||||
*
|
||||
*/
|
||||
onSubmit = () => {
|
||||
const { email, password } = this.state;
|
||||
if (this.shouldEnableLogin()) {
|
||||
this.setState({ loading: true });
|
||||
ConnectionManager.getInstance()
|
||||
.connect(email, password)
|
||||
.then(this.handleSuccess)
|
||||
.catch(this.showErrorDialog)
|
||||
.finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
const onSubmit = (email: string, password: string) => {
|
||||
setLoading(true);
|
||||
connectToAmicale(email, password)
|
||||
.then(handleSuccess)
|
||||
.catch(setCurrentError)
|
||||
.finally(() => setLoading(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 hideMascotDialog = () => setMascotDialogVisible(false);
|
||||
|
||||
/**
|
||||
* 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 showMascotDialog = () => setMascotDialogVisible(true);
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
};
|
||||
const hideErrorDialog = () =>
|
||||
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
handleSuccess = () => {
|
||||
const { navigation } = this.props;
|
||||
const handleSuccess = (token: string) => {
|
||||
// Do not show the home login banner again
|
||||
// TODO
|
||||
// AsyncStorageManager.set(
|
||||
// AsyncStorageManager.PREFERENCES.homeShowMascot.key,
|
||||
// false
|
||||
// );
|
||||
if (this.nextScreen == null) {
|
||||
if (homeMascot.shouldShow) {
|
||||
homeMascot.setShouldShow(false);
|
||||
}
|
||||
saveLoginToken(token);
|
||||
setLogin(token);
|
||||
if (!nextScreen) {
|
||||
navigation.goBack();
|
||||
} else {
|
||||
navigation.replace(this.nextScreen);
|
||||
navigation.replace(nextScreen);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
|
||||
*/
|
||||
handleNavigationParams() {
|
||||
this.nextScreen = this.props.route.params?.nextScreen;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the entered email is valid (matches the regex)
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isEmailValid(): boolean {
|
||||
const { email } = this.state;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we should tell the user his email is invalid.
|
||||
* We should only show this if his email is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
shouldShowEmailError(): boolean {
|
||||
const { isEmailValidated } = this.state;
|
||||
return isEmailValidated && !this.isEmailValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has entered a password
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isPasswordValid(): boolean {
|
||||
const { password } = this.state;
|
||||
return password !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we should tell the user his password is invalid.
|
||||
* We should only show this if his password is invalid and has been checked when un-focusing the input
|
||||
*
|
||||
* @returns {boolean|boolean}
|
||||
*/
|
||||
shouldShowPasswordError(): boolean {
|
||||
const { isPasswordValidated } = this.state;
|
||||
return isPasswordValidated && !this.isPasswordValid();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the email and password are valid, and we are not loading a request, then the login button can be enabled
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldEnableLogin(): boolean {
|
||||
const { loading } = this.state;
|
||||
return this.isEmailValid() && this.isPasswordValid() && !loading;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { mascotDialogVisible, dialogVisible, dialogError } = this.state;
|
||||
return (
|
||||
<LinearGradient
|
||||
style={GENERAL_STYLES.flex}
|
||||
|
@ -438,7 +125,14 @@ class LoginScreen extends React.Component<Props, StateType> {
|
|||
keyboardVerticalOffset={100}
|
||||
>
|
||||
<CollapsibleScrollView headerColors={'transparent'}>
|
||||
<View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<LoginForm
|
||||
loading={loading}
|
||||
onSubmit={onSubmit}
|
||||
onResetPasswordPress={onResetPasswordClick}
|
||||
onHelpPress={showMascotDialog}
|
||||
/>
|
||||
</View>
|
||||
<MascotPopup
|
||||
visible={mascotDialogVisible}
|
||||
title={i18n.t('screens.login.mascotDialog.title')}
|
||||
|
@ -448,22 +142,24 @@ class LoginScreen extends React.Component<Props, StateType> {
|
|||
cancel: {
|
||||
message: i18n.t('screens.login.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: this.hideMascotDialog,
|
||||
onPress: hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.NORMAL}
|
||||
/>
|
||||
<ErrorDialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={this.hideErrorDialog}
|
||||
status={dialogError.status}
|
||||
code={dialogError.code}
|
||||
visible={
|
||||
currentError.status !== REQUEST_STATUS.SUCCESS ||
|
||||
currentError.code !== undefined
|
||||
}
|
||||
onDismiss={hideErrorDialog}
|
||||
status={currentError.status}
|
||||
code={currentError.code}
|
||||
/>
|
||||
</CollapsibleScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</LinearGradient>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(LoginScreen);
|
||||
export default LoginScreen;
|
||||
|
|
|
@ -17,52 +17,29 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
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 React, { useLayoutEffect, useState } from 'react';
|
||||
import { View } from 'react-native';
|
||||
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} 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 GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
import RequestScreen from '../../components/Screens/RequestScreen';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import {
|
||||
getAmicaleServices,
|
||||
ServiceItemType,
|
||||
SERVICES_KEY,
|
||||
} from '../../utils/Services';
|
||||
import ProfileWelcomeCard from '../../components/Amicale/Profile/ProfileWelcomeCard';
|
||||
import ProfilePersonalCard from '../../components/Amicale/Profile/ProfilePersonalCard';
|
||||
import ProfileClubCard from '../../components/Amicale/Profile/ProfileClubCard';
|
||||
import ProfileMembershipCard from '../../components/Amicale/Profile/ProfileMembershipCard';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { useAuthenticatedRequest } from '../../context/loginContext';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
|
||||
type StateType = {
|
||||
dialogVisible: boolean;
|
||||
};
|
||||
|
||||
type ClubType = {
|
||||
export type ProfileClubType = {
|
||||
id: number;
|
||||
name: string;
|
||||
is_manager: boolean;
|
||||
};
|
||||
|
||||
type ProfileDataType = {
|
||||
export type ProfileDataType = {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
|
@ -71,87 +48,68 @@ type ProfileDataType = {
|
|||
branch: string;
|
||||
link: string;
|
||||
validity: boolean;
|
||||
clubs: Array<ClubType>;
|
||||
clubs: Array<ProfileClubType>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
margin: 10,
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
editButton: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
mascot: {
|
||||
width: 60,
|
||||
},
|
||||
title: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
});
|
||||
function ProfileScreen() {
|
||||
const navigation = useNavigation();
|
||||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
const request = useAuthenticatedRequest<ProfileDataType>('user/profile');
|
||||
|
||||
class ProfileScreen extends React.Component<PropsType, StateType> {
|
||||
data: ProfileDataType | undefined;
|
||||
|
||||
flatListData: Array<{ id: string }>;
|
||||
|
||||
amicaleDataset: Array<ServiceItemType>;
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.data = undefined;
|
||||
this.flatListData = [{ id: '0' }, { id: '1' }, { id: '2' }, { id: '3' }];
|
||||
this.amicaleDataset = getAmicaleServices(props.navigation.navigate, [
|
||||
SERVICES_KEY.PROFILE,
|
||||
]);
|
||||
this.state = {
|
||||
dialogVisible: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { navigation } = this.props;
|
||||
navigation.setOptions({
|
||||
headerRight: this.getHeaderButton,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the logout header button
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getHeaderButton = () => (
|
||||
useLayoutEffect(() => {
|
||||
const getHeaderButton = () => (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="logout"
|
||||
iconName="logout"
|
||||
onPress={this.showDisconnectDialog}
|
||||
title={'logout'}
|
||||
iconName={'logout'}
|
||||
onPress={showDisconnectDialog}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
navigation.setOptions({
|
||||
headerRight: getHeaderButton,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
const getScreen = (data: ProfileDataType | undefined) => {
|
||||
if (data) {
|
||||
this.data = data;
|
||||
const flatListData: Array<{
|
||||
id: string;
|
||||
render: () => React.ReactElement;
|
||||
}> = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
flatListData.push({
|
||||
id: i.toString(),
|
||||
render: () => <ProfileWelcomeCard firstname={data?.first_name} />,
|
||||
});
|
||||
break;
|
||||
case 1:
|
||||
flatListData.push({
|
||||
id: i.toString(),
|
||||
render: () => <ProfilePersonalCard profile={data} />,
|
||||
});
|
||||
break;
|
||||
case 2:
|
||||
flatListData.push({
|
||||
id: i.toString(),
|
||||
render: () => <ProfileClubCard clubs={data?.clubs} />,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
flatListData.push({
|
||||
id: i.toString(),
|
||||
render: () => <ProfileMembershipCard valid={data?.validity} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<CollapsibleFlatList
|
||||
renderItem={this.getRenderItem}
|
||||
data={this.flatListData}
|
||||
/>
|
||||
<CollapsibleFlatList renderItem={getRenderItem} data={flatListData} />
|
||||
<LogoutDialog
|
||||
visible={dialogVisible}
|
||||
onDismiss={this.hideDisconnectDialog}
|
||||
onDismiss={hideDisconnectDialog}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
@ -160,346 +118,17 @@ class ProfileScreen extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
};
|
||||
|
||||
getRenderItem = ({ item }: { item: { id: string } }) => {
|
||||
switch (item.id) {
|
||||
case '0':
|
||||
return this.getWelcomeCard();
|
||||
case '1':
|
||||
return this.getPersonalCard();
|
||||
case '2':
|
||||
return this.getClubCard();
|
||||
default:
|
||||
return this.getMembershipCar();
|
||||
}
|
||||
};
|
||||
const getRenderItem = ({
|
||||
item,
|
||||
}: {
|
||||
item: { id: string; render: () => React.ReactElement };
|
||||
}) => item.render();
|
||||
|
||||
/**
|
||||
* Gets the list of services available with the Amicale account
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
getServicesList() {
|
||||
return <CardList dataset={this.amicaleDataset} isHorizontal />;
|
||||
}
|
||||
const showDisconnectDialog = () => setDialogVisible(true);
|
||||
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
}
|
||||
const hideDisconnectDialog = () => setDialogVisible(false);
|
||||
|
||||
/**
|
||||
* 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <RequestScreen request={request} render={getScreen} />;
|
||||
}
|
||||
|
||||
export default withTheme(ProfileScreen);
|
||||
export default ProfileScreen;
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import i18n from 'i18n-js';
|
||||
import { Button } from 'react-native-paper';
|
||||
|
@ -30,10 +30,10 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
|||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import WebSectionList, {
|
||||
SectionListDataType,
|
||||
} from '../../components/Screens/WebSectionList';
|
||||
import { useAuthenticatedRequest } from '../../context/loginContext';
|
||||
|
||||
export type VoteTeamType = {
|
||||
id: number;
|
||||
|
@ -65,6 +65,13 @@ type ResponseType = {
|
|||
dates?: VoteDatesStringType;
|
||||
};
|
||||
|
||||
type FlatlistType = {
|
||||
teams: Array<VoteTeamType>;
|
||||
hasVoted: boolean;
|
||||
datesString?: VoteDatesStringType;
|
||||
dates?: VoteDatesObjectType;
|
||||
};
|
||||
|
||||
// const FAKE_DATE = {
|
||||
// "date_begin": "2020-08-19 15:50",
|
||||
// "date_end": "2020-08-19 15:50",
|
||||
|
@ -113,13 +120,6 @@ type ResponseType = {
|
|||
// ],
|
||||
// };
|
||||
|
||||
type PropsType = {};
|
||||
|
||||
type StateType = {
|
||||
hasVoted: boolean;
|
||||
mascotDialogVisible: boolean | undefined;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
button: {
|
||||
marginLeft: 'auto',
|
||||
|
@ -131,38 +131,19 @@ const styles = StyleSheet.create({
|
|||
/**
|
||||
* Screen displaying vote information and controls
|
||||
*/
|
||||
export default class VoteScreen extends React.Component<PropsType, StateType> {
|
||||
teams: Array<VoteTeamType>;
|
||||
export default function VoteScreen() {
|
||||
const [hasVoted, setHasVoted] = useState(false);
|
||||
const [mascotDialogVisible, setMascotDialogVisible] = useState<
|
||||
undefined | boolean
|
||||
>(undefined);
|
||||
|
||||
hasVoted: boolean;
|
||||
|
||||
datesString: undefined | VoteDatesStringType;
|
||||
|
||||
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 datesRequest =
|
||||
useAuthenticatedRequest<VoteDatesStringType>('elections/dates');
|
||||
const teamsRequest =
|
||||
useAuthenticatedRequest<TeamResponseType>('elections/teams');
|
||||
|
||||
const today = new Date();
|
||||
const refresh = useRef<() => void | undefined>();
|
||||
/**
|
||||
* Gets the string representation of the given date.
|
||||
*
|
||||
|
@ -173,22 +154,26 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
* @param dateString The string representation of the wanted date
|
||||
* @returns {string}
|
||||
*/
|
||||
getDateString(date: Date, dateString: string): string {
|
||||
if (this.today.getDate() === date.getDate()) {
|
||||
const getDateString = (date: Date, dateString: string) => {
|
||||
if (today.getDate() === date.getDate()) {
|
||||
const str = getTimeOnlyString(dateString);
|
||||
return str != null ? str : '';
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
getMainRenderItem = ({ item }: { item: { key: string } }) => {
|
||||
const getMainRenderItem = ({
|
||||
item,
|
||||
}: {
|
||||
item: { key: string; data?: FlatlistType };
|
||||
}) => {
|
||||
if (item.key === 'info') {
|
||||
return (
|
||||
<View>
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="help-circle"
|
||||
onPress={this.showMascotDialog}
|
||||
onPress={showMascotDialog}
|
||||
style={styles.button}
|
||||
>
|
||||
{i18n.t('screens.vote.mascotDialog.title')}
|
||||
|
@ -196,10 +181,14 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
return this.getContent();
|
||||
if (item.data) {
|
||||
return getContent(item.data);
|
||||
} else {
|
||||
return <View />;
|
||||
}
|
||||
};
|
||||
|
||||
createDataset = (
|
||||
const createDataset = (
|
||||
data: ResponseType | undefined,
|
||||
_loading: boolean,
|
||||
_lastRefreshDate: Date | undefined,
|
||||
|
@ -207,157 +196,158 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
) => {
|
||||
// data[0] = FAKE_TEAMS2;
|
||||
// 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) {
|
||||
const { teams, dates } = data;
|
||||
|
||||
if (dates && dates.date_begin == null) {
|
||||
this.datesString = undefined;
|
||||
} else {
|
||||
this.datesString = dates;
|
||||
const flatlistData: FlatlistType = {
|
||||
teams: [],
|
||||
hasVoted: false,
|
||||
};
|
||||
if (dates && dates.date_begin != null) {
|
||||
flatlistData.datesString = dates;
|
||||
}
|
||||
|
||||
if (teams) {
|
||||
this.teams = teams.teams;
|
||||
this.hasVoted = teams.has_voted;
|
||||
flatlistData.teams = teams.teams;
|
||||
flatlistData.hasVoted = teams.has_voted;
|
||||
}
|
||||
|
||||
this.generateDateObject();
|
||||
flatlistData.dates = generateDateObject(flatlistData.datesString);
|
||||
}
|
||||
return this.mainFlatListData;
|
||||
return mainFlatListData;
|
||||
};
|
||||
|
||||
getContent() {
|
||||
const { state } = this;
|
||||
if (!this.isVoteStarted()) {
|
||||
return this.getTeaseVoteCard();
|
||||
const getContent = (data: FlatlistType) => {
|
||||
const { dates } = data;
|
||||
if (!isVoteStarted(dates)) {
|
||||
return getTeaseVoteCard(data);
|
||||
}
|
||||
if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
|
||||
return this.getVoteCard();
|
||||
if (isVoteRunning(dates) && !data.hasVoted && !hasVoted) {
|
||||
return getVoteCard(data);
|
||||
}
|
||||
if (!this.isResultStarted()) {
|
||||
return this.getWaitVoteCard();
|
||||
if (!isResultStarted(dates)) {
|
||||
return getWaitVoteCard(data);
|
||||
}
|
||||
if (this.isResultRunning()) {
|
||||
return this.getVoteResultCard();
|
||||
if (isResultRunning(dates)) {
|
||||
return getVoteResultCard(data);
|
||||
}
|
||||
return <VoteNotAvailable />;
|
||||
}
|
||||
|
||||
onVoteSuccess = (): void => this.setState({ hasVoted: true });
|
||||
};
|
||||
|
||||
const onVoteSuccess = () => setHasVoted(true);
|
||||
/**
|
||||
* The user has not voted yet, and the votes are open
|
||||
*/
|
||||
getVoteCard() {
|
||||
const getVoteCard = (data: FlatlistType) => {
|
||||
return (
|
||||
<VoteSelect
|
||||
teams={this.teams}
|
||||
onVoteSuccess={this.onVoteSuccess}
|
||||
onVoteError={this.refreshData}
|
||||
teams={data.teams}
|
||||
onVoteSuccess={onVoteSuccess}
|
||||
onVoteError={() => {
|
||||
if (refresh.current) {
|
||||
refresh.current();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* Votes have ended, results can be displayed
|
||||
*/
|
||||
getVoteResultCard() {
|
||||
if (this.dates != null && this.datesString != null) {
|
||||
const getVoteResultCard = (data: FlatlistType) => {
|
||||
if (data.dates != null && data.datesString != null) {
|
||||
return (
|
||||
<VoteResults
|
||||
teams={this.teams}
|
||||
dateEnd={this.getDateString(
|
||||
this.dates.date_result_end,
|
||||
this.datesString.date_result_end
|
||||
teams={data.teams}
|
||||
dateEnd={getDateString(
|
||||
data.dates.date_result_end,
|
||||
data.datesString.date_result_end
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <VoteNotAvailable />;
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* Vote will open shortly
|
||||
*/
|
||||
getTeaseVoteCard() {
|
||||
if (this.dates != null && this.datesString != null) {
|
||||
const getTeaseVoteCard = (data: FlatlistType) => {
|
||||
if (data.dates != null && data.datesString != null) {
|
||||
return (
|
||||
<VoteTease
|
||||
startDate={this.getDateString(
|
||||
this.dates.date_begin,
|
||||
this.datesString.date_begin
|
||||
startDate={getDateString(
|
||||
data.dates.date_begin,
|
||||
data.datesString.date_begin
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <VoteNotAvailable />;
|
||||
}
|
||||
|
||||
};
|
||||
/**
|
||||
* Votes have ended, or user has voted waiting for results
|
||||
*/
|
||||
getWaitVoteCard() {
|
||||
const { state } = this;
|
||||
const getWaitVoteCard = (data: FlatlistType) => {
|
||||
let startDate = null;
|
||||
if (
|
||||
this.dates != null &&
|
||||
this.datesString != null &&
|
||||
this.dates.date_result_begin != null
|
||||
data.dates != null &&
|
||||
data.datesString != null &&
|
||||
data.dates.date_result_begin != null
|
||||
) {
|
||||
startDate = this.getDateString(
|
||||
this.dates.date_result_begin,
|
||||
this.datesString.date_result_begin
|
||||
startDate = getDateString(
|
||||
data.dates.date_result_begin,
|
||||
data.datesString.date_result_begin
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VoteWait
|
||||
startDate={startDate}
|
||||
hasVoted={this.hasVoted || state.hasVoted}
|
||||
justVoted={state.hasVoted}
|
||||
isVoteRunning={this.isVoteRunning()}
|
||||
hasVoted={data.hasVoted}
|
||||
justVoted={hasVoted}
|
||||
isVoteRunning={isVoteRunning()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
showMascotDialog = () => {
|
||||
this.setState({ mascotDialogVisible: true });
|
||||
};
|
||||
|
||||
hideMascotDialog = () => {
|
||||
this.setState({ mascotDialogVisible: false });
|
||||
const showMascotDialog = () => setMascotDialogVisible(true);
|
||||
|
||||
const hideMascotDialog = () => setMascotDialogVisible(false);
|
||||
|
||||
const isVoteStarted = (dates?: VoteDatesObjectType) => {
|
||||
return dates != null && today > dates.date_begin;
|
||||
};
|
||||
|
||||
isVoteStarted(): boolean {
|
||||
return this.dates != null && this.today > this.dates.date_begin;
|
||||
}
|
||||
|
||||
isResultRunning(): boolean {
|
||||
const isResultRunning = (dates?: VoteDatesObjectType) => {
|
||||
return (
|
||||
this.dates != null &&
|
||||
this.today > this.dates.date_result_begin &&
|
||||
this.today < this.dates.date_result_end
|
||||
dates != null &&
|
||||
today > dates.date_result_begin &&
|
||||
today < dates.date_result_end
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
isResultStarted(): boolean {
|
||||
return this.dates != null && this.today > this.dates.date_result_begin;
|
||||
}
|
||||
const isResultStarted = (dates?: VoteDatesObjectType) => {
|
||||
return dates != null && today > dates.date_result_begin;
|
||||
};
|
||||
|
||||
isVoteRunning(): boolean {
|
||||
return (
|
||||
this.dates != null &&
|
||||
this.today > this.dates.date_begin &&
|
||||
this.today < this.dates.date_end
|
||||
);
|
||||
}
|
||||
const isVoteRunning = (dates?: VoteDatesObjectType) => {
|
||||
return dates != null && today > dates.date_begin && today < dates.date_end;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the objects containing string and Date representations of key vote dates
|
||||
*/
|
||||
generateDateObject() {
|
||||
const strings = this.datesString;
|
||||
if (strings != null) {
|
||||
const generateDateObject = (
|
||||
strings?: VoteDatesStringType
|
||||
): VoteDatesObjectType | undefined => {
|
||||
if (strings) {
|
||||
const dateBegin = stringToDate(strings.date_begin);
|
||||
const dateEnd = stringToDate(strings.date_end);
|
||||
const dateResultBegin = stringToDate(strings.date_result_begin);
|
||||
|
@ -368,27 +358,25 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
dateResultBegin != null &&
|
||||
dateResultEnd != null
|
||||
) {
|
||||
this.dates = {
|
||||
return {
|
||||
date_begin: dateBegin,
|
||||
date_end: dateEnd,
|
||||
date_result_begin: dateResultBegin,
|
||||
date_result_end: dateResultEnd,
|
||||
};
|
||||
} else {
|
||||
this.dates = undefined;
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
this.dates = undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
request = () => {
|
||||
const request = () => {
|
||||
return new Promise((resolve: (data: ResponseType) => void) => {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest<VoteDatesStringType>('elections/dates')
|
||||
datesRequest()
|
||||
.then((datesData) => {
|
||||
ConnectionManager.getInstance()
|
||||
.authenticatedRequest<TeamResponseType>('elections/teams')
|
||||
teamsRequest()
|
||||
.then((teamsData) => {
|
||||
resolve({
|
||||
dates: datesData,
|
||||
|
@ -405,25 +393,16 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the authenticated screen.
|
||||
*
|
||||
* Teams and dates are not mandatory to allow showing the information box even if api requests fail
|
||||
*
|
||||
* @returns {*}
|
||||
*/
|
||||
render() {
|
||||
const { state } = this;
|
||||
return (
|
||||
<View style={GENERAL_STYLES.flex}>
|
||||
<WebSectionList
|
||||
request={this.request}
|
||||
createDataset={this.createDataset}
|
||||
extraData={state.hasVoted.toString()}
|
||||
renderItem={this.getMainRenderItem}
|
||||
request={request}
|
||||
createDataset={createDataset}
|
||||
extraData={hasVoted.toString()}
|
||||
renderItem={getMainRenderItem}
|
||||
/>
|
||||
<MascotPopup
|
||||
visible={state.mascotDialogVisible}
|
||||
visible={mascotDialogVisible}
|
||||
title={i18n.t('screens.vote.mascotDialog.title')}
|
||||
message={i18n.t('screens.vote.mascotDialog.message')}
|
||||
icon="vote"
|
||||
|
@ -431,12 +410,11 @@ export default class VoteScreen extends React.Component<PropsType, StateType> {
|
|||
cancel: {
|
||||
message: i18n.t('screens.vote.mascotDialog.button'),
|
||||
icon: 'check',
|
||||
onPress: this.hideMascotDialog,
|
||||
onPress: hideMascotDialog,
|
||||
},
|
||||
}}
|
||||
emotion={MASCOT_STYLE.CUTE}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -285,8 +285,9 @@ export default class GameLogic {
|
|||
getNextPiecesPreviews(): Array<GridType> {
|
||||
const finalArray = [];
|
||||
for (let i = 0; i < this.nextPieces.length; i += 1) {
|
||||
const gridSize = this.nextPieces[i].getCurrentShape().getCurrentShape()[0]
|
||||
.length;
|
||||
const gridSize = this.nextPieces[i]
|
||||
.getCurrentShape()
|
||||
.getCurrentShape()[0].length;
|
||||
finalArray.push(this.gridManager.getEmptyGrid(gridSize, gridSize));
|
||||
this.nextPieces[i].toGrid(finalArray[i], true);
|
||||
}
|
||||
|
|
|
@ -65,9 +65,8 @@ export default class Piece {
|
|||
* @param grid The grid to remove the piece from
|
||||
*/
|
||||
removeFromGrid(grid: GridType) {
|
||||
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates(
|
||||
true
|
||||
);
|
||||
const pos: Array<CoordinatesType> =
|
||||
this.currentShape.getCellsCoordinates(true);
|
||||
pos.forEach((coordinates: CoordinatesType) => {
|
||||
grid[coordinates.y][coordinates.x] = {
|
||||
color: this.theme.colors.tetrisBackground,
|
||||
|
@ -106,9 +105,8 @@ export default class Piece {
|
|||
*/
|
||||
isPositionValid(grid: GridType, width: number, height: number): boolean {
|
||||
let isValid = true;
|
||||
const pos: Array<CoordinatesType> = this.currentShape.getCellsCoordinates(
|
||||
true
|
||||
);
|
||||
const pos: Array<CoordinatesType> =
|
||||
this.currentShape.getCellsCoordinates(true);
|
||||
for (let i = 0; i < pos.length; i += 1) {
|
||||
if (
|
||||
pos[i].x >= width ||
|
||||
|
|
|
@ -21,7 +21,7 @@ import * as React from 'react';
|
|||
import { Linking, Image, StyleSheet } from 'react-native';
|
||||
import { Card, Text } from 'react-native-paper';
|
||||
import Autolink from 'react-native-autolink';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { StackScreenProps } from '@react-navigation/stack';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
} from '../../components/Overrides/CustomHeaderButton';
|
||||
|
@ -33,11 +33,15 @@ import NewsSourcesConstants, {
|
|||
AvailablePages,
|
||||
} from '../../constants/NewsSourcesConstants';
|
||||
import type { NewsSourceType } from '../../constants/NewsSourcesConstants';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
route: { params: { data: FeedItemType; date: string } };
|
||||
};
|
||||
type PropsType = StackScreenProps<
|
||||
MainStackParamsList,
|
||||
MainRoutes.FeedInformation
|
||||
>;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
|
|
@ -46,7 +46,6 @@ import MaterialHeaderButtons, {
|
|||
Item,
|
||||
} from '../../components/Overrides/CustomHeaderButton';
|
||||
import AnimatedFAB from '../../components/Animations/AnimatedFAB';
|
||||
import ConnectionManager from '../../managers/ConnectionManager';
|
||||
import LogoutDialog from '../../components/Amicale/LogoutDialog';
|
||||
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
|
@ -59,6 +58,7 @@ import { TabRoutes, TabStackParamsList } from '../../navigation/TabNavigator';
|
|||
import { ServiceItemType } from '../../utils/Services';
|
||||
import { useCurrentDashboard } from '../../context/preferencesContext';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
import { useLoginState } from '../../context/loginContext';
|
||||
|
||||
const FEED_ITEM_HEIGHT = 500;
|
||||
|
||||
|
@ -146,9 +146,7 @@ function HomeScreen(props: Props) {
|
|||
const [dialogVisible, setDialogVisible] = useState(false);
|
||||
const fabRef = useRef<AnimatedFAB>(null);
|
||||
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(
|
||||
ConnectionManager.getInstance().isLoggedIn()
|
||||
);
|
||||
const isLoggedIn = useLoginState();
|
||||
const { currentDashboard } = useCurrentDashboard();
|
||||
|
||||
let homeDashboard: FullDashboardType | null = null;
|
||||
|
@ -156,7 +154,7 @@ function HomeScreen(props: Props) {
|
|||
useLayoutEffect(() => {
|
||||
const getHeaderButton = () => {
|
||||
let onPressLog = () =>
|
||||
navigation.navigate('login', { nextScreen: 'profile' });
|
||||
navigation.navigate(MainRoutes.Login, { nextScreen: 'profile' });
|
||||
let logIcon = 'login';
|
||||
let logColor = theme.colors.primary;
|
||||
if (isLoggedIn) {
|
||||
|
@ -192,20 +190,15 @@ function HomeScreen(props: Props) {
|
|||
const handleNavigationParams = () => {
|
||||
const { route } = props;
|
||||
if (route.params != null) {
|
||||
if (route.params.nextScreen != null) {
|
||||
navigation.navigate(route.params.nextScreen, route.params.data);
|
||||
if (route.params.route != null) {
|
||||
navigation.navigate(route.params.route, route.params.data);
|
||||
// reset params to prevent infinite loop
|
||||
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
|
||||
handleNavigationParams();
|
||||
return () => {};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isLoggedIn])
|
||||
);
|
||||
|
@ -335,7 +328,7 @@ function HomeScreen(props: Props) {
|
|||
|
||||
const hideDisconnectDialog = () => setDialogVisible(false);
|
||||
|
||||
const openScanner = () => navigation.navigate('scanner');
|
||||
const openScanner = () => navigation.navigate(MainRoutes.Scanner);
|
||||
|
||||
/**
|
||||
* Creates the dataset to be used in the FlatList
|
||||
|
|
|
@ -26,7 +26,6 @@ import i18n from 'i18n-js';
|
|||
import { PERMISSIONS, request, RESULTS } from 'react-native-permissions';
|
||||
import URLHandler from '../../utils/URLHandler';
|
||||
import AlertDialog from '../../components/Dialogs/AlertDialog';
|
||||
import { TAB_BAR_HEIGHT } from '../../components/Tabbar/CustomTabBar';
|
||||
import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog';
|
||||
import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
||||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
|
@ -223,7 +222,6 @@ class ScannerScreen extends React.Component<{}, StateType> {
|
|||
<View
|
||||
style={{
|
||||
...styles.container,
|
||||
marginBottom: TAB_BAR_HEIGHT,
|
||||
}}
|
||||
>
|
||||
{state.hasPermission ? this.getScanner() : this.getPermissionScreen()}
|
||||
|
|
|
@ -13,13 +13,13 @@ import { Platform, SafeAreaView, View } from 'react-native';
|
|||
import { useDarkTheme } from '../context/preferencesContext';
|
||||
import { CustomDarkTheme, CustomWhiteTheme } from '../utils/Themes';
|
||||
import { setupStatusBar } from '../utils/Utils';
|
||||
import { ParsedUrlDataType } from '../utils/URLHandler';
|
||||
|
||||
type Props = {
|
||||
defaultHomeRoute?: string;
|
||||
defaultHomeData?: { [key: string]: string };
|
||||
defaultData?: ParsedUrlDataType;
|
||||
};
|
||||
|
||||
function MainApp(props: Props, ref?: Ref<NavigationContainerRef>) {
|
||||
function MainApp(props: Props, ref?: Ref<NavigationContainerRef<any>>) {
|
||||
const darkTheme = useDarkTheme();
|
||||
const theme = darkTheme ? CustomDarkTheme : CustomWhiteTheme;
|
||||
|
||||
|
@ -44,10 +44,7 @@ function MainApp(props: Props, ref?: Ref<NavigationContainerRef>) {
|
|||
>
|
||||
<SafeAreaView style={GENERAL_STYLES.flex}>
|
||||
<NavigationContainer theme={theme} ref={ref}>
|
||||
<MainNavigator
|
||||
defaultHomeRoute={props.defaultHomeRoute}
|
||||
defaultHomeData={props.defaultHomeData}
|
||||
/>
|
||||
<MainNavigator defaultData={props.defaultData} />
|
||||
</NavigationContainer>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
|
|
|
@ -23,11 +23,14 @@ import ImageViewer from 'react-native-image-zoom-viewer';
|
|||
import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
|
||||
import * as Animatable from 'react-native-animatable';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { MainStackParamsList } from '../../navigation/MainNavigator';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../navigation/MainNavigator';
|
||||
|
||||
type ImageGalleryScreenNavigationProp = StackScreenProps<
|
||||
MainStackParamsList,
|
||||
'gallery'
|
||||
MainRoutes.Gallery
|
||||
>;
|
||||
|
||||
type Props = ImageGalleryScreenNavigationProp & {
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
} from '../../../utils/Services';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { useCurrentDashboard } from '../../../context/preferencesContext';
|
||||
import { useLoginState } from '../../../context/loginContext';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dashboardContainer: {
|
||||
|
@ -63,12 +64,10 @@ const styles = StyleSheet.create({
|
|||
*/
|
||||
function DashboardEditScreen() {
|
||||
const navigation = useNavigation();
|
||||
const isLoggedIn = useLoginState();
|
||||
|
||||
const {
|
||||
currentDashboard,
|
||||
currentDashboardIdList,
|
||||
updateCurrentDashboard,
|
||||
} = useCurrentDashboard();
|
||||
const { currentDashboard, currentDashboardIdList, updateCurrentDashboard } =
|
||||
useCurrentDashboard();
|
||||
const initialDashboard = useRef(currentDashboardIdList);
|
||||
const [activeItem, setActiveItem] = useState(0);
|
||||
|
||||
|
@ -150,7 +149,8 @@ function DashboardEditScreen() {
|
|||
|
||||
return (
|
||||
<CollapsibleFlatList
|
||||
data={getCategories(navigation.navigate)}
|
||||
//@ts-ignore
|
||||
data={getCategories(navigation.navigate, isLoggedIn)}
|
||||
renderItem={getRenderItem}
|
||||
ListHeaderComponent={getListHeader()}
|
||||
style={{}}
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
GeneralPreferenceKeys,
|
||||
ProxiwashPreferenceKeys,
|
||||
} from '../../../utils/asyncStorage';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
slider: {
|
||||
|
@ -204,7 +205,7 @@ function SettingsScreen() {
|
|||
};
|
||||
|
||||
const getNavigateItem = (
|
||||
route: string,
|
||||
route: MainRoutes,
|
||||
icon: string,
|
||||
title: string,
|
||||
subtitle: string,
|
||||
|
@ -283,7 +284,7 @@ function SettingsScreen() {
|
|||
/>
|
||||
{getStartScreenPicker()}
|
||||
{getNavigateItem(
|
||||
'dashboard-edit',
|
||||
MainRoutes.DashboardEdit,
|
||||
'view-dashboard',
|
||||
i18n.t('screens.settings.dashboard'),
|
||||
i18n.t('screens.settings.dashboardSub')
|
||||
|
@ -328,21 +329,21 @@ function SettingsScreen() {
|
|||
<List.Section>
|
||||
{isDebugUnlocked
|
||||
? getNavigateItem(
|
||||
'debug',
|
||||
MainRoutes.Debug,
|
||||
'bug-check',
|
||||
i18n.t('screens.debug.title'),
|
||||
''
|
||||
)
|
||||
: null}
|
||||
{getNavigateItem(
|
||||
'about',
|
||||
MainRoutes.About,
|
||||
'information',
|
||||
i18n.t('screens.about.title'),
|
||||
i18n.t('screens.about.buttonDesc'),
|
||||
unlockDebugMode
|
||||
)}
|
||||
{getNavigateItem(
|
||||
'feedback',
|
||||
MainRoutes.Feedback,
|
||||
'comment-quote',
|
||||
i18n.t('screens.feedback.homeButtonTitle'),
|
||||
i18n.t('screens.feedback.homeButtonSubtitle')
|
||||
|
|
|
@ -81,16 +81,6 @@ function GroupSelectionScreen() {
|
|||
const favoriteGroups = getFavoriteGroups();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerTitle: getSearchBar,
|
||||
headerBackTitleVisible: false,
|
||||
headerTitleContainerStyle:
|
||||
Platform.OS === 'ios'
|
||||
? { marginHorizontal: 0, width: '70%' }
|
||||
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const getSearchBar = () => {
|
||||
return (
|
||||
// @ts-ignore
|
||||
|
@ -100,6 +90,15 @@ function GroupSelectionScreen() {
|
|||
/>
|
||||
);
|
||||
};
|
||||
navigation.setOptions({
|
||||
headerTitle: getSearchBar,
|
||||
headerBackTitleVisible: false,
|
||||
headerTitleContainerStyle:
|
||||
Platform.OS === 'ios'
|
||||
? { marginHorizontal: 0, width: '70%' }
|
||||
: { width: '100%' },
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* Gets a render item for the given article
|
||||
|
|
|
@ -42,6 +42,7 @@ import {
|
|||
} from '../../utils/asyncStorage';
|
||||
import { usePlanexPreferences } from '../../context/preferencesContext';
|
||||
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -105,7 +106,7 @@ function PlanexScreen() {
|
|||
* Callback used when the user clicks on the navigate to settings button.
|
||||
* This will hide the banner and open the SettingsScreen
|
||||
*/
|
||||
const onGoToSettings = () => navigation.navigate('settings');
|
||||
const onGoToSettings = () => navigation.navigate(MainRoutes.Settings);
|
||||
|
||||
/**
|
||||
* Sends a FullCalendar action to the web page inside the webview.
|
||||
|
|
|
@ -21,7 +21,7 @@ import * as React from 'react';
|
|||
import { StyleSheet, View } from 'react-native';
|
||||
import { Card } from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { StackScreenProps } from '@react-navigation/stack';
|
||||
import { getDateOnlyString, getTimeOnlyString } from '../../utils/Planning';
|
||||
import DateManager from '../../managers/DateManager';
|
||||
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
|
||||
|
@ -33,11 +33,15 @@ import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrol
|
|||
import type { PlanningEventType } from '../../utils/Planning';
|
||||
import ImageGalleryButton from '../../components/Media/ImageGalleryButton';
|
||||
import { API_REQUEST_CODES, REQUEST_STATUS } from '../../utils/Requests';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
route: { params: { data: PlanningEventType; id: number; eventId: number } };
|
||||
};
|
||||
type PropsType = StackScreenProps<
|
||||
MainStackParamsList,
|
||||
MainRoutes.PlanningInformation
|
||||
>;
|
||||
|
||||
type StateType = {
|
||||
loading: boolean;
|
||||
|
@ -78,7 +82,7 @@ class PlanningDisplayScreen extends React.Component<PropsType, StateType> {
|
|||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
|
||||
if (props.route.params.data != null) {
|
||||
if (props.route.params.type === 'full') {
|
||||
this.displayData = props.route.params.data;
|
||||
this.eventId = this.displayData.id;
|
||||
this.shouldFetchData = false;
|
||||
|
|
|
@ -36,6 +36,7 @@ import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
|
|||
import MascotPopup from '../../components/Mascot/MascotPopup';
|
||||
import GENERAL_STYLES from '../../constants/Styles';
|
||||
import Urls from '../../constants/Urls';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
LocaleConfig.locales.fr = {
|
||||
monthNames: [
|
||||
|
@ -76,6 +77,7 @@ LocaleConfig.locales.fr = {
|
|||
'Samedi',
|
||||
],
|
||||
dayNamesShort: ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'],
|
||||
today: "Aujourd'hui",
|
||||
};
|
||||
|
||||
type PropsType = {
|
||||
|
@ -216,7 +218,7 @@ class PlanningScreen extends React.Component<PropsType, StateType> {
|
|||
getRenderItem = (item: PlanningEventType) => {
|
||||
const { navigation } = this.props;
|
||||
const onPress = () => {
|
||||
navigation.navigate('planning-information', {
|
||||
navigation.navigate(MainRoutes.PlanningInformation, {
|
||||
data: item,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import React, { useLayoutEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Linking,
|
||||
SectionListData,
|
||||
SectionListRenderItemInfo,
|
||||
StyleSheet,
|
||||
|
@ -51,7 +52,7 @@ import GENERAL_STYLES from '../../constants/Styles';
|
|||
import { readData } from '../../utils/WebData';
|
||||
import { useNavigation } from '@react-navigation/core';
|
||||
import { setupMachineNotification } from '../../utils/Notifications';
|
||||
import ProximoListHeader from '../../components/Lists/Proximo/ProximoListHeader';
|
||||
import ProxiwashListHeader from '../../components/Lists/Proxiwash/ProxiwashListHeader';
|
||||
import {
|
||||
getPreferenceNumber,
|
||||
getPreferenceObject,
|
||||
|
@ -60,6 +61,7 @@ import {
|
|||
} from '../../utils/asyncStorage';
|
||||
import { useProxiwashPreferences } from '../../context/preferencesContext';
|
||||
import { useSubsequentEffect } from '../../utils/customHooks';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
const REFRESH_TIME = 1000 * 10; // Refresh every 10 seconds
|
||||
const LIST_ITEM_HEIGHT = 64;
|
||||
|
@ -75,7 +77,13 @@ export type ProxiwashMachineType = {
|
|||
program: string;
|
||||
};
|
||||
|
||||
export type ProxiwashInfoType = {
|
||||
message: string;
|
||||
last_checked: number;
|
||||
};
|
||||
|
||||
type FetchedDataType = {
|
||||
info: ProxiwashInfoType;
|
||||
dryers: Array<ProxiwashMachineType>;
|
||||
washers: Array<ProxiwashMachineType>;
|
||||
};
|
||||
|
@ -99,10 +107,8 @@ function ProxiwashScreen() {
|
|||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
const { preferences, updatePreferences } = useProxiwashPreferences();
|
||||
const [
|
||||
modalCurrentDisplayItem,
|
||||
setModalCurrentDisplayItem,
|
||||
] = useState<React.ReactElement | null>(null);
|
||||
const [modalCurrentDisplayItem, setModalCurrentDisplayItem] =
|
||||
useState<React.ReactElement | null>(null);
|
||||
const reminder = getPreferenceNumber(
|
||||
ProxiwashPreferenceKeys.proxiwashNotifications,
|
||||
preferences
|
||||
|
@ -155,15 +161,22 @@ function ProxiwashScreen() {
|
|||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title={'web'}
|
||||
iconName={'open-in-new'}
|
||||
onPress={() =>
|
||||
Linking.openURL(ProxiwashConstants[selectedWash].webPageUrl)
|
||||
}
|
||||
/>
|
||||
<Item
|
||||
title={'information'}
|
||||
iconName={'information'}
|
||||
onPress={() => navigation.navigate('proxiwash-about')}
|
||||
onPress={() => navigation.navigate(MainRoutes.ProxiwashAbout)}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
),
|
||||
});
|
||||
}, [navigation]);
|
||||
}, [navigation, selectedWash]);
|
||||
|
||||
/**
|
||||
* Callback used when the user clicks on enable notifications for a machine
|
||||
|
@ -438,7 +451,11 @@ function ProxiwashScreen() {
|
|||
) => {
|
||||
if (data) {
|
||||
return (
|
||||
<ProximoListHeader date={lastRefreshDate} selectedWash={selectedWash} />
|
||||
<ProxiwashListHeader
|
||||
date={lastRefreshDate}
|
||||
selectedWash={selectedWash}
|
||||
info={data?.info}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
|
@ -478,7 +495,7 @@ function ProxiwashScreen() {
|
|||
action: {
|
||||
message: i18n.t('screens.proxiwash.mascotDialog.ok'),
|
||||
icon: 'cog',
|
||||
onPress: () => navigation.navigate('settings'),
|
||||
onPress: () => navigation.navigate(MainRoutes.Settings),
|
||||
},
|
||||
cancel: {
|
||||
message: i18n.t('screens.proxiwash.mascotDialog.cancel'),
|
||||
|
|
|
@ -120,6 +120,7 @@ function ProximoListScreen(props: Props) {
|
|||
const theme = useTheme();
|
||||
const { articles, setArticles } = useCachedProximoArticles();
|
||||
const modalRef = useRef<Modalize>(null);
|
||||
const navParams = props.route.params;
|
||||
|
||||
const [currentSearchString, setCurrentSearchString] = useState('');
|
||||
const [currentSortMode, setCurrentSortMode] = useState(2);
|
||||
|
@ -130,6 +131,70 @@ function ProximoListScreen(props: Props) {
|
|||
const sortModes = [sortPrice, sortPriceReverse, sortName, sortNameReverse];
|
||||
|
||||
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({
|
||||
headerRight: getSortMenuButton,
|
||||
headerTitle: getSearchBar,
|
||||
|
@ -137,21 +202,9 @@ function ProximoListScreen(props: Props) {
|
|||
headerTitleContainerStyle:
|
||||
Platform.OS === 'ios'
|
||||
? { marginHorizontal: 0, width: '70%' }
|
||||
: { marginHorizontal: 0, right: 50, left: 50 },
|
||||
: { width: '100%' },
|
||||
});
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
}, [navigation, currentSortMode, navParams.shouldFocusSearchBar]);
|
||||
|
||||
/**
|
||||
* Callback used when clicking an article in the list.
|
||||
|
@ -166,19 +219,6 @@ function ProximoListScreen(props: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the current sort mode.
|
||||
*
|
||||
* @param mode The number representing the mode
|
||||
*/
|
||||
const setSortMode = (mode: string) => {
|
||||
const currentMode = parseInt(mode, 10);
|
||||
setCurrentSortMode(currentMode);
|
||||
if (modalRef.current && currentMode !== currentSortMode) {
|
||||
modalRef.current.close();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a color depending on the quantity available
|
||||
*
|
||||
|
@ -197,35 +237,6 @@ function ProximoListScreen(props: Props) {
|
|||
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
|
||||
*
|
||||
|
@ -262,42 +273,6 @@ function ProximoListScreen(props: Props) {
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the modal content to display a sort menu
|
||||
*
|
||||
* @return {*}
|
||||
*/
|
||||
const getModalSortMenu = () => {
|
||||
return (
|
||||
<View style={styles.modalContainer}>
|
||||
<Title style={styles.sortTitle}>
|
||||
{i18n.t('screens.proximo.sortOrder')}
|
||||
</Title>
|
||||
<RadioButton.Group
|
||||
onValueChange={setSortMode}
|
||||
value={currentSortMode.toString()}
|
||||
>
|
||||
<RadioButton.Item
|
||||
label={i18n.t('screens.proximo.sortPrice')}
|
||||
value={'0'}
|
||||
/>
|
||||
<RadioButton.Item
|
||||
label={i18n.t('screens.proximo.sortPriceReverse')}
|
||||
value={'1'}
|
||||
/>
|
||||
<RadioButton.Item
|
||||
label={i18n.t('screens.proximo.sortName')}
|
||||
value={'2'}
|
||||
/>
|
||||
<RadioButton.Item
|
||||
label={i18n.t('screens.proximo.sortNameReverse')}
|
||||
value={'3'}
|
||||
/>
|
||||
</RadioButton.Group>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a render item for the given article
|
||||
*
|
||||
|
@ -341,8 +316,8 @@ function ProximoListScreen(props: Props) {
|
|||
data: data
|
||||
.filter(
|
||||
(d) =>
|
||||
props.route.params.category === -1 ||
|
||||
props.route.params.category === d.category_id
|
||||
navParams.category === -1 ||
|
||||
navParams.category === d.category_id
|
||||
)
|
||||
.sort(sortModes[currentSortMode]),
|
||||
keyExtractor: keyExtractor,
|
||||
|
|
|
@ -32,6 +32,7 @@ import { useNavigation } from '@react-navigation/core';
|
|||
import { useLayoutEffect } from 'react';
|
||||
import { useCachedProximoCategories } from '../../../context/cacheContext';
|
||||
import GENERAL_STYLES from '../../../constants/Styles';
|
||||
import { MainRoutes } from '../../../navigation/MainNavigator';
|
||||
|
||||
const LIST_ITEM_HEIGHT = 84;
|
||||
|
||||
|
@ -122,10 +123,10 @@ function ProximoMainScreen() {
|
|||
shouldFocusSearchBar: true,
|
||||
category: -1,
|
||||
};
|
||||
navigation.navigate('proximo-list', searchScreenData);
|
||||
navigation.navigate(MainRoutes.ProximoList, searchScreenData);
|
||||
};
|
||||
|
||||
const onPressAboutBtn = () => navigation.navigate('proximo-about');
|
||||
const onPressAboutBtn = () => navigation.navigate(MainRoutes.ProximoAbout);
|
||||
|
||||
const getHeaderButtons = () => {
|
||||
return (
|
||||
|
@ -170,7 +171,8 @@ function ProximoMainScreen() {
|
|||
? i18n.t('screens.proximo.articles')
|
||||
: i18n.t('screens.proximo.article')
|
||||
}`;
|
||||
const onPress = () => navigation.navigate('proximo-list', dataToSend);
|
||||
const onPress = () =>
|
||||
navigation.navigate(MainRoutes.ProximoList, dataToSend);
|
||||
if (article_number > 0) {
|
||||
return (
|
||||
<List.Item
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
import { Image, StyleSheet, View } from 'react-native';
|
||||
import {
|
||||
Avatar,
|
||||
|
@ -25,10 +25,9 @@ import {
|
|||
Divider,
|
||||
List,
|
||||
TouchableRipple,
|
||||
withTheme,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import i18n from 'i18n-js';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import CardList from '../../components/Lists/CardList/CardList';
|
||||
import MaterialHeaderButtons, {
|
||||
Item,
|
||||
|
@ -41,11 +40,9 @@ import {
|
|||
ServiceCategoryType,
|
||||
SERVICES_CATEGORIES_KEY,
|
||||
} from '../../utils/Services';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
theme: ReactNativePaper.Theme;
|
||||
};
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useLoginState } from '../../context/loginContext';
|
||||
import { MainRoutes } from '../../navigation/MainNavigator';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -61,37 +58,30 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
class ServicesScreen extends React.Component<PropsType> {
|
||||
finalDataset: Array<ServiceCategoryType>;
|
||||
function ServicesScreen() {
|
||||
const navigation = useNavigation();
|
||||
const theme = useTheme();
|
||||
const isLoggedIn = useLoginState();
|
||||
|
||||
constructor(props: PropsType) {
|
||||
super(props);
|
||||
this.finalDataset = getCategories(props.navigation.navigate, [
|
||||
//@ts-ignore
|
||||
const finalDataset = getCategories(navigation.navigate, isLoggedIn, [
|
||||
SERVICES_CATEGORIES_KEY.SPECIAL,
|
||||
]);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { props } = this;
|
||||
props.navigation.setOptions({
|
||||
headerRight: this.getAboutButton,
|
||||
});
|
||||
}
|
||||
|
||||
getAboutButton = () => (
|
||||
useLayoutEffect(() => {
|
||||
const getAboutButton = () => (
|
||||
<MaterialHeaderButtons>
|
||||
<Item
|
||||
title="information"
|
||||
iconName="information"
|
||||
onPress={this.onAboutPress}
|
||||
onPress={() => navigation.navigate(MainRoutes.AmicaleContact)}
|
||||
/>
|
||||
</MaterialHeaderButtons>
|
||||
);
|
||||
|
||||
onAboutPress = () => {
|
||||
const { props } = this;
|
||||
props.navigation.navigate('amicale-contact');
|
||||
};
|
||||
navigation.setOptions({
|
||||
headerRight: getAboutButton,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* Gets the list title image for the list.
|
||||
|
@ -102,8 +92,7 @@ class ServicesScreen extends React.Component<PropsType> {
|
|||
* @param source The source image to display. Can be a string for icons or a number for local images
|
||||
* @returns {*}
|
||||
*/
|
||||
getListTitleImage(source: string | number) {
|
||||
const { props } = this;
|
||||
const getListTitleImage = (source: string | number) => {
|
||||
if (typeof source === 'number') {
|
||||
return <Image source={source} style={styles.image} />;
|
||||
}
|
||||
|
@ -111,11 +100,11 @@ class ServicesScreen extends React.Component<PropsType> {
|
|||
<Avatar.Icon
|
||||
size={48}
|
||||
icon={source}
|
||||
color={props.theme.colors.primary}
|
||||
color={theme.colors.primary}
|
||||
style={styles.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A list item showing a list of available services for the current category
|
||||
|
@ -123,20 +112,19 @@ class ServicesScreen extends React.Component<PropsType> {
|
|||
* @param item
|
||||
* @returns {*}
|
||||
*/
|
||||
getRenderItem = ({ item }: { item: ServiceCategoryType }) => {
|
||||
const { props } = this;
|
||||
const getRenderItem = ({ item }: { item: ServiceCategoryType }) => {
|
||||
return (
|
||||
<TouchableRipple
|
||||
style={styles.container}
|
||||
onPress={() => {
|
||||
props.navigation.navigate('services-section', { data: item });
|
||||
}}
|
||||
onPress={() =>
|
||||
navigation.navigate(MainRoutes.ServicesSection, { data: item })
|
||||
}
|
||||
>
|
||||
<View>
|
||||
<Card.Title
|
||||
title={item.title}
|
||||
subtitle={item.subtitle}
|
||||
left={() => this.getListTitleImage(item.image)}
|
||||
left={() => getListTitleImage(item.image)}
|
||||
right={() => <List.Icon icon="chevron-right" />}
|
||||
/>
|
||||
<CardList dataset={item.content} isHorizontal />
|
||||
|
@ -145,15 +133,14 @@ class ServicesScreen extends React.Component<PropsType> {
|
|||
);
|
||||
};
|
||||
|
||||
keyExtractor = (item: ServiceCategoryType): string => item.title;
|
||||
const keyExtractor = (item: ServiceCategoryType): string => item.title;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View>
|
||||
<CollapsibleFlatList
|
||||
data={this.finalDataset}
|
||||
renderItem={this.getRenderItem}
|
||||
keyExtractor={this.keyExtractor}
|
||||
data={finalDataset}
|
||||
renderItem={getRenderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
ItemSeparatorComponent={() => <Divider />}
|
||||
hasTab
|
||||
/>
|
||||
|
@ -171,7 +158,6 @@ class ServicesScreen extends React.Component<PropsType> {
|
|||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withTheme(ServicesScreen);
|
||||
export default ServicesScreen;
|
||||
|
|
|
@ -18,17 +18,19 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { Collapsible } from 'react-navigation-collapsible';
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { StackScreenProps } from '@react-navigation/stack';
|
||||
import CardList from '../../components/Lists/CardList/CardList';
|
||||
import { ServiceCategoryType } from '../../utils/Services';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../navigation/MainNavigator';
|
||||
|
||||
type PropsType = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
route: { params: { data: ServiceCategoryType | null } };
|
||||
collapsibleStack: Collapsible;
|
||||
};
|
||||
type PropsType = StackScreenProps<
|
||||
MainStackParamsList,
|
||||
MainRoutes.ServicesSection
|
||||
>;
|
||||
|
||||
class ServicesSectionScreen extends React.Component<PropsType> {
|
||||
finalDataset: null | ServiceCategoryType;
|
||||
|
|
|
@ -18,15 +18,16 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { StackScreenProps } from '@react-navigation/stack';
|
||||
import WebViewScreen from '../../components/Screens/WebViewScreen';
|
||||
import BasicLoadingScreen from '../../components/Screens/BasicLoadingScreen';
|
||||
import Urls from '../../constants/Urls';
|
||||
import {
|
||||
MainRoutes,
|
||||
MainStackParamsList,
|
||||
} from '../../navigation/MainNavigator';
|
||||
|
||||
type Props = {
|
||||
navigation: StackNavigationProp<any>;
|
||||
route: { params: { host: string; path: string | null; title: string } };
|
||||
};
|
||||
type Props = StackScreenProps<MainStackParamsList, MainRoutes.Website>;
|
||||
|
||||
type State = {
|
||||
url: string;
|
||||
|
|
|
@ -19,26 +19,53 @@
|
|||
|
||||
import i18n from 'i18n-js';
|
||||
import PushNotificationIOS from '@react-native-community/push-notification-ios';
|
||||
import PushNotification from 'react-native-push-notification';
|
||||
import PushNotification, {
|
||||
Importance,
|
||||
PushNotificationObject,
|
||||
} from 'react-native-push-notification';
|
||||
import { Platform } from 'react-native';
|
||||
import Update from '../constants/Update';
|
||||
|
||||
// Used to multiply the normal notification id to create the reminder one. It allows to find it back easily
|
||||
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
|
||||
*/
|
||||
function cleanChannels() {
|
||||
PushNotification.getChannels((idList) => {
|
||||
idList.forEach((i) => {
|
||||
if (i !== channelId) {
|
||||
PushNotification.deleteChannel(i);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function setupNotifications() {
|
||||
cleanChannels();
|
||||
PushNotification.channelExists(channelId, (exists) => {
|
||||
if (!exists) {
|
||||
PushNotification.createChannel(
|
||||
{
|
||||
channelId: 'reminders', // (required)
|
||||
channelName: 'Reminders', // (required)
|
||||
channelDescription: 'Get laundry reminders', // (optional) default: undefined.
|
||||
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: 4, // (optional) default: 4. Int value of the Android notification importance
|
||||
importance: Importance.HIGH, // (optional) default: Importance.HIGH. Int value of the Android notification importance
|
||||
vibrate: true, // (optional) default: true. Creates the default vibration patten if true.
|
||||
},
|
||||
(created) => console.log(`createChannel returned '${created}'`) // (optional) callback returns whether the channel was created, false means it already existed.
|
||||
);
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
PushNotification.configure({
|
||||
PushNotification.configure({
|
||||
// (required) Called when a remote is received or opened, or local notification is opened
|
||||
onNotification: function (notification) {
|
||||
console.log('NOTIFICATION:', notification);
|
||||
|
@ -68,7 +95,31 @@ PushNotification.configure({
|
|||
* requestPermissions: Platform.OS === 'ios'
|
||||
*/
|
||||
requestPermissions: Platform.OS === 'ios',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const DEFAULT_NOTIFICATIONS_OPTIONS: Partial<PushNotificationObject> = {
|
||||
/* Android Only Properties */
|
||||
channelId: channelId, // (required) channelId, if the channel doesn't exist, notification will not trigger.
|
||||
showWhen: true, // (optional) default: true
|
||||
autoCancel: true, // (optional) default: true
|
||||
vibrate: true, // (optional) default: true
|
||||
vibration: 300, // vibration length in milliseconds, ignored if vibrate=false, default: 1000
|
||||
priority: 'high', // (optional) set notification priority, default: high
|
||||
visibility: 'public', // (optional) set notification visibility, default: private
|
||||
ignoreInForeground: false, // (optional) if true, the notification will not be visible when the app is in the foreground (useful for parity with how iOS notifications appear). should be used in combine with `com.dieam.reactnativepushnotification.notification_foreground` setting
|
||||
onlyAlertOnce: false, // (optional) alert will open only once with sound and notify, default: false
|
||||
|
||||
when: null, // (optional) Add a timestamp (Unix timestamp value in milliseconds) pertaining to the notification (usually the time the event occurred). For apps targeting Build.VERSION_CODES.N and above, this time is not shown anymore by default and must be opted into by using `showWhen`, default: null.
|
||||
usesChronometer: false, // (optional) Show the `when` field as a stopwatch. Instead of presenting `when` as a timestamp, the notification will show an automatically updating display of the minutes and seconds since when. Useful when showing an elapsed time (like an ongoing phone call), default: false.
|
||||
timeoutAfter: null, // (optional) Specifies a duration in milliseconds after which this notification should be canceled, if it is not already canceled, default: null
|
||||
|
||||
invokeApp: true, // (optional) This enable click on actions to bring back the application to foreground or stay in background, default: true
|
||||
|
||||
/* iOS and Android properties */
|
||||
playSound: true, // (optional) default: true
|
||||
soundName: 'default', // (optional) Sound to play when the notification is shown. Value of 'default' plays the default sound. It can be set to a custom sound such as 'android.resource://com.xyz/raw/my_sound'. It will look for the 'my_sound' audio file in 'res/raw' directory and play it. default: 'default' (default sound is played)
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a notification for the given machine id at the given date.
|
||||
|
@ -84,6 +135,7 @@ function createNotifications(machineID: string, date: Date, reminder?: number) {
|
|||
const reminderDate = new Date(date);
|
||||
reminderDate.setMinutes(reminderDate.getMinutes() - reminder);
|
||||
PushNotification.localNotificationSchedule({
|
||||
...DEFAULT_NOTIFICATIONS_OPTIONS,
|
||||
title: i18n.t('screens.proxiwash.notifications.machineRunningTitle', {
|
||||
time: reminder,
|
||||
}),
|
||||
|
@ -96,6 +148,7 @@ function createNotifications(machineID: string, date: Date, reminder?: number) {
|
|||
}
|
||||
|
||||
PushNotification.localNotificationSchedule({
|
||||
...DEFAULT_NOTIFICATIONS_OPTIONS,
|
||||
title: i18n.t('screens.proxiwash.notifications.machineFinishedTitle'),
|
||||
message: i18n.t('screens.proxiwash.notifications.machineFinishedBody', {
|
||||
number: machineID,
|
||||
|
@ -124,8 +177,8 @@ export function setupMachineNotification(
|
|||
if (isEnabled && endDate) {
|
||||
createNotifications(machineID, endDate, reminder);
|
||||
} else {
|
||||
PushNotification.cancelLocalNotifications({ id: machineID });
|
||||
PushNotification.cancelLocalNotification(machineID);
|
||||
const reminderId = reminderIdFactor * parseInt(machineID, 10);
|
||||
PushNotification.cancelLocalNotifications({ id: reminderId.toString() });
|
||||
PushNotification.cancelLocalNotification(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
|
||||
* @return {Object}
|
||||
*/
|
||||
export function generateEmptyCalendar(
|
||||
numberOfMonths: number
|
||||
): { [key: string]: Array<PlanningEventType> } {
|
||||
export function generateEmptyCalendar(numberOfMonths: number): {
|
||||
[key: string]: Array<PlanningEventType>;
|
||||
} {
|
||||
const end = new Date(Date.now());
|
||||
end.setMonth(end.getMonth() + numberOfMonths);
|
||||
const daysOfYear: { [key: string]: Array<PlanningEventType> } = {};
|
||||
|
|
|
@ -86,8 +86,21 @@ export type ServiceCategoryType = {
|
|||
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(
|
||||
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||
onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
|
||||
isLoggedIn: boolean,
|
||||
excludedItems?: Array<string>
|
||||
): Array<ServiceItemType> {
|
||||
const amicaleDataset = [
|
||||
|
@ -96,21 +109,21 @@ export function getAmicaleServices(
|
|||
title: i18n.t('screens.clubs.title'),
|
||||
subtitle: i18n.t('screens.services.descriptions.clubs'),
|
||||
image: Urls.images.clubs,
|
||||
onPress: () => onPress(MainRoutes.ClubList),
|
||||
onPress: getAmicaleOnPress(MainRoutes.ClubList, onPress, isLoggedIn),
|
||||
},
|
||||
{
|
||||
key: SERVICES_KEY.PROFILE,
|
||||
title: i18n.t('screens.profile.title'),
|
||||
subtitle: i18n.t('screens.services.descriptions.profile'),
|
||||
image: Urls.images.profile,
|
||||
onPress: () => onPress(MainRoutes.Profile),
|
||||
onPress: getAmicaleOnPress(MainRoutes.Profile, onPress, isLoggedIn),
|
||||
},
|
||||
{
|
||||
key: SERVICES_KEY.EQUIPMENT,
|
||||
title: i18n.t('screens.equipment.title'),
|
||||
subtitle: i18n.t('screens.services.descriptions.equipment'),
|
||||
image: Urls.images.equipment,
|
||||
onPress: () => onPress(MainRoutes.EquipmentList),
|
||||
onPress: getAmicaleOnPress(MainRoutes.EquipmentList, onPress, isLoggedIn),
|
||||
},
|
||||
{
|
||||
key: SERVICES_KEY.AMICALE_WEBSITE,
|
||||
|
@ -135,7 +148,7 @@ export function getAmicaleServices(
|
|||
}
|
||||
|
||||
export function getStudentServices(
|
||||
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||
onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
|
||||
excludedItems?: Array<string>
|
||||
): Array<ServiceItemType> {
|
||||
const studentsDataset = [
|
||||
|
@ -188,7 +201,7 @@ export function getStudentServices(
|
|||
}
|
||||
|
||||
export function getINSAServices(
|
||||
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||
onPress: (route: MainRoutes, params?: { [key: string]: any }) => void,
|
||||
excludedItems?: Array<string>
|
||||
): Array<ServiceItemType> {
|
||||
const insaDataset = [
|
||||
|
@ -261,7 +274,10 @@ export function getINSAServices(
|
|||
}
|
||||
|
||||
export function getSpecialServices(
|
||||
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||
onPress: (
|
||||
route: MainRoutes | TabRoutes,
|
||||
params?: { [key: string]: any }
|
||||
) => void,
|
||||
excludedItems?: Array<string>
|
||||
): Array<ServiceItemType> {
|
||||
const specialDataset = [
|
||||
|
@ -288,7 +304,11 @@ export function getSpecialServices(
|
|||
}
|
||||
|
||||
export function getCategories(
|
||||
onPress: (route: string, params?: { [key: string]: any }) => void,
|
||||
onPress: (
|
||||
route: MainRoutes | TabRoutes,
|
||||
params?: { [key: string]: any }
|
||||
) => void,
|
||||
isLoggedIn: boolean,
|
||||
excludedItems?: Array<string>
|
||||
): Array<ServiceCategoryType> {
|
||||
const categoriesDataset = [
|
||||
|
@ -297,7 +317,7 @@ export function getCategories(
|
|||
title: i18n.t('screens.services.categories.amicale'),
|
||||
subtitle: i18n.t('screens.services.more'),
|
||||
image: AMICALE_LOGO,
|
||||
content: getAmicaleServices(onPress),
|
||||
content: getAmicaleServices(onPress, isLoggedIn),
|
||||
},
|
||||
{
|
||||
key: SERVICES_CATEGORIES_KEY.STUDENTS,
|
||||
|
|
|
@ -101,13 +101,13 @@ export const CustomWhiteTheme: ReactNativePaper.Theme = {
|
|||
// Tetris
|
||||
tetrisBackground: '#f0f0f0',
|
||||
tetrisScore: '#e2bd33',
|
||||
tetrisI: '#3cd9e6',
|
||||
tetrisO: '#ffdd00',
|
||||
tetrisT: '#a716e5',
|
||||
tetrisS: '#09c528',
|
||||
tetrisZ: '#ff0009',
|
||||
tetrisJ: '#2a67e3',
|
||||
tetrisL: '#da742d',
|
||||
tetrisI: '#be1522',
|
||||
tetrisO: '#EB6C1F',
|
||||
tetrisT: '#5cb85c',
|
||||
tetrisS: '#5294E2',
|
||||
tetrisZ: '#dede00',
|
||||
tetrisJ: '#69009d',
|
||||
tetrisL: '#553716',
|
||||
|
||||
gameGold: '#ffd610',
|
||||
gameSilver: '#7b7b7b',
|
||||
|
@ -160,13 +160,13 @@ export const CustomDarkTheme: ReactNativePaper.Theme = {
|
|||
// Tetris
|
||||
tetrisBackground: '#181818',
|
||||
tetrisScore: '#e2d707',
|
||||
tetrisI: '#30b3be',
|
||||
tetrisO: '#c1a700',
|
||||
tetrisT: '#9114c7',
|
||||
tetrisS: '#08a121',
|
||||
tetrisZ: '#b50008',
|
||||
tetrisJ: '#0f37b9',
|
||||
tetrisL: '#b96226',
|
||||
tetrisI: '#be1522',
|
||||
tetrisO: '#EB6C1F',
|
||||
tetrisT: '#5cb85c',
|
||||
tetrisS: '#5294E2',
|
||||
tetrisZ: '#dede00',
|
||||
tetrisJ: '#69009d',
|
||||
tetrisL: '#553716',
|
||||
|
||||
gameGold: '#ffd610',
|
||||
gameSilver: '#7b7b7b',
|
||||
|
|
|
@ -18,10 +18,15 @@
|
|||
*/
|
||||
|
||||
import { Linking } from 'react-native';
|
||||
import {
|
||||
ClubInformationScreenParams,
|
||||
MainRoutes,
|
||||
PlanningInformationScreenParams,
|
||||
} from '../navigation/MainNavigator';
|
||||
|
||||
export type ParsedUrlDataType = {
|
||||
route: string;
|
||||
data: { [key: string]: string };
|
||||
route: MainRoutes.ClubInformation | MainRoutes.PlanningInformation;
|
||||
data: ClubInformationScreenParams | PlanningInformationScreenParams;
|
||||
};
|
||||
|
||||
export type ParsedUrlCallbackType = (parsedData: ParsedUrlDataType) => void;
|
||||
|
@ -41,10 +46,6 @@ export default class URLHandler {
|
|||
|
||||
static EVENT_INFO_URL_PATH = 'event';
|
||||
|
||||
static CLUB_INFO_ROUTE = 'club-information';
|
||||
|
||||
static EVENT_INFO_ROUTE = 'planning-information';
|
||||
|
||||
onInitialURLParsed: ParsedUrlCallbackType;
|
||||
|
||||
onDetectURL: ParsedUrlCallbackType;
|
||||
|
@ -152,8 +153,11 @@ export default class URLHandler {
|
|||
const id = parseInt(params.id, 10);
|
||||
if (!Number.isNaN(id)) {
|
||||
return {
|
||||
route: URLHandler.CLUB_INFO_ROUTE,
|
||||
data: { clubId: id.toString() },
|
||||
route: MainRoutes.ClubInformation,
|
||||
data: {
|
||||
type: 'id',
|
||||
clubId: id,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -173,8 +177,11 @@ export default class URLHandler {
|
|||
const id = parseInt(params.id, 10);
|
||||
if (!Number.isNaN(id)) {
|
||||
return {
|
||||
route: URLHandler.EVENT_INFO_ROUTE,
|
||||
data: { eventId: id.toString() },
|
||||
route: MainRoutes.PlanningInformation,
|
||||
data: {
|
||||
type: 'id',
|
||||
eventId: id,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,7 +80,8 @@ export function isApiResponseValid<T>(response: ApiResponseType<T>): boolean {
|
|||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
method: string,
|
||||
params?: object
|
||||
params?: object,
|
||||
token?: string
|
||||
): Promise<T> {
|
||||
return new Promise(
|
||||
(resolve: (data: T) => void, reject: (error: ApiRejectType) => void) => {
|
||||
|
@ -88,7 +89,9 @@ export async function apiRequest<T>(
|
|||
if (params != null) {
|
||||
requestParams = { ...params };
|
||||
}
|
||||
console.log(Urls.amicale.api + path);
|
||||
if (token) {
|
||||
requestParams = { ...requestParams, token: token };
|
||||
}
|
||||
|
||||
fetch(Urls.amicale.api + path, {
|
||||
method,
|
||||
|
@ -101,11 +104,9 @@ export async function apiRequest<T>(
|
|||
.then((response: Response) => {
|
||||
const status = response.status;
|
||||
if (status === REQUEST_STATUS.SUCCESS) {
|
||||
return response.json().then(
|
||||
(data): ApiResponseType<T> => {
|
||||
return response.json().then((data): ApiResponseType<T> => {
|
||||
return { status: status, error: data.error, data: data.data };
|
||||
}
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return { status: status };
|
||||
}
|
||||
|
@ -135,6 +136,33 @@ export async function apiRequest<T>(
|
|||
);
|
||||
}
|
||||
|
||||
export async function connectToAmicale(email: string, password: string) {
|
||||
return new Promise(
|
||||
(
|
||||
resolve: (token: string) => void,
|
||||
reject: (error: ApiRejectType) => void
|
||||
) => {
|
||||
const data = {
|
||||
email,
|
||||
password,
|
||||
};
|
||||
apiRequest<ApiDataLoginType>('password', 'POST', data)
|
||||
.then((response: ApiDataLoginType) => {
|
||||
if (response.token != null) {
|
||||
resolve(response.token);
|
||||
} else {
|
||||
reject({
|
||||
status: REQUEST_STATUS.SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads data from the given url and returns it.
|
||||
*
|
||||
|
|
|
@ -110,13 +110,17 @@ export const defaultPreferences: { [key in GeneralPreferenceKeys]: string } = {
|
|||
export function isValidGeneralPreferenceKey(
|
||||
key: string
|
||||
): key is GeneralPreferenceKeys {
|
||||
return key in Object.values(GeneralPreferenceKeys);
|
||||
return Object.values(GeneralPreferenceKeys).includes(
|
||||
key as GeneralPreferenceKeys
|
||||
);
|
||||
}
|
||||
|
||||
export function isValidMascotPreferenceKey(
|
||||
key: string
|
||||
): key is MascotPreferenceKeys {
|
||||
return key in Object.values(MascotPreferenceKeys);
|
||||
return Object.values(MascotPreferenceKeys).includes(
|
||||
key as MascotPreferenceKeys
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
42
src/utils/loginToken.ts
Normal file
42
src/utils/loginToken.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import * as Keychain from 'react-native-keychain';
|
||||
|
||||
/**
|
||||
* Tries to recover login token from the secure keychain
|
||||
*
|
||||
* @returns Promise<string | undefined>
|
||||
*/
|
||||
export async function retrieveLoginToken(): Promise<string | undefined> {
|
||||
return new Promise((resolve: (token: string | undefined) => void) => {
|
||||
Keychain.getGenericPassword()
|
||||
.then((data: Keychain.UserCredentials | false) => {
|
||||
if (data && data.password) {
|
||||
resolve(data.password);
|
||||
} else {
|
||||
resolve(undefined);
|
||||
}
|
||||
})
|
||||
.catch(() => resolve(undefined));
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Saves the login token in the secure keychain
|
||||
*
|
||||
* @param token
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function saveLoginToken(token: string): Promise<void> {
|
||||
return new Promise((resolve: () => void, reject: () => void) => {
|
||||
Keychain.setGenericPassword('amicale', token).then(resolve).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the login token from the keychain
|
||||
*
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function deleteLoginToken(): Promise<void> {
|
||||
return new Promise((resolve: () => void, reject: () => void) => {
|
||||
Keychain.resetGenericPassword().then(resolve).catch(reject);
|
||||
});
|
||||
}
|
13
src/utils/logout.ts
Normal file
13
src/utils/logout.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useLogin } from '../context/loginContext';
|
||||
import { deleteLoginToken } from './loginToken';
|
||||
|
||||
export const useLogout = () => {
|
||||
const { setLogin } = useLogin();
|
||||
|
||||
const onLogout = useCallback(() => {
|
||||
deleteLoginToken();
|
||||
setLogin(undefined);
|
||||
}, [setLogin]);
|
||||
return onLogout;
|
||||
};
|
|
@ -30,17 +30,19 @@
|
|||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "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'. */
|
||||
// "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. */
|
||||
"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'. */
|
||||
|
@ -51,12 +53,11 @@
|
|||
// "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. */
|
||||
// "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 */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
|
||||
|
|
Loading…
Reference in a new issue