Compare commits

..

No commits in common. "93d12b27f8c19020caa6a2d422977e5597dadf12" and "925bded69bba0af20237fb52d96a72c498718e0b" have entirely different histories.

30 changed files with 2884 additions and 3099 deletions

View file

@ -1,114 +1,101 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from 'react-native'; import {View} from "react-native";
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import Collapsible from 'react-native-collapsible'; import Collapsible from "react-native-collapsible";
import * as Animatable from 'react-native-animatable'; import * as Animatable from "react-native-animatable";
import type {CustomTheme} from '../../managers/ThemeManager'; import type {CustomTheme} from "../../managers/ThemeManager";
type PropsType = { type Props = {
theme: CustomTheme, theme: CustomTheme,
title: string, title: string,
subtitle?: string, subtitle?: string,
left?: () => React.Node, left?: (props: { [keys: string]: any }) => React.Node,
opened?: boolean, opened?: boolean,
unmountWhenCollapsed?: boolean, unmountWhenCollapsed: boolean,
children?: React.Node, children?: React.Node,
}; }
type StateType = { type State = {
expanded: boolean, expanded: boolean,
}; }
const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon); const AnimatedListIcon = Animatable.createAnimatableComponent(List.Icon);
class AnimatedAccordion extends React.Component<PropsType, StateType> { class AnimatedAccordion extends React.Component<Props, State> {
static defaultProps = { static defaultProps = {
subtitle: '',
left: null,
opened: null,
unmountWhenCollapsed: false, unmountWhenCollapsed: false,
children: null, }
}; chevronRef: { current: null | AnimatedListIcon };
chevronRef: {current: null | AnimatedListIcon};
chevronIcon: string; chevronIcon: string;
animStart: string; animStart: string;
animEnd: string; animEnd: string;
constructor(props: PropsType) { state = {
expanded: this.props.opened != null ? this.props.opened : false,
}
constructor(props) {
super(props); super(props);
this.state = {
expanded: props.opened != null ? props.opened : false,
};
this.chevronRef = React.createRef(); this.chevronRef = React.createRef();
this.setupChevron(); this.setupChevron();
} }
shouldComponentUpdate(nextProps: PropsType): boolean {
const {state, props} = this;
if (nextProps.opened != null && nextProps.opened !== props.opened)
state.expanded = nextProps.opened;
return true;
}
setupChevron() { setupChevron() {
const {state} = this; if (this.state.expanded) {
if (state.expanded) { this.chevronIcon = "chevron-up";
this.chevronIcon = 'chevron-up'; this.animStart = "180deg";
this.animStart = '180deg'; this.animEnd = "0deg";
this.animEnd = '0deg';
} else { } else {
this.chevronIcon = 'chevron-down'; this.chevronIcon = "chevron-down";
this.animStart = '0deg'; this.animStart = "0deg";
this.animEnd = '180deg'; this.animEnd = "180deg";
} }
} }
toggleAccordion = () => { toggleAccordion = () => {
const {state} = this;
if (this.chevronRef.current != null) { if (this.chevronRef.current != null) {
this.chevronRef.current.transitionTo({ this.chevronRef.current.transitionTo({rotate: this.state.expanded ? this.animStart : this.animEnd});
rotate: state.expanded ? this.animStart : this.animEnd, this.setState({expanded: !this.state.expanded})
});
this.setState({expanded: !state.expanded});
} }
}; };
render(): React.Node { shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
const {props, state} = this; if (nextProps.opened != null && nextProps.opened !== this.props.opened)
const {colors} = props.theme; this.state.expanded = nextProps.opened;
return true;
}
render() {
const colors = this.props.theme.colors;
return ( return (
<View> <View>
<List.Item <List.Item
title={props.title} {...this.props}
subtitle={props.subtitle} title={this.props.title}
titleStyle={state.expanded ? {color: colors.primary} : undefined} subtitle={this.props.subtitle}
titleStyle={this.state.expanded ? {color: colors.primary} : undefined}
onPress={this.toggleAccordion} onPress={this.toggleAccordion}
right={({size}: {size: number}): React.Node => ( right={(props) => <AnimatedListIcon
<AnimatedListIcon
ref={this.chevronRef} ref={this.chevronRef}
size={size} {...props}
icon={this.chevronIcon} icon={this.chevronIcon}
color={state.expanded ? colors.primary : undefined} color={this.state.expanded ? colors.primary : undefined}
useNativeDriver useNativeDriver
/>}
left={this.props.left}
/> />
)} <Collapsible collapsed={!this.state.expanded}>
left={props.left} {!this.props.unmountWhenCollapsed || (this.props.unmountWhenCollapsed && this.state.expanded)
/> ? this.props.children
<Collapsible collapsed={!state.expanded}>
{!props.unmountWhenCollapsed ||
(props.unmountWhenCollapsed && state.expanded)
? props.children
: null} : null}
</Collapsible> </Collapsible>
</View> </View>
); );
} }
} }
export default withTheme(AnimatedAccordion); export default withTheme(AnimatedAccordion);

View file

@ -1,32 +1,142 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from "react-native";
import {FAB, IconButton, Surface, withTheme} from 'react-native-paper'; import {FAB, IconButton, Surface, withTheme} from "react-native-paper";
import AutoHideHandler from "../../utils/AutoHideHandler";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import {StackNavigationProp} from '@react-navigation/stack'; import CustomTabBar from "../Tabbar/CustomTabBar";
import AutoHideHandler from '../../utils/AutoHideHandler'; import {StackNavigationProp} from "@react-navigation/stack";
import CustomTabBar from '../Tabbar/CustomTabBar'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {CustomTheme} from '../../managers/ThemeManager';
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
onPress: (action: string, data?: string) => void, onPress: (action: string, data: any) => void,
seekAttention: boolean, seekAttention: boolean,
}; }
type StateType = { type State = {
currentMode: string, currentMode: string,
}; }
const DISPLAY_MODES = { const DISPLAY_MODES = {
DAY: 'agendaDay', DAY: "agendaDay",
WEEK: 'agendaWeek', WEEK: "agendaWeek",
MONTH: 'month', MONTH: "month",
}; }
class AnimatedBottomBar extends React.Component<Props, State> {
ref: { current: null | Animatable.View };
hideHandler: AutoHideHandler;
displayModeIcons: { [key: string]: string };
state = {
currentMode: DISPLAY_MODES.WEEK,
}
constructor() {
super();
this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
this.displayModeIcons = {};
this.displayModeIcons[DISPLAY_MODES.DAY] = "calendar-text";
this.displayModeIcons[DISPLAY_MODES.WEEK] = "calendar-week";
this.displayModeIcons[DISPLAY_MODES.MONTH] = "calendar-range";
}
shouldComponentUpdate(nextProps: Props, nextState: State) {
return (nextProps.seekAttention !== this.props.seekAttention)
|| (nextState.currentMode !== this.state.currentMode);
}
onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) {
if (shouldHide)
this.ref.current.fadeOutDown(500);
else
this.ref.current.fadeInUp(500);
}
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event);
};
changeDisplayMode = () => {
let newMode;
switch (this.state.currentMode) {
case DISPLAY_MODES.DAY:
newMode = DISPLAY_MODES.WEEK;
break;
case DISPLAY_MODES.WEEK:
newMode = DISPLAY_MODES.MONTH;
break;
case DISPLAY_MODES.MONTH:
newMode = DISPLAY_MODES.DAY;
break;
}
this.setState({currentMode: newMode});
this.props.onPress("changeView", newMode);
};
render() {
const buttonColor = this.props.theme.colors.primary;
return (
<Animatable.View
ref={this.ref}
useNativeDriver
style={{
...styles.container,
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT
}}>
<Surface style={styles.surface}>
<View style={styles.fabContainer}>
<AnimatedFAB
animation={this.props.seekAttention ? "bounce" : undefined}
easing="ease-out"
iterationDelay={500}
iterationCount="infinite"
useNativeDriver
style={styles.fab}
icon="account-clock"
onPress={() => this.props.navigation.navigate('group-select')}
/>
</View>
<View style={{flexDirection: 'row'}}>
<IconButton
icon={this.displayModeIcons[this.state.currentMode]}
color={buttonColor}
onPress={this.changeDisplayMode}/>
<IconButton
icon="clock-in"
color={buttonColor}
style={{marginLeft: 5}}
onPress={() => this.props.onPress('today', undefined)}/>
</View>
<View style={{flexDirection: 'row'}}>
<IconButton
icon="chevron-left"
color={buttonColor}
onPress={() => this.props.onPress('prev', undefined)}/>
<IconButton
icon="chevron-right"
color={buttonColor}
style={{marginLeft: 5}}
onPress={() => this.props.onPress('next', undefined)}/>
</View>
</Surface>
</Animatable.View>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -43,136 +153,18 @@ const styles = StyleSheet.create({
elevation: 2, elevation: 2,
}, },
fabContainer: { fabContainer: {
position: 'absolute', position: "absolute",
left: 0, left: 0,
right: 0, right: 0,
alignItems: 'center', alignItems: "center",
width: '100%', width: '100%',
height: '100%', height: '100%'
}, },
fab: { fab: {
position: 'absolute', position: 'absolute',
alignSelf: 'center', alignSelf: 'center',
top: '-25%', top: '-25%',
}, }
}); });
class AnimatedBottomBar extends React.Component<PropsType, StateType> {
ref: {current: null | Animatable.View};
hideHandler: AutoHideHandler;
displayModeIcons: {[key: string]: string};
constructor() {
super();
this.state = {
currentMode: DISPLAY_MODES.WEEK,
};
this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
this.displayModeIcons = {};
this.displayModeIcons[DISPLAY_MODES.DAY] = 'calendar-text';
this.displayModeIcons[DISPLAY_MODES.WEEK] = 'calendar-week';
this.displayModeIcons[DISPLAY_MODES.MONTH] = 'calendar-range';
}
shouldComponentUpdate(nextProps: PropsType, nextState: StateType): boolean {
const {props, state} = this;
return (
nextProps.seekAttention !== props.seekAttention ||
nextState.currentMode !== state.currentMode
);
}
onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) {
if (shouldHide) this.ref.current.fadeOutDown(500);
else this.ref.current.fadeInUp(500);
}
};
onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event);
};
changeDisplayMode = () => {
const {props, state} = this;
let newMode;
switch (state.currentMode) {
case DISPLAY_MODES.DAY:
newMode = DISPLAY_MODES.WEEK;
break;
case DISPLAY_MODES.WEEK:
newMode = DISPLAY_MODES.MONTH;
break;
case DISPLAY_MODES.MONTH:
newMode = DISPLAY_MODES.DAY;
break;
default:
newMode = DISPLAY_MODES.WEEK;
break;
}
this.setState({currentMode: newMode});
props.onPress('changeView', newMode);
};
render(): React.Node {
const {props, state} = this;
const buttonColor = props.theme.colors.primary;
return (
<Animatable.View
ref={this.ref}
useNativeDriver
style={{
...styles.container,
bottom: 10 + CustomTabBar.TAB_BAR_HEIGHT,
}}>
<Surface style={styles.surface}>
<View style={styles.fabContainer}>
<AnimatedFAB
animation={props.seekAttention ? 'bounce' : undefined}
easing="ease-out"
iterationDelay={500}
iterationCount="infinite"
useNativeDriver
style={styles.fab}
icon="account-clock"
onPress={(): void => props.navigation.navigate('group-select')}
/>
</View>
<View style={{flexDirection: 'row'}}>
<IconButton
icon={this.displayModeIcons[state.currentMode]}
color={buttonColor}
onPress={this.changeDisplayMode}
/>
<IconButton
icon="clock-in"
color={buttonColor}
style={{marginLeft: 5}}
onPress={(): void => props.onPress('today')}
/>
</View>
<View style={{flexDirection: 'row'}}>
<IconButton
icon="chevron-left"
color={buttonColor}
onPress={(): void => props.onPress('prev')}
/>
<IconButton
icon="chevron-right"
color={buttonColor}
style={{marginLeft: 5}}
onPress={(): void => props.onPress('next')}
/>
</View>
</Surface>
</Animatable.View>
);
}
}
export default withTheme(AnimatedBottomBar); export default withTheme(AnimatedBottomBar);

View file

@ -1,30 +1,24 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet} from 'react-native'; import {StyleSheet} from "react-native";
import {FAB} from 'react-native-paper'; import {FAB} from "react-native-paper";
import AutoHideHandler from "../../utils/AutoHideHandler";
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import AutoHideHandler from '../../utils/AutoHideHandler'; import CustomTabBar from "../Tabbar/CustomTabBar";
import CustomTabBar from '../Tabbar/CustomTabBar'; import {StackNavigationProp} from "@react-navigation/stack";
type PropsType = { type Props = {
navigation: StackNavigationProp,
icon: string, icon: string,
onPress: () => void, onPress: () => void,
}; }
const AnimatedFab = Animatable.createAnimatableComponent(FAB); const AnimatedFab = Animatable.createAnimatableComponent(FAB);
const styles = StyleSheet.create({ export default class AnimatedFAB extends React.Component<Props> {
fab: {
position: 'absolute',
margin: 16,
right: 0,
},
});
export default class AnimatedFAB extends React.Component<PropsType> {
ref: {current: null | Animatable.View};
ref: { current: null | Animatable.View };
hideHandler: AutoHideHandler; hideHandler: AutoHideHandler;
constructor() { constructor() {
@ -40,24 +34,33 @@ export default class AnimatedFAB extends React.Component<PropsType> {
onHideChange = (shouldHide: boolean) => { onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) { if (this.ref.current != null) {
if (shouldHide) this.ref.current.bounceOutDown(1000); if (shouldHide)
else this.ref.current.bounceInUp(1000); this.ref.current.bounceOutDown(1000);
else
this.ref.current.bounceInUp(1000);
}
} }
};
render(): React.Node { render() {
const {props} = this;
return ( return (
<AnimatedFab <AnimatedFab
ref={this.ref} ref={this.ref}
useNativeDriver useNativeDriver
icon={props.icon} icon={this.props.icon}
onPress={props.onPress} onPress={this.props.onPress}
style={{ style={{
...styles.fab, ...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT, bottom: CustomTabBar.TAB_BAR_HEIGHT
}} }}
/> />
); );
} }
} }
const styles = StyleSheet.create({
fab: {
position: 'absolute',
margin: 16,
right: 0,
},
});

View file

@ -1,56 +1,48 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Collapsible} from 'react-navigation-collapsible'; import {withCollapsible} from "../../utils/withCollapsible";
import {withCollapsible} from '../../utils/withCollapsible'; import {Collapsible} from "react-navigation-collapsible";
import CustomTabBar from '../Tabbar/CustomTabBar'; import CustomTabBar from "../Tabbar/CustomTabBar";
export type CollapsibleComponentPropsType = { export type CollapsibleComponentProps = {
children?: React.Node, children?: React.Node,
hasTab?: boolean, hasTab?: boolean,
onScroll?: (event: SyntheticEvent<EventTarget>) => void, onScroll?: (event: SyntheticEvent<EventTarget>) => void,
}; };
type PropsType = { type Props = {
...CollapsibleComponentPropsType, ...CollapsibleComponentProps,
collapsibleStack: Collapsible, collapsibleStack: Collapsible,
// eslint-disable-next-line flowtype/no-weak-types
component: any, component: any,
}; }
class CollapsibleComponent extends React.Component<Props> {
class CollapsibleComponent extends React.Component<PropsType> {
static defaultProps = { static defaultProps = {
children: null,
hasTab: false, hasTab: false,
onScroll: null, }
};
onScroll = (event: SyntheticEvent<EventTarget>) => { onScroll = (event: SyntheticEvent<EventTarget>) => {
const {props} = this; if (this.props.onScroll)
if (props.onScroll) props.onScroll(event); this.props.onScroll(event);
}; }
render(): React.Node {
const {props} = this;
const Comp = props.component;
const {
containerPaddingTop,
scrollIndicatorInsetTop,
onScrollWithListener,
} = props.collapsibleStack;
render() {
const Comp = this.props.component;
const {containerPaddingTop, scrollIndicatorInsetTop, onScrollWithListener} = this.props.collapsibleStack;
return ( return (
<Comp <Comp
// eslint-disable-next-line react/jsx-props-no-spreading {...this.props}
{...props}
onScroll={onScrollWithListener(this.onScroll)} onScroll={onScrollWithListener(this.onScroll)}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: containerPaddingTop, paddingTop: containerPaddingTop,
paddingBottom: props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0, paddingBottom: this.props.hasTab ? CustomTabBar.TAB_BAR_HEIGHT : 0,
minHeight: '100%', minHeight: '100%'
}} }}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}> scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
{props.children} >
{this.props.children}
</Comp> </Comp>
); );
} }

View file

