Compare commits
5 commits
925bded69b
...
93d12b27f8
| Author | SHA1 | Date | |
|---|---|---|---|
| 93d12b27f8 | |||
| 33d98b024b | |||
| 6b12b4cde2 | |||
| 34ccf9c4c9 | |||
| 9d92a88627 |
30 changed files with 3101 additions and 2886 deletions
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
123
src/utils/Home.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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
19
src/utils/Services.js
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue