Improved doc and typing and removed unused file

This commit is contained in:
Arnaud Vergnet 2020-06-29 15:09:33 +02:00
parent 869a8e5ec0
commit b66e50eaf8
15 changed files with 427 additions and 228 deletions

View file

@ -4,6 +4,7 @@ import * as React from 'react';
import {FlatList} from "react-native"; import {FlatList} from "react-native";
import packageJson from '../../../package'; import packageJson from '../../../package';
import {List} from 'react-native-paper'; import {List} from 'react-native-paper';
import {StackNavigationProp} from "@react-navigation/stack";
type listItem = { type listItem = {
name: string, name: string,
@ -16,7 +17,7 @@ type listItem = {
* @param object The raw json * @param object The raw json
* @return {Array<listItem>} * @return {Array<listItem>}
*/ */
function generateListFromObject(object: { [string]: string }): Array<listItem> { function generateListFromObject(object: { [key: string]: string }): Array<listItem> {
let list = []; let list = [];
let keys = Object.keys(object); let keys = Object.keys(object);
let values = Object.values(object); let values = Object.values(object);
@ -28,8 +29,7 @@ function generateListFromObject(object: { [string]: string }): Array<listItem> {
} }
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
route: Object
} }
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -39,23 +39,23 @@ const LIST_ITEM_HEIGHT = 64;
*/ */
export default class AboutDependenciesScreen extends React.Component<Props> { export default class AboutDependenciesScreen extends React.Component<Props> {
data: Array<Object>; data: Array<listItem>;
constructor() { constructor() {
super(); super();
this.data = generateListFromObject(packageJson.dependencies); this.data = generateListFromObject(packageJson.dependencies);
} }
keyExtractor = (item: Object) => item.name; keyExtractor = (item: listItem) => item.name;
renderItem = ({item}: Object) => renderItem = ({item}: { item: listItem }) =>
<List.Item <List.Item
title={item.name} title={item.name}
description={item.version.replace('^', '').replace('~', '')} description={item.version.replace('^', '').replace('~', '')}
style={{height: LIST_ITEM_HEIGHT}} style={{height: LIST_ITEM_HEIGHT}}
/>; />;
itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index}); itemLayout = (data: any, index: number) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
render() { render() {
return ( return (

View file

@ -5,6 +5,14 @@ import {FlatList, Linking, Platform, View} from 'react-native';
import i18n from "i18n-js"; import i18n from "i18n-js";
import {Avatar, Card, List, Title, withTheme} from 'react-native-paper'; import {Avatar, Card, List, Title, withTheme} from 'react-native-paper';
import packageJson from "../../../package.json"; import packageJson from "../../../package.json";
import {StackNavigationProp} from "@react-navigation/stack";
type ListItem = {
onPressCallback: () => void,
icon: string,
text: string,
showChevron: boolean
};
const links = { const links = {
appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148', appstore: 'https://apps.apple.com/us/app/campus-amicale-insat/id1477722148',
@ -29,7 +37,7 @@ const links = {
}; };
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
}; };
/** /**
@ -48,7 +56,7 @@ class AboutScreen extends React.Component<Props> {
/** /**
* Data to be displayed in the app card * Data to be displayed in the app card
*/ */
appData: Array<Object> = [ appData = [
{ {
onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore), onPressCallback: () => openWebLink(Platform.OS === "ios" ? links.appstore : links.playstore),
icon: Platform.OS === "ios" ? 'apple' : 'google-play', icon: Platform.OS === "ios" ? 'apple' : 'google-play',
@ -83,7 +91,7 @@ class AboutScreen extends React.Component<Props> {
/** /**
* Data to be displayed in the author card * Data to be displayed in the author card
*/ */
authorData: Array<Object> = [ authorData = [
{ {
onPressCallback: () => openWebLink(links.meme), onPressCallback: () => openWebLink(links.meme),
icon: 'account-circle', icon: 'account-circle',
@ -106,7 +114,7 @@ class AboutScreen extends React.Component<Props> {
/** /**
* Data to be displayed in the additional developer card * Data to be displayed in the additional developer card
*/ */
additionalDevData: Array<Object> = [ additionalDevData = [
{ {
onPressCallback: () => console.log('Meme this'), onPressCallback: () => console.log('Meme this'),
icon: 'account', icon: 'account',
@ -129,7 +137,7 @@ class AboutScreen extends React.Component<Props> {
/** /**
* Data to be displayed in the technologies card * Data to be displayed in the technologies card
*/ */
technoData: Array<Object> = [ technoData = [
{ {
onPressCallback: () => openWebLink(links.react), onPressCallback: () => openWebLink(links.react),
icon: 'react', icon: 'react',
@ -146,7 +154,7 @@ class AboutScreen extends React.Component<Props> {
/** /**
* Order of information cards * Order of information cards
*/ */
dataOrder: Array<Object> = [ dataOrder = [
{ {
id: 'app', id: 'app',
}, },
@ -158,16 +166,9 @@ class AboutScreen extends React.Component<Props> {
}, },
]; ];
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
/** /**
* Gets the app icon * Gets the app icon
*
* @param props * @param props
* @return {*} * @return {*}
*/ */
@ -187,7 +188,7 @@ class AboutScreen extends React.Component<Props> {
* @param item The item to extract the key from * @param item The item to extract the key from
* @return {string} The extracted key * @return {string} The extracted key
*/ */
keyExtractor(item: Object): string { keyExtractor(item: ListItem): string {
return item.icon; return item.icon;
} }
@ -271,7 +272,7 @@ class AboutScreen extends React.Component<Props> {
* @param props * @param props
* @return {*} * @return {*}
*/ */
getChevronIcon(props: Object) { getChevronIcon(props) {
return ( return (
<List.Icon {...props} icon={'chevron-right'}/> <List.Icon {...props} icon={'chevron-right'}/>
); );
@ -284,18 +285,18 @@ class AboutScreen extends React.Component<Props> {
* @param props * @param props
* @return {*} * @return {*}
*/ */
getItemIcon(item: Object, props: Object) { getItemIcon(item: ListItem, props) {
return ( return (
<List.Icon {...props} icon={item.icon}/> <List.Icon {...props} icon={item.icon}/>
); );
} }
/** /**
* Get a clickable card item to be rendered inside a card. * Gets a clickable card item to be rendered inside a card.
* *
* @returns {*} * @returns {*}
*/ */
getCardItem = ({item}: Object) => { getCardItem = ({item}: { item: ListItem }) => {
const getItemIcon = this.getItemIcon.bind(this, item); const getItemIcon = this.getItemIcon.bind(this, item);
if (item.showChevron) { if (item.showChevron) {
return ( return (
@ -323,7 +324,7 @@ class AboutScreen extends React.Component<Props> {
* @param item The item to show * @param item The item to show
* @return {*} * @return {*}
*/ */
getMainCard = ({item}: Object) => { getMainCard = ({item}: { item: { id: string } }) => {
switch (item.id) { switch (item.id) {
case 'app': case 'app':
return this.getAppCard(); return this.getAppCard();

View file

@ -5,14 +5,24 @@ import {FlatList, View} from "react-native";
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import CustomModal from "../../components/Overrides/CustomModal"; import CustomModal from "../../components/Overrides/CustomModal";
import {Button, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper'; import {Button, List, Subheading, TextInput, Title, withTheme} from 'react-native-paper';
import {StackNavigationProp} from "@react-navigation/stack";
import {Modalize} from "react-native-modalize";
import type {CustomTheme} from "../../managers/ThemeManager";
type PreferenceItem = {
key: string,
default: string,
current: string,
}
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
theme: CustomTheme
}; };
type State = { type State = {
modalCurrentDisplayItem: Object, modalCurrentDisplayItem: PreferenceItem,
currentPreferences: Array<Object>, currentPreferences: Array<PreferenceItem>,
} }
/** /**
@ -21,20 +31,20 @@ type State = {
*/ */
class DebugScreen extends React.Component<Props, State> { class DebugScreen extends React.Component<Props, State> {
modalRef: Object; modalRef: Modalize;
modalInputValue = ''; modalInputValue: string;
onModalRef: Function;
colors: Object;
/**
* Copies user preferences to state for easier manipulation
*
* @param props
*/
constructor(props) { constructor(props) {
super(props); super(props);
this.onModalRef = this.onModalRef.bind(this); this.modalInputValue = "";
this.colors = props.theme.colors;
let copy = {...AsyncStorageManager.getInstance().preferences}; let copy = {...AsyncStorageManager.getInstance().preferences};
let currentPreferences = []; let currentPreferences : Array<PreferenceItem> = [];
Object.values(copy).map((object) => { Object.values(copy).map((object: any) => {
currentPreferences.push(object); currentPreferences.push(object);
}); });
this.state = { this.state = {
@ -44,10 +54,11 @@ class DebugScreen extends React.Component<Props, State> {
} }
/** /**
* Show the edit modal * Shows the edit modal
*
* @param item * @param item
*/ */
showEditModal(item: Object) { showEditModal(item: PreferenceItem) {
this.setState({ this.setState({
modalCurrentDisplayItem: item modalCurrentDisplayItem: item
}); });
@ -81,14 +92,14 @@ class DebugScreen extends React.Component<Props, State> {
<Button <Button
mode="contained" mode="contained"
dark={true} dark={true}
color={this.colors.success} color={this.props.theme.colors.success}
onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.modalInputValue)}> onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.modalInputValue)}>
Save new value Save new value
</Button> </Button>
<Button <Button
mode="contained" mode="contained"
dark={true} dark={true}
color={this.colors.danger} color={this.props.theme.colors.danger}
onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.state.modalCurrentDisplayItem.default)}> onPress={() => this.saveNewPrefs(this.state.modalCurrentDisplayItem.key, this.state.modalCurrentDisplayItem.default)}>
Reset to default Reset to default
</Button> </Button>
@ -98,6 +109,12 @@ class DebugScreen extends React.Component<Props, State> {
); );
} }
/**
* Finds the index of the given key in the preferences array
*
* @param key THe key to find the index of
* @returns {number}
*/
findIndexOfKey(key: string) { findIndexOfKey(key: string) {
let index = -1; let index = -1;
for (let i = 0; i < this.state.currentPreferences.length; i++) { for (let i = 0; i < this.state.currentPreferences.length; i++) {
@ -130,11 +147,11 @@ class DebugScreen extends React.Component<Props, State> {
* *
* @param ref * @param ref
*/ */
onModalRef(ref: Object) { onModalRef = (ref: Modalize) => {
this.modalRef = ref; this.modalRef = ref;
} }
renderItem = ({item}: Object) => { renderItem = ({item}: {item: PreferenceItem}) => {
return ( return (
<List.Item <List.Item
title={item.key} title={item.key}

View file

@ -12,14 +12,18 @@ type Props = {
collapsibleStack: Collapsible collapsibleStack: Collapsible
}; };
type State = {}; type DatasetItem = {
name: string,
email: string,
icon: string,
}
/** /**
* Class defining a planning event information page. * Class defining a planning event information page.
*/ */
class AmicaleContactScreen extends React.Component<Props, State> { class AmicaleContactScreen extends React.Component<Props> {
// Dataset containing information about contacts
CONTACT_DATASET = [ CONTACT_DATASET = [
{ {
name: i18n.t("amicaleAbout.roles.interSchools"), name: i18n.t("amicaleAbout.roles.interSchools"),
@ -68,18 +72,11 @@ class AmicaleContactScreen extends React.Component<Props, State> {
}, },
]; ];
colors: Object; keyExtractor = (item: DatasetItem) => item.email;
constructor(props) { getChevronIcon = (props) => <List.Icon {...props} icon={'chevron-right'}/>;
super(props);
this.colors = props.theme.colors;
}
keyExtractor = (item: Object) => item.email; renderItem = ({item}: { item: DatasetItem }) => {
getChevronIcon = (props: Object) => <List.Icon {...props} icon={'chevron-right'}/>;
renderItem = ({item}: Object) => {
const onPress = () => Linking.openURL('mailto:' + item.email); const onPress = () => Linking.openURL('mailto:' + item.email);
return <List.Item return <List.Item
title={item.name} title={item.name}

View file

@ -1,85 +0,0 @@
// @flow
import * as React from 'react';
import {ScrollView, StyleSheet} from "react-native";
import {Button, withTheme} from 'react-native-paper';
type Props = {
navigation: Object,
route: Object,
}
type State = {}
class AmicaleHomeScreen extends React.Component<Props, State> {
state = {};
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
render() {
const nav = this.props.navigation;
return (
<ScrollView>
<Button
icon={"login"}
onPress={() => nav.navigate("login")}
>
LOGIN
</Button>
<Button
icon={"information"}
onPress={() => nav.navigate("amicale-contact")}
>
INFO
</Button>
<Button
icon={"information"}
onPress={() => nav.navigate("club-list")}
>
CLUBS
</Button>
<Button
icon={"information"}
onPress={() => nav.navigate("profile")}
>
PROFILE
</Button>
<Button
icon={"information"}
onPress={() => nav.navigate("vote")}
>
VOTE
</Button>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
card: {
margin: 10,
},
header: {
fontSize: 36,
marginBottom: 48
},
textInput: {},
btnContainer: {
marginTop: 5,
marginBottom: 10,
}
});
export default withTheme(AmicaleHomeScreen);

View file

@ -6,25 +6,11 @@ import {Card, List, Text, withTheme} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import Autolink from "react-native-autolink"; import Autolink from "react-native-autolink";
type Props = { type Props = {};
};
type State = {
};
const CONTACT_LINK = 'clubs@amicale-insat.fr'; const CONTACT_LINK = 'clubs@amicale-insat.fr';
/** class ClubAboutScreen extends React.Component<Props> {
* Class defining a planning event information page.
*/
class ClubAboutScreen extends React.Component<Props, State> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
render() { render() {
return ( return (

View file

@ -65,6 +65,12 @@ class ClubDisplayScreen extends React.Component<Props, State> {
} }
} }
/**
* Gets the name of the category with the given ID
*
* @param id The category's ID
* @returns {string|*}
*/
getCategoryName(id: number) { getCategoryName(id: number) {
if (this.categories !== null) { if (this.categories !== null) {
for (let i = 0; i < this.categories.length; i++) { for (let i = 0; i < this.categories.length; i++) {
@ -75,6 +81,12 @@ class ClubDisplayScreen extends React.Component<Props, State> {
return ""; return "";
} }
/**
* Gets the view for rendering categories
*
* @param categories The categories to display (max 2)
* @returns {null|*}
*/
getCategoriesRender(categories: [number, number]) { getCategoriesRender(categories: [number, number]) {
if (this.categories === null) if (this.categories === null)
return null; return null;
@ -95,12 +107,19 @@ class ClubDisplayScreen extends React.Component<Props, State> {
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>; return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
} }
getManagersRender(resp: Array<string>, email: string | null) { /**
let final = []; * Gets the view for rendering club managers if any
for (let i = 0; i < resp.length; i++) { *
final.push(<Paragraph key={i.toString()}>{resp[i]}</Paragraph>) * @param managers The list of manager names
* @param email The club contact email
* @returns {*}
*/
getManagersRender(managers: Array<string>, email: string | null) {
let managersListView = [];
for (let i = 0; i < managers.length; i++) {
managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
} }
const hasManagers = resp.length > 0; const hasManagers = managers.length > 0;
return ( return (
<Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> <Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title <Card.Title
@ -113,13 +132,20 @@ class ClubDisplayScreen extends React.Component<Props, State> {
icon="account-tie"/>} icon="account-tie"/>}
/> />
<Card.Content> <Card.Content>
{final} {managersListView}
{this.getEmailButton(email, hasManagers)} {this.getEmailButton(email, hasManagers)}
</Card.Content> </Card.Content>
</Card> </Card>
); );
} }
/**
* Gets the email button to contact the club, or the amicale if the club does not have any managers
*
* @param email The club contact email
* @param hasManagers True if the club has managers
* @returns {*}
*/
getEmailButton(email: string | null, hasManagers: boolean) { getEmailButton(email: string | null, hasManagers: boolean) {
const destinationEmail = email != null && hasManagers const destinationEmail = email != null && hasManagers
? email ? email
@ -141,13 +167,21 @@ class ClubDisplayScreen extends React.Component<Props, State> {
); );
} }
updateHeaderTitle(data: Object) { /**
* Updates the header title to match the given club
*
* @param data The club data
*/
updateHeaderTitle(data: club) {
this.props.navigation.setOptions({title: data.name}) this.props.navigation.setOptions({title: data.name})
} }
getScreen = (response: Array<Object>) => { getScreen = (response: Array<{ [key: string]: any } | null>) => {
let data: club = response[0]; let data: club | null = null;
this.updateHeaderTitle(data); if (response[0] != null) {
data = response[0];
this.updateHeaderTitle(data);
}
if (data != null) { if (data != null) {
return ( return (
<ScrollView style={{paddingLeft: 5, paddingRight: 5}}> <ScrollView style={{paddingLeft: 5, paddingRight: 5}}>
@ -184,7 +218,6 @@ class ClubDisplayScreen extends React.Component<Props, State> {
); );
} else } else
return null; return null;
}; };
render() { render() {

View file

@ -131,6 +131,15 @@ class ClubListScreen extends React.Component<Props, State> {
onChipSelect = (id: number) => this.updateFilteredData(null, id); onChipSelect = (id: number) => this.updateFilteredData(null, id);
/**
* Updates the search string and category filter, saving them to the State.
*
* If the given category is already in the filter, it removes it.
* Otherwise it adds it to the filter.
*
* @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) { updateFilteredData(filterStr: string | null, categoryId: number | null) {
let newCategoriesState = [...this.state.currentlySelectedCategories]; let newCategoriesState = [...this.state.currentlySelectedCategories];
let newStrState = this.state.currentSearchString; let newStrState = this.state.currentSearchString;
@ -150,6 +159,11 @@ class ClubListScreen extends React.Component<Props, State> {
}) })
} }
/**
* Gets the list header, with controls to change the categories filter
*
* @returns {*}
*/
getListHeader() { getListHeader() {
return <ClubListHeader return <ClubListHeader
categories={this.categories} categories={this.categories}
@ -158,6 +172,12 @@ class ClubListScreen extends React.Component<Props, State> {
/>; />;
} }
/**
* Gets the category object of the given ID
*
* @param id The ID of the category to find
* @returns {*}
*/
getCategoryOfId = (id: number) => { getCategoryOfId = (id: number) => {
for (let i = 0; i < this.categories.length; i++) { for (let i = 0; i < this.categories.length; i++) {
if (id === this.categories[i].id) if (id === this.categories[i].id)
@ -165,6 +185,12 @@ class ClubListScreen extends React.Component<Props, State> {
} }
}; };
/**
* Checks if the given item should be rendered according to current name and category filters
*
* @param item The club to check
* @returns {boolean}
*/
shouldRenderItem(item: club) { shouldRenderItem(item: club) {
let shouldRender = this.state.currentlySelectedCategories.length === 0 let shouldRender = this.state.currentlySelectedCategories.length === 0
|| isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category); || isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category);

View file

@ -11,10 +11,11 @@ import {Collapsible} from "react-navigation-collapsible";
import CustomTabBar from "../../components/Tabbar/CustomTabBar"; import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import type {CustomTheme} from "../../managers/ThemeManager"; import type {CustomTheme} from "../../managers/ThemeManager";
import AsyncStorageManager from "../../managers/AsyncStorageManager"; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import {StackNavigationProp} from "@react-navigation/stack";
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
route: Object, route: { params: { nextScreen: string } },
collapsibleStack: Collapsible, collapsibleStack: Collapsible,
theme: CustomTheme theme: CustomTheme
} }
@ -47,9 +48,9 @@ class LoginScreen extends React.Component<Props, State> {
dialogError: 0, dialogError: 0,
}; };
onEmailChange: Function; onEmailChange: (value: string) => null;
onPasswordChange: Function; onPasswordChange: (value: string) => null;
passwordInputRef: Object; passwordInputRef: { current: null | TextInput };
nextScreen: string | null; nextScreen: string | null;
@ -64,7 +65,10 @@ class LoginScreen extends React.Component<Props, State> {
this.handleNavigationParams(); this.handleNavigationParams();
}; };
handleNavigationParams () { /**
* Saves the screen to navigate to after a successful login if one was provided in navigation parameters
*/
handleNavigationParams() {
if (this.props.route.params != null) { if (this.props.route.params != null) {
if (this.props.route.params.nextScreen != null) if (this.props.route.params.nextScreen != null)
this.nextScreen = this.props.route.params.nextScreen; this.nextScreen = this.props.route.params.nextScreen;
@ -73,6 +77,11 @@ class LoginScreen extends React.Component<Props, State> {
} }
} }
/**
* Shows an error dialog with the corresponding login error
*
* @param error The error given by the login request
*/
showErrorDialog = (error: number) => showErrorDialog = (error: number) =>
this.setState({ this.setState({
dialogVisible: true, dialogVisible: true,
@ -81,6 +90,10 @@ class LoginScreen extends React.Component<Props, State> {
hideErrorDialog = () => this.setState({dialogVisible: false}); hideErrorDialog = () => this.setState({dialogVisible: false});
/**
* 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 = () => { handleSuccess = () => {
// Do not show the login banner again // Do not show the login banner again
AsyncStorageManager.getInstance().savePref( AsyncStorageManager.getInstance().savePref(
@ -93,32 +106,75 @@ class LoginScreen extends React.Component<Props, State> {
this.props.navigation.replace(this.nextScreen); this.props.navigation.replace(this.nextScreen);
}; };
/**
* Navigates to the Amicale website screen with the reset password link as navigation parameters
*/
onResetPasswordClick = () => this.props.navigation.navigate('amicale-website', {path: RESET_PASSWORD_PATH}); onResetPasswordClick = () => this.props.navigation.navigate('amicale-website', {path: RESET_PASSWORD_PATH});
/**
* The user has unfocused the input, his email is ready to be validated
*/
validateEmail = () => this.setState({isEmailValidated: true}); validateEmail = () => this.setState({isEmailValidated: true});
/**
* Checks if the entered email is valid (matches the regex)
*
* @returns {boolean}
*/
isEmailValid() { isEmailValid() {
return emailRegex.test(this.state.email); return emailRegex.test(this.state.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() { shouldShowEmailError() {
return this.state.isEmailValidated && !this.isEmailValid(); return this.state.isEmailValidated && !this.isEmailValid();
} }
/**
* The user has unfocused the input, his password is ready to be validated
*/
validatePassword = () => this.setState({isPasswordValidated: true}); validatePassword = () => this.setState({isPasswordValidated: true});
/**
* Checks if the user has entered a password
*
* @returns {boolean}
*/
isPasswordValid() { isPasswordValid() {
return this.state.password !== ''; return this.state.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() { shouldShowPasswordError() {
return this.state.isPasswordValidated && !this.isPasswordValid(); return this.state.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() { shouldEnableLogin() {
return this.isEmailValid() && this.isPasswordValid() && !this.state.loading; return this.isEmailValid() && this.isPasswordValid() && !this.state.loading;
} }
/**
* 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) { onInputChange(isEmail: boolean, value: string) {
if (isEmail) { if (isEmail) {
this.setState({ this.setState({
@ -133,8 +189,23 @@ class LoginScreen extends React.Component<Props, State> {
} }
} }
onEmailSubmit = () => this.passwordInputRef.focus(); /**
* 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.
*
* Checks if we should allow the user to login,
* then makes the login request and enters a loading state until the request finishes
*
*/
onSubmit = () => { onSubmit = () => {
if (this.shouldEnableLogin()) { if (this.shouldEnableLogin()) {
this.setState({loading: true}); this.setState({loading: true});
@ -147,6 +218,11 @@ class LoginScreen extends React.Component<Props, State> {
} }
}; };
/**
* Gets the form input
*
* @returns {*}
*/
getFormInput() { getFormInput() {
return ( return (
<View> <View>
@ -173,9 +249,7 @@ class LoginScreen extends React.Component<Props, State> {
{i18n.t("loginScreen.emailError")} {i18n.t("loginScreen.emailError")}
</HelperText> </HelperText>
<TextInput <TextInput
ref={(ref) => { ref={this.passwordInputRef}
this.passwordInputRef = ref;
}}
label={i18n.t("loginScreen.password")} label={i18n.t("loginScreen.password")}
mode='outlined' mode='outlined'
value={this.state.password} value={this.state.password}
@ -201,6 +275,10 @@ class LoginScreen extends React.Component<Props, State> {
); );
} }
/**
* Gets the card containing the input form
* @returns {*}
*/
getMainCard() { getMainCard() {
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
@ -239,6 +317,11 @@ class LoginScreen extends React.Component<Props, State> {
); );
} }
/**
* Gets the card containing the information about the Amicale account
*
* @returns {*}
*/
getSecondaryCard() { getSecondaryCard() {
return ( return (
<Card style={styles.card}> <Card style={styles.card}>

View file

@ -12,10 +12,12 @@ import {Collapsible} from "react-navigation-collapsible";
import {withCollapsible} from "../../utils/withCollapsible"; import {withCollapsible} from "../../utils/withCollapsible";
import type {cardList} from "../../components/Lists/CardList/CardList"; import type {cardList} from "../../components/Lists/CardList/CardList";
import CardList from "../../components/Lists/CardList/CardList"; import CardList from "../../components/Lists/CardList/CardList";
import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from "../../managers/ThemeManager";
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
theme: Object, theme: CustomTheme,
collapsibleStack: Collapsible, collapsibleStack: Collapsible,
} }
@ -23,6 +25,23 @@ type State = {
dialogVisible: boolean, dialogVisible: boolean,
} }
type ProfileData = {
first_name: string,
last_name: string,
email: string,
birthday: string,
phone: string,
branch: string,
link: string,
validity: boolean,
clubs: Array<Club>,
}
type Club = {
id: number,
name: string,
is_manager: boolean,
}
const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png"; const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png";
const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png"; const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png";
@ -34,9 +53,9 @@ class ProfileScreen extends React.Component<Props, State> {
dialogVisible: false, dialogVisible: false,
}; };
data: Object; data: ProfileData;
flatListData: Array<Object>; flatListData: Array<{ id: string }>;
amicaleDataset: cardList; amicaleDataset: cardList;
constructor() { constructor() {
@ -79,12 +98,25 @@ class ProfileScreen extends React.Component<Props, State> {
hideDisconnectDialog = () => this.setState({dialogVisible: false}); hideDisconnectDialog = () => this.setState({dialogVisible: false});
/**
* Gets the logout header button
*
* @returns {*}
*/
getHeaderButton = () => <MaterialHeaderButtons> getHeaderButton = () => <MaterialHeaderButtons>
<Item title="logout" iconName="logout" onPress={this.showDisconnectDialog}/> <Item title="logout" iconName="logout" onPress={this.showDisconnectDialog}/>
</MaterialHeaderButtons>; </MaterialHeaderButtons>;
getScreen = (data: Object) => { /**
this.data = data[0]; * Gets the main screen component with the fetched data
*
* @param data The data fetched from the server
* @returns {*}
*/
getScreen = (data: Array<{ [key: string]: any } | null>) => {
if (data[0] != null) {
this.data = data[0];
}
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack; const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
@ -109,7 +141,7 @@ class ProfileScreen extends React.Component<Props, State> {
) )
}; };
getRenderItem = ({item}: Object) => { getRenderItem = ({item}: { item: { id: string } }) => {
switch (item.id) { switch (item.id) {
case '0': case '0':
return this.getWelcomeCard(); return this.getWelcomeCard();
@ -122,6 +154,11 @@ class ProfileScreen extends React.Component<Props, State> {
} }
}; };
/**
* Gets the list of services available with the Amicale account
*
* @returns {*}
*/
getServicesList() { getServicesList() {
return ( return (
<CardList <CardList
@ -131,12 +168,17 @@ class ProfileScreen extends React.Component<Props, State> {
); );
} }
/**
* Gets a card welcoming the user to his account
*
* @returns {*}
*/
getWelcomeCard() { getWelcomeCard() {
return ( return (
<Card style={styles.card}> <Card style={styles.card}>
<Card.Title <Card.Title
title={i18n.t("profileScreen.welcomeTitle", {name: this.data.first_name})} title={i18n.t("profileScreen.welcomeTitle", {name: this.data.first_name})}
left={(props) => <Avatar.Image left={() => <Avatar.Image
size={64} size={64}
source={ICON_AMICALE} source={ICON_AMICALE}
style={{backgroundColor: 'transparent',}} style={{backgroundColor: 'transparent',}}
@ -340,7 +382,7 @@ class ProfileScreen extends React.Component<Props, State> {
* @param item The club to render * @param item The club to render
* @return {*} * @return {*}
*/ */
clubListItem = ({item}: Object) => { clubListItem = ({item}: { item: Club }) => {
const onPress = () => this.openClubDetailsScreen(item.id); const onPress = () => this.openClubDetailsScreen(item.id);
let description = i18n.t("profileScreen.isMember"); let description = i18n.t("profileScreen.isMember");
let icon = (props) => <List.Icon {...props} icon="chevron-right"/>; let icon = (props) => <List.Icon {...props} icon="chevron-right"/>;
@ -356,9 +398,9 @@ class ProfileScreen extends React.Component<Props, State> {
/>; />;
}; };
clubKeyExtractor = (item: Object) => item.name; clubKeyExtractor = (item: Club) => item.name;
sortClubList = (a: Object, b: Object) => a.is_manager ? -1 : 1; sortClubList = (a: Club, b: Club) => a.is_manager ? -1 : 1;
/** /**
* Renders the list of clubs the user is part of * Renders the list of clubs the user is part of
@ -366,7 +408,7 @@ class ProfileScreen extends React.Component<Props, State> {
* @param list The club list * @param list The club list
* @return {*} * @return {*}
*/ */
getClubList(list: Array<Object>) { getClubList(list: Array<Club>) {
list.sort(this.sortClubList); list.sort(this.sortClubList);
return ( return (
//$FlowFixMe //$FlowFixMe

View file

@ -9,6 +9,7 @@ import VoteTease from "../../components/Amicale/Vote/VoteTease";
import VoteSelect from "../../components/Amicale/Vote/VoteSelect"; import VoteSelect from "../../components/Amicale/Vote/VoteSelect";
import VoteResults from "../../components/Amicale/Vote/VoteResults"; import VoteResults from "../../components/Amicale/Vote/VoteResults";
import VoteWait from "../../components/Amicale/Vote/VoteWait"; import VoteWait from "../../components/Amicale/Vote/VoteWait";
import {StackNavigationProp} from "@react-navigation/stack";
export type team = { export type team = {
id: number, id: number,
@ -86,13 +87,16 @@ type objectVoteDates = {
const MIN_REFRESH_TIME = 5 * 1000; const MIN_REFRESH_TIME = 5 * 1000;
type Props = { type Props = {
navigation: Object navigation: StackNavigationProp
} }
type State = { type State = {
hasVoted: boolean, hasVoted: boolean,
} }
/**
* Screen displaying vote information and controls
*/
export default class VoteScreen extends React.Component<Props, State> { export default class VoteScreen extends React.Component<Props, State> {
state = { state = {
@ -107,7 +111,7 @@ export default class VoteScreen extends React.Component<Props, State> {
today: Date; today: Date;
mainFlatListData: Array<{ key: string }>; mainFlatListData: Array<{ key: string }>;
lastRefresh: Date; lastRefresh: Date | null;
authRef: { current: null | AuthenticatedScreen }; authRef: { current: null | AuthenticatedScreen };
@ -116,22 +120,30 @@ export default class VoteScreen extends React.Component<Props, State> {
this.hasVoted = false; this.hasVoted = false;
this.today = new Date(); this.today = new Date();
this.authRef = React.createRef(); this.authRef = React.createRef();
this.lastRefresh = null;
this.mainFlatListData = [ this.mainFlatListData = [
{key: 'main'}, {key: 'main'},
{key: 'info'}, {key: 'info'},
] ]
} }
/**
* Reloads vote data if last refresh delta is smaller than the minimum refresh time
*/
reloadData = () => { reloadData = () => {
let canRefresh; let canRefresh;
if (this.lastRefresh !== undefined) const lastRefresh = this.lastRefresh;
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) > MIN_REFRESH_TIME; if (lastRefresh != null)
canRefresh = (new Date().getTime() - lastRefresh.getTime()) > MIN_REFRESH_TIME;
else else
canRefresh = true; canRefresh = true;
if (canRefresh && this.authRef.current != null) if (canRefresh && this.authRef.current != null)
this.authRef.current.reload() this.authRef.current.reload()
}; };
/**
* Generates the objects containing string and Date representations of key vote dates
*/
generateDateObject() { generateDateObject() {
const strings = this.datesString; const strings = this.datesString;
if (strings != null) { if (strings != null) {
@ -152,6 +164,16 @@ export default class VoteScreen extends React.Component<Props, State> {
this.dates = null; this.dates = null;
} }
/**
* Gets the string representation of the given date.
*
* If the given date is the same day as today, only return the tile.
* Otherwise, return the full date.
*
* @param date The Date object representation of the wanted date
* @param dateString The string representation of the wanted date
* @returns {string}
*/
getDateString(date: Date, dateString: string): string { getDateString(date: Date, dateString: string): string {
if (this.today.getDate() === date.getDate()) { if (this.today.getDate() === date.getDate()) {
const str = getTimeOnlyString(dateString); const str = getTimeOnlyString(dateString);
@ -176,7 +198,7 @@ export default class VoteScreen extends React.Component<Props, State> {
return this.dates != null && this.today > this.dates.date_result_begin; return this.dates != null && this.today > this.dates.date_result_begin;
} }
mainRenderItem = ({item}: Object) => { mainRenderItem = ({item}: { item: { key: string } }) => {
if (item.key === 'info') if (item.key === 'info')
return <VoteTitle/>; return <VoteTitle/>;
else if (item.key === 'main' && this.dates != null) else if (item.key === 'main' && this.dates != null)
@ -190,8 +212,8 @@ export default class VoteScreen extends React.Component<Props, State> {
// data[1] = FAKE_DATE; // data[1] = FAKE_DATE;
this.lastRefresh = new Date(); this.lastRefresh = new Date();
const teams : teamResponse | null = data[0]; const teams: teamResponse | null = data[0];
const dateStrings : stringVoteDates | null = data[1]; const dateStrings: stringVoteDates | null = data[1];
if (dateStrings != null && dateStrings.date_begin == null) if (dateStrings != null && dateStrings.date_begin == null)
this.datesString = null; this.datesString = null;
@ -282,6 +304,13 @@ export default class VoteScreen extends React.Component<Props, State> {
isVoteRunning={this.isVoteRunning()}/>; isVoteRunning={this.isVoteRunning()}/>;
} }
/**
* Renders the authenticated screen.
*
* Teams and dates are not mandatory to allow showing the information box even if api requests fail
*
* @returns {*}
*/
render() { render() {
return ( return (
<AuthenticatedScreen <AuthenticatedScreen

View file

@ -7,29 +7,29 @@ import ImageModal from 'react-native-image-modal';
import Autolink from "react-native-autolink"; import Autolink from "react-native-autolink";
import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton"; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import CustomTabBar from "../../components/Tabbar/CustomTabBar"; import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import {StackNavigationProp} from "@react-navigation/stack";
import type {feedItem} from "./HomeScreen";
type Props = { type Props = {
navigation: Object, navigation: StackNavigationProp,
route: Object route: { params: { data: feedItem, date: string } }
}; };
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
const NAME_AMICALE = 'Amicale INSA Toulouse'; const NAME_AMICALE = 'Amicale INSA Toulouse';
/** /**
* Class defining a planning event information page. * Class defining a feed item page.
*/ */
class FeedItemScreen extends React.Component<Props> { class FeedItemScreen extends React.Component<Props> {
displayData: Object; displayData: feedItem;
date: string; date: string;
colors: Object;
constructor(props) { constructor(props) {
super(props); super(props);
this.colors = props.theme.colors; this.displayData = props.route.params.data;
this.displayData = this.props.route.params.data; this.date = props.route.params.date;
this.date = this.props.route.params.date;
} }
componentDidMount() { componentDidMount() {
@ -38,16 +38,29 @@ class FeedItemScreen extends React.Component<Props> {
}); });
} }
/**
* Opens the feed item out link in browser or compatible app
*/
onOutLinkPress = () => { onOutLinkPress = () => {
Linking.openURL(this.displayData.permalink_url); Linking.openURL(this.displayData.permalink_url);
}; };
/**
* Gets the out link header button
*
* @returns {*}
*/
getHeaderButton = () => { getHeaderButton = () => {
return <MaterialHeaderButtons> return <MaterialHeaderButtons>
<Item title="main" iconName={'facebook'} color={"#2e88fe"} onPress={this.onOutLinkPress}/> <Item title="main" iconName={'facebook'} color={"#2e88fe"} onPress={this.onOutLinkPress}/>
</MaterialHeaderButtons>; </MaterialHeaderButtons>;
}; };
/**
* Gets the Amicale INSA avatar
*
* @returns {*}
*/
getAvatar() { getAvatar() {
return ( return (
<Avatar.Image size={48} source={ICON_AMICALE} <Avatar.Image size={48} source={ICON_AMICALE}
@ -55,8 +68,8 @@ class FeedItemScreen extends React.Component<Props> {
); );
} }
getContent() { render() {
const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture !== undefined; const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture != null;
return ( return (
<ScrollView style={{margin: 5,}}> <ScrollView style={{margin: 5,}}>
<Card.Title <Card.Title
@ -89,10 +102,6 @@ class FeedItemScreen extends React.Component<Props> {
</ScrollView> </ScrollView>
); );
} }
render() {
return this.getContent();
}
} }
export default withTheme(FeedItemScreen); export default withTheme(FeedItemScreen);

View file

@ -111,8 +111,6 @@ type State = {
*/ */
class HomeScreen extends React.Component<Props, State> { class HomeScreen extends React.Component<Props, State> {
colors: Object;
isLoggedIn: boolean | null; isLoggedIn: boolean | null;
fabRef: { current: null | AnimatedFAB }; fabRef: { current: null | AnimatedFAB };
@ -125,7 +123,6 @@ class HomeScreen extends React.Component<Props, State> {
constructor(props) { constructor(props) {
super(props); super(props);
this.colors = props.theme.colors;
this.fabRef = React.createRef(); this.fabRef = React.createRef();
this.currentNewFeed = []; this.currentNewFeed = [];
this.isLoggedIn = null; this.isLoggedIn = null;
@ -155,6 +152,9 @@ class HomeScreen extends React.Component<Props, State> {
}) })
} }
/**
* Updates login state and navigation parameters on screen focus
*/
onScreenFocus = () => { onScreenFocus = () => {
if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) { if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) {
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
@ -169,6 +169,9 @@ class HomeScreen extends React.Component<Props, State> {
this.handleNavigationParams(); this.handleNavigationParams();
}; };
/**
* Navigates to the a new screen if navigation parameters specify one
*/
handleNavigationParams = () => { handleNavigationParams = () => {
if (this.props.route.params != null) { if (this.props.route.params != null) {
if (this.props.route.params.nextScreen != null) { if (this.props.route.params.nextScreen != null) {
@ -179,6 +182,11 @@ class HomeScreen extends React.Component<Props, State> {
} }
}; };
/**
* Gets header buttons based on login state
*
* @returns {*}
*/
getHeaderButton = () => { getHeaderButton = () => {
let onPressLog = () => this.props.navigation.navigate("login", {nextScreen: "profile"}); let onPressLog = () => this.props.navigation.navigate("login", {nextScreen: "profile"});
let logIcon = "login"; let logIcon = "login";
@ -262,7 +270,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'washers', id: 'washers',
data: dashboardData == null ? 0 : dashboardData.available_machines.washers, data: dashboardData == null ? 0 : dashboardData.available_machines.washers,
icon: 'washing-machine', icon: 'washing-machine',
color: this.colors.proxiwashColor, color: this.props.theme.colors.proxiwashColor,
onPress: this.onProxiwashClick, onPress: this.onProxiwashClick,
isAvailable: dashboardData == null ? false : dashboardData.available_machines.washers > 0 isAvailable: dashboardData == null ? false : dashboardData.available_machines.washers > 0
}, },
@ -270,7 +278,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'dryers', id: 'dryers',
data: dashboardData == null ? 0 : dashboardData.available_machines.dryers, data: dashboardData == null ? 0 : dashboardData.available_machines.dryers,
icon: 'tumble-dryer', icon: 'tumble-dryer',
color: this.colors.proxiwashColor, color: this.props.theme.colors.proxiwashColor,
onPress: this.onProxiwashClick, onPress: this.onProxiwashClick,
isAvailable: dashboardData == null ? false : dashboardData.available_machines.dryers > 0 isAvailable: dashboardData == null ? false : dashboardData.available_machines.dryers > 0
}, },
@ -278,7 +286,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'available_tutorials', id: 'available_tutorials',
data: dashboardData == null ? 0 : dashboardData.available_tutorials, data: dashboardData == null ? 0 : dashboardData.available_tutorials,
icon: 'school', icon: 'school',
color: this.colors.tutorinsaColor, color: this.props.theme.colors.tutorinsaColor,
onPress: this.onTutorInsaClick, onPress: this.onTutorInsaClick,
isAvailable: dashboardData == null ? false : dashboardData.available_tutorials > 0 isAvailable: dashboardData == null ? false : dashboardData.available_tutorials > 0
}, },
@ -286,7 +294,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'proximo_articles', id: 'proximo_articles',
data: dashboardData == null ? 0 : dashboardData.proximo_articles, data: dashboardData == null ? 0 : dashboardData.proximo_articles,
icon: 'shopping', icon: 'shopping',
color: this.colors.proximoColor, color: this.props.theme.colors.proximoColor,
onPress: this.onProximoClick, onPress: this.onProximoClick,
isAvailable: dashboardData == null ? false : dashboardData.proximo_articles > 0 isAvailable: dashboardData == null ? false : dashboardData.proximo_articles > 0
}, },
@ -294,7 +302,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'today_menu', id: 'today_menu',
data: dashboardData == null ? [] : dashboardData.today_menu, data: dashboardData == null ? [] : dashboardData.today_menu,
icon: 'silverware-fork-knife', icon: 'silverware-fork-knife',
color: this.colors.menuColor, color: this.props.theme.colors.menuColor,
onPress: this.onMenuClick, onPress: this.onMenuClick,
isAvailable: dashboardData == null ? false : dashboardData.today_menu.length > 0 isAvailable: dashboardData == null ? false : dashboardData.today_menu.length > 0
}, },
@ -324,6 +332,11 @@ class HomeScreen extends React.Component<Props, State> {
return this.getDashboardActions(); return this.getDashboardActions();
} }
/**
* Gets a dashboard item with action buttons
*
* @returns {*}
*/
getDashboardActions() { getDashboardActions() {
return <ActionsDashBoardItem {...this.props} isLoggedIn={this.isLoggedIn}/>; return <ActionsDashBoardItem {...this.props} isLoggedIn={this.isLoggedIn}/>;
} }
@ -446,7 +459,7 @@ class HomeScreen extends React.Component<Props, State> {
onEventContainerClick = () => this.props.navigation.navigate('planning'); onEventContainerClick = () => this.props.navigation.navigate('planning');
/** /**
* Gets the event render item. * Gets the event dashboard render item.
* If a preview is available, it will be rendered inside * If a preview is available, it will be rendered inside
* *
* @param content * @param content
@ -473,6 +486,12 @@ class HomeScreen extends React.Component<Props, State> {
); );
} }
/**
* Gets a dashboard shortcut item
*
* @param item
* @returns {*}
*/
dashboardRowRenderItem = ({item}: { item: dashboardSmallItem }) => { dashboardRowRenderItem = ({item}: { item: dashboardSmallItem }) => {
return ( return (
<SquareDashboardItem <SquareDashboardItem
@ -486,7 +505,7 @@ class HomeScreen extends React.Component<Props, State> {
}; };
/** /**
* Gets a classic dashboard item. * Gets a dashboard item with a row of shortcut buttons.
* *
* @param content * @param content
* @return {*} * @return {*}
@ -553,7 +572,7 @@ class HomeScreen extends React.Component<Props, State> {
/** /**
* Callback used when closing the banner. * Callback used when closing the banner.
* This hides the banner and saves to preferences to prevent it from reopening * This hides the banner and saves to preferences to prevent it from reopening.
*/ */
onHideBanner = () => { onHideBanner = () => {
this.setState({bannerVisible: false}); this.setState({bannerVisible: false});
@ -563,6 +582,10 @@ class HomeScreen extends React.Component<Props, State> {
); );
}; };
/**
* Callback when pressing the login button on the banner.
* This hides the banner and takes the user to the login page.
*/
onLoginBanner = () => { onLoginBanner = () => {
this.onHideBanner(); this.onHideBanner();
this.props.navigation.navigate("login", {nextScreen: "profile"}); this.props.navigation.navigate("login", {nextScreen: "profile"});

View file

@ -41,6 +41,9 @@ class ScannerScreen extends React.Component<Props, State> {
this.requestPermissions(); this.requestPermissions();
} }
/**
* Requests permission to use the camera
*/
requestPermissions = () => { requestPermissions = () => {
if (Platform.OS === 'android') if (Platform.OS === 'android')
request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus) request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus)
@ -48,8 +51,19 @@ class ScannerScreen extends React.Component<Props, State> {
request(PERMISSIONS.IOS.CAMERA).then(this.updatePermissionStatus) request(PERMISSIONS.IOS.CAMERA).then(this.updatePermissionStatus)
}; };
/**
* Updates the state permission status
*
* @param result
*/
updatePermissionStatus = (result) => this.setState({hasPermission: result === RESULTS.GRANTED}); updatePermissionStatus = (result) => this.setState({hasPermission: result === RESULTS.GRANTED});
/**
* Opens scanned link if it is a valid app link or shows and error dialog
*
* @param type The barcode type
* @param data The scanned value
*/
handleCodeScanned = ({type, data}) => { handleCodeScanned = ({type, data}) => {
if (!URLHandler.isUrlValid(data)) if (!URLHandler.isUrlValid(data))
this.showErrorDialog(); this.showErrorDialog();
@ -59,6 +73,11 @@ class ScannerScreen extends React.Component<Props, State> {
} }
}; };
/**
* Gets a view asking user for permission to use the camera
*
* @returns {*}
*/
getPermissionScreen() { getPermissionScreen() {
return <View style={{marginLeft: 10, marginRight: 10}}> return <View style={{marginLeft: 10, marginRight: 10}}>
<Text>{i18n.t("scannerScreen.errorPermission")}</Text> <Text>{i18n.t("scannerScreen.errorPermission")}</Text>
@ -77,6 +96,9 @@ class ScannerScreen extends React.Component<Props, State> {
</View> </View>
} }
/**
* Shows a dialog indicating how to use the scanner
*/
showHelpDialog = () => { showHelpDialog = () => {
this.setState({ this.setState({
dialogVisible: true, dialogVisible: true,
@ -86,6 +108,9 @@ class ScannerScreen extends React.Component<Props, State> {
}); });
}; };
/**
* Shows a loading dialog
*/
showOpeningDialog = () => { showOpeningDialog = () => {
this.setState({ this.setState({
loading: true, loading: true,
@ -93,6 +118,9 @@ class ScannerScreen extends React.Component<Props, State> {
}); });
}; };
/**
* Shows a dialog indicating the user the scanned code was invalid
*/
showErrorDialog() { showErrorDialog() {
this.setState({ this.setState({
dialogVisible: true, dialogVisible: true,
@ -102,11 +130,21 @@ class ScannerScreen extends React.Component<Props, State> {
}); });
} }
/**
* Hide any dialog
*/
onDialogDismiss = () => this.setState({ onDialogDismiss = () => this.setState({
dialogVisible: false, dialogVisible: false,
scanned: false, scanned: false,
}); });
/**
* Gets a view with the scanner.
* This scanner uses the back camera, can only scan qr codes and has a square mask on the center.
* The mask is only for design purposes as a code is scanned as soon as it enters the camera view
*
* @returns {*}
*/
getScanner() { getScanner() {
return ( return (
<RNCamera <RNCamera

View file

@ -32,10 +32,10 @@ export function stringMatchQuery(str: string, query: string) {
* Checks if the given arrays have an item in common * Checks if the given arrays have an item in common
* *
* @param filter The filter array * @param filter The filter array
* @param categories The item's categories array * @param categories The item's categories tuple
* @returns {boolean} True if at least one entry is in both arrays * @returns {boolean} True if at least one entry is in both arrays
*/ */
export function isItemInCategoryFilter(filter: Array<string>, categories: Array<string>) { export function isItemInCategoryFilter(filter: Array<number>, categories: [number, number]) {
for (const category of categories) { for (const category of categories) {
if (filter.indexOf(category) !== -1) if (filter.indexOf(category) !== -1)
return true; return true;