Compare commits

..

5 commits

30 changed files with 3101 additions and 2886 deletions

View file

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

View file

@ -1,170 +1,178 @@
// @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 CustomTabBar from "../Tabbar/CustomTabBar"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import AutoHideHandler from '../../utils/AutoHideHandler';
import type {CustomTheme} from "../../managers/ThemeManager"; import CustomTabBar from '../Tabbar/CustomTabBar';
import type {CustomTheme} from '../../managers/ThemeManager';
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
onPress: (action: string, data: any) => void, onPress: (action: string, data?: string) => void,
seekAttention: boolean, seekAttention: boolean,
} };
type State = { type StateType = {
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: {
position: 'absolute', position: 'absolute',
left: '5%', left: '5%',
width: '90%', width: '90%',
}, },
surface: { surface: {
position: 'relative', position: 'relative',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
borderRadius: 50, borderRadius: 50,
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,66 +1,63 @@
// @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 CustomTabBar from "../Tabbar/CustomTabBar"; import AutoHideHandler from '../../utils/AutoHideHandler';
import {StackNavigationProp} from "@react-navigation/stack"; import CustomTabBar from '../Tabbar/CustomTabBar';
type Props = { type PropsType = {
navigation: StackNavigationProp, icon: string,
icon: string, onPress: () => void,
onPress: () => void, };
}
const AnimatedFab = Animatable.createAnimatableComponent(FAB); const AnimatedFab = Animatable.createAnimatableComponent(FAB);
export default class AnimatedFAB extends React.Component<Props> {
ref: { current: null | Animatable.View };
hideHandler: AutoHideHandler;
constructor() {
super();
this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event);
};
onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) {
if (shouldHide)
this.ref.current.bounceOutDown(1000);
else
this.ref.current.bounceInUp(1000);
}
}
render() {
return (
<AnimatedFab
ref={this.ref}
useNativeDriver
icon={this.props.icon}
onPress={this.props.onPress}
style={{
...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT
}}
/>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
fab: { fab: {
position: 'absolute', position: 'absolute',
margin: 16, margin: 16,
right: 0, right: 0,
}, },
}); });
export default class AnimatedFAB extends React.Component<PropsType> {
ref: {current: null | Animatable.View};
hideHandler: AutoHideHandler;
constructor() {
super();
this.ref = React.createRef();
this.hideHandler = new AutoHideHandler(false);
this.hideHandler.addListener(this.onHideChange);
}
onScroll = (event: SyntheticEvent<EventTarget>) => {
this.hideHandler.onScroll(event);
};
onHideChange = (shouldHide: boolean) => {
if (this.ref.current != null) {
if (shouldHide) this.ref.current.bounceOutDown(1000);
else this.ref.current.bounceInUp(1000);
}
};
render(): React.Node {
const {props} = this;
return (
<AnimatedFab
ref={this.ref}
useNativeDriver
icon={props.icon}
onPress={props.onPress}
style={{
...styles.fab,
bottom: CustomTabBar.TAB_BAR_HEIGHT,
}}
/>
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -2,37 +2,46 @@
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 Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, theme: CustomTheme,
} };
class ActionsDashBoardItem extends React.Component<Props> { class ActionsDashBoardItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {props} = this;
return nextProps.theme.dark !== props.theme.dark;
}
shouldComponentUpdate(nextProps: Props): boolean { render(): React.Node {
return (nextProps.theme.dark !== this.props.theme.dark); const {props} = this;
} return (
<View>
render() { <List.Item
return ( title={i18n.t('screens.feedback.homeButtonTitle')}
<View> description={i18n.t('screens.feedback.homeButtonSubtitle')}
<List.Item left={({size}: {size: number}): React.Node => (
title={i18n.t("screens.feedback.homeButtonTitle")} <List.Icon size={size} icon="comment-quote" />
description={i18n.t("screens.feedback.homeButtonSubtitle")} )}
left={props => <List.Icon {...props} icon={"comment-quote"}/>} right={({size}: {size: number}): React.Node => (
right={props => <List.Icon {...props} icon={"chevron-right"}/>} <List.Icon size={size} icon="chevron-right" />
onPress={() => this.props.navigation.navigate("feedback")} )}
style={{paddingTop: 0, paddingBottom: 0, marginLeft: 10, marginRight: 10}} onPress={(): void => props.navigation.navigate('feedback')}
/> style={{
</View> paddingTop: 0,
paddingBottom: 0,
); marginLeft: 10,
} marginRight: 10,
}}
/>
</View>
);
}
} }
export default withTheme(ActionsDashBoardItem); export default withTheme(ActionsDashBoardItem);

View file

@ -1,91 +1,96 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Avatar, Card, Text, TouchableRipple, withTheme} from 'react-native-paper'; import {
import {StyleSheet, View} from "react-native"; Avatar,
import i18n from "i18n-js"; Card,
import type {CustomTheme} from "../../managers/ThemeManager"; Text,
TouchableRipple,
withTheme,
} from 'react-native-paper';
import {StyleSheet, View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type PropsType = {
eventNumber: number; eventNumber: number,
clickAction: () => void, clickAction: () => void,
theme: CustomTheme, theme: CustomTheme,
children?: React.Node children?: React.Node,
} };
const styles = StyleSheet.create({
card: {
width: 'auto',
marginLeft: 10,
marginRight: 10,
marginTop: 10,
overflow: 'hidden',
},
avatar: {
backgroundColor: 'transparent',
},
});
/** /**
* Component used to display a dashboard item containing a preview event * Component used to display a dashboard item containing a preview event
*/ */
class EventDashBoardItem extends React.Component<Props> { class EventDashBoardItem extends React.Component<PropsType> {
static defaultProps = {
children: null,
};
shouldComponentUpdate(nextProps: Props) { shouldComponentUpdate(nextProps: PropsType): boolean {
return (nextProps.theme.dark !== this.props.theme.dark) const {props} = this;
|| (nextProps.eventNumber !== this.props.eventNumber); return (
} nextProps.theme.dark !== props.theme.dark ||
nextProps.eventNumber !== 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>
);
}
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>
);
}
} }
const styles = StyleSheet.create({
card: {
width: 'auto',
marginLeft: 10,
marginRight: 10,
marginTop: 10,
overflow: 'hidden',
},
avatar: {
backgroundColor: 'transparent'
}
});
export default withTheme(EventDashBoardItem); export default withTheme(EventDashBoardItem);

View file

