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

View file

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

View file

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

View file

@ -12,14 +12,18 @@ type Props = {
collapsibleStack: Collapsible
};
type State = {};
type DatasetItem = {
name: string,
email: string,
icon: string,
}
/**
* 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 = [
{
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) {
super(props);
this.colors = props.theme.colors;
}
getChevronIcon = (props) => <List.Icon {...props} icon={'chevron-right'}/>;
keyExtractor = (item: Object) => item.email;
getChevronIcon = (props: Object) => <List.Icon {...props} icon={'chevron-right'}/>;
renderItem = ({item}: Object) => {
renderItem = ({item}: { item: DatasetItem }) => {
const onPress = () => Linking.openURL('mailto:' + item.email);
return <List.Item
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 Autolink from "react-native-autolink";
type Props = {
};
type State = {
};
type Props = {};
const CONTACT_LINK = 'clubs@amicale-insat.fr';
/**
* 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;
}
class ClubAboutScreen extends React.Component<Props> {
render() {
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) {
if (this.categories !== null) {
for (let i = 0; i < this.categories.length; i++) {
@ -75,6 +81,12 @@ class ClubDisplayScreen extends React.Component<Props, State> {
return "";
}
/**
* Gets the view for rendering categories
*
* @param categories The categories to display (max 2)
* @returns {null|*}
*/
getCategoriesRender(categories: [number, number]) {
if (this.categories === null)
return null;
@ -95,12 +107,19 @@ class ClubDisplayScreen extends React.Component<Props, State> {
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
}
getManagersRender(resp: Array<string>, email: string | null) {
let final = [];
for (let i = 0; i < resp.length; i++) {
final.push(<Paragraph key={i.toString()}>{resp[i]}</Paragraph>)
/**
* Gets the view for rendering club managers if any
*
* @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 (
<Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title
@ -113,13 +132,20 @@ class ClubDisplayScreen extends React.Component<Props, State> {
icon="account-tie"/>}
/>
<Card.Content>
{final}
{managersListView}
{this.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
*
* @param email The club contact email
* @param hasManagers True if the club has managers
* @returns {*}
*/
getEmailButton(email: string | null, hasManagers: boolean) {
const destinationEmail = email != null && hasManagers
? 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})
}
getScreen = (response: Array<Object>) => {
let data: club = response[0];
this.updateHeaderTitle(data);
getScreen = (response: Array<{ [key: string]: any } | null>) => {
let data: club | null = null;
if (response[0] != null) {
data = response[0];
this.updateHeaderTitle(data);
}
if (data != null) {
return (
<ScrollView style={{paddingLeft: 5, paddingRight: 5}}>
@ -184,7 +218,6 @@ class ClubDisplayScreen extends React.Component<Props, State> {
);
} else
return null;
};
render() {

View file

@ -131,6 +131,15 @@ class ClubListScreen extends React.Component<Props, State> {
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) {
let newCategoriesState = [...this.state.currentlySelectedCategories];
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() {
return <ClubListHeader
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) => {
for (let i = 0; i < this.categories.length; i++) {
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) {
let shouldRender = this.state.currentlySelectedCategories.length === 0
|| 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 type {CustomTheme} from "../../managers/ThemeManager";
import AsyncStorageManager from "../../managers/AsyncStorageManager";
import {StackNavigationProp} from "@react-navigation/stack";
type Props = {
navigation: Object,
route: Object,
navigation: StackNavigationProp,
route: { params: { nextScreen: string } },
collapsibleStack: Collapsible,
theme: CustomTheme
}
@ -47,9 +48,9 @@ class LoginScreen extends React.Component<Props, State> {
dialogError: 0,
};
onEmailChange: Function;
onPasswordChange: Function;
passwordInputRef: Object;
onEmailChange: (value: string) => null;
onPasswordChange: (value: string) => null;
passwordInputRef: { current: null | TextInput };
nextScreen: string | null;
@ -64,7 +65,10 @@ class LoginScreen extends React.Component<Props, State> {
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.nextScreen != null)
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) =>
this.setState({
dialogVisible: true,
@ -81,6 +90,10 @@ class LoginScreen extends React.Component<Props, State> {
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 = () => {
// Do not show the login banner again
AsyncStorageManager.getInstance().savePref(
@ -93,32 +106,75 @@ class LoginScreen extends React.Component<Props, State> {
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});
/**
* The user has unfocused the input, his email is ready to be validated
*/
validateEmail = () => this.setState({isEmailValidated: true});
/**
* Checks if the entered email is valid (matches the regex)
*
* @returns {boolean}
*/
isEmailValid() {
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() {
return this.state.isEmailValidated && !this.isEmailValid();
}
/**
* The user has unfocused the input, his password is ready to be validated
*/
validatePassword = () => this.setState({isPasswordValidated: true});
/**
* Checks if the user has entered a password
*
* @returns {boolean}
*/
isPasswordValid() {
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() {
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() {
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) {
if (isEmail) {
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 = () => {
if (this.shouldEnableLogin()) {
this.setState({loading: true});
@ -147,6 +218,11 @@ class LoginScreen extends React.Component<Props, State> {
}
};
/**
* Gets the form input
*
* @returns {*}
*/
getFormInput() {
return (
<View>
@ -173,9 +249,7 @@ class LoginScreen extends React.Component<Props, State> {
{i18n.t("loginScreen.emailError")}
</HelperText>
<TextInput
ref={(ref) => {
this.passwordInputRef = ref;
}}
ref={this.passwordInputRef}
label={i18n.t("loginScreen.password")}
mode='outlined'
value={this.state.password}
@ -201,6 +275,10 @@ class LoginScreen extends React.Component<Props, State> {
);
}
/**
* Gets the card containing the input form
* @returns {*}
*/
getMainCard() {
return (
<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() {
return (
<Card style={styles.card}>

View file

@ -12,10 +12,12 @@ import {Collapsible} from "react-navigation-collapsible";
import {withCollapsible} from "../../utils/withCollapsible";
import type {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 = {
navigation: Object,
theme: Object,
navigation: StackNavigationProp,
theme: CustomTheme,
collapsibleStack: Collapsible,
}
@ -23,6 +25,23 @@ type State = {
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 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,
};
data: Object;
data: ProfileData;
flatListData: Array<Object>;
flatListData: Array<{ id: string }>;
amicaleDataset: cardList;
constructor() {
@ -79,12 +98,25 @@ class ProfileScreen extends React.Component<Props, State> {
hideDisconnectDialog = () => this.setState({dialogVisible: false});
/**
* Gets the logout header button
*
* @returns {*}
*/
getHeaderButton = () => <MaterialHeaderButtons>
<Item title="logout" iconName="logout" onPress={this.showDisconnectDialog}/>
</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;
return (
<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) {
case '0':
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() {
return (
<CardList
@ -131,12 +168,17 @@ class ProfileScreen extends React.Component<Props, State> {
);
}
/**
* Gets a card welcoming the user to his account
*
* @returns {*}
*/
getWelcomeCard() {
return (
<Card style={styles.card}>
<Card.Title
title={i18n.t("profileScreen.welcomeTitle", {name: this.data.first_name})}
left={(props) => <Avatar.Image
left={() => <Avatar.Image
size={64}
source={ICON_AMICALE}
style={{backgroundColor: 'transparent',}}
@ -340,7 +382,7 @@ class ProfileScreen extends React.Component<Props, State> {
* @param item The club to render
* @return {*}
*/
clubListItem = ({item}: Object) => {
clubListItem = ({item}: { item: Club }) => {
const onPress = () => this.openClubDetailsScreen(item.id);
let description = i18n.t("profileScreen.isMember");
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
@ -366,7 +408,7 @@ class ProfileScreen extends React.Component<Props, State> {
* @param list The club list
* @return {*}
*/
getClubList(list: Array<Object>) {
getClubList(list: Array<Club>) {
list.sort(this.sortClubList);
return (
//$FlowFixMe

View file

@ -9,6 +9,7 @@ import VoteTease from "../../components/Amicale/Vote/VoteTease";
import VoteSelect from "../../components/Amicale/Vote/VoteSelect";
import VoteResults from "../../components/Amicale/Vote/VoteResults";
import VoteWait from "../../components/Amicale/Vote/VoteWait";
import {StackNavigationProp} from "@react-navigation/stack";
export type team = {
id: number,
@ -86,13 +87,16 @@ type objectVoteDates = {
const MIN_REFRESH_TIME = 5 * 1000;
type Props = {
navigation: Object
navigation: StackNavigationProp
}
type State = {
hasVoted: boolean,
}
/**
* Screen displaying vote information and controls
*/
export default class VoteScreen extends React.Component<Props, State> {
state = {
@ -107,7 +111,7 @@ export default class VoteScreen extends React.Component<Props, State> {
today: Date;
mainFlatListData: Array<{ key: string }>;
lastRefresh: Date;
lastRefresh: Date | null;
authRef: { current: null | AuthenticatedScreen };
@ -116,22 +120,30 @@ export default class VoteScreen extends React.Component<Props, State> {
this.hasVoted = false;
this.today = new Date();
this.authRef = React.createRef();
this.lastRefresh = null;
this.mainFlatListData = [
{key: 'main'},
{key: 'info'},
]
}
/**
* Reloads vote data if last refresh delta is smaller than the minimum refresh time
*/
reloadData = () => {
let canRefresh;
if (this.lastRefresh !== undefined)
canRefresh = (new Date().getTime() - this.lastRefresh.getTime()) > MIN_REFRESH_TIME;
const lastRefresh = this.lastRefresh;
if (lastRefresh != null)
canRefresh = (new Date().getTime() - lastRefresh.getTime()) > MIN_REFRESH_TIME;
else
canRefresh = true;
if (canRefresh && this.authRef.current != null)
this.authRef.current.reload()
};
/**
* Generates the objects containing string and Date representations of key vote dates
*/
generateDateObject() {
const strings = this.datesString;
if (strings != null) {
@ -152,6 +164,16 @@ export default class VoteScreen extends React.Component<Props, State> {
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 {
if (this.today.getDate() === date.getDate()) {
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;
}
mainRenderItem = ({item}: Object) => {
mainRenderItem = ({item}: { item: { key: string } }) => {
if (item.key === 'info')
return <VoteTitle/>;
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;
this.lastRefresh = new Date();
const teams : teamResponse | null = data[0];
const dateStrings : stringVoteDates | null = data[1];
const teams: teamResponse | null = data[0];
const dateStrings: stringVoteDates | null = data[1];
if (dateStrings != null && dateStrings.date_begin == null)
this.datesString = null;
@ -282,6 +304,13 @@ export default class VoteScreen extends React.Component<Props, State> {
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() {
return (
<AuthenticatedScreen

View file

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

View file

@ -111,8 +111,6 @@ type State = {
*/
class HomeScreen extends React.Component<Props, State> {
colors: Object;
isLoggedIn: boolean | null;
fabRef: { current: null | AnimatedFAB };
@ -125,7 +123,6 @@ class HomeScreen extends React.Component<Props, State> {
constructor(props) {
super(props);
this.colors = props.theme.colors;
this.fabRef = React.createRef();
this.currentNewFeed = [];
this.isLoggedIn = null;
@ -155,6 +152,9 @@ class HomeScreen extends React.Component<Props, State> {
})
}
/**
* Updates login state and navigation parameters on screen focus
*/
onScreenFocus = () => {
if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) {
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
@ -169,6 +169,9 @@ class HomeScreen extends React.Component<Props, State> {
this.handleNavigationParams();
};
/**
* Navigates to the a new screen if navigation parameters specify one
*/
handleNavigationParams = () => {
if (this.props.route.params != 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 = () => {
let onPressLog = () => this.props.navigation.navigate("login", {nextScreen: "profile"});
let logIcon = "login";
@ -262,7 +270,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'washers',
data: dashboardData == null ? 0 : dashboardData.available_machines.washers,
icon: 'washing-machine',
color: this.colors.proxiwashColor,
color: this.props.theme.colors.proxiwashColor,
onPress: this.onProxiwashClick,
isAvailable: dashboardData == null ? false : dashboardData.available_machines.washers > 0
},
@ -270,7 +278,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'dryers',
data: dashboardData == null ? 0 : dashboardData.available_machines.dryers,
icon: 'tumble-dryer',
color: this.colors.proxiwashColor,
color: this.props.theme.colors.proxiwashColor,
onPress: this.onProxiwashClick,
isAvailable: dashboardData == null ? false : dashboardData.available_machines.dryers > 0
},
@ -278,7 +286,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'available_tutorials',
data: dashboardData == null ? 0 : dashboardData.available_tutorials,
icon: 'school',
color: this.colors.tutorinsaColor,
color: this.props.theme.colors.tutorinsaColor,
onPress: this.onTutorInsaClick,
isAvailable: dashboardData == null ? false : dashboardData.available_tutorials > 0
},
@ -286,7 +294,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'proximo_articles',
data: dashboardData == null ? 0 : dashboardData.proximo_articles,
icon: 'shopping',
color: this.colors.proximoColor,
color: this.props.theme.colors.proximoColor,
onPress: this.onProximoClick,
isAvailable: dashboardData == null ? false : dashboardData.proximo_articles > 0
},
@ -294,7 +302,7 @@ class HomeScreen extends React.Component<Props, State> {
id: 'today_menu',
data: dashboardData == null ? [] : dashboardData.today_menu,
icon: 'silverware-fork-knife',
color: this.colors.menuColor,
color: this.props.theme.colors.menuColor,
onPress: this.onMenuClick,
isAvailable: dashboardData == null ? false : dashboardData.today_menu.length > 0
},
@ -324,6 +332,11 @@ class HomeScreen extends React.Component<Props, State> {
return this.getDashboardActions();
}
/**
* Gets a dashboard item with action buttons
*
* @returns {*}
*/
getDashboardActions() {
return <ActionsDashBoardItem {...this.props} isLoggedIn={this.isLoggedIn}/>;
}
@ -446,7 +459,7 @@ class HomeScreen extends React.Component<Props, State> {
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
*
* @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 }) => {
return (
<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
* @return {*}
@ -553,7 +572,7 @@ class HomeScreen extends React.Component<Props, State> {
/**
* 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 = () => {
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 = () => {
this.onHideBanner();
this.props.navigation.navigate("login", {nextScreen: "profile"});

View file

@ -41,6 +41,9 @@ class ScannerScreen extends React.Component<Props, State> {
this.requestPermissions();
}
/**
* Requests permission to use the camera
*/
requestPermissions = () => {
if (Platform.OS === 'android')
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)
};
/**
* Updates the state permission status
*
* @param result
*/
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}) => {
if (!URLHandler.isUrlValid(data))
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() {
return <View style={{marginLeft: 10, marginRight: 10}}>
<Text>{i18n.t("scannerScreen.errorPermission")}</Text>
@ -77,6 +96,9 @@ class ScannerScreen extends React.Component<Props, State> {
</View>
}
/**
* Shows a dialog indicating how to use the scanner
*/
showHelpDialog = () => {
this.setState({
dialogVisible: true,
@ -86,6 +108,9 @@ class ScannerScreen extends React.Component<Props, State> {
});
};
/**
* Shows a loading dialog
*/
showOpeningDialog = () => {
this.setState({
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() {
this.setState({
dialogVisible: true,
@ -102,11 +130,21 @@ class ScannerScreen extends React.Component<Props, State> {
});
}
/**
* Hide any dialog
*/
onDialogDismiss = () => this.setState({
dialogVisible: 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() {
return (
<RNCamera

View file

@ -32,10 +32,10 @@ export function stringMatchQuery(str: string, query: string) {
* Checks if the given arrays have an item in common
*
* @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
*/
export function isItemInCategoryFilter(filter: Array<string>, categories: Array<string>) {
export function isItemInCategoryFilter(filter: Array<number>, categories: [number, number]) {
for (const category of categories) {
if (filter.indexOf(category) !== -1)
return true;