@ -1,23 +1,23 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native'; import {Animated} from "react-native";
import type {CollapsibleComponentPropsType} from './CollapsibleComponent'; import type {CollapsibleComponentProps} from "./CollapsibleComponent";
import CollapsibleComponent from './CollapsibleComponent'; import CollapsibleComponent from "./CollapsibleComponent";
type PropsType = { type Props = {
...CollapsibleComponentPropsType, ...CollapsibleComponentProps
}; }
// eslint-disable-next-line react/prefer-stateless-function class CollapsibleFlatList extends React.Component<Props> {
class CollapsibleFlatList extends React.Component<PropsType> {
render(): React.Node { render() {
const {props} = this;
return ( return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading <CollapsibleComponent
{...props} {...this.props}
component={Animated.FlatList}> component={Animated.FlatList}
{props.children} >
{this.props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
} }

View file

@ -1,23 +1,23 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native'; import {Animated} from "react-native";
import type {CollapsibleComponentPropsType} from './CollapsibleComponent'; import type {CollapsibleComponentProps} from "./CollapsibleComponent";
import CollapsibleComponent from './CollapsibleComponent'; import CollapsibleComponent from "./CollapsibleComponent";
type PropsType = { type Props = {
...CollapsibleComponentPropsType, ...CollapsibleComponentProps
}; }
// eslint-disable-next-line react/prefer-stateless-function class CollapsibleScrollView extends React.Component<Props> {
class CollapsibleScrollView extends React.Component<PropsType> {
render(): React.Node { render() {
const {props} = this;
return ( return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading <CollapsibleComponent
{...props} {...this.props}
component={Animated.ScrollView}> component={Animated.ScrollView}
{props.children} >
{this.props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
} }

View file

@ -1,23 +1,23 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated} from 'react-native'; import {Animated} from "react-native";
import type {CollapsibleComponentPropsType} from './CollapsibleComponent'; import type {CollapsibleComponentProps} from "./CollapsibleComponent";
import CollapsibleComponent from './CollapsibleComponent'; import CollapsibleComponent from "./CollapsibleComponent";
type PropsType = { type Props = {
...CollapsibleComponentPropsType, ...CollapsibleComponentProps
}; }
// eslint-disable-next-line react/prefer-stateless-function class CollapsibleSectionList extends React.Component<Props> {
class CollapsibleSectionList extends React.Component<PropsType> {
render(): React.Node { render() {
const {props} = this;
return ( return (
<CollapsibleComponent // eslint-disable-next-line react/jsx-props-no-spreading <CollapsibleComponent
{...props} {...this.props}
component={Animated.SectionList}> component={Animated.SectionList}
{props.children} >
{this.props.children}
</CollapsibleComponent> </CollapsibleComponent>
); );
} }

View file

@ -2,44 +2,35 @@
import * as React from 'react'; import * as React from 'react';
import {List, withTheme} from 'react-native-paper'; import {List, withTheme} from 'react-native-paper';
import {View} from 'react-native'; import {View} from "react-native";
import type {CustomTheme} from "../../managers/ThemeManager";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from '../../managers/ThemeManager';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
}; }
class ActionsDashBoardItem extends React.Component<PropsType> { class ActionsDashBoardItem extends React.Component<Props> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this; shouldComponentUpdate(nextProps: Props): boolean {
return nextProps.theme.dark !== props.theme.dark; return (nextProps.theme.dark !== this.props.theme.dark);
} }
render(): React.Node { render() {
const {props} = this;
return ( return (
<View> <View>
<List.Item <List.Item
title={i18n.t('screens.feedback.homeButtonTitle')} title={i18n.t("screens.feedback.homeButtonTitle")}
description={i18n.t('screens.feedback.homeButtonSubtitle')} description={i18n.t("screens.feedback.homeButtonSubtitle")}
left={({size}: {size: number}): React.Node => ( left={props => <List.Icon {...props} icon={"comment-quote"}/>}
<List.Icon size={size} icon="comment-quote" /> right={props => <List.Icon {...props} icon={"chevron-right"}/>}
)} onPress={() => this.props.navigation.navigate("feedback")}
right={({size}: {size: number}): React.Node => ( style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}}
<List.Icon size={size} icon="chevron-right" />
)}
onPress={(): void => props.navigation.navigate('feedback')}
style={{
paddingTop: 0,
paddingBottom: 0,
marginLeft: 10,
marginRight: 10,
}}
/> />
</View> </View>
); );
} }
} }

View file