@ -2,126 +2,114 @@
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 {CustomTheme} from "../../managers/ThemeManager"; import type {FeedItemType} from '../../screens/Home/HomeScreen';
import type {feedItem} from "../../screens/Home/HomeScreen";
const ICON_AMICALE = require('../../../assets/amicale.png'); const ICON_AMICALE = require('../../../assets/amicale.png');
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, item: FeedItemType,
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<Props> { class FeedItem extends React.Component<PropsType> {
shouldComponentUpdate(): boolean {
return false;
}
shouldComponentUpdate() { onPress = () => {
return false; const {props} = this;
} props.navigation.navigate('feed-information', {
data: props.item,
date: props.subtitle,
});
};
/** render(): React.Node {
* Gets the amicale INSAT logo const {props} = this;
* const {item} = props;
* @return {*} const hasImage =
*/ item.full_picture !== '' && item.full_picture !== undefined;
getAvatar() {
return ( const cardMargin = 10;
<Image const cardHeight = props.height - 2 * cardMargin;
size={48} const imageSize = 250;
source={ICON_AMICALE} const titleHeight = 80;
style={{ const actionsHeight = 60;
const textHeight = hasImage
? cardHeight - titleHeight - actionsHeight - imageSize
: cardHeight - titleHeight - actionsHeight;
return (
<Card
style={{
margin: cardMargin,
height: cardHeight,
}}>
<TouchableRipple style={{flex: 1}} onPress={this.onPress}>
<View>
<Card.Title
title={props.title}
subtitle={props.subtitle}
left={(): React.Node => (
<Image
size={48}
source={ICON_AMICALE}
style={{
width: 48, width: 48,
height: 48, height: 48,
}}/> }}
); />
} )}
style={{height: titleHeight}}
onPress = () => { />
this.props.navigation.navigate( {hasImage ? (
'feed-information', <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
{ <ImageModal
data: this.props.item, resizeMode="contain"
date: this.props.subtitle imageBackgroundColor="#000"
}); style={{
}; width: imageSize,
height: imageSize,
render() { }}
const item = this.props.item; source={{
const hasImage = item.full_picture !== '' && item.full_picture !== undefined; uri: item.full_picture,
}}
const cardMargin = 10; />
const cardHeight = this.props.height - 2 * cardMargin; </View>
const imageSize = 250; ) : null}
const titleHeight = 80; <Card.Content>
const actionsHeight = 60; {item.message !== undefined ? (
const textHeight = hasImage <Autolink
? cardHeight - titleHeight - actionsHeight - imageSize text={item.message}
: cardHeight - titleHeight - actionsHeight; hashtag="facebook"
return ( component={Text}
<Card style={{height: textHeight}}
style={{ />
margin: cardMargin, ) : null}
height: cardHeight, </Card.Content>
}} <Card.Actions style={{height: actionsHeight}}>
> <Button
<TouchableRipple onPress={this.onPress}
style={{flex: 1}} icon="plus"
onPress={this.onPress}> style={{marginLeft: 'auto'}}>
<View> {i18n.t('screens.home.dashboard.seeMore')}
<Card.Title </Button>
title={this.props.title} </Card.Actions>
subtitle={this.props.subtitle} </View>
left={this.getAvatar} </TouchableRipple>
style={{height: titleHeight}} </Card>
/> );
{hasImage ? }
<View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<ImageModal
resizeMode="contain"
imageBackgroundColor={"#000"}
style={{
width: imageSize,
height: imageSize,
}}
source={{
uri: item.full_picture,
}}
/></View> : null}
<Card.Content>
{item.message !== undefined ?
<Autolink
text={item.message}
hashtag="facebook"
component={Text}
style={{height: textHeight}}
/> : null
}
</Card.Content>
<Card.Actions style={{height: actionsHeight}}>
<Button
onPress={this.onPress}
icon={'plus'}
style={{marginLeft: 'auto'}}>
{i18n.t('screens.home.dashboard.seeMore')}
</Button>
</Card.Actions>
</View>
</TouchableRipple>
</Card>
);
}
} }
export default FeedItem; export default FeedItem;

View file

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

View file

@ -2,15 +2,15 @@
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 type {CustomTheme} from "../../managers/ThemeManager"; import * as Animatable from 'react-native-animatable';
import * as Animatable from "react-native-animatable"; import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type PropsType = {
image: string, image: string | null,
onPress: () => void, onPress: () => void | null,
badgeCount: number | null, badgeCount: number | null,
theme: CustomTheme, theme: CustomTheme,
}; };
const AnimatableBadge = Animatable.createAnimatableComponent(Badge); const AnimatableBadge = Animatable.createAnimatableComponent(Badge);
@ -18,69 +18,68 @@ 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<Props> { class SmallDashboardItem extends React.Component<PropsType> {
itemSize: number;
itemSize: number; constructor(props: PropsType) {
super(props);
this.itemSize = Dimensions.get('window').width / 8;
}
constructor(props: Props) { shouldComponentUpdate(nextProps: PropsType): boolean {
super(props); const {props} = this;
this.itemSize = Dimensions.get('window').width / 8; return (
} nextProps.theme.dark !== props.theme.dark ||
nextProps.badgeCount !== props.badgeCount
shouldComponentUpdate(nextProps: Props) { );
return (nextProps.theme.dark !== this.props.theme.dark) }
|| (nextProps.badgeCount !== this.props.badgeCount);
}
render() {
const props = this.props;
return (
<TouchableRipple
onPress={this.props.onPress}
borderless={true}
style={{
marginLeft: this.itemSize / 6,
marginRight: this.itemSize / 6,
}}
>
<View style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: "80%",
height: "80%",
marginLeft: "auto",
marginRight: "auto",
marginTop: "auto",
marginBottom: "auto",
}}
/>
{
props.badgeCount != null && props.badgeCount > 0 ?
<AnimatableBadge
animation={"zoomIn"}
duration={300}
useNativeDriver
style={{
position: 'absolute',
top: 0,
right: 0,
backgroundColor: props.theme.colors.primary,
borderColor: props.theme.colors.background,
borderWidth: 2,
}}>
{props.badgeCount}
</AnimatableBadge> : null
}
</View>
</TouchableRipple>
);
}
render(): React.Node {
const {props} = this;
return (
<TouchableRipple
onPress={props.onPress}
borderless
style={{
marginLeft: this.itemSize / 6,
marginRight: this.itemSize / 6,
}}>
<View
style={{
width: this.itemSize,
height: this.itemSize,
}}>
<Image
source={{uri: props.image}}
style={{
width: '80%',
height: '80%',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 'auto',
marginBottom: 'auto',
}}
/>
{props.badgeCount != null && props.badgeCount > 0 ? (
<AnimatableBadge
animation="zoomIn"
duration={300}
useNativeDriver
style={{
position: 'absolute',
top: 0,
right: 0,
backgroundColor: props.theme.colors.primary,
borderColor: props.theme.colors.background,
borderWidth: 2,
}}>
{props.badgeCount}
</AnimatableBadge>
) : null}
</View>
</TouchableRipple>
);
}
} }
export default withTheme(SmallDashboardItem); export default withTheme(SmallDashboardItem);

View file

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

View file

@ -2,50 +2,41 @@
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 {cardItem} from "./CardList"; import type {ServiceItemType} from '../../../managers/ServicesManager';
type Props = { type PropsType = {
item: cardItem, item: ServiceItemType,
} };
export default class CardListItem extends React.Component<Props> { export default class CardListItem extends React.Component<PropsType> {
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 return (
: {uri: item.image}; <Card
return ( style={{
<Card width: '40%',
style={{ margin: 5,
width: '40%', marginLeft: 'auto',
margin: 5, marginRight: 'auto',
marginLeft: 'auto', }}>
marginRight: 'auto', <TouchableRipple style={{flex: 1}} onPress={item.onPress}>
}} <View>
> <Card.Cover style={{height: 80}} source={source} />
<TouchableRipple <Card.Content>
style={{flex: 1}} <Paragraph>{item.title}</Paragraph>
onPress={item.onPress}> <Caption>{item.subtitle}</Caption>
<View> </Card.Content>
<Card.Cover </View>
style={{height: 80}} </TouchableRipple>
source={source} </Card>
/> );
<Card.Content> }
<Paragraph>{item.title}</Paragraph>
<Caption>{item.subtitle}</Caption>
</Card.Content>
</View>
</TouchableRipple>
</Card>
);
}
} }

View file

@ -3,53 +3,52 @@
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 {cardItem} from "./CardList"; import type {ServiceItemType} from '../../../managers/ServicesManager';
type Props = { type PropsType = {
item: cardItem, item: ServiceItemType,
width: number, width: number,
} };
export default class ImageListItem extends React.Component<Props> { export default class ImageListItem extends React.Component<PropsType> {
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>
> <Image
<View> style={{
<Image width: props.width - 20,
style={{ height: props.width - 20,
width: this.props.width - 20, marginLeft: 'auto',
height: this.props.width - 20, marginRight: 'auto',
marginLeft: 'auto', }}
marginRight: 'auto', source={source}
}} />
source={source} <Text
/> style={{
<Text style={{ marginTop: 5,
marginTop: 5, marginLeft: 'auto',
marginLeft: 'auto', marginRight: 'auto',
marginRight: 'auto', textAlign: 'center',
textAlign: 'center' }}>
}}> {item.title}
{item.title} </Text>
</Text> </View>
</View> </TouchableRipple>
</TouchableRipple> );
); }
}
} }

View file

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

View file