@ -1,23 +1,79 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { import {Avatar, Card, Text, TouchableRipple, withTheme} from 'react-native-paper';
Avatar, import {StyleSheet, View} from "react-native";
Card, import i18n from "i18n-js";
Text, import type {CustomTheme} from "../../managers/ThemeManager";
TouchableRipple,
withTheme,
} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomTheme} from '../../managers/ThemeManager';
type PropsType = { type Props = {
eventNumber: number, eventNumber: number;
clickAction: () => void, clickAction: () => void,
theme: CustomTheme, theme: CustomTheme,
children?: React.Node, children?: React.Node
}; }
/**
* Component used to display a dashboard item containing a preview event
*/
class EventDashBoardItem extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props) {
return (nextProps.theme.dark !== this.props.theme.dark)
|| (nextProps.eventNumber !== this.props.eventNumber);
}
render() {
const props = this.props;
const colors = props.theme.colors;
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable ?
colors.planningColor :
colors.textDisabled;
const textColor = isAvailable ?
colors.text :
colors.textDisabled;
let subtitle;
if (isAvailable) {
subtitle =
<Text>
<Text style={{fontWeight: "bold"}}>{props.eventNumber}</Text>
<Text>
{props.eventNumber > 1
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
</Text>
</Text>;
} else
subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
return (
<Card style={styles.card}>
<TouchableRipple
style={{flex: 1}}
onPress={props.clickAction}>
<View>
<Card.Title
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
titleStyle={{color: textColor}}
subtitle={subtitle}
subtitleStyle={{color: textColor}}
left={() =>
<Avatar.Icon
icon={'calendar-range'}
color={iconColor}
size={60}
style={styles.avatar}/>}
/>
<Card.Content>
{props.children}
</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
@ -28,69 +84,8 @@ const styles = StyleSheet.create({
overflow: 'hidden', overflow: 'hidden',
}, },
avatar: { avatar: {
backgroundColor: 'transparent', backgroundColor: 'transparent'
}, }
}); });
/**
* Component used to display a dashboard item containing a preview event
*/
class EventDashBoardItem extends React.Component<PropsType> {
static defaultProps = {
children: null,
};
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.theme.dark !== props.theme.dark ||
nextProps.eventNumber !== props.eventNumber
);
}
render(): React.Node {
const {props} = this;
const {colors} = props.theme;
const isAvailable = props.eventNumber > 0;
const iconColor = isAvailable ? colors.planningColor : colors.textDisabled;
const textColor = isAvailable ? colors.text : colors.textDisabled;
let subtitle;
if (isAvailable) {
subtitle = (
<Text>
<Text style={{fontWeight: 'bold'}}>{props.eventNumber}</Text>
<Text>
{props.eventNumber > 1
? i18n.t('screens.home.dashboard.todayEventsSubtitlePlural')
: i18n.t('screens.home.dashboard.todayEventsSubtitle')}
</Text>
</Text>
);
} else subtitle = i18n.t('screens.home.dashboard.todayEventsSubtitleNA');
return (
<Card style={styles.card}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
<Card.Title
title={i18n.t('screens.home.dashboard.todayEventsTitle')}
titleStyle={{color: textColor}}
subtitle={subtitle}
subtitleStyle={{color: textColor}}
left={(): React.Node => (
<Avatar.Icon
icon="calendar-range"
color={iconColor}
size={60}
style={styles.avatar}
/>
)}
/>
<Card.Content>{props.children}</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
}
export default withTheme(EventDashBoardItem); export default withTheme(EventDashBoardItem);

View file

@ -2,47 +2,67 @@
import * as React from 'react'; import * as React from 'react';
import {Button, Card, Text, TouchableRipple} from 'react-native-paper'; import {Button, Card, Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native'; import {Image, View} from "react-native";
import Autolink from 'react-native-autolink'; import Autolink from "react-native-autolink";
import i18n from 'i18n-js'; import i18n from "i18n-js";
import ImageModal from 'react-native-image-modal'; import ImageModal from 'react-native-image-modal';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from "@react-navigation/stack";
import type {FeedItemType} from '../../screens/Home/HomeScreen'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {feedItem} from "../../screens/Home/HomeScreen";
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
item: FeedItemType, theme: CustomTheme,
item: feedItem,
title: string, title: string,
subtitle: string, subtitle: string,
height: number, height: number,
}; }
/** /**
* Component used to display a feed item * Component used to display a feed item
*/ */
class FeedItem extends React.Component<PropsType> { class FeedItem extends React.Component<Props> {
shouldComponentUpdate(): boolean {
shouldComponentUpdate() {
return false; return false;
} }
/**
* Gets the amicale INSAT logo
*
* @return {*}
*/
getAvatar() {
return (
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48,
height: 48,
}}/>
);
}
onPress = () => { onPress = () => {
const {props} = this; this.props.navigation.navigate(
props.navigation.navigate('feed-information', { 'feed-information',
data: props.item, {
date: props.subtitle, data: this.props.item,
date: this.props.subtitle
}); });
}; };
render(): React.Node { render() {
const {props} = this; const item = this.props.item;
const {item} = props; const hasImage = item.full_picture !== '' && item.full_picture !== undefined;
const hasImage =
item.full_picture !== '' && item.full_picture !== undefined;
const cardMargin = 10; const cardMargin = 10;
const cardHeight = props.height - 2 * cardMargin; const cardHeight = this.props.height - 2 * cardMargin;
const imageSize = 250; const imageSize = 250;
const titleHeight = 80; const titleHeight = 80;
const actionsHeight = 60; const actionsHeight = 60;
@ -54,29 +74,23 @@ class FeedItem extends React.Component<PropsType> {
style={{ style={{
margin: cardMargin, margin: cardMargin,
height: cardHeight, height: cardHeight,
}}> }}
<TouchableRipple style={{flex: 1}} onPress={this.onPress}> >
<TouchableRipple
style={{flex: 1}}
onPress={this.onPress}>
<View> <View>
<Card.Title <Card.Title
title={props.title} title={this.props.title}
subtitle={props.subtitle} subtitle={this.props.subtitle}
left={(): React.Node => ( left={this.getAvatar}
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48,
height: 48,
}}
/>
)}
style={{height: titleHeight}} style={{height: titleHeight}}
/> />
{hasImage ? ( {hasImage ?
<View style={{marginLeft: 'auto', marginRight: 'auto'}}> <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal <ImageModal
resizeMode="contain" resizeMode="contain"
imageBackgroundColor="#000" imageBackgroundColor={"#000"}
style={{ style={{
width: imageSize, width: imageSize,
height: imageSize, height: imageSize,
@ -84,23 +98,21 @@ class FeedItem extends React.Component<PropsType> {
source={{ source={{
uri: item.full_picture, uri: item.full_picture,
}} }}
/> /></View> : null}
</View>
) : null}
<Card.Content> <Card.Content>
{item.message !== undefined ? ( {item.message !== undefined ?
<Autolink <Autolink
text={item.message} text={item.message}
hashtag="facebook" hashtag="facebook"
component={Text} component={Text}
style={{height: textHeight}} style={{height: textHeight}}
/> /> : null
) : null} }
</Card.Content> </Card.Content>
<Card.Actions style={{height: actionsHeight}}> <Card.Actions style={{height: actionsHeight}}>
<Button <Button
onPress={this.onPress} onPress={this.onPress}
icon="plus" icon={'plus'}
style={{marginLeft: 'auto'}}> style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')} {i18n.t('screens.home.dashboard.seeMore')}
</Button> </Button>

View file

@ -1,21 +1,81 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from "react-native";
import i18n from 'i18n-js'; import i18n from "i18n-js";
import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper'; import {Avatar, Button, Card, TouchableRipple} from 'react-native-paper';
import {getFormattedEventTime, isDescriptionEmpty} from '../../utils/Planning'; import {getFormattedEventTime, isDescriptionEmpty} from "../../utils/Planning";
import CustomHTML from '../Overrides/CustomHTML'; import CustomHTML from "../Overrides/CustomHTML";
import type {EventType} from '../../screens/Home/HomeScreen'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {event} from "../../screens/Home/HomeScreen";
type PropsType = { type Props = {
event?: EventType | null, event?: event,
clickAction: () => void, clickAction: () => void,
}; theme?: CustomTheme,
}
/**
* Component used to display an event preview if an event is available
*/
class PreviewEventDashboardItem extends React.Component<Props> {
render() {
const props = this.props;
const isEmpty = props.event == null
? true
: isDescriptionEmpty(props.event.description);
if (props.event != null) {
const event = props.event;
const hasImage = event.logo !== '' && event.logo != null;
const getImage = () => <Avatar.Image
source={{uri: event.logo}}
size={50}
style={styles.avatar}/>;
return (
<Card
style={styles.card}
elevation={3}
>
<TouchableRipple
style={{flex: 1}}
onPress={props.clickAction}>
<View>
{hasImage ?
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
left={getImage}
/> :
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(event.date_begin, event.date_end)}
/>}
{!isEmpty ?
<Card.Content style={styles.content}>
<CustomHTML html={event.description}/>
</Card.Content> : null}
<Card.Actions style={styles.actions}>
<Button
icon={'chevron-right'}
>
{i18n.t("screens.home.dashboard.seeMore")}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
} else
return null;
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
marginBottom: 10, marginBottom: 10
}, },
content: { content: {
maxHeight: 150, maxHeight: 150,
@ -24,77 +84,11 @@ const styles = StyleSheet.create({
actions: { actions: {
marginLeft: 'auto', marginLeft: 'auto',
marginTop: 'auto', marginTop: 'auto',
flexDirection: 'row', flexDirection: 'row'
}, },
avatar: { avatar: {
backgroundColor: 'transparent', backgroundColor: 'transparent'
}, }
}); });
/**
* Component used to display an event preview if an event is available
*/
// eslint-disable-next-line react/prefer-stateless-function
class PreviewEventDashboardItem extends React.Component<PropsType> {
static defaultProps = {
event: null,
};
render(): React.Node {
const {props} = this;
const {event} = props;
const isEmpty =
event == null ? true : isDescriptionEmpty(event.description);
if (event != null) {
const hasImage = event.logo !== '' && event.logo != null;
const getImage = (): React.Node => (
<Avatar.Image
source={{uri: event.logo}}
size={50}
style={styles.avatar}
/>
);
return (
<Card style={styles.card} elevation={3}>
<TouchableRipple style={{flex: 1}} onPress={props.clickAction}>
<View>
{hasImage ? (
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(
event.date_begin,
event.date_end,
)}
left={getImage}
/>
) : (
<Card.Title
title={event.title}
subtitle={getFormattedEventTime(
event.date_begin,
event.date_end,
)}
/>
)}
{!isEmpty ? (
<Card.Content style={styles.content}>
<CustomHTML html={event.description} />
</Card.Content>
) : null}
<Card.Actions style={styles.actions}>
<Button icon="chevron-right">
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
return null;
}
}
export default PreviewEventDashboardItem; export default PreviewEventDashboardItem;

View file

@ -2,13 +2,13 @@
import * as React from 'react'; import * as React from 'react';
import {Badge, TouchableRipple, withTheme} from 'react-native-paper'; import {Badge, TouchableRipple, withTheme} from 'react-native-paper';
import {Dimensions, Image, View} from 'react-native'; import {Dimensions, Image, View} from "react-native";
import * as Animatable from 'react-native-animatable'; import type {CustomTheme} from "../../managers/ThemeManager";
import type {CustomTheme} from '../../managers/ThemeManager'; import * as Animatable from "react-native-animatable";
type PropsType = { type Props = {
image: string | null, image: string,
onPress: () => void | null, onPress: () => void,
badgeCount: number | null, badgeCount: number | null,
theme: CustomTheme, theme: CustomTheme,
}; };
@ -18,51 +18,50 @@ const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
/** /**
* Component used to render a small dashboard item * Component used to render a small dashboard item
*/ */
class SmallDashboardItem extends React.Component<PropsType> { class SmallDashboardItem extends React.Component<Props> {
itemSize: number; itemSize: number;
constructor(props: PropsType) { constructor(props: Props) {
super(props); super(props);
this.itemSize = Dimensions.get('window').width / 8; this.itemSize = Dimensions.get('window').width / 8;
} }
shouldComponentUpdate(nextProps: PropsType): boolean { shouldComponentUpdate(nextProps: Props) {
const {props} = this; return (nextProps.theme.dark !== this.props.theme.dark)
return ( || (nextProps.badgeCount !== this.props.badgeCount);
nextProps.theme.dark !== props.theme.dark ||
nextProps.badgeCount !== props.badgeCount
);
} }
render(): React.Node { render() {
const {props} = this; const props = this.props;
return ( return (
<TouchableRipple <TouchableRipple
onPress={props.onPress} onPress={this.props.onPress}
borderless borderless={true}
style={{ style={{
marginLeft: this.itemSize / 6, marginLeft: this.itemSize / 6,
marginRight: this.itemSize / 6, marginRight: this.itemSize / 6,
}}> }}
<View >
style={{ <View style={{
width: this.itemSize, width: this.itemSize,
height: this.itemSize, height: this.itemSize,
}}> }}>
<Image <Image
source={{uri: props.image}} source={{uri: props.image}}
style={{ style={{
width: '80%', width: "80%",
height: '80%', height: "80%",
marginLeft: 'auto', marginLeft: "auto",
marginRight: 'auto', marginRight: "auto",
marginTop: 'auto', marginTop: "auto",
marginBottom: 'auto', marginBottom: "auto",
}} }}
/> />
{props.badgeCount != null && props.badgeCount > 0 ? ( {
props.badgeCount != null && props.badgeCount > 0 ?
<AnimatableBadge <AnimatableBadge
animation="zoomIn" animation={"zoomIn"}
duration={300} duration={300}
useNativeDriver useNativeDriver
style={{ style={{
@ -74,12 +73,14 @@ class SmallDashboardItem extends React.Component<PropsType> {
borderWidth: 2, borderWidth: 2,
}}> }}>
{props.badgeCount} {props.badgeCount}
</AnimatableBadge> </AnimatableBadge> : null
) : null} }
</View> </View>
</TouchableRipple> </TouchableRipple>
); );
} }
} }
export default withTheme(SmallDashboardItem); export default withTheme(SmallDashboardItem);

View file

@ -1,53 +1,55 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Animated, Dimensions} from 'react-native'; import {Animated, Dimensions} from "react-native";
import type {ViewStyle} from 'react-native/Libraries/StyleSheet/StyleSheet'; import ImageListItem from "./ImageListItem";
import ImageListItem from './ImageListItem'; import CardListItem from "./CardListItem";
import CardListItem from './CardListItem'; import type {ViewStyle} from "react-native/Libraries/StyleSheet/StyleSheet";
import type {ServiceItemType} from '../../../managers/ServicesManager';
type PropsType = { type Props = {
dataset: Array<ServiceItemType>, dataset: Array<cardItem>,
isHorizontal?: boolean, isHorizontal: boolean,
contentContainerStyle?: ViewStyle | null, contentContainerStyle?: ViewStyle,
}
export type cardItem = {
key: string,
title: string,
subtitle: string,
image: string | number,
onPress: () => void,
}; };
export default class CardList extends React.Component<PropsType> { export type cardList = Array<cardItem>;
export default class CardList extends React.Component<Props> {
static defaultProps = { static defaultProps = {
isHorizontal: false, isHorizontal: false,
contentContainerStyle: null,
};
windowWidth: number;
horizontalItemSize: number;
constructor(props: PropsType) {
super(props);
this.windowWidth = Dimensions.get('window').width;
this.horizontalItemSize = this.windowWidth / 4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll
} }
getRenderItem = ({item}: {item: ServiceItemType}): React.Node => { windowWidth: number;
const {props} = this; horizontalItemSize: number;
if (props.isHorizontal)
return ( constructor(props: Props) {
<ImageListItem super(props);
item={item} this.windowWidth = Dimensions.get('window').width;
key={item.title} this.horizontalItemSize = this.windowWidth/4; // So that we can fit 3 items and a part of the 4th => user knows he can scroll
width={this.horizontalItemSize} }
/>
); renderItem = ({item}: { item: cardItem }) => {
return <CardListItem item={item} key={item.title} />; if (this.props.isHorizontal)
return <ImageListItem item={item} key={item.title} width={this.horizontalItemSize}/>;
else
return <CardListItem item={item} key={item.title}/>;
}; };
keyExtractor = (item: ServiceItemType): string => item.key; keyExtractor = (item: cardItem) => item.key;
render(): React.Node { render() {
const {props} = this;
let containerStyle = {}; let containerStyle = {};
if (props.isHorizontal) { if (this.props.isHorizontal) {
containerStyle = { containerStyle = {
height: this.horizontalItemSize + 50, height: this.horizontalItemSize + 50,
justifyContent: 'space-around', justifyContent: 'space-around',
@ -55,18 +57,15 @@ export default class CardList extends React.Component<PropsType> {
} }
return ( return (
<Animated.FlatList <Animated.FlatList
data={props.dataset} {...this.props}
renderItem={this.getRenderItem} data={this.props.dataset}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
numColumns={props.isHorizontal ? undefined : 2} numColumns={this.props.isHorizontal ? undefined : 2}
horizontal={props.isHorizontal} horizontal={this.props.isHorizontal}
contentContainerStyle={ contentContainerStyle={this.props.isHorizontal ? containerStyle : this.props.contentContainerStyle}
props.isHorizontal ? containerStyle : props.contentContainerStyle pagingEnabled={this.props.isHorizontal}
} snapToInterval={this.props.isHorizontal ? (this.horizontalItemSize+5)*3 : null}
pagingEnabled={props.isHorizontal}
snapToInterval={
props.isHorizontal ? (this.horizontalItemSize + 5) * 3 : null
}
/> />
); );
} }

View file

@ -2,23 +2,25 @@
import * as React from 'react'; import * as React from 'react';
import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper'; import {Caption, Card, Paragraph, TouchableRipple} from 'react-native-paper';
import {View} from 'react-native'; import {View} from "react-native";
import type {ServiceItemType} from '../../../managers/ServicesManager'; import type {cardItem} from "./CardList";
type PropsType = { type Props = {
item: ServiceItemType, item: cardItem,
}; }
export default class CardListItem extends React.Component<PropsType> { export default class CardListItem extends React.Component<Props> {
shouldComponentUpdate(): boolean {
shouldComponentUpdate() {
return false; return false;
} }
render(): React.Node { render() {
const {props} = this; const props = this.props;
const {item} = props; const item = props.item;
const source = const source = typeof item.image === "number"
typeof item.image === 'number' ? item.image : {uri: item.image}; ? item.image
: {uri: item.image};
return ( return (
<Card <Card
style={{ style={{
@ -26,16 +28,23 @@ export default class CardListItem extends React.Component<PropsType> {
margin: 5, margin: 5,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}}> }}
<TouchableRipple style={{flex: 1}} onPress={item.onPress}> >
<TouchableRipple
style={{flex: 1}}
onPress={item.onPress}>
<View> <View>
<Card.Cover style={{height: 80}} source={source} /> <Card.Cover
style={{height: 80}}
source={source}
/>
<Card.Content> <Card.Content>
<Paragraph>{item.title}</Paragraph> <Paragraph>{item.title}</Paragraph>
<Caption>{item.subtitle}</Caption> <Caption>{item.subtitle}</Caption>
</Card.Content> </Card.Content>
</View> </View>
</TouchableRipple> </TouchableRipple>
</Card> </Card>
); );
} }

View file

@ -3,47 +3,48 @@
import * as React from 'react'; import * as React from 'react';
import {Text, TouchableRipple} from 'react-native-paper'; import {Text, TouchableRipple} from 'react-native-paper';
import {Image, View} from 'react-native'; import {Image, View} from 'react-native';
import type {ServiceItemType} from '../../../managers/ServicesManager'; import type {cardItem} from "./CardList";
type PropsType = { type Props = {
item: ServiceItemType, item: cardItem,
width: number, width: number,
}; }
export default class ImageListItem extends React.Component<PropsType> { export default class ImageListItem extends React.Component<Props> {
shouldComponentUpdate(): boolean {
shouldComponentUpdate() {
return false; return false;
} }
render(): React.Node { render() {
const {props} = this; const item = this.props.item;
const {item} = props; const source = typeof item.image === "number"
const source = ? item.image
typeof item.image === 'number' ? item.image : {uri: item.image}; : {uri: item.image};
return ( return (
<TouchableRipple <TouchableRipple
style={{ style={{
width: props.width, width: this.props.width,
height: props.width + 40, height: this.props.width + 40,
margin: 5, margin: 5,
}} }}
onPress={item.onPress}> onPress={item.onPress}
>
<View> <View>
<Image <Image
style={{ style={{
width: props.width - 20, width: this.props.width - 20,
height: props.width - 20, height: this.props.width - 20,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}} }}
source={source} source={source}
/> />
<Text <Text style={{
style={{
marginTop: 5, marginTop: 5,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
textAlign: 'center', textAlign: 'center'
}}> }}>
{item.title} {item.title}
</Text> </Text>

View file

@ -2,21 +2,67 @@
import * as React from 'react'; import * as React from 'react';
import {Card, Chip, List, Text} from 'react-native-paper'; import {Card, Chip, List, Text} from 'react-native-paper';
import {StyleSheet, View} from 'react-native'; import {StyleSheet, View} from "react-native";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AnimatedAccordion from '../../Animations/AnimatedAccordion'; import AnimatedAccordion from "../../Animations/AnimatedAccordion";
import {isItemInCategoryFilter} from '../../../utils/Search'; import {isItemInCategoryFilter} from "../../../utils/Search";
import type {ClubCategoryType} from '../../../screens/Amicale/Clubs/ClubListScreen'; import type {category} from "../../../screens/Amicale/Clubs/ClubListScreen";
type PropsType = { type Props = {
categories: Array<ClubCategoryType>, categories: Array<category>,
onChipSelect: (id: number) => void, onChipSelect: (id: number) => void,
selectedCategories: Array<number>, selectedCategories: Array<number>,
}; }
class ClubListHeader extends React.Component<Props> {
shouldComponentUpdate(nextProps: Props) {
return nextProps.selectedCategories.length !== this.props.selectedCategories.length;
}
getChipRender = (category: category, key: string) => {
const onPress = () => this.props.onChipSelect(category.id);
return <Chip
selected={isItemInCategoryFilter(this.props.selectedCategories, [category.id])}
mode={'outlined'}
onPress={onPress}
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
key={key}
>
{category.name}
</Chip>;
};
getCategoriesRender() {
let final = [];
for (let i = 0; i < this.props.categories.length; i++) {
final.push(this.getChipRender(this.props.categories[i], this.props.categories[i].id.toString()));
}
return final;
}
render() {
return (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t("screens.clubs.categories")}
left={props => <List.Icon {...props} icon="star"/>}
opened={true}
>
<Text style={styles.text}>{i18n.t("screens.clubs.categoriesFilterMessage")}</Text>
<View style={styles.chipContainer}>
{this.getCategoriesRender()}
</View>
</AnimatedAccordion>
</Card>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
margin: 5, margin: 5
}, },
text: { text: {
paddingLeft: 0, paddingLeft: 0,
@ -34,58 +80,4 @@ const styles = StyleSheet.create({
}, },
}); });
class ClubListHeader extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return (
nextProps.selectedCategories.length !== props.selectedCategories.length
);
}
getChipRender = (category: ClubCategoryType, key: string): React.Node => {
const {props} = this;
const onPress = (): void => props.onChipSelect(category.id);
return (
<Chip
selected={isItemInCategoryFilter(props.selectedCategories, [
category.id,
null,
])}
mode="outlined"
onPress={onPress}
style={{marginRight: 5, marginLeft: 5, marginBottom: 5}}
key={key}>
{category.name}
</Chip>
);
};
getCategoriesRender(): React.Node {
const {props} = this;
const final = [];
props.categories.forEach((cat: ClubCategoryType) => {
final.push(this.getChipRender(cat, cat.id.toString()));
});
return final;
}
render(): React.Node {
return (
<Card style={styles.card}>
<AnimatedAccordion
title={i18n.t('screens.clubs.categories')}
left={({size}: {size: number}): React.Node => (
<List.Icon size={size} icon="star" />
)}
opened>
<Text style={styles.text}>
{i18n.t('screens.clubs.categoriesFilterMessage')}
</Text>
<View style={styles.chipContainer}>{this.getCategoriesRender()}</View>
</AnimatedAccordion>
</Card>
);
}
}
export default ClubListHeader; export default ClubListHeader;

View file

@ -2,88 +2,79 @@
import * as React from 'react'; import * as React from 'react';
import {Avatar, Chip, List, withTheme} from 'react-native-paper'; import {Avatar, Chip, List, withTheme} from 'react-native-paper';
import {View} from 'react-native'; import {View} from "react-native";
import type { import type {category, club} from "../../../screens/Amicale/Clubs/ClubListScreen";
ClubCategoryType, import type {CustomTheme} from "../../../managers/ThemeManager";
ClubType,
} from '../../../screens/Amicale/Clubs/ClubListScreen';
import type {CustomTheme} from '../../../managers/ThemeManager';
type PropsType = { type Props = {
onPress: () => void, onPress: () => void,
categoryTranslator: (id: number) => ClubCategoryType, categoryTranslator: (id: number) => category,
item: ClubType, item: club,
height: number, height: number,
theme: CustomTheme, theme: CustomTheme,
}; }
class ClubListItem extends React.Component<Props> {
class ClubListItem extends React.Component<PropsType> {
hasManagers: boolean; hasManagers: boolean;
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
this.hasManagers = props.item.responsibles.length > 0; this.hasManagers = props.item.responsibles.length > 0;
} }
shouldComponentUpdate(): boolean { shouldComponentUpdate() {
return false; return false;
} }
getCategoriesRender(categories: Array<number | null>): React.Node { getCategoriesRender(categories: Array<number | null>) {
const {props} = this; let final = [];
const final = []; for (let i = 0; i < categories.length; i++) {
categories.forEach((cat: number | null) => { if (categories[i] !== null) {
if (cat != null) { const category: category = this.props.categoryTranslator(categories[i]);
const category: ClubCategoryType = props.categoryTranslator(cat);
final.push( final.push(
<Chip <Chip
style={{marginRight: 5, marginBottom: 5}} style={{marginRight: 5, marginBottom: 5}}
key={`${props.item.id}:${category.id}`}> key={this.props.item.id + ':' + category.id}
>
{category.name} {category.name}
</Chip>, </Chip>
); );
} }
}); }
return <View style={{flexDirection: 'row'}}>{final}</View>; return <View style={{flexDirection: 'row'}}>{final}</View>;
} }
render(): React.Node { render() {
const {props} = this; const categoriesRender = this.getCategoriesRender.bind(this, this.props.item.category);
const categoriesRender = (): React.Node => const colors = this.props.theme.colors;
this.getCategoriesRender(props.item.category);
const {colors} = props.theme;
return ( return (
<List.Item <List.Item
title={props.item.name} title={this.props.item.name}
description={categoriesRender} description={categoriesRender}
onPress={props.onPress} onPress={this.props.onPress}
left={(): React.Node => ( left={(props) => <Avatar.Image
<Avatar.Image {...props}
style={{ style={{
backgroundColor: 'transparent', backgroundColor: 'transparent',
marginLeft: 10, marginLeft: 10,
marginRight: 10, marginRight: 10,
}} }}
size={64} size={64}
source={{uri: props.item.logo}} source={{uri: this.props.item.logo}}/>}
/> right={(props) => <Avatar.Icon
)} {...props}
right={(): React.Node => (
<Avatar.Icon
style={{ style={{
marginTop: 'auto', marginTop: 'auto',
marginBottom: 'auto', marginBottom: 'auto',
backgroundColor: 'transparent', backgroundColor: 'transparent',
}} }}
size={48} size={48}
icon={ icon={this.hasManagers ? "check-circle-outline" : "alert-circle-outline"}
this.hasManagers ? 'check-circle-outline' : 'alert-circle-outline'
}
color={this.hasManagers ? colors.success : colors.primary} color={this.hasManagers ? colors.success : colors.primary}
/> />}
)}
style={{ style={{
height: props.height, height: this.props.height,
justifyContent: 'center', justifyContent: 'center',
}} }}
/> />

View file

@ -1,12 +1,12 @@
// @flow // @flow
import type {ServiceItemType} from './ServicesManager'; import type {ServiceItem} from './ServicesManager';
import ServicesManager from './ServicesManager'; import ServicesManager from './ServicesManager';
import {getSublistWithIds} from '../utils/Utils'; import {getSublistWithIds} from '../utils/Utils';
import AsyncStorageManager from './AsyncStorageManager'; import AsyncStorageManager from './AsyncStorageManager';
export default class DashboardManager extends ServicesManager { export default class DashboardManager extends ServicesManager {
getCurrentDashboard(): Array<ServiceItemType | null> { getCurrentDashboard(): Array<ServiceItem | null> {
const dashboardIdList = AsyncStorageManager.getObject( const dashboardIdList = AsyncStorageManager.getObject(
AsyncStorageManager.PREFERENCES.dashboardItems.key, AsyncStorageManager.PREFERENCES.dashboardItems.key,
); );

View file

@ -1,107 +1,94 @@
// @flow // @flow
import i18n from 'i18n-js'; import i18n from "i18n-js";
import {StackNavigationProp} from '@react-navigation/stack'; import AvailableWebsites from "../constants/AvailableWebsites";
import AvailableWebsites from '../constants/AvailableWebsites'; import {StackNavigationProp} from "@react-navigation/stack";
import ConnectionManager from './ConnectionManager'; import ConnectionManager from "./ConnectionManager";
import type {FullDashboardType} from '../screens/Home/HomeScreen'; import type {fullDashboard} from "../screens/Home/HomeScreen";
import getStrippedServicesList from '../utils/Services';
// AMICALE // AMICALE
const CLUBS_IMAGE = const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png'; const PROFILE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png";
const PROFILE_IMAGE = const EQUIPMENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png'; const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png";
const EQUIPMENT_IMAGE = const AMICALE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png';
const VOTE_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png';
const AMICALE_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png';
// STUDENTS // STUDENTS
const PROXIMO_IMAGE = const PROXIMO_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png"
'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png'; const WIKETUD_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png";
const WIKETUD_IMAGE = const EE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png'; const TUTORINSA_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.png";
const EE_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png';
const TUTORINSA_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.png';
// INSA // INSA
const BIB_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/Bib.png'; const BIB_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bib.png";
const RU_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/RU.png'; const RU_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/RU.png";
const ROOM_IMAGE = const ROOM_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png'; const EMAIL_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png";
const EMAIL_IMAGE = const ENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png'; const ACCOUNT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Account.png";
const ENT_IMAGE = 'https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png';
const ACCOUNT_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/Account.png';
// SPECIAL // SPECIAL
const WASHER_IMAGE = const WASHER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png";
'https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png'; const DRYER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png";
const DRYER_IMAGE =
'https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.png';
const AMICALE_LOGO = require('../../assets/amicale.png'); const AMICALE_LOGO = require("../../assets/amicale.png");
export const SERVICES_KEY = { export const SERVICES_KEY = {
CLUBS: 'clubs', CLUBS: "clubs",
PROFILE: 'profile', PROFILE: "profile",
EQUIPMENT: 'equipment', EQUIPMENT: "equipment",
AMICALE_WEBSITE: 'amicale_website', AMICALE_WEBSITE: "amicale_website",
VOTE: 'vote', VOTE: "vote",
PROXIMO: 'proximo', PROXIMO: "proximo",
WIKETUD: 'wiketud', WIKETUD: "wiketud",
ELUS_ETUDIANTS: 'elus_etudiants', ELUS_ETUDIANTS: "elus_etudiants",
TUTOR_INSA: 'tutor_insa', TUTOR_INSA: "tutor_insa",
RU: 'ru', RU: "ru",
AVAILABLE_ROOMS: 'available_rooms', AVAILABLE_ROOMS: "available_rooms",
BIB: 'bib', BIB: "bib",
EMAIL: 'email', EMAIL: "email",
ENT: 'ent', ENT: "ent",
INSA_ACCOUNT: 'insa_account', INSA_ACCOUNT: "insa_account",
WASHERS: 'washers', WASHERS: "washers",
DRYERS: 'dryers', DRYERS: "dryers",
}; }
export const SERVICES_CATEGORIES_KEY = { export const SERVICES_CATEGORIES_KEY = {
AMICALE: 'amicale', AMICALE: "amicale",
STUDENTS: 'students', STUDENTS: "students",
INSA: 'insa', INSA: "insa",
SPECIAL: 'special', SPECIAL: "special",
}; }
export type ServiceItemType = {
export type ServiceItem = {
key: string, key: string,
title: string, title: string,
subtitle: string, subtitle: string,
image: string, image: string,
onPress: () => void, onPress: () => void,
badgeFunction?: (dashboard: FullDashboardType) => number, badgeFunction?: (dashboard: fullDashboard) => number,
}; }
export type ServiceCategoryType = { export type ServiceCategory = {
key: string, key: string,
title: string, title: string,
subtitle: string, subtitle: string,
image: string | number, image: string | number,
content: Array<ServiceItemType>, content: Array<ServiceItem>
}; }
export default class ServicesManager { export default class ServicesManager {
navigation: StackNavigationProp; navigation: StackNavigationProp;
amicaleDataset: Array<ServiceItemType>; amicaleDataset: Array<ServiceItem>;
studentsDataset: Array<ServiceItem>;
insaDataset: Array<ServiceItem>;
specialDataset: Array<ServiceItem>;
studentsDataset: Array<ServiceItemType>; categoriesDataset: Array<ServiceCategory>;
insaDataset: Array<ServiceItemType>;
specialDataset: Array<ServiceItemType>;
categoriesDataset: Array<ServiceCategoryType>;
constructor(nav: StackNavigationProp) { constructor(nav: StackNavigationProp) {
this.navigation = nav; this.navigation = nav;
@ -111,31 +98,30 @@ export default class ServicesManager {
title: i18n.t('screens.clubs.title'), title: i18n.t('screens.clubs.title'),
subtitle: i18n.t('screens.services.descriptions.clubs'), subtitle: i18n.t('screens.services.descriptions.clubs'),
image: CLUBS_IMAGE, image: CLUBS_IMAGE,
onPress: (): void => this.onAmicaleServicePress('club-list'), onPress: () => this.onAmicaleServicePress("club-list"),
}, },
{ {
key: SERVICES_KEY.PROFILE, key: SERVICES_KEY.PROFILE,
title: i18n.t('screens.profile.title'), title: i18n.t('screens.profile.title'),
subtitle: i18n.t('screens.services.descriptions.profile'), subtitle: i18n.t('screens.services.descriptions.profile'),
image: PROFILE_IMAGE, image: PROFILE_IMAGE,
onPress: (): void => this.onAmicaleServicePress('profile'), onPress: () => this.onAmicaleServicePress("profile"),
}, },
{ {
key: SERVICES_KEY.EQUIPMENT, key: SERVICES_KEY.EQUIPMENT,
title: i18n.t('screens.equipment.title'), title: i18n.t('screens.equipment.title'),
subtitle: i18n.t('screens.services.descriptions.equipment'), subtitle: i18n.t('screens.services.descriptions.equipment'),
image: EQUIPMENT_IMAGE, image: EQUIPMENT_IMAGE,
onPress: (): void => this.onAmicaleServicePress('equipment-list'), onPress: () => this.onAmicaleServicePress("equipment-list"),
}, },
{ {
key: SERVICES_KEY.AMICALE_WEBSITE, key: SERVICES_KEY.AMICALE_WEBSITE,
title: i18n.t('screens.websites.amicale'), title: i18n.t('screens.websites.amicale'),
subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'), subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'),
image: AMICALE_IMAGE, image: AMICALE_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.AMICALE, host: AvailableWebsites.websites.AMICALE,
title: i18n.t('screens.websites.amicale'), title: i18n.t('screens.websites.amicale')
}), }),
}, },
{ {
@ -143,7 +129,7 @@ export default class ServicesManager {
title: i18n.t('screens.vote.title'), title: i18n.t('screens.vote.title'),
subtitle: i18n.t('screens.services.descriptions.vote'), subtitle: i18n.t('screens.services.descriptions.vote'),
image: VOTE_IMAGE, image: VOTE_IMAGE,
onPress: (): void => this.onAmicaleServicePress('vote'), onPress: () => this.onAmicaleServicePress("vote"),
}, },
]; ];
this.studentsDataset = [ this.studentsDataset = [
@ -152,30 +138,24 @@ export default class ServicesManager {
title: i18n.t('screens.proximo.title'), title: i18n.t('screens.proximo.title'),
subtitle: i18n.t('screens.services.descriptions.proximo'), subtitle: i18n.t('screens.services.descriptions.proximo'),
image: PROXIMO_IMAGE, image: PROXIMO_IMAGE,
onPress: (): void => nav.navigate('proximo'), onPress: () => nav.navigate("proximo"),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.proximo_articles
dashboard.proximo_articles,
}, },
{ {
key: SERVICES_KEY.WIKETUD, key: SERVICES_KEY.WIKETUD,
title: 'Wiketud', title: "Wiketud",
subtitle: i18n.t('screens.services.descriptions.wiketud'), subtitle: i18n.t('screens.services.descriptions.wiketud'),
image: WIKETUD_IMAGE, image: WIKETUD_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {host: AvailableWebsites.websites.WIKETUD, title: "Wiketud"}),
nav.navigate('website', {
host: AvailableWebsites.websites.WIKETUD,
title: 'Wiketud',
}),
}, },
{ {
key: SERVICES_KEY.ELUS_ETUDIANTS, key: SERVICES_KEY.ELUS_ETUDIANTS,
title: 'Élus Étudiants', title: "Élus Étudiants",
subtitle: i18n.t('screens.services.descriptions.elusEtudiants'), subtitle: i18n.t('screens.services.descriptions.elusEtudiants'),
image: EE_IMAGE, image: EE_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.ELUS_ETUDIANTS, host: AvailableWebsites.websites.ELUS_ETUDIANTS,
title: 'Élus Étudiants', title: "Élus Étudiants"
}), }),
}, },
{ {
@ -183,13 +163,11 @@ export default class ServicesManager {
title: "Tutor'INSA", title: "Tutor'INSA",
subtitle: i18n.t('screens.services.descriptions.tutorInsa'), subtitle: i18n.t('screens.services.descriptions.tutorInsa'),
image: TUTORINSA_IMAGE, image: TUTORINSA_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.TUTOR_INSA, host: AvailableWebsites.websites.TUTOR_INSA,
title: "Tutor'INSA", title: "Tutor'INSA"
}), }),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.available_tutorials
dashboard.available_tutorials,
}, },
]; ];
this.insaDataset = [ this.insaDataset = [
@ -198,19 +176,17 @@ export default class ServicesManager {
title: i18n.t('screens.menu.title'), title: i18n.t('screens.menu.title'),
subtitle: i18n.t('screens.services.descriptions.self'), subtitle: i18n.t('screens.services.descriptions.self'),
image: RU_IMAGE, image: RU_IMAGE,
onPress: (): void => nav.navigate('self-menu'), onPress: () => nav.navigate("self-menu"),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.today_menu.length
dashboard.today_menu.length,
}, },
{ {
key: SERVICES_KEY.AVAILABLE_ROOMS, key: SERVICES_KEY.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'), title: i18n.t('screens.websites.rooms'),
subtitle: i18n.t('screens.services.descriptions.availableRooms'), subtitle: i18n.t('screens.services.descriptions.availableRooms'),
image: ROOM_IMAGE, image: ROOM_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.AVAILABLE_ROOMS, host: AvailableWebsites.websites.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'), title: i18n.t('screens.websites.rooms')
}), }),
}, },
{ {
@ -218,10 +194,9 @@ export default class ServicesManager {
title: i18n.t('screens.websites.bib'), title: i18n.t('screens.websites.bib'),
subtitle: i18n.t('screens.services.descriptions.bib'), subtitle: i18n.t('screens.services.descriptions.bib'),
image: BIB_IMAGE, image: BIB_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.BIB, host: AvailableWebsites.websites.BIB,
title: i18n.t('screens.websites.bib'), title: i18n.t('screens.websites.bib')
}), }),
}, },
{ {
@ -229,10 +204,9 @@ export default class ServicesManager {
title: i18n.t('screens.websites.mails'), title: i18n.t('screens.websites.mails'),
subtitle: i18n.t('screens.services.descriptions.mails'), subtitle: i18n.t('screens.services.descriptions.mails'),
image: EMAIL_IMAGE, image: EMAIL_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.BLUEMIND, host: AvailableWebsites.websites.BLUEMIND,
title: i18n.t('screens.websites.mails'), title: i18n.t('screens.websites.mails')
}), }),
}, },
{ {
@ -240,10 +214,9 @@ export default class ServicesManager {
title: i18n.t('screens.websites.ent'), title: i18n.t('screens.websites.ent'),
subtitle: i18n.t('screens.services.descriptions.ent'), subtitle: i18n.t('screens.services.descriptions.ent'),
image: ENT_IMAGE, image: ENT_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.ENT, host: AvailableWebsites.websites.ENT,
title: i18n.t('screens.websites.ent'), title: i18n.t('screens.websites.ent')
}), }),
}, },
{ {
@ -251,10 +224,9 @@ export default class ServicesManager {
title: i18n.t('screens.insaAccount.title'), title: i18n.t('screens.insaAccount.title'),
subtitle: i18n.t('screens.services.descriptions.insaAccount'), subtitle: i18n.t('screens.services.descriptions.insaAccount'),
image: ACCOUNT_IMAGE, image: ACCOUNT_IMAGE,
onPress: (): void => onPress: () => nav.navigate("website", {
nav.navigate('website', {
host: AvailableWebsites.websites.INSA_ACCOUNT, host: AvailableWebsites.websites.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title'), title: i18n.t('screens.insaAccount.title')
}), }),
}, },
]; ];
@ -264,48 +236,46 @@ export default class ServicesManager {
title: i18n.t('screens.proxiwash.washers'), title: i18n.t('screens.proxiwash.washers'),
subtitle: i18n.t('screens.services.descriptions.washers'), subtitle: i18n.t('screens.services.descriptions.washers'),
image: WASHER_IMAGE, image: WASHER_IMAGE,
onPress: (): void => nav.navigate('proxiwash'), onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.available_washers
dashboard.available_washers,
}, },
{ {
key: SERVICES_KEY.DRYERS, key: SERVICES_KEY.DRYERS,
title: i18n.t('screens.proxiwash.dryers'), title: i18n.t('screens.proxiwash.dryers'),
subtitle: i18n.t('screens.services.descriptions.washers'), subtitle: i18n.t('screens.services.descriptions.washers'),
image: DRYER_IMAGE, image: DRYER_IMAGE,
onPress: (): void => nav.navigate('proxiwash'), onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: FullDashboardType): number => badgeFunction: (dashboard: fullDashboard) => dashboard.available_dryers
dashboard.available_dryers, }
},
]; ];
this.categoriesDataset = [ this.categoriesDataset = [
{ {
key: SERVICES_CATEGORIES_KEY.AMICALE, key: SERVICES_CATEGORIES_KEY.AMICALE,
title: i18n.t('screens.services.categories.amicale'), title: i18n.t("screens.services.categories.amicale"),
subtitle: i18n.t('screens.services.more'), subtitle: i18n.t("screens.services.more"),
image: AMICALE_LOGO, image: AMICALE_LOGO,
content: this.amicaleDataset, content: this.amicaleDataset
}, },
{ {
key: SERVICES_CATEGORIES_KEY.STUDENTS, key: SERVICES_CATEGORIES_KEY.STUDENTS,
title: i18n.t('screens.services.categories.students'), title: i18n.t("screens.services.categories.students"),
subtitle: i18n.t('screens.services.more'), subtitle: i18n.t("screens.services.more"),
image: 'account-group', image: 'account-group',
content: this.studentsDataset, content: this.studentsDataset
}, },
{ {
key: SERVICES_CATEGORIES_KEY.INSA, key: SERVICES_CATEGORIES_KEY.INSA,
title: i18n.t('screens.services.categories.insa'), title: i18n.t("screens.services.categories.insa"),
subtitle: i18n.t('screens.services.more'), subtitle: i18n.t("screens.services.more"),
image: 'school', image: 'school',
content: this.insaDataset, content: this.insaDataset
}, },
{ {
key: SERVICES_CATEGORIES_KEY.SPECIAL, key: SERVICES_CATEGORIES_KEY.SPECIAL,
title: i18n.t('screens.services.categories.special'), title: i18n.t("screens.services.categories.special"),
subtitle: i18n.t('screens.services.categories.special'), subtitle: i18n.t("screens.services.categories.special"),
image: 'star', image: 'star',
content: this.specialDataset, content: this.specialDataset
}, },
]; ];
} }
@ -319,18 +289,37 @@ export default class ServicesManager {
onAmicaleServicePress(route: string) { onAmicaleServicePress(route: string) {
if (ConnectionManager.getInstance().isLoggedIn()) if (ConnectionManager.getInstance().isLoggedIn())
this.navigation.navigate(route); this.navigation.navigate(route);
else this.navigation.navigate('login', {nextScreen: route}); else
this.navigation.navigate("login", {nextScreen: route});
}
/**
* Gets the given services list without items of the given ids
*
* @param idList The ids of items to remove
* @param sourceList The item list to use as source
* @returns {[]}
*/
getStrippedList(idList: Array<string>, sourceList: Array<{key: string, [key: string]: any}>) {
let newArray = [];
for (let i = 0; i < sourceList.length; i++) {
const item = sourceList[i];
if (!(idList.includes(item.key)))
newArray.push(item);
}
return newArray;
} }
/** /**
* Gets the list of amicale's services * Gets the list of amicale's services
* *
* @param excludedItems Ids of items to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceItemType>} * @returns {Array<ServiceItem>}
*/ */
getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> { getAmicaleServices(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.amicaleDataset); return this.getStrippedList(excludedItems, this.amicaleDataset)
else
return this.amicaleDataset; return this.amicaleDataset;
} }
@ -338,11 +327,12 @@ export default class ServicesManager {
* Gets the list of students' services * Gets the list of students' services
* *
* @param excludedItems Ids of items to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceItemType>} * @returns {Array<ServiceItem>}
*/ */
getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> { getStudentServices(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.studentsDataset); return this.getStrippedList(excludedItems, this.studentsDataset)
else
return this.studentsDataset; return this.studentsDataset;
} }
@ -350,11 +340,12 @@ export default class ServicesManager {
* Gets the list of INSA's services * Gets the list of INSA's services
* *
* @param excludedItems Ids of items to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceItemType>} * @returns {Array<ServiceItem>}
*/ */
getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> { getINSAServices(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.insaDataset); return this.getStrippedList(excludedItems, this.insaDataset)
else
return this.insaDataset; return this.insaDataset;
} }
@ -362,11 +353,12 @@ export default class ServicesManager {
* Gets the list of special services * Gets the list of special services
* *
* @param excludedItems Ids of items to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceItemType>} * @returns {Array<ServiceItem>}
*/ */
getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> { getSpecialServices(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.specialDataset); return this.getStrippedList(excludedItems, this.specialDataset)
else
return this.specialDataset; return this.specialDataset;
} }
@ -374,11 +366,13 @@ export default class ServicesManager {
* Gets all services sorted by category * Gets all services sorted by category
* *
* @param excludedItems Ids of categories to exclude from the returned list * @param excludedItems Ids of categories to exclude from the returned list
* @returns {Array<ServiceCategoryType>} * @returns {Array<ServiceCategory>}
*/ */
getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> { getCategories(excludedItems?: Array<string>) {
if (excludedItems != null) if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.categoriesDataset); return this.getStrippedList(excludedItems, this.categoriesDataset)
else
return this.categoriesDataset; return this.categoriesDataset;
} }
} }