@ -1,12 +1,12 @@
// @flow // @flow
import type {ServiceItem} from './ServicesManager'; import type {ServiceItemType} 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<ServiceItem | null> { getCurrentDashboard(): Array<ServiceItemType | null> {
const dashboardIdList = AsyncStorageManager.getObject( const dashboardIdList = AsyncStorageManager.getObject(
AsyncStorageManager.PREFERENCES.dashboardItems.key, AsyncStorageManager.PREFERENCES.dashboardItems.key,
); );

View file

@ -1,378 +1,384 @@
// @flow // @flow
import i18n from "i18n-js"; import i18n from 'i18n-js';
import AvailableWebsites from "../constants/AvailableWebsites"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import AvailableWebsites from '../constants/AvailableWebsites';
import ConnectionManager from "./ConnectionManager"; import ConnectionManager from './ConnectionManager';
import type {fullDashboard} from "../screens/Home/HomeScreen"; import type {FullDashboardType} from '../screens/Home/HomeScreen';
import getStrippedServicesList from '../utils/Services';
// AMICALE // AMICALE
const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png"; const CLUBS_IMAGE =
const PROFILE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png';
const EQUIPMENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png"; const PROFILE_IMAGE =
const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png';
const AMICALE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png"; const EQUIPMENT_IMAGE =
'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 = "https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png" const PROXIMO_IMAGE =
const WIKETUD_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Proximo.png';
const EE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/EEC.png"; const WIKETUD_IMAGE =
const TUTORINSA_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/TutorINSA.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Wiketud.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 = "https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png"; const ROOM_IMAGE =
const EMAIL_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Salles.png';
const ENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ENT.png"; const EMAIL_IMAGE =
const ACCOUNT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Account.png"; 'https://etud.insa-toulouse.fr/~amicale_app/images/Bluemind.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 = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashLaveLinge.png"; const WASHER_IMAGE =
const DRYER_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProxiwashSecheLinge.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 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 = {
key: string,
title: string,
subtitle: string,
image: string,
onPress: () => void,
badgeFunction?: (dashboard: FullDashboardType) => number,
};
export type ServiceItem = { export type ServiceCategoryType = {
key: string, key: string,
title: string, title: string,
subtitle: string, subtitle: string,
image: string, image: string | number,
onPress: () => void, content: Array<ServiceItemType>,
badgeFunction?: (dashboard: fullDashboard) => number, };
}
export type ServiceCategory = {
key: string,
title: string,
subtitle: string,
image: string | number,
content: Array<ServiceItem>
}
export default class ServicesManager { export default class ServicesManager {
navigation: StackNavigationProp;
navigation: StackNavigationProp; amicaleDataset: Array<ServiceItemType>;
amicaleDataset: Array<ServiceItem>; studentsDataset: Array<ServiceItemType>;
studentsDataset: Array<ServiceItem>;
insaDataset: Array<ServiceItem>;
specialDataset: Array<ServiceItem>;
categoriesDataset: Array<ServiceCategory>; insaDataset: Array<ServiceItemType>;
constructor(nav: StackNavigationProp) { specialDataset: Array<ServiceItemType>;
this.navigation = nav;
this.amicaleDataset = [
{
key: SERVICES_KEY.CLUBS,
title: i18n.t('screens.clubs.title'),
subtitle: i18n.t('screens.services.descriptions.clubs'),
image: CLUBS_IMAGE,
onPress: () => this.onAmicaleServicePress("club-list"),
},
{
key: SERVICES_KEY.PROFILE,
title: i18n.t('screens.profile.title'),
subtitle: i18n.t('screens.services.descriptions.profile'),
image: PROFILE_IMAGE,
onPress: () => this.onAmicaleServicePress("profile"),
},
{
key: SERVICES_KEY.EQUIPMENT,
title: i18n.t('screens.equipment.title'),
subtitle: i18n.t('screens.services.descriptions.equipment'),
image: EQUIPMENT_IMAGE,
onPress: () => this.onAmicaleServicePress("equipment-list"),
},
{
key: SERVICES_KEY.AMICALE_WEBSITE,
title: i18n.t('screens.websites.amicale'),
subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'),
image: AMICALE_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.AMICALE,
title: i18n.t('screens.websites.amicale')
}),
},
{
key: SERVICES_KEY.VOTE,
title: i18n.t('screens.vote.title'),
subtitle: i18n.t('screens.services.descriptions.vote'),
image: VOTE_IMAGE,
onPress: () => this.onAmicaleServicePress("vote"),
},
];
this.studentsDataset = [
{
key: SERVICES_KEY.PROXIMO,
title: i18n.t('screens.proximo.title'),
subtitle: i18n.t('screens.services.descriptions.proximo'),
image: PROXIMO_IMAGE,
onPress: () => nav.navigate("proximo"),
badgeFunction: (dashboard: fullDashboard) => dashboard.proximo_articles
},
{
key: SERVICES_KEY.WIKETUD,
title: "Wiketud",
subtitle: i18n.t('screens.services.descriptions.wiketud'),
image: WIKETUD_IMAGE,
onPress: () => nav.navigate("website", {host: AvailableWebsites.websites.WIKETUD, title: "Wiketud"}),
},
{
key: SERVICES_KEY.ELUS_ETUDIANTS,
title: "Élus Étudiants",
subtitle: i18n.t('screens.services.descriptions.elusEtudiants'),
image: EE_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.ELUS_ETUDIANTS,
title: "Élus Étudiants"
}),
},
{
key: SERVICES_KEY.TUTOR_INSA,
title: "Tutor'INSA",
subtitle: i18n.t('screens.services.descriptions.tutorInsa'),
image: TUTORINSA_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.TUTOR_INSA,
title: "Tutor'INSA"
}),
badgeFunction: (dashboard: fullDashboard) => dashboard.available_tutorials
},
];
this.insaDataset = [
{
key: SERVICES_KEY.RU,
title: i18n.t('screens.menu.title'),
subtitle: i18n.t('screens.services.descriptions.self'),
image: RU_IMAGE,
onPress: () => nav.navigate("self-menu"),
badgeFunction: (dashboard: fullDashboard) => dashboard.today_menu.length
},
{
key: SERVICES_KEY.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'),
subtitle: i18n.t('screens.services.descriptions.availableRooms'),
image: ROOM_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms')
}),
},
{
key: SERVICES_KEY.BIB,
title: i18n.t('screens.websites.bib'),
subtitle: i18n.t('screens.services.descriptions.bib'),
image: BIB_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.BIB,
title: i18n.t('screens.websites.bib')
}),
},
{
key: SERVICES_KEY.EMAIL,
title: i18n.t('screens.websites.mails'),
subtitle: i18n.t('screens.services.descriptions.mails'),
image: EMAIL_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.BLUEMIND,
title: i18n.t('screens.websites.mails')
}),
},
{
key: SERVICES_KEY.ENT,
title: i18n.t('screens.websites.ent'),
subtitle: i18n.t('screens.services.descriptions.ent'),
image: ENT_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.ENT,
title: i18n.t('screens.websites.ent')
}),
},
{
key: SERVICES_KEY.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title'),
subtitle: i18n.t('screens.services.descriptions.insaAccount'),
image: ACCOUNT_IMAGE,
onPress: () => nav.navigate("website", {
host: AvailableWebsites.websites.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title')
}),
},
];
this.specialDataset = [
{
key: SERVICES_KEY.WASHERS,
title: i18n.t('screens.proxiwash.washers'),
subtitle: i18n.t('screens.services.descriptions.washers'),
image: WASHER_IMAGE,
onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: fullDashboard) => dashboard.available_washers
},
{
key: SERVICES_KEY.DRYERS,
title: i18n.t('screens.proxiwash.dryers'),
subtitle: i18n.t('screens.services.descriptions.washers'),
image: DRYER_IMAGE,
onPress: () => nav.navigate("proxiwash"),
badgeFunction: (dashboard: fullDashboard) => dashboard.available_dryers
}
];
this.categoriesDataset = [
{
key: SERVICES_CATEGORIES_KEY.AMICALE,
title: i18n.t("screens.services.categories.amicale"),
subtitle: i18n.t("screens.services.more"),
image: AMICALE_LOGO,
content: this.amicaleDataset
},
{
key: SERVICES_CATEGORIES_KEY.STUDENTS,
title: i18n.t("screens.services.categories.students"),
subtitle: i18n.t("screens.services.more"),
image: 'account-group',
content: this.studentsDataset
},
{
key: SERVICES_CATEGORIES_KEY.INSA,
title: i18n.t("screens.services.categories.insa"),
subtitle: i18n.t("screens.services.more"),
image: 'school',
content: this.insaDataset
},
{
key: SERVICES_CATEGORIES_KEY.SPECIAL,
title: i18n.t("screens.services.categories.special"),
subtitle: i18n.t("screens.services.categories.special"),
image: 'star',
content: this.specialDataset
},
];
}
/** categoriesDataset: Array<ServiceCategoryType>;
* Redirects the user to the login screen if he is not logged in
*
* @param route
* @returns {null}
*/
onAmicaleServicePress(route: string) {
if (ConnectionManager.getInstance().isLoggedIn())
this.navigation.navigate(route);
else
this.navigation.navigate("login", {nextScreen: route});
}
/** constructor(nav: StackNavigationProp) {
* Gets the given services list without items of the given ids this.navigation = nav;
* this.amicaleDataset = [
* @param idList The ids of items to remove {
* @param sourceList The item list to use as source key: SERVICES_KEY.CLUBS,
* @returns {[]} title: i18n.t('screens.clubs.title'),
*/ subtitle: i18n.t('screens.services.descriptions.clubs'),
getStrippedList(idList: Array<string>, sourceList: Array<{key: string, [key: string]: any}>) { image: CLUBS_IMAGE,
let newArray = []; onPress: (): void => this.onAmicaleServicePress('club-list'),
for (let i = 0; i < sourceList.length; i++) { },
const item = sourceList[i]; {
if (!(idList.includes(item.key))) key: SERVICES_KEY.PROFILE,
newArray.push(item); title: i18n.t('screens.profile.title'),
} subtitle: i18n.t('screens.services.descriptions.profile'),
return newArray; image: PROFILE_IMAGE,
} onPress: (): void => this.onAmicaleServicePress('profile'),
},
{
key: SERVICES_KEY.EQUIPMENT,
title: i18n.t('screens.equipment.title'),
subtitle: i18n.t('screens.services.descriptions.equipment'),
image: EQUIPMENT_IMAGE,
onPress: (): void => this.onAmicaleServicePress('equipment-list'),
},
{
key: SERVICES_KEY.AMICALE_WEBSITE,
title: i18n.t('screens.websites.amicale'),
subtitle: i18n.t('screens.services.descriptions.amicaleWebsite'),
image: AMICALE_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.AMICALE,
title: i18n.t('screens.websites.amicale'),
}),
},
{
key: SERVICES_KEY.VOTE,
title: i18n.t('screens.vote.title'),
subtitle: i18n.t('screens.services.descriptions.vote'),
image: VOTE_IMAGE,
onPress: (): void => this.onAmicaleServicePress('vote'),
},
];
this.studentsDataset = [
{
key: SERVICES_KEY.PROXIMO,
title: i18n.t('screens.proximo.title'),
subtitle: i18n.t('screens.services.descriptions.proximo'),
image: PROXIMO_IMAGE,
onPress: (): void => nav.navigate('proximo'),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.proximo_articles,
},
{
key: SERVICES_KEY.WIKETUD,
title: 'Wiketud',
subtitle: i18n.t('screens.services.descriptions.wiketud'),
image: WIKETUD_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.WIKETUD,
title: 'Wiketud',
}),
},
{
key: SERVICES_KEY.ELUS_ETUDIANTS,
title: 'Élus Étudiants',
subtitle: i18n.t('screens.services.descriptions.elusEtudiants'),
image: EE_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.ELUS_ETUDIANTS,
title: 'Élus Étudiants',
}),
},
{
key: SERVICES_KEY.TUTOR_INSA,
title: "Tutor'INSA",
subtitle: i18n.t('screens.services.descriptions.tutorInsa'),
image: TUTORINSA_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.TUTOR_INSA,
title: "Tutor'INSA",
}),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.available_tutorials,
},
];
this.insaDataset = [
{
key: SERVICES_KEY.RU,
title: i18n.t('screens.menu.title'),
subtitle: i18n.t('screens.services.descriptions.self'),
image: RU_IMAGE,
onPress: (): void => nav.navigate('self-menu'),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.today_menu.length,
},
{
key: SERVICES_KEY.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'),
subtitle: i18n.t('screens.services.descriptions.availableRooms'),
image: ROOM_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.AVAILABLE_ROOMS,
title: i18n.t('screens.websites.rooms'),
}),
},
{
key: SERVICES_KEY.BIB,
title: i18n.t('screens.websites.bib'),
subtitle: i18n.t('screens.services.descriptions.bib'),
image: BIB_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.BIB,
title: i18n.t('screens.websites.bib'),
}),
},
{
key: SERVICES_KEY.EMAIL,
title: i18n.t('screens.websites.mails'),
subtitle: i18n.t('screens.services.descriptions.mails'),
image: EMAIL_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.BLUEMIND,
title: i18n.t('screens.websites.mails'),
}),
},
{
key: SERVICES_KEY.ENT,
title: i18n.t('screens.websites.ent'),
subtitle: i18n.t('screens.services.descriptions.ent'),
image: ENT_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.ENT,
title: i18n.t('screens.websites.ent'),
}),
},
{
key: SERVICES_KEY.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title'),
subtitle: i18n.t('screens.services.descriptions.insaAccount'),
image: ACCOUNT_IMAGE,
onPress: (): void =>
nav.navigate('website', {
host: AvailableWebsites.websites.INSA_ACCOUNT,
title: i18n.t('screens.insaAccount.title'),
}),
},
];
this.specialDataset = [
{
key: SERVICES_KEY.WASHERS,
title: i18n.t('screens.proxiwash.washers'),
subtitle: i18n.t('screens.services.descriptions.washers'),
image: WASHER_IMAGE,
onPress: (): void => nav.navigate('proxiwash'),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.available_washers,
},
{
key: SERVICES_KEY.DRYERS,
title: i18n.t('screens.proxiwash.dryers'),
subtitle: i18n.t('screens.services.descriptions.washers'),
image: DRYER_IMAGE,
onPress: (): void => nav.navigate('proxiwash'),
badgeFunction: (dashboard: FullDashboardType): number =>
dashboard.available_dryers,
},
];
this.categoriesDataset = [
{
key: SERVICES_CATEGORIES_KEY.AMICALE,
title: i18n.t('screens.services.categories.amicale'),
subtitle: i18n.t('screens.services.more'),
image: AMICALE_LOGO,
content: this.amicaleDataset,
},
{
key: SERVICES_CATEGORIES_KEY.STUDENTS,
title: i18n.t('screens.services.categories.students'),
subtitle: i18n.t('screens.services.more'),
image: 'account-group',
content: this.studentsDataset,
},
{
key: SERVICES_CATEGORIES_KEY.INSA,
title: i18n.t('screens.services.categories.insa'),
subtitle: i18n.t('screens.services.more'),
image: 'school',
content: this.insaDataset,
},
{
key: SERVICES_CATEGORIES_KEY.SPECIAL,
title: i18n.t('screens.services.categories.special'),
subtitle: i18n.t('screens.services.categories.special'),
image: 'star',
content: this.specialDataset,
},
];
}
/** /**
* Gets the list of amicale's services * Redirects the user to the login screen if he is not logged in
* *
* @param excludedItems Ids of items to exclude from the returned list * @param route
* @returns {Array<ServiceItem>} * @returns {null}
*/ */
getAmicaleServices(excludedItems?: Array<string>) { onAmicaleServicePress(route: string) {
if (excludedItems != null) if (ConnectionManager.getInstance().isLoggedIn())
return this.getStrippedList(excludedItems, this.amicaleDataset) this.navigation.navigate(route);
else else this.navigation.navigate('login', {nextScreen: route});
return this.amicaleDataset; }
}
/** /**
* Gets the list of students' 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<ServiceItem>} * @returns {Array<ServiceItemType>}
*/ */
getStudentServices(excludedItems?: Array<string>) { getAmicaleServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null)
return this.getStrippedList(excludedItems, this.studentsDataset) return getStrippedServicesList(excludedItems, this.amicaleDataset);
else return this.amicaleDataset;
return this.studentsDataset; }
}
/** /**
* Gets the list of INSA's 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<ServiceItem>} * @returns {Array<ServiceItemType>}
*/ */
getINSAServices(excludedItems?: Array<string>) { getStudentServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null)
return this.getStrippedList(excludedItems, this.insaDataset) return getStrippedServicesList(excludedItems, this.studentsDataset);
else return this.studentsDataset;
return this.insaDataset; }
}
/** /**
* Gets the list of special 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<ServiceItem>} * @returns {Array<ServiceItemType>}
*/ */
getSpecialServices(excludedItems?: Array<string>) { getINSAServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null)
return this.getStrippedList(excludedItems, this.specialDataset) return getStrippedServicesList(excludedItems, this.insaDataset);
else return this.insaDataset;
return this.specialDataset; }
}
/** /**
* Gets all services sorted by category * Gets the list of special services
* *
* @param excludedItems Ids of categories to exclude from the returned list * @param excludedItems Ids of items to exclude from the returned list
* @returns {Array<ServiceCategory>} * @returns {Array<ServiceItemType>}
*/ */
getCategories(excludedItems?: Array<string>) { getSpecialServices(excludedItems?: Array<string>): Array<ServiceItemType> {
if (excludedItems != null) if (excludedItems != null)
return this.getStrippedList(excludedItems, this.categoriesDataset) return getStrippedServicesList(excludedItems, this.specialDataset);
else return this.specialDataset;
return this.categoriesDataset; }
}
/**
* Gets all services sorted by category
*
* @param excludedItems Ids of categories to exclude from the returned list
* @returns {Array<ServiceCategoryType>}
*/
getCategories(excludedItems?: Array<string>): Array<ServiceCategoryType> {
if (excludedItems != null)
return getStrippedServicesList(excludedItems, this.categoriesDataset);
return this.categoriesDataset;
}
} }

View file

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

View file

@ -2,252 +2,276 @@
import * as React from 'react'; import * as React from 'react';
import {Linking, View} from 'react-native'; import {Linking, View} from 'react-native';
import {Avatar, Button, Card, Chip, Paragraph, withTheme} from 'react-native-paper'; import {
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 AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; import {StackNavigationProp} from '@react-navigation/stack';
import CustomHTML from "../../../components/Overrides/CustomHTML"; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import CustomTabBar from "../../../components/Tabbar/CustomTabBar"; import CustomHTML from '../../../components/Overrides/CustomHTML';
import type {category, club} from "./ClubListScreen"; import CustomTabBar from '../../../components/Tabbar/CustomTabBar';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {ClubCategoryType, ClubType} from './ClubListScreen';
import {StackNavigationProp} from "@react-navigation/stack"; import type {CustomTheme} from '../../../managers/ThemeManager';
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 Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
data?: club, data?: ClubType,
categories?: Array<category>, categories?: Array<ClubCategoryType>,
clubId?: number, clubId?: number,
}, ...
}, },
theme: CustomTheme ...
},
theme: CustomTheme,
}; };
type State = { const AMICALE_MAIL = 'clubs@amicale-insat.fr';
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<Props, State> { class ClubDisplayScreen extends React.Component<PropsType> {
displayData: ClubType | null;
displayData: club | null; categories: Array<ClubCategoryType> | null;
categories: Array<category> | null;
clubId: number;
shouldFetchData: boolean; clubId: number;
state = { shouldFetchData: boolean;
imageModalVisible: false,
};
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
if (this.props.route.params != null) { if (props.route.params != null) {
if (this.props.route.params.data != null && this.props.route.params.categories != null) { if (
this.displayData = this.props.route.params.data; props.route.params.data != null &&
this.categories = this.props.route.params.categories; props.route.params.categories != null
this.clubId = this.props.route.params.data.id; ) {
this.shouldFetchData = false; this.displayData = props.route.params.data;
} else if (this.props.route.params.clubId != null) { this.categories = props.route.params.categories;
this.displayData = null; this.clubId = props.route.params.data.id;
this.categories = null; this.shouldFetchData = false;
this.clubId = this.props.route.params.clubId; } else if (props.route.params.clubId != null) {
this.shouldFetchData = true; this.displayData = null;
} this.categories = null;
} this.clubId = props.route.params.clubId;
this.shouldFetchData = true;
}
} }
}
/** /**
* Gets the name of the category with the given ID * Gets the name of the category with the given ID
* *
* @param id The category's ID * @param id The category's ID
* @returns {string|*} * @returns {string|*}
*/ */
getCategoryName(id: number) { getCategoryName(id: number): string {
if (this.categories !== null) { let categoryName = '';
for (let i = 0; i < this.categories.length; i++) { if (this.categories !== null) {
if (id === this.categories[i].id) this.categories.forEach((item: ClubCategoryType) => {
return this.categories[i].name; if (id === item.id) categoryName = item.name;
} });
}
return "";
} }
return categoryName;
}
/** /**
* Gets the view for rendering categories * Gets the view for rendering categories
* *
* @param categories The categories to display (max 2) * @param categories The categories to display (max 2)
* @returns {null|*} * @returns {null|*}
*/ */
getCategoriesRender(categories: [number, number]) { getCategoriesRender(categories: Array<number | null>): React.Node {
if (this.categories === null) if (this.categories == null) return null;
return null;
let final = []; const final = [];
for (let i = 0; i < categories.length; i++) { categories.forEach((cat: number | null) => {
let cat = categories[i]; if (cat != null) {
if (cat !== null) { final.push(
final.push( <Chip style={{marginRight: 5}} key={cat}>
<Chip {this.getCategoryName(cat)}
style={{marginRight: 5}} </Chip>,
key={i.toString()}>
{this.getCategoryName(cat)}
</Chip>
);
}
}
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
}
/**
* Gets the view for rendering club managers if any
*
* @param managers The list of manager names
* @param email The club contact email
* @returns {*}
*/
getManagersRender(managers: Array<string>, email: string | null) {
let managersListView = [];
for (let i = 0; i < managers.length; i++) {
managersListView.push(<Paragraph key={i.toString()}>{managers[i]}</Paragraph>)
}
const hasManagers = managers.length > 0;
return (
<Card style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title
title={i18n.t('screens.clubs.managers')}
subtitle={hasManagers ? i18n.t('screens.clubs.managersSubtitle') : i18n.t('screens.clubs.managersUnavailable')}
left={(props) => <Avatar.Icon
{...props}
style={{backgroundColor: 'transparent'}}
color={hasManagers ? this.props.theme.colors.success : this.props.theme.colors.primary}
icon="account-tie"/>}
/>
<Card.Content>
{managersListView}
{this.getEmailButton(email, hasManagers)}
</Card.Content>
</Card>
); );
}
});
return <View style={{flexDirection: 'row', marginTop: 5}}>{final}</View>;
}
/**
* Gets the view for rendering club managers if any
*
* @param managers The list of manager names
* @param email The club contact email
* @returns {*}
*/
getManagersRender(managers: Array<string>, email: string | null): React.Node {
const {props} = this;
const managersListView = [];
managers.forEach((item: string) => {
managersListView.push(<Paragraph key={item}>{item}</Paragraph>);
});
const hasManagers = managers.length > 0;
return (
<Card
style={{marginTop: 10, marginBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
<Card.Title
title={i18n.t('screens.clubs.managers')}
subtitle={
hasManagers
? i18n.t('screens.clubs.managersSubtitle')
: i18n.t('screens.clubs.managersUnavailable')
}
left={({size}: {size: number}): React.Node => (
<Avatar.Icon
size={size}
style={{backgroundColor: 'transparent'}}
color={
hasManagers
? props.theme.colors.success
: props.theme.colors.primary
}
icon="account-tie"
/>
)}
/>
<Card.Content>
{managersListView}
{ClubDisplayScreen.getEmailButton(email, hasManagers)}
</Card.Content>
</Card>
);
}
/**
* Gets the email button to contact the club, or the amicale if the club does not have any managers
*
* @param email The club contact email
* @param hasManagers True if the club has managers
* @returns {*}
*/
static getEmailButton(
email: string | null,
hasManagers: boolean,
): React.Node {
const destinationEmail =
email != null && hasManagers ? email : AMICALE_MAIL;
const text =
email != null && hasManagers
? i18n.t('screens.clubs.clubContact')
: i18n.t('screens.clubs.amicaleContact');
return (
<Card.Actions>
<Button
icon="email"
mode="contained"
onPress={() => {
Linking.openURL(`mailto:${destinationEmail}`);
}}
style={{marginLeft: 'auto'}}>
{text}
</Button>
</Card.Actions>
);
}
getScreen = (response: Array<ApiGenericDataType | null>): React.Node => {
const {props} = this;
let data: ClubType | null = null;
if (response[0] != null) {
[data] = response;
this.updateHeaderTitle(data);
} }
if (data != null) {
return (
<CollapsibleScrollView style={{paddingLeft: 5, paddingRight: 5}} hasTab>
{this.getCategoriesRender(data.category)}
{data.logo !== null ? (
<View
style={{
marginLeft: 'auto',
marginRight: 'auto',
marginTop: 10,
marginBottom: 10,
}}>
<ImageModal
resizeMode="contain"
imageBackgroundColor={props.theme.colors.background}
style={{
width: 300,
height: 300,
}}
source={{
uri: data.logo,
}}
/>
</View>
) : (
<View />
)}
/** {data.description !== null ? (
* Gets the email button to contact the club, or the amicale if the club does not have any managers // Surround description with div to allow text styling if the description is not html
* <Card.Content>
* @param email The club contact email <CustomHTML html={data.description} />
* @param hasManagers True if the club has managers </Card.Content>
* @returns {*} ) : (
*/ <View />
getEmailButton(email: string | null, hasManagers: boolean) { )}
const destinationEmail = email != null && hasManagers {this.getManagersRender(data.responsibles, data.email)}
? email </CollapsibleScrollView>
: AMICALE_MAIL; );
const text = email != null && hasManagers
? i18n.t("screens.clubs.clubContact")
: i18n.t("screens.clubs.amicaleContact");
return (
<Card.Actions>
<Button
icon="email"
mode="contained"
onPress={() => Linking.openURL('mailto:' + destinationEmail)}
style={{marginLeft: 'auto'}}
>
{text}
</Button>
</Card.Actions>
);
} }
return null;
};
/** /**
* Updates the header title to match the given club * Updates the header title to match the given club
* *
* @param data The club data * @param data The club data
*/ */
updateHeaderTitle(data: club) { updateHeaderTitle(data: ClubType) {
this.props.navigation.setOptions({title: data.name}) const {props} = this;
} props.navigation.setOptions({title: data.name});
}
getScreen = (response: Array<{ [key: string]: any } | null>) => { render(): React.Node {
let data: club | null = null; const {props} = this;
if (response[0] != null) { if (this.shouldFetchData)
data = response[0]; return (
this.updateHeaderTitle(data); <AuthenticatedScreen
} navigation={props.navigation}
if (data != null) { requests={[
return ( {
<CollapsibleScrollView link: 'clubs/info',
style={{paddingLeft: 5, paddingRight: 5}} params: {id: this.clubId},
hasTab={true} mandatory: true,
> },
{this.getCategoriesRender(data.category)} ]}
{data.logo !== null ? renderFunction={this.getScreen}
<View style={{ errorViewOverride={[
marginLeft: 'auto', {
marginRight: 'auto', errorCode: ERROR_TYPE.BAD_INPUT,
marginTop: 10, message: i18n.t('screens.clubs.invalidClub'),
marginBottom: 10, icon: 'account-question',
}}> showRetryButton: false,
<ImageModal },
resizeMode="contain" ]}
imageBackgroundColor={this.props.theme.colors.background} />
style={{ );
width: 300, return this.getScreen([this.displayData]);
height: 300, }
}}
source={{
uri: data.logo,
}}
/>
</View>
: <View/>}
{data.description !== null ?
// Surround description with div to allow text styling if the description is not html
<Card.Content>
<CustomHTML html={data.description}/>
</Card.Content>
: <View/>}
{this.getManagersRender(data.responsibles, data.email)}
</CollapsibleScrollView>
);
} else
return null;
};
render() {
if (this.shouldFetchData)
return <AuthenticatedScreen
{...this.props}
requests={[
{
link: 'clubs/info',
params: {'id': this.clubId},
mandatory: true
}
]}
renderFunction={this.getScreen}
errorViewOverride={[
{
errorCode: ERROR_TYPE.BAD_INPUT,
message: i18n.t("screens.clubs.invalidClub"),
icon: "account-question",
showRetryButton: false
}
]}
/>;
else
return this.getScreen([this.displayData]);
}
} }
export default withTheme(ClubDisplayScreen); export default withTheme(ClubDisplayScreen);