View file

@ -4,44 +4,44 @@ import * as React from 'react';
import {Image, View} from 'react-native'; import {Image, View} from 'react-native';
import {Card, List, Text, withTheme} from 'react-native-paper'; 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";
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
import AMICALE_ICON from '../../../../assets/amicale.png';
type Props = {};
const CONTACT_LINK = 'clubs@amicale-insat.fr'; const CONTACT_LINK = 'clubs@amicale-insat.fr';
// eslint-disable-next-line react/prefer-stateless-function class ClubAboutScreen extends React.Component<Props> {
class ClubAboutScreen extends React.Component<null> {
render(): React.Node { render() {
return ( return (
<CollapsibleScrollView style={{padding: 5}}> <CollapsibleScrollView style={{padding: 5}}>
<View <View style={{
style={{
width: '100%', width: '100%',
height: 100, height: 100,
marginTop: 20, marginTop: 20,
marginBottom: 20, marginBottom: 20,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center'
}}> }}>
<Image <Image
source={AMICALE_ICON} source={require('../../../../assets/amicale.png')}
style={{flex: 1, resizeMode: 'contain'}} style={{flex: 1, resizeMode: "contain"}}
resizeMode="contain" resizeMode="contain"/>
/>
</View> </View>
<Text>{i18n.t('screens.clubs.about.text')}</Text> <Text>{i18n.t("screens.clubs.about.text")}</Text>
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Title <Card.Title
title={i18n.t('screens.clubs.about.title')} title={i18n.t("screens.clubs.about.title")}
subtitle={i18n.t('screens.clubs.about.subtitle')} subtitle={i18n.t("screens.clubs.about.subtitle")}
left={({size}: {size: number}): React.Node => ( left={props => <List.Icon {...props} icon={'information'}/>}
<List.Icon size={size} icon="information" />
)}
/> />
<Card.Content> <Card.Content>
<Text>{i18n.t('screens.clubs.about.message')}</Text> <Text>{i18n.t("screens.clubs.about.message")}</Text>
<Autolink text={CONTACT_LINK} component={Text} /> <Autolink
text={CONTACT_LINK}
component={Text}
/>
</Card.Content> </Card.Content>
</Card> </Card>
</CollapsibleScrollView> </CollapsibleScrollView>

View file

@ -2,70 +2,65 @@
import * as React from 'react'; import * as React from 'react';
import {Linking, View} from 'react-native'; import {Linking, View} from 'react-native';
import { import {Avatar, Button, Card, Chip, Paragraph, withTheme} from 'react-native-paper';
Avatar,
Button,
Card,
Chip,
Paragraph,
withTheme,
} from 'react-native-paper';
import ImageModal from 'react-native-image-modal'; import ImageModal from 'react-native-image-modal';
import i18n from 'i18n-js'; import i18n from "i18n-js";
import {StackNavigationProp} from '@react-navigation/stack'; import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; import CustomHTML from "../../../components/Overrides/CustomHTML";
import CustomHTML from '../../../components/Overrides/CustomHTML'; import CustomTabBar from "../../../components/Tabbar/CustomTabBar";
import CustomTabBar from '../../../components/Tabbar/CustomTabBar'; import type {category, club} from "./ClubListScreen";
import type {ClubCategoryType, ClubType} from './ClubListScreen'; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {CustomTheme} from '../../../managers/ThemeManager'; import {StackNavigationProp} from "@react-navigation/stack";
import {ERROR_TYPE} from '../../../utils/WebData'; import {ERROR_TYPE} from "../../../utils/WebData";
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView";
import type {ApiGenericDataType} from '../../../utils/WebData';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
data?: ClubType, data?: club,
categories?: Array<ClubCategoryType>, categories?: Array<category>,
clubId?: number, clubId?: number,
}, ...
}, },
... theme: CustomTheme
},
theme: CustomTheme,
}; };
const AMICALE_MAIL = 'clubs@amicale-insat.fr'; type State = {
imageModalVisible: boolean,
};
const AMICALE_MAIL = "clubs@amicale-insat.fr";
/** /**
* Class defining a club event information page. * Class defining a club event information page.
* If called with data and categories navigation parameters, will use those to display the data. * If called with data and categories navigation parameters, will use those to display the data.
* If called with clubId parameter, will fetch the information on the server * If called with clubId parameter, will fetch the information on the server
*/ */
class ClubDisplayScreen extends React.Component<PropsType> { class ClubDisplayScreen extends React.Component<Props, State> {
displayData: ClubType | null;
categories: Array<ClubCategoryType> | null;
displayData: club | null;
categories: Array<category> | null;
clubId: number; clubId: number;
shouldFetchData: boolean; shouldFetchData: boolean;
constructor(props: PropsType) { state = {
imageModalVisible: false,
};
constructor(props) {
super(props); super(props);
if (props.route.params != null) { if (this.props.route.params != null) {
if ( if (this.props.route.params.data != null && this.props.route.params.categories != null) {
props.route.params.data != null && this.displayData = this.props.route.params.data;
props.route.params.categories != null this.categories = this.props.route.params.categories;
) { this.clubId = this.props.route.params.data.id;
this.displayData = props.route.params.data;
this.categories = props.route.params.categories;
this.clubId = props.route.params.data.id;
this.shouldFetchData = false; this.shouldFetchData = false;
} else if (props.route.params.clubId != null) { } else if (this.props.route.params.clubId != null) {
this.displayData = null; this.displayData = null;
this.categories = null; this.categories = null;
this.clubId = props.route.params.clubId; this.clubId = this.props.route.params.clubId;
this.shouldFetchData = true; this.shouldFetchData = true;
} }
} }
@ -77,14 +72,14 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param id The category's ID * @param id The category's ID
* @returns {string|*} * @returns {string|*}
*/ */
getCategoryName(id: number): string { getCategoryName(id: number) {
let categoryName = '';
if (this.categories !== null) { if (this.categories !== null) {
this.categories.forEach((item: ClubCategoryType) => { for (let i = 0; i < this.categories.length; i++) {
if (id === item.id) categoryName = item.name; if (id === this.categories[i].id)
}); return this.categories[i].name;
} }
return categoryName; }
return "";
} }
/** /**
@ -93,19 +88,23 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param categories The categories to display (max 2) * @param categories The categories to display (max 2)
* @returns {null|*} * @returns {null|*}
*/ */
getCategoriesRender(categories: Array<number | null>): React.Node { getCategoriesRender(categories: [number, number]) {
if (this.categories == null) return null; if (this.categories === null)
return null;
const final = []; let final = [];
categories.forEach((cat: number | null) => { for (let i = 0; i < categories.length; i++) {
if (cat != null) { let cat = categories[i];
if (cat !== null) {
final.push( final.push(
<Chip style={{marginRight: 5}} key={cat}> <Chip
style={{marginRight: 5}}
key={i.toString()}>
{this.getCategoryName(cat)} {this.getCategoryName(cat)}
</Chip>, </Chip>
); );
} }
}); }
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>; return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
} }
@ -116,39 +115,26 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param email The club contact email * @param email The club contact email
* @returns {*} * @returns {*}
*/ */
getManagersRender(managers: Array<string>, email: string | null): React.Node { getManagersRender(managers: Array<string>, email: string | null) {
const {props} = this; let managersListView = [];
const managersListView = []; for (let i = 0; i < managers.length; i++) {
managers.forEach((item: string) => { managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
managersListView.push(<Paragraph key={item}>{item}</Paragraph>); }
});
const hasManagers = managers.length > 0; const hasManagers = managers.length > 0;
return ( return (
<Card <Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title <Card.Title
title={i18n.t('screens.clubs.managers')} title={i18n.t('screens.clubs.managers')}
subtitle={ subtitle={hasManagers ? i18n.t('screens.clubs.managersSubtitle') : i18n.t('screens.clubs.managersUnavailable')}
hasManagers left={(props) => <Avatar.Icon
? i18n.t('screens.clubs.managersSubtitle') {...props}
: i18n.t('screens.clubs.managersUnavailable')
}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
color={ color={hasManagers ? this.props.theme.colors.success : this.props.theme.colors.primary}
hasManagers icon="account-tie"/>}
? props.theme.colors.success
: props.theme.colors.primary
}
icon="account-tie"
/>
)}
/> />
<Card.Content> <Card.Content>
{managersListView} {managersListView}
{ClubDisplayScreen.getEmailButton(email, hasManagers)} {this.getEmailButton(email, hasManagers)}
</Card.Content> </Card.Content>
</Card> </Card>
); );
@ -161,45 +147,51 @@ class ClubDisplayScreen extends React.Component<PropsType> {
* @param hasManagers True if the club has managers * @param hasManagers True if the club has managers
* @returns {*} * @returns {*}
*/ */
static getEmailButton( getEmailButton(email: string | null, hasManagers: boolean) {
email: string | null, const destinationEmail = email != null && hasManagers
hasManagers: boolean, ? email
): React.Node { : AMICALE_MAIL;
const destinationEmail = const text = email != null && hasManagers
email != null && hasManagers ? email : AMICALE_MAIL; ? i18n.t("screens.clubs.clubContact")
const text = : i18n.t("screens.clubs.amicaleContact");
email != null && hasManagers
? i18n.t('screens.clubs.clubContact')
: i18n.t('screens.clubs.amicaleContact');
return ( return (
<Card.Actions> <Card.Actions>
<Button <Button
icon="email" icon="email"
mode="contained" mode="contained"
onPress={() => { onPress={() => Linking.openURL('mailto:' + destinationEmail)}
Linking.openURL(`mailto:${destinationEmail}`); style={{marginLeft: 'auto'}}
}} >
style={{marginLeft: 'auto'}}>
{text} {text}
</Button> </Button>
</Card.Actions> </Card.Actions>
); );
} }
getScreen = (response: Array<ApiGenericDataType | null>): React.Node => { /**
const {props} = this; * Updates the header title to match the given club
let data: ClubType | null = null; *
* @param data The club data
*/
updateHeaderTitle(data: club) {
this.props.navigation.setOptions({title: data.name})
}
getScreen = (response: Array<{ [key: string]: any } | null>) => {
let data: club | null = null;
if (response[0] != null) { if (response[0] != null) {
[data] = response; data = response[0];
this.updateHeaderTitle(data); this.updateHeaderTitle(data);
} }
if (data != null) { if (data != null) {
return ( return (
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab> <CollapsibleScrollView
style={{paddingLeft: 5, paddingRight: 5}}
hasTab={true}
>
{this.getCategoriesRender(data.category)} {this.getCategoriesRender(data.category)}
{data.logo !== null ? ( {data.logo !== null ?
<View <View style={{
style={{
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
marginTop: 10, marginTop: 10,
@ -207,7 +199,7 @@ class ClubDisplayScreen extends React.Component<PropsType> {
}}> }}>
<ImageModal <ImageModal
resizeMode="contain" resizeMode="contain"
imageBackgroundColor={props.theme.colors.background} imageBackgroundColor={this.props.theme.colors.background}
style={{ style={{
width: 300, width: 300,
height: 300, height: 300,
@ -217,59 +209,43 @@ class ClubDisplayScreen extends React.Component<PropsType> {
}} }}
/> />
</View> </View>
) : ( : <View/>}
<View />
)}
{data.description !== null ? ( {data.description !== null ?
// Surround description with div to allow text styling if the description is not html // Surround description with div to allow text styling if the description is not html
<Card.Content> <Card.Content>
<CustomHTML html={data.description} /> <CustomHTML html={data.description}/>
</Card.Content> </Card.Content>
) : ( : <View/>}
<View />
)}
{this.getManagersRender(data.responsibles, data.email)} {this.getManagersRender(data.responsibles, data.email)}
</CollapsibleScrollView> </CollapsibleScrollView>
); );
} } else
return null; return null;
}; };
/** render() {
* Updates the header title to match the given club
*
* @param data The club data
*/
updateHeaderTitle(data: ClubType) {
const {props} = this;
props.navigation.setOptions({title: data.name});
}
render(): React.Node {
const {props} = this;
if (this.shouldFetchData) if (this.shouldFetchData)
return ( return <AuthenticatedScreen
<AuthenticatedScreen {...this.props}
navigation={props.navigation}
requests={[ requests={[
{ {
link: 'clubs/info', link: 'clubs/info',
params: {id: this.clubId}, params: {'id': this.clubId},
mandatory: true, mandatory: true
}, }
]} ]}
renderFunction={this.getScreen} renderFunction={this.getScreen}
errorViewOverride={[ errorViewOverride={[
{ {
errorCode: ERROR_TYPE.BAD_INPUT, errorCode: ERROR_TYPE.BAD_INPUT,
message: i18n.t('screens.clubs.invalidClub'), message: i18n.t("screens.clubs.invalidClub"),
icon: 'account-question', icon: "account-question",
showRetryButton: false, showRetryButton: false
}, }
]} ]}
/> />;
); else
return this.getScreen([this.displayData]); return this.getScreen([this.displayData]);
} }
} }

View file

@ -1,85 +1,92 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Platform} from 'react-native'; import {Platform} from "react-native";
import {Searchbar} from 'react-native-paper'; import {Searchbar} from 'react-native-paper';
import i18n from 'i18n-js'; import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
import {StackNavigationProp} from '@react-navigation/stack'; import i18n from "i18n-js";
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; import ClubListItem from "../../../components/Lists/Clubs/ClubListItem";
import ClubListItem from '../../../components/Lists/Clubs/ClubListItem'; import {isItemInCategoryFilter, stringMatchQuery} from "../../../utils/Search";
import {isItemInCategoryFilter, stringMatchQuery} from '../../../utils/Search'; import ClubListHeader from "../../../components/Lists/Clubs/ClubListHeader";
import ClubListHeader from '../../../components/Lists/Clubs/ClubListHeader'; import MaterialHeaderButtons, {Item} from "../../../components/Overrides/CustomHeaderButton";
import MaterialHeaderButtons, { import {StackNavigationProp} from "@react-navigation/stack";
Item, import type {CustomTheme} from "../../../managers/ThemeManager";
} from '../../../components/Overrides/CustomHeaderButton'; import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList";
import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
export type ClubCategoryType = { export type category = {
id: number, id: number,
name: string, name: string,
}; };
export type ClubType = { export type club = {
id: number, id: number,
name: string, name: string,
description: string, description: string,
logo: string, logo: string,
email: string | null, email: string | null,
category: Array<number | null>, category: [number, number],
responsibles: Array<string>, responsibles: Array<string>,
}; };
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
}; theme: CustomTheme,
}
type StateType = { type State = {
currentlySelectedCategories: Array<number>, currentlySelectedCategories: Array<number>,
currentSearchString: string, currentSearchString: string,
}; }
const LIST_ITEM_HEIGHT = 96; const LIST_ITEM_HEIGHT = 96;
class ClubListScreen extends React.Component<PropsType, StateType> { class ClubListScreen extends React.Component<Props, State> {
categories: Array<ClubCategoryType>;
constructor() { state = {
super();
this.state = {
currentlySelectedCategories: [], currentlySelectedCategories: [],
currentSearchString: '', currentSearchString: '',
}; };
}
categories: Array<category>;
/** /**
* Creates the header content * Creates the header content
*/ */
componentDidMount() { componentDidMount() {
const {props} = this; this.props.navigation.setOptions({
props.navigation.setOptions({
headerTitle: this.getSearchBar, headerTitle: this.getSearchBar,
headerRight: this.getHeaderButtons, headerRight: this.getHeaderButtons,
headerBackTitleVisible: false, headerBackTitleVisible: false,
headerTitleContainerStyle: headerTitleContainerStyle: Platform.OS === 'ios' ?
Platform.OS === 'ios' {marginHorizontal: 0, width: '70%'} :
? {marginHorizontal: 0, width: '70%'} {marginHorizontal: 0, right: 50, left: 50},
: {marginHorizontal: 0, right: 50, left: 50},
}); });
} }
/** /**
* Callback used when clicking an article in the list. * Gets the header search bar
* It opens the modal to show detailed information about the article
* *
* @param item The article pressed * @return {*}
*/ */
onListItemPress(item: ClubType) { getSearchBar = () => {
const {props} = this; return (
props.navigation.navigate('club-information', { <Searchbar
data: item, placeholder={i18n.t('screens.proximo.search')}
categories: this.categories, onChangeText={this.onSearchStringChange}
}); />
} );
};
/**
* Gets the header button
* @return {*}
*/
getHeaderButtons = () => {
const onPress = () => this.props.navigation.navigate("club-about");
return <MaterialHeaderButtons>
<Item title="main" iconName="information" onPress={onPress}/>
</MaterialHeaderButtons>;
};
/** /**
* Callback used when the search changes * Callback used when the search changes
@ -90,46 +97,11 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
this.updateFilteredData(str, null); this.updateFilteredData(str, null);
}; };
/** keyExtractor = (item: club) => item.id.toString();
* Gets the header search bar
*
* @return {*}
*/
getSearchBar = (): React.Node => {
return (
<Searchbar
placeholder={i18n.t('screens.proximo.search')}
onChangeText={this.onSearchStringChange}
/>
);
};
onChipSelect = (id: number) => { itemLayout = (data, index) => ({length: LIST_ITEM_HEIGHT, offset: LIST_ITEM_HEIGHT * index, index});
this.updateFilteredData(null, id);
};
/** getScreen = (data: Array<{ categories: Array<category>, clubs: Array<club> } | null>) => {
* Gets the header button
* @return {*}
*/
getHeaderButtons = (): React.Node => {
const onPress = () => {
const {props} = this;
props.navigation.navigate('club-about');
};
return (
<MaterialHeaderButtons>
<Item title="main" iconName="information" onPress={onPress} />
</MaterialHeaderButtons>
);
};
getScreen = (
data: Array<{
categories: Array<ClubCategoryType>,
clubs: Array<ClubType>,
} | null>,
): React.Node => {
let categoryList = []; let categoryList = [];
let clubList = []; let clubList = [];
if (data[0] != null) { if (data[0] != null) {
@ -144,69 +116,13 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()} ListHeaderComponent={this.getListHeader()}
// Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration // Performance props, see https://reactnative.dev/docs/optimizing-flatlist-configuration
removeClippedSubviews removeClippedSubviews={true}
getItemLayout={this.itemLayout} getItemLayout={this.itemLayout}
/> />
); )
}; };
/** onChipSelect = (id: number) => this.updateFilteredData(null, id);
* Gets the list header, with controls to change the categories filter
*
* @returns {*}
*/
getListHeader(): React.Node {
const {state} = this;
return (
<ClubListHeader
categories={this.categories}
selectedCategories={state.currentlySelectedCategories}
onChipSelect={this.onChipSelect}
/>
);
}
/**
* Gets the category object of the given ID
*
* @param id The ID of the category to find
* @returns {*}
*/
getCategoryOfId = (id: number): ClubCategoryType | null => {
let cat = null;
this.categories.forEach((item: ClubCategoryType) => {
if (id === item.id) cat = item;
});
return cat;
};
getRenderItem = ({item}: {item: ClubType}): React.Node => {
const onPress = () => {
this.onListItemPress(item);
};
if (this.shouldRenderItem(item)) {
return (
<ClubListItem
categoryTranslator={this.getCategoryOfId}
item={item}
onPress={onPress}
height={LIST_ITEM_HEIGHT}
/>
);
}
return null;
};
keyExtractor = (item: ClubType): string => item.id.toString();
itemLayout = (
data: {...},
index: number,
): {length: number, offset: number, index: number} => ({
length: LIST_ITEM_HEIGHT,
offset: LIST_ITEM_HEIGHT * index,
index,
});
/** /**
* Updates the search string and category filter, saving them to the State. * Updates the search string and category filter, saving them to the State.
@ -218,49 +134,99 @@ class ClubListScreen extends React.Component<PropsType, StateType> {
* @param categoryId The category to add/remove from the filter * @param categoryId The category to add/remove from the filter
*/ */
updateFilteredData(filterStr: string | null, categoryId: number | null) { updateFilteredData(filterStr: string | null, categoryId: number | null) {
const {state} = this; let newCategoriesState = [...this.state.currentlySelectedCategories];
const newCategoriesState = [...state.currentlySelectedCategories]; let newStrState = this.state.currentSearchString;
let newStrState = state.currentSearchString; if (filterStr !== null)
if (filterStr !== null) newStrState = filterStr; newStrState = filterStr;
if (categoryId !== null) { if (categoryId !== null) {
const index = newCategoriesState.indexOf(categoryId); let index = newCategoriesState.indexOf(categoryId);
if (index === -1) newCategoriesState.push(categoryId); if (index === -1)
else newCategoriesState.splice(index, 1); newCategoriesState.push(categoryId);
else
newCategoriesState.splice(index, 1);
} }
if (filterStr !== null || categoryId !== null) if (filterStr !== null || categoryId !== null)
this.setState({ this.setState({
currentSearchString: newStrState, currentSearchString: newStrState,
currentlySelectedCategories: newCategoriesState, currentlySelectedCategories: newCategoriesState,
}); })
} }
/**
* Gets the list header, with controls to change the categories filter
*
* @returns {*}
*/
getListHeader() {
return <ClubListHeader
categories={this.categories}
selectedCategories={this.state.currentlySelectedCategories}
onChipSelect={this.onChipSelect}
/>;
}
/**
* 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)
return this.categories[i];
}
};
/** /**
* Checks if the given item should be rendered according to current name and category filters * Checks if the given item should be rendered according to current name and category filters
* *
* @param item The club to check * @param item The club to check
* @returns {boolean} * @returns {boolean}
*/ */
shouldRenderItem(item: ClubType): boolean { shouldRenderItem(item: club) {
const {state} = this; let shouldRender = this.state.currentlySelectedCategories.length === 0
let shouldRender = || isItemInCategoryFilter(this.state.currentlySelectedCategories, item.category);
state.currentlySelectedCategories.length === 0 ||
isItemInCategoryFilter(state.currentlySelectedCategories, item.category);
if (shouldRender) if (shouldRender)
shouldRender = stringMatchQuery(item.name, state.currentSearchString); shouldRender = stringMatchQuery(item.name, this.state.currentSearchString);
return shouldRender; return shouldRender;
} }
render(): React.Node { getRenderItem = ({item}: { item: club }) => {
const {props} = this; const onPress = this.onListItemPress.bind(this, item);
if (this.shouldRenderItem(item)) {
return (
<ClubListItem
categoryTranslator={this.getCategoryOfId}
item={item}
onPress={onPress}
height={LIST_ITEM_HEIGHT}
/>
);
} else
return null;
};
/**
* Callback used when clicking an article in the list.
* It opens the modal to show detailed information about the article
*
* @param item The article pressed
*/
onListItemPress(item: club) {
this.props.navigation.navigate("club-information", {data: item, categories: this.categories});
}
render() {
return ( return (
<AuthenticatedScreen <AuthenticatedScreen
navigation={props.navigation} {...this.props}
requests={[ requests={[
{ {
link: 'clubs/list', link: 'clubs/list',
params: {}, params: {},
mandatory: true, mandatory: true,
}, }
]} ]}
renderFunction={this.getScreen} renderFunction={this.getScreen}
/> />

View file

@ -4,41 +4,37 @@ import * as React from 'react';
import {Linking, View} from 'react-native'; import {Linking, View} from 'react-native';
import {Avatar, Card, Text, withTheme} from 'react-native-paper'; import {Avatar, Card, Text, withTheme} from 'react-native-paper';
import ImageModal from 'react-native-image-modal'; import ImageModal from 'react-native-image-modal';
import Autolink from 'react-native-autolink'; import Autolink from "react-native-autolink";
import {StackNavigationProp} from '@react-navigation/stack'; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import MaterialHeaderButtons, { import CustomTabBar from "../../components/Tabbar/CustomTabBar";
Item, import {StackNavigationProp} from "@react-navigation/stack";
} from '../../components/Overrides/CustomHeaderButton'; import type {feedItem} from "./HomeScreen";
import CustomTabBar from '../../components/Tabbar/CustomTabBar'; import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView";
import type {FeedItemType} from './HomeScreen';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {data: FeedItemType, date: string}}, 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 feed item page. * Class defining a feed item page.
*/ */
class FeedItemScreen extends React.Component<PropsType> { class FeedItemScreen extends React.Component<Props> {
displayData: FeedItemType;
displayData: feedItem;
date: string; date: string;
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
this.displayData = props.route.params.data; this.displayData = props.route.params.data;
this.date = props.route.params.date; this.date = props.route.params.date;
} }
componentDidMount() { componentDidMount() {
const {props} = this; this.props.navigation.setOptions({
props.navigation.setOptions({
headerRight: this.getHeaderButton, headerRight: this.getHeaderButton,
}); });
} }
@ -55,41 +51,41 @@ class FeedItemScreen extends React.Component<PropsType> {
* *
* @returns {*} * @returns {*}
*/ */
getHeaderButton = (): React.Node => { getHeaderButton = () => {
return ( return <MaterialHeaderButtons>
<MaterialHeaderButtons> <Item title="main" iconName={'facebook'} color={"#2e88fe"} onPress={this.onOutLinkPress}/>
<Item </MaterialHeaderButtons>;
title="main"
iconName="facebook"
color="#2e88fe"
onPress={this.onOutLinkPress}
/>
</MaterialHeaderButtons>
);
}; };
render(): React.Node { /**
const hasImage = * Gets the Amicale INSA avatar
this.displayData.full_picture !== '' && *
this.displayData.full_picture != null; * @returns {*}
*/
getAvatar() {
return ( return (
<CollapsibleScrollView style={{margin: 5}} hasTab> <Avatar.Image size={48} source={ICON_AMICALE}
style={{backgroundColor: 'transparent'}}/>
);
}
render() {
const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture != null;
return (
<CollapsibleScrollView
style={{margin: 5,}}
hasTab={true}
>
<Card.Title <Card.Title
title={NAME_AMICALE} title={NAME_AMICALE}
subtitle={this.date} subtitle={this.date}
left={(): React.Node => ( left={this.getAvatar}
<Avatar.Image
size={48}
source={ICON_AMICALE}
style={{backgroundColor: 'transparent'}}
/> />
)} {hasImage ?
/>
{hasImage ? (
<View style={{marginLeft: 'auto', marginRight: 'auto'}}> <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal <ImageModal
resizeMode="contain" resizeMode="contain"
imageBackgroundColor="#000" imageBackgroundColor={"#000"}
style={{ style={{
width: 250, width: 250,
height: 250, height: 250,
@ -97,17 +93,15 @@ class FeedItemScreen extends React.Component<PropsType> {
source={{ source={{
uri: this.displayData.full_picture, uri: this.displayData.full_picture,
}} }}
/> /></View> : null}
</View>
) : null}
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> <Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
{this.displayData.message !== undefined ? ( {this.displayData.message !== undefined ?
<Autolink <Autolink
text={this.displayData.message} text={this.displayData.message}
hashtag="facebook" hashtag="facebook"
component={Text} component={Text}
/> /> : null
) : null} }
</Card.Content> </Card.Content>
</CollapsibleScrollView> </CollapsibleScrollView>
); );

View file

@ -2,44 +2,52 @@
import * as React from 'react'; import * as React from 'react';
import {FlatList} from 'react-native'; import {FlatList} from 'react-native';
import i18n from 'i18n-js'; import i18n from "i18n-js";
import DashboardItem from "../../components/Home/EventDashboardItem";
import WebSectionList from "../../components/Screens/WebSectionList";
import {ActivityIndicator, Headline, withTheme} from 'react-native-paper'; import {ActivityIndicator, Headline, withTheme} from 'react-native-paper';
import FeedItem from "../../components/Home/FeedItem";
import SmallDashboardItem from "../../components/Home/SmallDashboardItem";
import PreviewEventDashboardItem from "../../components/Home/PreviewEventDashboardItem";
import {stringToDate} from "../../utils/Planning";
import ActionsDashBoardItem from "../../components/Home/ActionsDashboardItem";
import {CommonActions} from '@react-navigation/native'; import {CommonActions} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack'; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import * as Animatable from 'react-native-animatable'; import AnimatedFAB from "../../components/Animations/AnimatedFAB";
import {View} from 'react-native-animatable'; import {StackNavigationProp} from "@react-navigation/stack";
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; import type {CustomTheme} from "../../managers/ThemeManager";
import DashboardItem from '../../components/Home/EventDashboardItem'; import * as Animatable from "react-native-animatable";
import WebSectionList from '../../components/Screens/WebSectionList'; import {View} from "react-native-animatable";
import FeedItem from '../../components/Home/FeedItem'; import ConnectionManager from "../../managers/ConnectionManager";
import SmallDashboardItem from '../../components/Home/SmallDashboardItem'; import LogoutDialog from "../../components/Amicale/LogoutDialog";
import PreviewEventDashboardItem from '../../components/Home/PreviewEventDashboardItem'; import AsyncStorageManager from "../../managers/AsyncStorageManager";
import ActionsDashBoardItem from '../../components/Home/ActionsDashboardItem'; import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import MaterialHeaderButtons, { import MascotPopup from "../../components/Mascot/MascotPopup";
Item, import DashboardManager from "../../managers/DashboardManager";
} from '../../components/Overrides/CustomHeaderButton'; import type {ServiceItem} from "../../managers/ServicesManager";
import AnimatedFAB from '../../components/Animations/AnimatedFAB'; import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons";
import type {CustomTheme} from '../../managers/ThemeManager';
import ConnectionManager from '../../managers/ConnectionManager';
import LogoutDialog from '../../components/Amicale/LogoutDialog';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
import DashboardManager from '../../managers/DashboardManager';
import type {ServiceItemType} from '../../managers/ServicesManager';
import {getDisplayEvent, getFutureEvents} from '../../utils/Home';
// import DATA from "../dashboard_data.json"; // import DATA from "../dashboard_data.json";
const NAME_AMICALE = 'Amicale INSA Toulouse'; const NAME_AMICALE = 'Amicale INSA Toulouse';
const DATA_URL = const DATA_URL = "https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json";
'https://etud.insa-toulouse.fr/~amicale_app/v2/dashboard/dashboard_data.json';
const FEED_ITEM_HEIGHT = 500; const FEED_ITEM_HEIGHT = 500;
const SECTIONS_ID = ['dashboard', 'news_feed']; const SECTIONS_ID = [
'dashboard',
'news_feed'
];
const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds const REFRESH_TIME = 1000 * 20; // Refresh every 20 seconds
export type FeedItemType = { type rawDashboard = {
news_feed: {
data: Array<feedItem>,
},
dashboard: fullDashboard,
}
export type feedItem = {
full_picture: string, full_picture: string,
message: string, message: string,
permalink_url: string, permalink_url: string,
@ -47,7 +55,16 @@ export type FeedItemType = {
id: string, id: string,
}; };
export type EventType = { export type fullDashboard = {
today_menu: Array<{ [key: string]: any }>,
proximo_articles: number,
available_dryers: number,
available_washers: number,
today_events: Array<{ [key: string]: any }>,
available_tutorials: number,
}
export type event = {
id: number, id: number,
title: string, title: string,
logo: string | null, logo: string | null,
@ -57,68 +74,44 @@ export type EventType = {
club: string, club: string,
category_id: number, category_id: number,
url: string, url: string,
}; }
export type FullDashboardType = { type Props = {
today_menu: Array<{[key: string]: {...}}>,
proximo_articles: number,
available_dryers: number,
available_washers: number,
today_events: Array<EventType>,
available_tutorials: number,
};
type RawDashboardType = {
news_feed: {
data: Array<FeedItemType>,
},
dashboard: FullDashboardType,
};
type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {nextScreen: string, data: {...}}}, route: { params: any, ... },
theme: CustomTheme, theme: CustomTheme,
}; }
type StateType = { type State = {
dialogVisible: boolean, dialogVisible: boolean,
}; }
/** /**
* Class defining the app's home screen * Class defining the app's home screen
*/ */
class HomeScreen extends React.Component<PropsType, StateType> { class HomeScreen extends React.Component<Props, State> {
isLoggedIn: boolean | null; isLoggedIn: boolean | null;
fabRef: {current: null | AnimatedFAB}; fabRef: { current: null | AnimatedFAB };
currentNewFeed: Array<feedItem>;
currentNewFeed: Array<FeedItemType>; currentDashboard: fullDashboard | null;
currentDashboard: FullDashboardType | null;
dashboardManager: DashboardManager; dashboardManager: DashboardManager;
constructor(props: PropsType) { constructor(props) {
super(props); super(props);
this.fabRef = React.createRef(); this.fabRef = React.createRef();
this.dashboardManager = new DashboardManager(props.navigation); this.dashboardManager = new DashboardManager(this.props.navigation);
this.currentNewFeed = []; this.currentNewFeed = [];
this.currentDashboard = null; this.currentDashboard = null;
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
props.navigation.setOptions({ this.props.navigation.setOptions({
headerRight: this.getHeaderButton, headerRight: this.getHeaderButton,
}); });
this.state = { this.state = {
dialogVisible: false, dialogVisible: false,
};
} }
componentDidMount() {
const {props} = this;
props.navigation.addListener('focus', this.onScreenFocus);
// Handle link open when home is focused
props.navigation.addListener('state', this.handleNavigationParams);
} }
/** /**
@ -127,19 +120,24 @@ class HomeScreen extends React.Component<PropsType, StateType> {
* @param dateString {string} The Unix Timestamp representation of a date * @param dateString {string} The Unix Timestamp representation of a date
* @return {string} The formatted output date * @return {string} The formatted output date
*/ */
static getFormattedDate(dateString: number): string { static getFormattedDate(dateString: number) {
const date = new Date(dateString * 1000); let date = new Date(dateString * 1000);
return date.toLocaleString(); return date.toLocaleString();
} }
componentDidMount() {
this.props.navigation.addListener('focus', this.onScreenFocus);
// Handle link open when home is focused
this.props.navigation.addListener('state', this.handleNavigationParams);
}
/** /**
* Updates login state and navigation parameters on screen focus * Updates login state and navigation parameters on screen focus
*/ */
onScreenFocus = () => { onScreenFocus = () => {
const {props} = this;
if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) { if (ConnectionManager.getInstance().isLoggedIn() !== this.isLoggedIn) {
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn(); this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
props.navigation.setOptions({ this.props.navigation.setOptions({
headerRight: this.getHeaderButton, headerRight: this.getHeaderButton,
}); });
} }
@ -147,41 +145,197 @@ class HomeScreen extends React.Component<PropsType, StateType> {
this.handleNavigationParams(); 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) {
this.props.navigation.navigate(this.props.route.params.nextScreen, this.props.route.params.data);
// reset params to prevent infinite loop
this.props.navigation.dispatch(CommonActions.setParams({nextScreen: null}));
}
}
};
/** /**
* Gets header buttons based on login state * Gets header buttons based on login state
* *
* @returns {*} * @returns {*}
*/ */
getHeaderButton = (): React.Node => { getHeaderButton = () => {
const {props} = this; let onPressLog = () => this.props.navigation.navigate("login", {nextScreen: "profile"});
let onPressLog = (): void => let logIcon = "login";
props.navigation.navigate('login', {nextScreen: 'profile'}); let logColor = this.props.theme.colors.primary;
let logIcon = 'login';
let logColor = props.theme.colors.primary;
if (this.isLoggedIn) { if (this.isLoggedIn) {
onPressLog = (): void => this.showDisconnectDialog(); onPressLog = () => this.showDisconnectDialog();
logIcon = 'logout'; logIcon = "logout";
logColor = props.theme.colors.text; logColor = this.props.theme.colors.text;
} }
const onPressSettings = (): void => props.navigation.navigate('settings'); const onPressSettings = () => this.props.navigation.navigate("settings");
return ( return <MaterialHeaderButtons>
<MaterialHeaderButtons> <Item title="log" iconName={logIcon} color={logColor} onPress={onPressLog}/>
<Item <Item title={i18n.t("screens.settings.title")} iconName={"cog"} onPress={onPressSettings}/>
title="log" </MaterialHeaderButtons>;
iconName={logIcon}
color={logColor}
onPress={onPressLog}
/>
<Item
title={i18n.t('screens.settings.title')}
iconName="cog"
onPress={onPressSettings}
/>
</MaterialHeaderButtons>
);
}; };
showDisconnectDialog = () => this.setState({dialogVisible: true});
hideDisconnectDialog = () => this.setState({dialogVisible: false});
openScanner = () => this.props.navigation.navigate("scanner");
/**
* Creates the dataset to be used in the FlatList
*
* @param fetchedData
* @param isLoading
* @return {*}
*/
createDataset = (fetchedData: rawDashboard | null, isLoading: boolean) => {
// fetchedData = DATA;
if (fetchedData != null) {
if (fetchedData.news_feed != null)
this.currentNewFeed = fetchedData.news_feed.data;
if (fetchedData.dashboard != null)
this.currentDashboard = fetchedData.dashboard;
}
if (this.currentNewFeed.length > 0)
return [
{
title: i18n.t("screens.home.feedTitle"),
data: this.currentNewFeed,
id: SECTIONS_ID[1]
}
];
else
return [
{
title: isLoading ? i18n.t("screens.home.feedLoading") : i18n.t("screens.home.feedError"),
data: [],
id: SECTIONS_ID[1]
}
];
};
/**
* Gets the time limit depending on the current day:
* 17:30 for every day of the week except for thursday 11:30
* 00:00 on weekends
*/
getTodayEventTimeLimit() {
let now = new Date();
if (now.getDay() === 4) // Thursday
now.setHours(11, 30, 0);
else if (now.getDay() === 6 || now.getDay() === 0) // Weekend
now.setHours(0, 0, 0);
else
now.setHours(17, 30, 0);
return now;
}
/**
* Gets the duration (in milliseconds) of an event
*
* @param event {event}
* @return {number} The number of milliseconds
*/
getEventDuration(event: event): number {
let start = stringToDate(event.date_begin);
let end = stringToDate(event.date_end);
let duration = 0;
if (start != null && end != null)
duration = end - start;
return duration;
}
/**
* Gets events starting after the limit
*
* @param events
* @param limit
* @return {Array<Object>}
*/
getEventsAfterLimit(events: Array<event>, limit: Date): Array<event> {
let validEvents = [];
for (let event of events) {
let startDate = stringToDate(event.date_begin);
if (startDate != null && startDate >= limit) {
validEvents.push(event);
}
}
return validEvents;
}
/**
* Gets the event with the longest duration in the given array.
* If all events have the same duration, return the first in the array.
*
* @param events
*/
getLongestEvent(events: Array<event>): event {
let longestEvent = events[0];
let longestTime = 0;
for (let event of events) {
let time = this.getEventDuration(event);
if (time > longestTime) {
longestTime = time;
longestEvent = event;
}
}
return longestEvent;
}
/**
* Gets events that have not yet ended/started
*
* @param events
*/
getFutureEvents(events: Array<event>): Array<event> {
let validEvents = [];
let now = new Date();
for (let event of events) {
let startDate = stringToDate(event.date_begin);
let endDate = stringToDate(event.date_end);
if (startDate != null) {
if (startDate > now)
validEvents.push(event);
else if (endDate != null) {
if (endDate > now || endDate < startDate) // Display event if it ends the following day
validEvents.push(event);
}
}
}
return validEvents;
}
/**
* Gets the event to display in the preview
*
* @param events
* @return {Object}
*/
getDisplayEvent(events: Array<event>): event | null {
let displayEvent = null;
if (events.length > 1) {
let eventsAfterLimit = this.getEventsAfterLimit(events, this.getTodayEventTimeLimit());
if (eventsAfterLimit.length > 0) {
if (eventsAfterLimit.length === 1)
displayEvent = eventsAfterLimit[0];
else
displayEvent = this.getLongestEvent(events);
} else {
displayEvent = this.getLongestEvent(events);
}
} else if (events.length === 1) {
displayEvent = events[0];
}
return displayEvent;
}
onEventContainerClick = () => this.props.navigation.navigate('planning');
/** /**
* Gets the event dashboard 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
@ -189,9 +343,9 @@ class HomeScreen extends React.Component<PropsType, StateType> {
* @param content * @param content
* @return {*} * @return {*}
*/ */
getDashboardEvent(content: Array<EventType>): React.Node { getDashboardEvent(content: Array<event>) {
const futureEvents = getFutureEvents(content); let futureEvents = this.getFutureEvents(content);
const displayEvent = getDisplayEvent(futureEvents); let displayEvent = this.getDisplayEvent(futureEvents);
// const clickPreviewAction = () => // const clickPreviewAction = () =>
// this.props.navigation.navigate('students', { // this.props.navigation.navigate('students', {
// screen: 'planning-information', // screen: 'planning-information',
@ -200,9 +354,10 @@ class HomeScreen extends React.Component<PropsType, StateType> {
return ( return (
<DashboardItem <DashboardItem
eventNumber={futureEvents.length} eventNumber={futureEvents.length}
clickAction={this.onEventContainerClick}> clickAction={this.onEventContainerClick}
>
<PreviewEventDashboardItem <PreviewEventDashboardItem
event={displayEvent} event={displayEvent != null ? displayEvent : undefined}
clickAction={this.onEventContainerClick} clickAction={this.onEventContainerClick}
/> />
</DashboardItem> </DashboardItem>
@ -214,14 +369,8 @@ class HomeScreen extends React.Component<PropsType, StateType> {
* *
* @returns {*} * @returns {*}
*/ */
getDashboardActions(): React.Node { getDashboardActions() {
const {props} = this; return <ActionsDashBoardItem {...this.props} isLoggedIn={this.isLoggedIn}/>;
return (
<ActionsDashBoardItem
navigation={props.navigation}
isLoggedIn={this.isLoggedIn}
/>
);
} }
/** /**
@ -230,21 +379,20 @@ class HomeScreen extends React.Component<PropsType, StateType> {
* @param content * @param content
* @return {*} * @return {*}
*/ */
getDashboardRow(content: Array<ServiceItemType | null>): React.Node { getDashboardRow(content: Array<ServiceItem>) {
return ( return (
// $FlowFixMe //$FlowFixMe
<FlatList <FlatList
data={content} data={content}
renderItem={this.getDashboardRowRenderItem} renderItem={this.dashboardRowRenderItem}
horizontal horizontal={true}
contentContainerStyle={{ contentContainerStyle={{
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
marginTop: 10, marginTop: 10,
marginBottom: 10, marginBottom: 10,
}} }}
/> />);
);
} }
/** /**
@ -253,24 +401,16 @@ class HomeScreen extends React.Component<PropsType, StateType> {
* @param item * @param item
* @returns {*} * @returns {*}
*/ */
getDashboardRowRenderItem = ({ dashboardRowRenderItem = ({item}: { item: ServiceItem }) => {
item,
}: {
item: ServiceItemType | null,
}): React.Node => {
if (item != null)
return ( return (
<SmallDashboardItem <SmallDashboardItem
image={item.image} image={item.image}
onPress={item.onPress} onPress={item.onPress}
badgeCount={ badgeCount={this.currentDashboard != null && item.badgeFunction != null
this.currentDashboard != null && item.badgeFunction != null
? item.badgeFunction(this.currentDashboard) ? item.badgeFunction(this.currentDashboard)
: null : null}
}
/> />
); );
return <SmallDashboardItem image={null} onPress={null} badgeCount={null} />;
}; };
/** /**
@ -279,11 +419,10 @@ class HomeScreen extends React.Component<PropsType, StateType> {
* @param item The feed item to display * @param item The feed item to display
* @return {*} * @return {*}
*/ */
getFeedItem(item: FeedItemType): React.Node { getFeedItem(item: feedItem) {
const {props} = this;
return ( return (
<FeedItem <FeedItem
navigation={props.navigation} {...this.props}
item={item} item={item}
title={NAME_AMICALE} title={NAME_AMICALE}
subtitle={HomeScreen.getFormattedDate(item.created_time)} subtitle={HomeScreen.getFormattedDate(item.created_time)}
@ -299,218 +438,139 @@ class HomeScreen extends React.Component<PropsType, StateType> {
* @param section The current section * @param section The current section
* @return {*} * @return {*}
*/ */
getRenderItem = ({item}: {item: FeedItemType}): React.Node => getRenderItem = ({item}: { item: feedItem, }) => this.getFeedItem(item);
this.getFeedItem(item);
getRenderSectionHeader = ( onScroll = (event: SyntheticEvent<EventTarget>) => {
data: { if (this.fabRef.current != null)
section: { this.fabRef.current.onScroll(event);
data: Array<{...}>, };
title: string,
}, renderSectionHeader = (data: { section: { [key: string]: any } }, isLoading: boolean) => {
},
isLoading: boolean,
): React.Node => {
const {props} = this;
if (data.section.data.length > 0) if (data.section.data.length > 0)
return ( return (
<Headline <Headline style={{
style={{ textAlign: "center",
textAlign: 'center',
marginTop: 50, marginTop: 50,
marginBottom: 10, marginBottom: 10,
}}> }}>
{data.section.title} {data.section.title}
</Headline> </Headline>
); )
else
return ( return (
<View> <View>
<Headline <Headline style={{
style={{ textAlign: "center",
textAlign: 'center',
marginTop: 50, marginTop: 50,
marginBottom: 10, marginBottom: 10,
marginLeft: 20, marginLeft: 20,
marginRight: 20, marginRight: 20,
color: props.theme.colors.textDisabled, color: this.props.theme.colors.textDisabled
}}> }}>
{data.section.title} {data.section.title}
</Headline> </Headline>
{isLoading ? ( {isLoading
<ActivityIndicator ? <ActivityIndicator
style={{ style={{
marginTop: 10, marginTop: 10
}} }}
/> />
) : ( : <MaterialCommunityIcons
<MaterialCommunityIcons name={"access-point-network-off"}
name="access-point-network-off"
size={100} size={100}
color={props.theme.colors.textDisabled} color={this.props.theme.colors.textDisabled}
style={{ style={{
marginLeft: 'auto', marginLeft: "auto",
marginRight: 'auto', marginRight: "auto",
}} }}
/> />}
)}
</View> </View>
); );
}; }
getListHeader = (fetchedData: RawDashboardType): React.Node => { getListHeader = (fetchedData: rawDashboard) => {
let dashboard = null; let dashboard = null;
if (fetchedData != null) dashboard = fetchedData.dashboard; if (fetchedData != null) {
dashboard = fetchedData.dashboard;
}
return ( return (
<Animatable.View animation="fadeInDown" duration={500} useNativeDriver> <Animatable.View
animation={"fadeInDown"}
duration={500}
useNativeDriver={true}
>
{this.getDashboardActions()} {this.getDashboardActions()}
{this.getDashboardRow(this.dashboardManager.getCurrentDashboard())} {this.getDashboardRow(this.dashboardManager.getCurrentDashboard())}
{this.getDashboardEvent( {this.getDashboardEvent(
dashboard == null ? [] : dashboard.today_events, dashboard == null
? []
: dashboard.today_events
)} )}
</Animatable.View> </Animatable.View>
); );
};
/**
* Navigates to the a new screen if navigation parameters specify one
*/
handleNavigationParams = () => {
const {props} = this;
if (props.route.params != null) {
if (props.route.params.nextScreen != null) {
props.navigation.navigate(
props.route.params.nextScreen,
props.route.params.data,
);
// reset params to prevent infinite loop
props.navigation.dispatch(CommonActions.setParams({nextScreen: null}));
} }
}
};
showDisconnectDialog = (): void => this.setState({dialogVisible: true});
hideDisconnectDialog = (): void => this.setState({dialogVisible: false});
openScanner = () => {
const {props} = this;
props.navigation.navigate('scanner');
};
/**
* Creates the dataset to be used in the FlatList
*
* @param fetchedData
* @param isLoading
* @return {*}
*/
createDataset = (
fetchedData: RawDashboardType | null,
isLoading: boolean,
): Array<{
title: string,
data: [] | Array<FeedItemType>,
id: string,
}> => {
// fetchedData = DATA;
if (fetchedData != null) {
if (fetchedData.news_feed != null)
this.currentNewFeed = fetchedData.news_feed.data;
if (fetchedData.dashboard != null)
this.currentDashboard = fetchedData.dashboard;
}
if (this.currentNewFeed.length > 0)
return [
{
title: i18n.t('screens.home.feedTitle'),
data: this.currentNewFeed,
id: SECTIONS_ID[1],
},
];
return [
{
title: isLoading
? i18n.t('screens.home.feedLoading')
: i18n.t('screens.home.feedError'),
data: [],
id: SECTIONS_ID[1],
},
];
};
onEventContainerClick = () => {
const {props} = this;
props.navigation.navigate('planning');
};
onScroll = (event: SyntheticEvent<EventTarget>) => {
if (this.fabRef.current != null) this.fabRef.current.onScroll(event);
};
/** /**
* Callback when pressing the login button on the banner. * Callback when pressing the login button on the banner.
* This hides the banner and takes the user to the login page. * This hides the banner and takes the user to the login page.
*/ */
onLogin = () => { onLogin = () => this.props.navigation.navigate("login", {nextScreen: "profile"});
const {props} = this;
props.navigation.navigate('login', {
nextScreen: 'profile',
});
};
render(): React.Node { render() {
const {props, state} = this;
return ( return (
<View style={{flex: 1}}>
<View <View
style={{ style={{flex: 1}}
position: 'absolute', >
width: '100%', <View style={{
height: '100%', position: "absolute",
width: "100%",
height: "100%",
}}> }}>
<WebSectionList <WebSectionList
navigation={props.navigation} {...this.props}
createDataset={this.createDataset} createDataset={this.createDataset}
autoRefreshTime={REFRESH_TIME} autoRefreshTime={REFRESH_TIME}
refreshOnFocus refreshOnFocus={true}
fetchUrl={DATA_URL} fetchUrl={DATA_URL}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
itemHeight={FEED_ITEM_HEIGHT} itemHeight={FEED_ITEM_HEIGHT}
onScroll={this.onScroll} onScroll={this.onScroll}
showError={false} showError={false}
renderSectionHeader={this.getRenderSectionHeader} renderSectionHeader={this.renderSectionHeader}
renderListHeaderComponent={this.getListHeader} renderListHeaderComponent={this.getListHeader}
/> />
</View> </View>
{!this.isLoggedIn ? ( {!this.isLoggedIn
<MascotPopup ? <MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.homeShowBanner.key} prefKey={AsyncStorageManager.PREFERENCES.homeShowBanner.key}
title={i18n.t('screens.home.mascotDialog.title')} title={i18n.t("screens.home.mascotDialog.title")}
message={i18n.t('screens.home.mascotDialog.message')} message={i18n.t("screens.home.mascotDialog.message")}
icon="human-greeting" icon={"human-greeting"}
buttons={{ buttons={{
action: { action: {
message: i18n.t('screens.home.mascotDialog.login'), message: i18n.t("screens.home.mascotDialog.login"),
icon: 'login', icon: "login",
onPress: this.onLogin, onPress: this.onLogin,
}, },
cancel: { cancel: {
message: i18n.t('screens.home.mascotDialog.later'), message: i18n.t("screens.home.mascotDialog.later"),
icon: 'close', icon: "close",
color: props.theme.colors.warning, color: this.props.theme.colors.warning,
}, }
}} }}
emotion={MASCOT_STYLE.CUTE} emotion={MASCOT_STYLE.CUTE}
/> /> : null}
) : null}
<AnimatedFAB <AnimatedFAB
{...this.props}
ref={this.fabRef} ref={this.fabRef}
icon="qrcode-scan" icon="qrcode-scan"
onPress={this.openScanner} onPress={this.openScanner}
/> />
<LogoutDialog <LogoutDialog
navigation={props.navigation} {...this.props}
visible={state.dialogVisible} visible={this.state.dialogVisible}
onDismiss={this.hideDisconnectDialog} onDismiss={this.hideDisconnectDialog}
/> />
</View> </View>

View file

@ -1,65 +1,90 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Linking, Platform, StyleSheet, View} from 'react-native'; import {Linking, Platform, StyleSheet, View} from "react-native";
import {Button, Text, withTheme} from 'react-native-paper'; import {Button, Text, withTheme} from 'react-native-paper';
import {RNCamera} from 'react-native-camera'; import {RNCamera} from 'react-native-camera';
import {BarcodeMask} from '@nartc/react-native-barcode-mask'; import {BarcodeMask} from '@nartc/react-native-barcode-mask';
import URLHandler from "../../utils/URLHandler";
import AlertDialog from "../../components/Dialogs/AlertDialog";
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import LoadingConfirmDialog from "../../components/Dialogs/LoadingConfirmDialog";
import {PERMISSIONS, request, RESULTS} from 'react-native-permissions'; import {PERMISSIONS, request, RESULTS} from 'react-native-permissions';
import URLHandler from '../../utils/URLHandler'; import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import AlertDialog from '../../components/Dialogs/AlertDialog'; import MascotPopup from "../../components/Mascot/MascotPopup";
import CustomTabBar from '../../components/Tabbar/CustomTabBar';
import LoadingConfirmDialog from '../../components/Dialogs/LoadingConfirmDialog';
import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
import MascotPopup from '../../components/Mascot/MascotPopup';
type StateType = { type Props = {};
type State = {
hasPermission: boolean, hasPermission: boolean,
scanned: boolean, scanned: boolean,
dialogVisible: boolean, dialogVisible: boolean,
mascotDialogVisible: boolean, mascotDialogVisible: boolean,
dialogTitle: string,
dialogMessage: string,
loading: boolean, loading: boolean,
}; };
const styles = StyleSheet.create({ class ScannerScreen extends React.Component<Props, State> {
container: {
flex: 1,
justifyContent: 'center',
},
button: {
position: 'absolute',
bottom: 20,
width: '80%',
left: '10%',
},
});
class ScannerScreen extends React.Component<null, StateType> { state = {
constructor() {
super();
this.state = {
hasPermission: false, hasPermission: false,
scanned: false, scanned: false,
mascotDialogVisible: false, mascotDialogVisible: false,
dialogVisible: false, dialogVisible: false,
dialogTitle: "",
dialogMessage: "",
loading: false, loading: false,
}; };
constructor() {
super();
} }
componentDidMount() { componentDidMount() {
this.requestPermissions(); this.requestPermissions();
} }
/**
* Requests permission to use the camera
*/
requestPermissions = () => {
if (Platform.OS === 'android')
request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus)
else
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();
else {
this.showOpeningDialog();
Linking.openURL(data);
}
};
/** /**
* Gets a view asking user for permission to use the camera * Gets a view asking user for permission to use the camera
* *
* @returns {*} * @returns {*}
*/ */
getPermissionScreen(): React.Node { getPermissionScreen() {
return ( return <View style={{marginLeft: 10, marginRight: 10}}>
<View style={{marginLeft: 10, marginRight: 10}}> <Text>{i18n.t("screens.scanner.permissions.error")}</Text>
<Text>{i18n.t('screens.scanner.permissions.error')}</Text>
<Button <Button
icon="camera" icon="camera"
mode="contained" mode="contained"
@ -68,72 +93,11 @@ class ScannerScreen extends React.Component<null, StateType> {
marginTop: 10, marginTop: 10,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}}> }}
{i18n.t('screens.scanner.permissions.button')} >
{i18n.t("screens.scanner.permissions.button")}
</Button> </Button>
</View> </View>
);
}
/**
* 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(): React.Node {
const {state} = this;
return (
<RNCamera
onBarCodeRead={state.scanned ? null : this.onCodeScanned}
type={RNCamera.Constants.Type.back}
barCodeScannerSettings={{
barCodeTypes: [RNCamera.Constants.BarCodeType.qr],
}}
style={StyleSheet.absoluteFill}
captureAudio={false}>
<BarcodeMask
backgroundColor="#000"
maskOpacity={0.5}
animatedLineThickness={1}
animationDuration={1000}
width={250}
height={250}
/>
</RNCamera>
);
}
/**
* Requests permission to use the camera
*/
requestPermissions = () => {
if (Platform.OS === 'android')
request(PERMISSIONS.ANDROID.CAMERA).then(this.updatePermissionStatus);
else request(PERMISSIONS.IOS.CAMERA).then(this.updatePermissionStatus);
};
/**
* Updates the state permission status
*
* @param result
*/
updatePermissionStatus = (result: RESULTS) => {
this.setState({
hasPermission: result === RESULTS.GRANTED,
});
};
/**
* Shows a dialog indicating the user the scanned code was invalid
*/
// eslint-disable-next-line react/sort-comp
showErrorDialog() {
this.setState({
dialogVisible: true,
scanned: true,
});
} }
/** /**
@ -156,82 +120,119 @@ class ScannerScreen extends React.Component<null, StateType> {
}); });
}; };
/**
* Shows a dialog indicating the user the scanned code was invalid
*/
showErrorDialog() {
this.setState({
dialogVisible: true,
scanned: true,
});
}
/** /**
* Hide any dialog * Hide any dialog
*/ */
onDialogDismiss = () => { onDialogDismiss = () => this.setState({
this.setState({
dialogVisible: false, dialogVisible: false,
scanned: false, scanned: false,
}); });
};
onMascotDialogDismiss = () => { onMascotDialogDismiss = () => this.setState({
this.setState({
mascotDialogVisible: false, mascotDialogVisible: false,
scanned: false, scanned: false,
}); });
};
/** /**
* Opens scanned link if it is a valid app link or shows and error dialog * 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
* *
* @param type The barcode type * @returns {*}
* @param data The scanned value
*/ */
onCodeScanned = ({data}: {data: string}) => { getScanner() {
if (!URLHandler.isUrlValid(data)) this.showErrorDialog();
else {
this.showOpeningDialog();
Linking.openURL(data);
}
};
render(): React.Node {
const {state} = this;
return ( return (
<View <RNCamera
style={{ onBarCodeRead={this.state.scanned ? undefined : this.handleCodeScanned}
type={RNCamera.Constants.Type.back}
barCodeScannerSettings={{
barCodeTypes: [RNCamera.Constants.BarCodeType.qr],
}}
style={StyleSheet.absoluteFill}
captureAudio={false}
>
<BarcodeMask
backgroundColor={"#000"}
maskOpacity={0.5}
animatedLineThickness={1}
animationDuration={1000}
width={250}
height={250}
/>
</RNCamera>
);
}
render() {
return (
<View style={{
...styles.container, ...styles.container,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT, marginBottom: CustomTabBar.TAB_BAR_HEIGHT
}}> }}>
{state.hasPermission ? this.getScanner() : this.getPermissionScreen()} {this.state.hasPermission
? this.getScanner()
: this.getPermissionScreen()
}
<Button <Button
icon="information" icon="information"
mode="contained" mode="contained"
onPress={this.showHelpDialog} onPress={this.showHelpDialog}
style={styles.button}> style={styles.button}
{i18n.t('screens.scanner.help.button')} >
{i18n.t("screens.scanner.help.button")}
</Button> </Button>
<MascotPopup <MascotPopup
visible={state.mascotDialogVisible} visible={this.state.mascotDialogVisible}
title={i18n.t('screens.scanner.mascotDialog.title')} title={i18n.t("screens.scanner.mascotDialog.title")}
message={i18n.t('screens.scanner.mascotDialog.message')} message={i18n.t("screens.scanner.mascotDialog.message")}
icon="camera-iris" icon={"camera-iris"}
buttons={{ buttons={{
action: null, action: null,
cancel: { cancel: {
message: i18n.t('screens.scanner.mascotDialog.button'), message: i18n.t("screens.scanner.mascotDialog.button"),
icon: 'check', icon: "check",
onPress: this.onMascotDialogDismiss, onPress: this.onMascotDialogDismiss,
}, }
}} }}
emotion={MASCOT_STYLE.NORMAL} emotion={MASCOT_STYLE.NORMAL}
/> />
<AlertDialog <AlertDialog
visible={state.dialogVisible} visible={this.state.dialogVisible}
onDismiss={this.onDialogDismiss} onDismiss={this.onDialogDismiss}
title={i18n.t('screens.scanner.error.title')} title={i18n.t("screens.scanner.error.title")}
message={i18n.t('screens.scanner.error.message')} message={i18n.t("screens.scanner.error.message")}
/> />
<LoadingConfirmDialog <LoadingConfirmDialog
visible={state.loading} visible={this.state.loading}
titleLoading={i18n.t('general.loading')} titleLoading={i18n.t("general.loading")}
startLoading startLoading={true}
/> />
</View> </View>
); );
} }
} }
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
button: {
position: 'absolute',
bottom: 20,
width: '80%',
left: '10%'
},
});
export default withTheme(ScannerScreen); export default withTheme(ScannerScreen);

View file

@ -1,68 +1,55 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Image, View} from 'react-native'; import type {cardList} from "../../components/Lists/CardList/CardList";
import { import CardList from "../../components/Lists/CardList/CardList";
Avatar, import {Image, View} from "react-native";
Card, import {Avatar, Card, Divider, List, TouchableRipple, withTheme} from "react-native-paper";
Divider, import type {CustomTheme} from "../../managers/ThemeManager";
List,
TouchableRipple,
withTheme,
} from 'react-native-paper';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {StackNavigationProp} from '@react-navigation/stack'; import MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton";
import CardList from '../../components/Lists/CardList/CardList'; import {StackNavigationProp} from "@react-navigation/stack";
import type {CustomTheme} from '../../managers/ThemeManager'; import {MASCOT_STYLE} from "../../components/Mascot/Mascot";
import MaterialHeaderButtons, { import MascotPopup from "../../components/Mascot/MascotPopup";
Item, import AsyncStorageManager from "../../managers/AsyncStorageManager";
} from '../../components/Overrides/CustomHeaderButton'; import ServicesManager, {SERVICES_CATEGORIES_KEY} from "../../managers/ServicesManager";
import {MASCOT_STYLE} from '../../components/Mascot/Mascot'; import CollapsibleFlatList from "../../components/Collapsible/CollapsibleFlatList";
import MascotPopup from '../../components/Mascot/MascotPopup';
import AsyncStorageManager from '../../managers/AsyncStorageManager';
import ServicesManager, {
SERVICES_CATEGORIES_KEY,
} from '../../managers/ServicesManager';
import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
import type {ServiceCategoryType} from '../../managers/ServicesManager';
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
}; }
class ServicesScreen extends React.Component<PropsType> { export type listItem = {
finalDataset: Array<ServiceCategoryType>; title: string,
description: string,
image: string | number,
content: cardList,
}
constructor(props: PropsType) {
class ServicesScreen extends React.Component<Props> {
finalDataset: Array<listItem>
constructor(props) {
super(props); super(props);
const services = new ServicesManager(props.navigation); const services = new ServicesManager(props.navigation);
this.finalDataset = services.getCategories([ this.finalDataset = services.getCategories([SERVICES_CATEGORIES_KEY.SPECIAL])
SERVICES_CATEGORIES_KEY.SPECIAL,
]);
} }
componentDidMount() { componentDidMount() {
const {props} = this; this.props.navigation.setOptions({
props.navigation.setOptions({
headerRight: this.getAboutButton, headerRight: this.getAboutButton,
}); });
} }
getAboutButton = (): React.Node => ( getAboutButton = () =>
<MaterialHeaderButtons> <MaterialHeaderButtons>
<Item <Item title="information" iconName="information" onPress={this.onAboutPress}/>
title="information" </MaterialHeaderButtons>;
iconName="information"
onPress={this.onAboutPress}
/>
</MaterialHeaderButtons>
);
onAboutPress = () => { onAboutPress = () => this.props.navigation.navigate('amicale-contact');
const {props} = this;
props.navigation.navigate('amicale-contact');
};
/** /**
* Gets the list title image for the list. * Gets the list title image for the list.
@ -70,30 +57,27 @@ class ServicesScreen extends React.Component<PropsType> {
* If the source is a string, we are using an icon. * If the source is a string, we are using an icon.
* If the source is a number, we are using an internal image. * If the source is a number, we are using an internal image.
* *
* @param props Props to pass to the component
* @param source The source image to display. Can be a string for icons or a number for local images * @param source The source image to display. Can be a string for icons or a number for local images
* @returns {*} * @returns {*}
*/ */
getListTitleImage(source: string | number): React.Node { getListTitleImage(props, source: string | number) {
const {props} = this; if (typeof source === "number")
if (typeof source === 'number') return <Image
return (
<Image
size={48} size={48}
source={source} source={source}
style={{ style={{
width: 48, width: 48,
height: 48, height: 48,
}} }}/>
/> else
); return <Avatar.Icon
return ( {...props}
<Avatar.Icon
size={48} size={48}
icon={source} icon={source}
color={props.theme.colors.primary} color={this.props.theme.colors.primary}
style={{backgroundColor: 'transparent'}} style={{backgroundColor: 'transparent'}}
/> />
);
} }
/** /**
@ -102,55 +86,58 @@ class ServicesScreen extends React.Component<PropsType> {
* @param item * @param item
* @returns {*} * @returns {*}
*/ */
getRenderItem = ({item}: {item: ServiceCategoryType}): React.Node => { renderItem = ({item}: { item: listItem }) => {
const {props} = this;
return ( return (
<TouchableRipple <TouchableRipple
style={{ style={{
margin: 5, margin: 5,
marginBottom: 20, marginBottom: 20,
}} }}
onPress={() => { onPress={() => this.props.navigation.navigate("services-section", {data: item})}
props.navigation.navigate('services-section', {data: item}); >
}}>
<View> <View>
<Card.Title <Card.Title
title={item.title} title={item.title}
subtitle={item.subtitle} subtitle={item.description}
left={(): React.Node => this.getListTitleImage(item.image)} left={(props) => this.getListTitleImage(props, item.image)}
right={({size}: {size: number}): React.Node => ( right={(props) => <List.Icon {...props} icon="chevron-right"/>}
<List.Icon size={size} icon="chevron-right" /> />
)} <CardList
dataset={item.content}
isHorizontal={true}
/> />
<CardList dataset={item.content} isHorizontal />
</View> </View>
</TouchableRipple> </TouchableRipple>
); );
}; };
keyExtractor = (item: ServiceCategoryType): string => item.title; keyExtractor = (item: listItem) => {
return item.title;
}
render(): React.Node { render() {
return ( return (
<View> <View>
<CollapsibleFlatList <CollapsibleFlatList
data={this.finalDataset} data={this.finalDataset}
renderItem={this.getRenderItem} renderItem={this.renderItem}
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
ItemSeparatorComponent={(): React.Node => <Divider />} ItemSeparatorComponent={() => <Divider/>}
hasTab hasTab={true}
/> />
<MascotPopup <MascotPopup
prefKey={AsyncStorageManager.PREFERENCES.servicesShowBanner.key} prefKey={AsyncStorageManager.PREFERENCES.servicesShowBanner.key}
title={i18n.t('screens.services.mascotDialog.title')} title={i18n.t("screens.services.mascotDialog.title")}
message={i18n.t('screens.services.mascotDialog.message')} message={i18n.t("screens.services.mascotDialog.message")}
icon="cloud-question" icon={"cloud-question"}
buttons={{ buttons={{
action: null, action: null,
cancel: { cancel: {
message: i18n.t('screens.services.mascotDialog.button'), message: i18n.t("screens.services.mascotDialog.button"),
icon: 'check', icon: "check",
}, onPress: this.onHideMascotDialog,
}
}} }}
emotion={MASCOT_STYLE.WINK} emotion={MASCOT_STYLE.WINK}
/> />

View file

@ -1,24 +1,25 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Collapsible} from 'react-navigation-collapsible'; import CardList from "../../components/Lists/CardList/CardList";
import {CommonActions} from '@react-navigation/native'; import CustomTabBar from "../../components/Tabbar/CustomTabBar";
import {StackNavigationProp} from '@react-navigation/stack'; import {withCollapsible} from "../../utils/withCollapsible";
import CardList from '../../components/Lists/CardList/CardList'; import {Collapsible} from "react-navigation-collapsible";
import CustomTabBar from '../../components/Tabbar/CustomTabBar'; import {CommonActions} from "@react-navigation/native";
import {withCollapsible} from '../../utils/withCollapsible'; import type {listItem} from "./ServicesScreen";
import type {ServiceCategoryType} from '../../managers/ServicesManager'; import {StackNavigationProp} from "@react-navigation/stack";
type PropsType = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: {params: {data: ServiceCategoryType | null}}, route: { params: { data: listItem | null } },
collapsibleStack: Collapsible, collapsibleStack: Collapsible,
}; }
class ServicesSectionScreen extends React.Component<PropsType> { class ServicesSectionScreen extends React.Component<Props> {
finalDataset: ServiceCategoryType;
constructor(props: PropsType) { finalDataset: listItem;
constructor(props) {
super(props); super(props);
this.handleNavigationParams(); this.handleNavigationParams();
} }
@ -27,38 +28,30 @@ class ServicesSectionScreen extends React.Component<PropsType> {
* Recover the list to display from navigation parameters * Recover the list to display from navigation parameters
*/ */
handleNavigationParams() { handleNavigationParams() {
const {props} = this; if (this.props.route.params != null) {
if (props.route.params != null) { if (this.props.route.params.data != null) {
if (props.route.params.data != null) { this.finalDataset = this.props.route.params.data;
this.finalDataset = props.route.params.data;
// reset params to prevent infinite loop // reset params to prevent infinite loop
props.navigation.dispatch(CommonActions.setParams({data: null})); this.props.navigation.dispatch(CommonActions.setParams({data: null}));
props.navigation.setOptions({ this.props.navigation.setOptions({
headerTitle: this.finalDataset.title, headerTitle: this.finalDataset.title,
}); });
} }
} }
} }
render(): React.Node { render() {
const {props} = this; const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
const { return <CardList
containerPaddingTop,
scrollIndicatorInsetTop,
onScroll,
} = props.collapsibleStack;
return (
<CardList
dataset={this.finalDataset.content} dataset={this.finalDataset.content}
isHorizontal={false} isHorizontal={false}
onScroll={onScroll} onScroll={onScroll}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: containerPaddingTop, paddingTop: containerPaddingTop,
paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20, paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20
}} }}
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}} scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
/> />
);
} }
} }

View file

@ -1,123 +0,0 @@
// @flow
import {stringToDate} from './Planning';
import type {EventType} from '../screens/Home/HomeScreen';
/**
* Gets the time limit depending on the current day:
* 17:30 for every day of the week except for thursday 11:30
* 00:00 on weekends
*/
export function getTodayEventTimeLimit(): Date {
const now = new Date();
if (now.getDay() === 4)
// Thursday
now.setHours(11, 30, 0);
else if (now.getDay() === 6 || now.getDay() === 0)
// Weekend
now.setHours(0, 0, 0);
else now.setHours(17, 30, 0);
return now;
}
/**
* Gets the duration (in milliseconds) of an event
*
* @param event {EventType}
* @return {number} The number of milliseconds
*/
export function getEventDuration(event: EventType): number {
const start = stringToDate(event.date_begin);
const end = stringToDate(event.date_end);
let duration = 0;
if (start != null && end != null) duration = end - start;
return duration;
}
/**
* Gets events starting after the limit
*
* @param events
* @param limit
* @return {Array<Object>}
*/
export function getEventsAfterLimit(
events: Array<EventType>,
limit: Date,
): Array<EventType> {
const validEvents = [];
events.forEach((event: EventType) => {
const startDate = stringToDate(event.date_begin);
if (startDate != null && startDate >= limit) {
validEvents.push(event);
}
});
return validEvents;
}
/**
* Gets the event with the longest duration in the given array.
* If all events have the same duration, return the first in the array.
*
* @param events
*/
export function getLongestEvent(events: Array<EventType>): EventType {
let longestEvent = events[0];
let longestTime = 0;
events.forEach((event: EventType) => {
const time = getEventDuration(event);
if (time > longestTime) {
longestTime = time;
longestEvent = event;
}
});
return longestEvent;
}
/**
* Gets events that have not yet ended/started
*
* @param events
*/
export function getFutureEvents(events: Array<EventType>): Array<EventType> {
const validEvents = [];
const now = new Date();
events.forEach((event: EventType) => {
const startDate = stringToDate(event.date_begin);
const endDate = stringToDate(event.date_end);
if (startDate != null) {
if (startDate > now) validEvents.push(event);
else if (endDate != null) {
if (endDate > now || endDate < startDate)
// Display event if it ends the following day
validEvents.push(event);
}
}
});
return validEvents;
}
/**
* Gets the event to display in the preview
*
* @param events
* @return {EventType | null}
*/
export function getDisplayEvent(events: Array<EventType>): EventType | null {
let displayEvent = null;
if (events.length > 1) {
const eventsAfterLimit = getEventsAfterLimit(
events,
getTodayEventTimeLimit(),
);
if (eventsAfterLimit.length > 0) {
if (eventsAfterLimit.length === 1) [displayEvent] = eventsAfterLimit;
else displayEvent = getLongestEvent(events);
} else {
displayEvent = getLongestEvent(events);
}
} else if (events.length === 1) {
[displayEvent] = events;
}
return displayEvent;
}

View file

@ -1,5 +1,6 @@
// @flow // @flow
/** /**
* Sanitizes the given string to improve search performance. * Sanitizes the given string to improve search performance.
* *
@ -9,12 +10,11 @@
* @return {string} The sanitized string * @return {string} The sanitized string
*/ */
export function sanitizeString(str: string): string { export function sanitizeString(str: string): string {
return str return str.toLowerCase()
.toLowerCase() .normalize("NFD")
.normalize('NFD') .replace(/[\u0300-\u036f]/g, "")
.replace(/[\u0300-\u036f]/g, '') .replace(/ /g, "")
.replace(/ /g, '') .replace(/_/g, "");
.replace(/_/g, '');
} }
/** /**
@ -24,7 +24,7 @@ export function sanitizeString(str: string): string {
* @param query The query string used to find a match * @param query The query string used to find a match
* @returns {boolean} * @returns {boolean}
*/ */
export function stringMatchQuery(str: string, query: string): boolean { export function stringMatchQuery(str: string, query: string) {
return sanitizeString(str).includes(sanitizeString(query)); return sanitizeString(str).includes(sanitizeString(query));
} }
@ -35,13 +35,10 @@ export function stringMatchQuery(str: string, query: string): boolean {
* @param categories The item's categories tuple * @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( export function isItemInCategoryFilter(filter: Array<number>, categories: [number, number]) {
filter: Array<number>, for (const category of categories) {
categories: Array<number | null>, if (filter.indexOf(category) !== -1)
): boolean { return true;
let itemFound = false; }
categories.forEach((cat: number | null) => { return false;
if (cat != null && filter.indexOf(cat) !== -1) itemFound = true;
});
return itemFound;
} }

View file

@ -1,19 +0,0 @@
// @flow
/**
* Gets the given services list without items of the given ids
*
* @param idList The ids of items to remove
* @param sourceList The item list to use as source
* @returns {[]}
*/
export default function getStrippedServicesList<T>(
idList: Array<string>,
sourceList: Array<{key: string, ...T}>,
): Array<{key: string, ...T}> {
const newArray = [];
sourceList.forEach((item: {key: string, ...T}) => {
if (!idList.includes(item.key)) newArray.push(item);
});
return newArray;
}