View file

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

View file

@ -4,108 +4,114 @@ 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 MaterialHeaderButtons, {Item} from "../../components/Overrides/CustomHeaderButton"; import {StackNavigationProp} from '@react-navigation/stack';
import CustomTabBar from "../../components/Tabbar/CustomTabBar"; import MaterialHeaderButtons, {
import {StackNavigationProp} from "@react-navigation/stack"; Item,
import type {feedItem} from "./HomeScreen"; } from '../../components/Overrides/CustomHeaderButton';
import CollapsibleScrollView from "../../components/Collapsible/CollapsibleScrollView"; import CustomTabBar from '../../components/Tabbar/CustomTabBar';
import type {FeedItemType} from './HomeScreen';
import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { params: { data: feedItem, date: string } } route: {params: {data: FeedItemType, 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<Props> { class FeedItemScreen extends React.Component<PropsType> {
displayData: FeedItemType;
displayData: feedItem; date: string;
date: string;
constructor(props) { constructor(props: PropsType) {
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() {
this.props.navigation.setOptions({ const {props} = this;
headerRight: this.getHeaderButton, props.navigation.setOptions({
}); headerRight: this.getHeaderButton,
} });
}
/** /**
* Opens the feed item out link in browser or compatible app * Opens the feed item out link in browser or compatible app
*/ */
onOutLinkPress = () => { onOutLinkPress = () => {
Linking.openURL(this.displayData.permalink_url); Linking.openURL(this.displayData.permalink_url);
}; };
/** /**
* Gets the out link header button * Gets the out link header button
* *
* @returns {*} * @returns {*}
*/ */
getHeaderButton = () => { getHeaderButton = (): React.Node => {
return <MaterialHeaderButtons> return (
<Item title="main" iconName={'facebook'} color={"#2e88fe"} onPress={this.onOutLinkPress}/> <MaterialHeaderButtons>
</MaterialHeaderButtons>; <Item
}; title="main"
iconName="facebook"
color="#2e88fe"
onPress={this.onOutLinkPress}
/>
</MaterialHeaderButtons>
);
};
/** render(): React.Node {
* Gets the Amicale INSA avatar const hasImage =
* this.displayData.full_picture !== '' &&
* @returns {*} this.displayData.full_picture != null;
*/ return (
getAvatar() { <CollapsibleScrollView style={{margin: 5}} hasTab>
return ( <Card.Title
<Avatar.Image size={48} source={ICON_AMICALE} title={NAME_AMICALE}
style={{backgroundColor: 'transparent'}}/> subtitle={this.date}
); left={(): React.Node => (
} <Avatar.Image
size={48}
render() { source={ICON_AMICALE}
const hasImage = this.displayData.full_picture !== '' && this.displayData.full_picture != null; style={{backgroundColor: 'transparent'}}
return ( />
<CollapsibleScrollView )}
style={{margin: 5,}} />
hasTab={true} {hasImage ? (
> <View style={{marginLeft: 'auto', marginRight: 'auto'}}>
<Card.Title <ImageModal
title={NAME_AMICALE} resizeMode="contain"
subtitle={this.date} imageBackgroundColor="#000"
left={this.getAvatar} style={{
/> width: 250,
{hasImage ? height: 250,
<View style={{marginLeft: 'auto', marginRight: 'auto'}}> }}
<ImageModal source={{
resizeMode="contain" uri: this.displayData.full_picture,
imageBackgroundColor={"#000"} }}
style={{ />
width: 250, </View>
height: 250, ) : null}
}} <Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}>
source={{ {this.displayData.message !== undefined ? (
uri: this.displayData.full_picture, <Autolink
}} text={this.displayData.message}
/></View> : null} hashtag="facebook"
<Card.Content style={{paddingBottom: CustomTabBar.TAB_BAR_HEIGHT + 20}}> component={Text}
{this.displayData.message !== undefined ? />
<Autolink ) : null}
text={this.displayData.message} </Card.Content>
hashtag="facebook" </CollapsibleScrollView>
component={Text} );
/> : null }
}
</Card.Content>
</CollapsibleScrollView>
);
}
} }
export default withTheme(FeedItemScreen); export default withTheme(FeedItemScreen);

File diff suppressed because it is too large Load diff

View file

@ -1,238 +1,237 @@
// @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 {MASCOT_STYLE} from "../../components/Mascot/Mascot"; import URLHandler from '../../utils/URLHandler';
import MascotPopup from "../../components/Mascot/MascotPopup"; import AlertDialog from '../../components/Dialogs/AlertDialog';
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 Props = {}; type StateType = {
type State = { hasPermission: boolean,
hasPermission: boolean, scanned: boolean,
scanned: boolean, dialogVisible: boolean,
dialogVisible: boolean, mascotDialogVisible: boolean,
mascotDialogVisible: boolean, loading: boolean,
dialogTitle: string,
dialogMessage: string,
loading: boolean,
}; };
class ScannerScreen extends React.Component<Props, State> {
state = {
hasPermission: false,
scanned: false,
mascotDialogVisible: false,
dialogVisible: false,
dialogTitle: "",
dialogMessage: "",
loading: false,
};
constructor() {
super();
}
componentDidMount() {
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
*
* @returns {*}
*/
getPermissionScreen() {
return <View style={{marginLeft: 10, marginRight: 10}}>
<Text>{i18n.t("screens.scanner.permissions.error")}</Text>
<Button
icon="camera"
mode="contained"
onPress={this.requestPermissions}
style={{
marginTop: 10,
marginLeft: 'auto',
marginRight: 'auto',
}}
>
{i18n.t("screens.scanner.permissions.button")}
</Button>
</View>
}
/**
* Shows a dialog indicating how to use the scanner
*/
showHelpDialog = () => {
this.setState({
mascotDialogVisible: true,
scanned: true,
});
};
/**
* Shows a loading dialog
*/
showOpeningDialog = () => {
this.setState({
loading: true,
scanned: true,
});
};
/**
* Shows a dialog indicating the user the scanned code was invalid
*/
showErrorDialog() {
this.setState({
dialogVisible: true,
scanned: true,
});
}
/**
* Hide any dialog
*/
onDialogDismiss = () => this.setState({
dialogVisible: false,
scanned: false,
});
onMascotDialogDismiss = () => this.setState({
mascotDialogVisible: false,
scanned: false,
});
/**
* Gets a view with the scanner.
* This scanner uses the back camera, can only scan qr codes and has a square mask on the center.
* The mask is only for design purposes as a code is scanned as soon as it enters the camera view
*
* @returns {*}
*/
getScanner() {
return (
<RNCamera
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,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT
}}>
{this.state.hasPermission
? this.getScanner()
: this.getPermissionScreen()
}
<Button
icon="information"
mode="contained"
onPress={this.showHelpDialog}
style={styles.button}
>
{i18n.t("screens.scanner.help.button")}
</Button>
<MascotPopup
visible={this.state.mascotDialogVisible}
title={i18n.t("screens.scanner.mascotDialog.title")}
message={i18n.t("screens.scanner.mascotDialog.message")}
icon={"camera-iris"}
buttons={{
action: null,
cancel: {
message: i18n.t("screens.scanner.mascotDialog.button"),
icon: "check",
onPress: this.onMascotDialogDismiss,
}
}}
emotion={MASCOT_STYLE.NORMAL}
/>
<AlertDialog
visible={this.state.dialogVisible}
onDismiss={this.onDialogDismiss}
title={i18n.t("screens.scanner.error.title")}
message={i18n.t("screens.scanner.error.message")}
/>
<LoadingConfirmDialog
visible={this.state.loading}
titleLoading={i18n.t("general.loading")}
startLoading={true}
/>
</View>
);
}
}
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}, },
button: { button: {
position: 'absolute', position: 'absolute',
bottom: 20, bottom: 20,
width: '80%', width: '80%',
left: '10%' left: '10%',
}, },
}); });
class ScannerScreen extends React.Component<null, StateType> {
constructor() {
super();
this.state = {
hasPermission: false,
scanned: false,
mascotDialogVisible: false,
dialogVisible: false,
loading: false,
};
}
componentDidMount() {
this.requestPermissions();
}
/**
* Gets a view asking user for permission to use the camera
*
* @returns {*}
*/
getPermissionScreen(): React.Node {
return (
<View style={{marginLeft: 10, marginRight: 10}}>
<Text>{i18n.t('screens.scanner.permissions.error')}</Text>
<Button
icon="camera"
mode="contained"
onPress={this.requestPermissions}
style={{
marginTop: 10,
marginLeft: 'auto',
marginRight: 'auto',
}}>
{i18n.t('screens.scanner.permissions.button')}
</Button>
</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,
});
}
/**
* Shows a dialog indicating how to use the scanner
*/
showHelpDialog = () => {
this.setState({
mascotDialogVisible: true,
scanned: true,
});
};
/**
* Shows a loading dialog
*/
showOpeningDialog = () => {
this.setState({
loading: true,
scanned: true,
});
};
/**
* Hide any dialog
*/
onDialogDismiss = () => {
this.setState({
dialogVisible: false,
scanned: false,
});
};
onMascotDialogDismiss = () => {
this.setState({
mascotDialogVisible: false,
scanned: false,
});
};
/**
* 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
*/
onCodeScanned = ({data}: {data: string}) => {
if (!URLHandler.isUrlValid(data)) this.showErrorDialog();
else {
this.showOpeningDialog();
Linking.openURL(data);
}
};
render(): React.Node {
const {state} = this;
return (
<View
style={{
...styles.container,
marginBottom: CustomTabBar.TAB_BAR_HEIGHT,
}}>
{state.hasPermission ? this.getScanner() : this.getPermissionScreen()}
<Button
icon="information"
mode="contained"
onPress={this.showHelpDialog}
style={styles.button}>
{i18n.t('screens.scanner.help.button')}
</Button>
<MascotPopup
visible={state.mascotDialogVisible}
title={i18n.t('screens.scanner.mascotDialog.title')}
message={i18n.t('screens.scanner.mascotDialog.message')}
icon="camera-iris"
buttons={{
action: null,
cancel: {
message: i18n.t('screens.scanner.mascotDialog.button'),
icon: 'check',
onPress: this.onMascotDialogDismiss,
},
}}
emotion={MASCOT_STYLE.NORMAL}
/>
<AlertDialog
visible={state.dialogVisible}
onDismiss={this.onDialogDismiss}
title={i18n.t('screens.scanner.error.title')}
message={i18n.t('screens.scanner.error.message')}
/>
<LoadingConfirmDialog
visible={state.loading}
titleLoading={i18n.t('general.loading')}
startLoading
/>
</View>
);
}
}
export default withTheme(ScannerScreen); export default withTheme(ScannerScreen);

View file

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

View file

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

123
src/utils/Home.js Normal file
View file

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

19
src/utils/Services.js Normal file
View file

@ -0,0 +1,19 @@
// @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;
}