Compare commits

...

8 commits

24 changed files with 314 additions and 333 deletions

5
App.js
View file

@ -14,6 +14,7 @@ import {initExpoToken} from "./utils/Notifications";
import {Provider as PaperProvider} from 'react-native-paper';
import AprilFoolsManager from "./managers/AprilFoolsManager";
import Update from "./constants/Update";
import ConnectionManager from "./managers/ConnectionManager";
type Props = {};
@ -91,6 +92,10 @@ export default class App extends React.Component<Props, State> {
await AsyncStorageManager.getInstance().loadPreferences();
ThemeManager.getInstance().setUpdateThemeCallback(this.onUpdateTheme);
await initExpoToken();
try {
await ConnectionManager.getInstance().recoverLogin();
} catch (e) {}
this.onLoadFinished();
}

View file

@ -11,20 +11,19 @@ afterEach(() => {
});
test('isLoggedIn yes', () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve(true);
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
return 'token';
});
return expect(c.isLoggedIn()).resolves.toBe(true);
return expect(c.isLoggedIn()).toBe(true);
});
test('isLoggedIn no', () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.reject(false);
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
return null;
});
return expect(c.isLoggedIn()).rejects.toBe(false);
return expect(c.isLoggedIn()).toBe(false);
});
test('recoverLogin error crypto', () => {
jest.spyOn(SecureStore, 'getItemAsync').mockImplementationOnce(() => {
return Promise.reject();
@ -111,7 +110,12 @@ test("isConnectionResponseValid", () => {
state: false,
};
expect(c.isConnectionResponseValid(json)).toBeTrue();
json = {
state: false,
message: 'Adresse mail ou mot de passe incorrect',
token: ''
};
expect(c.isConnectionResponseValid(json)).toBeTrue();
json = {
state: true,
message: 'Connexion confirmée',
@ -143,7 +147,6 @@ test("isConnectionResponseValid", () => {
expect(c.isConnectionResponseValid(json)).toBeFalse();
});
test("connect bad credentials", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
@ -178,6 +181,25 @@ test("connect good credentials", () => {
return expect(c.connect('email', 'password')).resolves.toBeTruthy();
});
test("connect good credentials no consent", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
json: () => {
return {
state: false,
message: 'pas de consent',
token: '',
data: {
consent: false,
}
}
},
})
});
return expect(c.connect('email', 'password'))
.rejects.toBe(ERROR_TYPE.NO_CONSENT);
});
test("connect good credentials, fail save token", () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
@ -221,8 +243,8 @@ test("connect bogus response 1", () => {
test("authenticatedRequest success", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve('token');
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
return 'token';
});
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
@ -236,8 +258,8 @@ test("authenticatedRequest success", () => {
});
test("authenticatedRequest error wrong token", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve('token');
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
return 'token';
});
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
@ -251,8 +273,8 @@ test("authenticatedRequest error wrong token", () => {
});
test("authenticatedRequest error bogus response", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve('token');
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
return 'token';
});
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({
@ -266,8 +288,8 @@ test("authenticatedRequest error bogus response", () => {
});
test("authenticatedRequest connection error", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.resolve('token');
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
return 'token';
});
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.reject()
@ -277,8 +299,8 @@ test("authenticatedRequest connection error", () => {
});
test("authenticatedRequest error no token", () => {
jest.spyOn(ConnectionManager.prototype, 'recoverLogin').mockImplementationOnce(() => {
return Promise.reject(false);
jest.spyOn(ConnectionManager.prototype, 'getToken').mockImplementationOnce(() => {
return null;
});
jest.spyOn(global, 'fetch').mockImplementationOnce(() => {
return Promise.resolve({

View file

@ -24,7 +24,7 @@ class AuthenticatedScreen extends React.Component<Props, State> {
loading: true,
};
currentUserToken: string;
currentUserToken: string | null;
connectionManager: ConnectionManager;
errorCode: number;
data: Object;
@ -35,8 +35,6 @@ class AuthenticatedScreen extends React.Component<Props, State> {
this.colors = props.theme.colors;
this.connectionManager = ConnectionManager.getInstance();
this.props.navigation.addListener('focus', this.onScreenFocus.bind(this));
this.fetchData();
}
onScreenFocus() {
@ -47,26 +45,24 @@ class AuthenticatedScreen extends React.Component<Props, State> {
fetchData = () => {
if (!this.state.loading)
this.setState({loading: true});
this.connectionManager.isLoggedIn()
.then(() => {
this.connectionManager.authenticatedRequest(this.props.link)
.then((data) => {
this.onFinishedLoading(data, -1);
})
.catch((error) => {
this.onFinishedLoading(undefined, error);
});
})
.catch((error) => {
this.onFinishedLoading(undefined, ERROR_TYPE.BAD_CREDENTIALS);
});
if (this.connectionManager.isLoggedIn()) {
this.connectionManager.authenticatedRequest(this.props.link)
.then((data) => {
this.onFinishedLoading(data, -1);
})
.catch((error) => {
this.onFinishedLoading(undefined, error);
});
} else {
this.onFinishedLoading(undefined, ERROR_TYPE.BAD_CREDENTIALS);
}
};
onFinishedLoading(data: Object, error: number) {
this.data = data;
this.currentUserToken = data !== undefined
? this.connectionManager.getToken()
: '';
: null;
this.errorCode = error;
this.setState({loading: false});
}

View file

@ -13,7 +13,7 @@ function HeaderButton(props) {
<IconButton
icon={props.icon}
size={26}
color={colors.text}
color={props.color !== undefined ? props.color : colors.text}
onPress={props.onPress}
/>
);

View file

@ -3,6 +3,7 @@
import * as React from 'react';
import {Button, Card, withTheme} from 'react-native-paper';
import {StyleSheet} from "react-native";
import i18n from 'i18n-js';
type Props = {
navigation: Object,
@ -20,6 +21,8 @@ class ActionsDashBoardItem extends React.PureComponent<Props> {
openDrawer = () => this.props.navigation.openDrawer();
gotToSettings = () => this.props.navigation.navigate("SettingsScreen");
render() {
return (
<Card style={{
@ -31,10 +34,17 @@ class ActionsDashBoardItem extends React.PureComponent<Props> {
icon="information"
mode="contained"
onPress={this.openDrawer}
style={styles.button}
style={styles.servicesButton}
>
PLUS DE SERVICES
{i18n.t("homeScreen.servicesButton")}
</Button>
<Button
icon="settings"
mode="contained"
onPress={this.gotToSettings}
style={styles.settingsButton}
compact
/>
</Card.Content>
</Card>
);
@ -58,8 +68,12 @@ const styles = StyleSheet.create({
flex: 1,
flexDirection: 'row',
},
button: {
servicesButton: {
marginLeft: 'auto',
marginRight: 5,
},
settingsButton: {
marginLeft: 5,
marginRight: 'auto',
}
});

View file

@ -0,0 +1,40 @@
// @flow
import * as React from 'react';
import {Avatar, List, Text, withTheme} from 'react-native-paper';
import i18n from "i18n-js";
type Props = {
onPress: Function,
color: string,
item: Object,
}
class ProximoListItem extends React.PureComponent<Props> {
colors: Object;
constructor(props) {
super(props);
this.colors = props.theme.colors;
}
render() {
return (
<List.Item
title={this.props.item.name}
description={this.props.item.quantity + ' ' + i18n.t('proximoScreen.inStock')}
descriptionStyle={{color: this.props.color}}
onPress={this.props.onPress}
left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64}
source={{uri: this.props.item.image}}/>}
right={() =>
<Text style={{fontWeight: "bold"}}>
{this.props.item.price}
</Text>}
/>
);
}
}
export default withTheme(ProximoListItem);

View file

@ -1,33 +0,0 @@
import * as React from 'react';
import {FlatList} from "react-native";
type Props = {
data: Array<Object>,
keyExtractor: Function,
renderItem: Function,
updateData: number,
}
/**
* FlatList implementing PureComponent for increased performance.
*
* This is a pure component, meaning it will only update if a shallow comparison of state and props is different.
* To force the component to update, change the value of updateData.
*/
export default class PureFlatList extends React.PureComponent<Props> {
static defaultProps = {
updateData: null,
};
render() {
return (
<FlatList
data={this.props.data}
keyExtractor={this.props.keyExtractor}
style={{minHeight: 300, width: '100%'}}
renderItem={this.props.renderItem}
/>
);
}
}

View file

@ -57,8 +57,6 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
onFetchSuccess: Function;
onFetchError: Function;
getEmptySectionHeader: Function;
showSnackBar: Function;
hideSnackBar: Function;
constructor() {
super();
@ -67,8 +65,6 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
this.onFetchSuccess = this.onFetchSuccess.bind(this);
this.onFetchError = this.onFetchError.bind(this);
this.getEmptySectionHeader = this.getEmptySectionHeader.bind(this);
this.showSnackBar = this.showSnackBar.bind(this);
this.hideSnackBar = this.hideSnackBar.bind(this);
}
/**
@ -159,16 +155,12 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
/**
* Shows the error popup
*/
showSnackBar() {
this.setState({snackbarVisible: true})
}
showSnackBar = () => this.setState({snackbarVisible: true});
/**
* Hides the error popup
*/
hideSnackBar() {
this.setState({snackbarVisible: false})
}
hideSnackBar = () => this.setState({snackbarVisible: false});
render() {
let dataset = [];
@ -177,20 +169,10 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
const shouldRenderHeader = this.props.renderSectionHeader !== null;
return (
<View>
<Snackbar
visible={this.state.snackbarVisible}
onDismiss={this.hideSnackBar}
action={{
label: 'OK',
onPress: this.hideSnackBar,
}}
duration={4000}
>
{i18n.t("homeScreen.listUpdateFail")}
</Snackbar>
{/*$FlowFixMe*/}
<SectionList
sections={dataset}
extraData={this.props.updateData}
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
@ -212,6 +194,17 @@ export default class WebSectionList extends React.PureComponent<Props, State> {
onRefresh={this.onRefresh}/>
}
/>
<Snackbar
visible={this.state.snackbarVisible}
onDismiss={this.hideSnackBar}
action={{
label: 'OK',
onPress: () => {},
}}
duration={4000}
>
{i18n.t("homeScreen.listUpdateFail")}
</Snackbar>
</View>
);
}

View file

@ -19,7 +19,6 @@ type Props = {
};
type State = {
active: string,
isLoggedIn: boolean,
dialogVisible: boolean,
};
@ -154,18 +153,16 @@ class SideBar extends React.PureComponent<Props, State> {
];
this.getRenderItem = this.getRenderItem.bind(this);
this.colors = props.theme.colors;
ConnectionManager.getInstance().setLoginCallback((value) => this.onLoginStateChange(value));
ConnectionManager.getInstance().addLoginStateListener((value) => this.onLoginStateChange(value));
this.state = {
active: 'Home',
isLoggedIn: false,
isLoggedIn: ConnectionManager.getInstance().isLoggedIn(),
dialogVisible: false,
};
ConnectionManager.getInstance().isLoggedIn().then(data => undefined).catch(error => undefined);
}
showDisconnectDialog = () => this.setState({ dialogVisible: true });
showDisconnectDialog = () => this.setState({dialogVisible: true});
hideDisconnectDialog = () => this.setState({ dialogVisible: false });
hideDisconnectDialog = () => this.setState({dialogVisible: false});
onLoginStateChange(isLoggedIn: boolean) {
@ -239,9 +236,10 @@ class SideBar extends React.PureComponent<Props, State> {
style={styles.drawerCover}
/>
</TouchableRipple>
{/*$FlowFixMe*/}
<FlatList
data={this.dataSet}
extraData={this.state}
extraData={this.state.isLoggedIn}
keyExtractor={this.listKeyExtractor}
renderItem={this.getRenderItem}
/>

View file

@ -16,9 +16,14 @@ export default class ConnectionManager {
static instance: ConnectionManager | null = null;
#email: string;
#token: string;
#token: string | null;
loginCallback: Function;
listeners: Array<Function>;
constructor() {
this.#token = null;
this.listeners = [];
}
/**
* Get this class instance or create one if none is found
@ -35,20 +40,24 @@ export default class ConnectionManager {
}
onLoginStateChange(newState: boolean) {
this.loginCallback(newState);
for (let i = 0; i < this.listeners.length; i++) {
if (this.listeners[i] !== undefined)
this.listeners[i](newState);
}
}
setLoginCallback(callback: Function) {
this.loginCallback = callback;
addLoginStateListener(listener: Function) {
this.listeners.push(listener);
}
async recoverLogin() {
return new Promise((resolve, reject) => {
if (this.#token !== undefined)
resolve(this.#token);
if (this.getToken() !== null)
resolve(this.getToken());
else {
SecureStore.getItemAsync('token')
.then((token) => {
this.#token = token;
if (token !== null) {
this.onLoginStateChange(true);
resolve(token);
@ -62,16 +71,8 @@ export default class ConnectionManager {
});
}
async isLoggedIn() {
return new Promise((resolve, reject) => {
this.recoverLogin()
.then(() => {
resolve(true);
})
.catch(() => {
reject(false);
})
});
isLoggedIn() {
return this.getToken() !== null;
}
async saveLogin(email: string, token: string) {
@ -93,6 +94,7 @@ export default class ConnectionManager {
return new Promise((resolve, reject) => {
SecureStore.deleteItemAsync('token')
.then(() => {
this.#token = null;
this.onLoginStateChange(false);
resolve(true);
})
@ -126,7 +128,9 @@ export default class ConnectionManager {
.catch(() => {
reject(ERROR_TYPE.SAVE_TOKEN);
});
} else if (data.data.consent !== undefined && !data.data.consent)
} else if (data.data !== undefined
&& data.data.consent !== undefined
&& !data.data.consent)
reject(ERROR_TYPE.NO_CONSENT);
else
reject(ERROR_TYPE.BAD_CREDENTIALS);
@ -166,32 +170,29 @@ export default class ConnectionManager {
async authenticatedRequest(url: string) {
return new Promise((resolve, reject) => {
this.recoverLogin()
.then(token => {
fetch(url, {
method: 'POST',
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
body: JSON.stringify({token: token})
}).then(async (response) => response.json())
.then((data) => {
if (this.isRequestResponseValid(data)) {
if (data.state)
resolve(data.data);
else
reject(ERROR_TYPE.BAD_CREDENTIALS);
} else
reject(ERROR_TYPE.CONNECTION_ERROR);
})
.catch(() => {
if (this.getToken() !== null) {
fetch(url, {
method: 'POST',
headers: new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
}),
body: JSON.stringify({token: this.getToken()})
}).then(async (response) => response.json())
.then((data) => {
if (this.isRequestResponseValid(data)) {
if (data.state)
resolve(data.data);
else
reject(ERROR_TYPE.BAD_CREDENTIALS);
} else
reject(ERROR_TYPE.CONNECTION_ERROR);
});
})
.catch(() => {
reject(ERROR_TYPE.NO_TOKEN);
});
})
.catch(() => {
reject(ERROR_TYPE.CONNECTION_ERROR);
});
} else
reject(ERROR_TYPE.NO_TOKEN);
});
}
}

View file

@ -244,9 +244,7 @@ class AboutScreen extends React.Component<Props, State> {
<Card.Content>
<FlatList
data={this.appData}
extraData={this.state}
keyExtractor={this.keyExtractor}
listKey={"app"}
renderItem={this.getCardItem}
/>
</Card.Content>
@ -269,17 +267,15 @@ class AboutScreen extends React.Component<Props, State> {
<Title>{i18n.t('aboutScreen.author')}</Title>
<FlatList
data={this.authorData}
extraData={this.state}
keyExtractor={this.keyExtractor}
listKey={"team1"}
listKey={"1"}
renderItem={this.getCardItem}
/>
<Title>{i18n.t('aboutScreen.additionalDev')}</Title>
<FlatList
data={this.additionalDevData}
extraData={this.state}
keyExtractor={this.keyExtractor}
listKey={"team2"}
listKey={"2"}
renderItem={this.getCardItem}
/>
</Card.Content>
@ -299,9 +295,7 @@ class AboutScreen extends React.Component<Props, State> {
<Title>{i18n.t('aboutScreen.technologies')}</Title>
<FlatList
data={this.technoData}
extraData={this.state}
keyExtractor={this.keyExtractor}
listKey={"techno"}
renderItem={this.getCardItem}
/>
</Card.Content>
@ -404,28 +398,24 @@ class AboutScreen extends React.Component<Props, State> {
<Button
icon="email"
mode="contained"
dark={true}
color={this.colors.primary}
style={{
marginTop: 20,
marginLeft: 'auto',
marginRight: 'auto',
}}
onPress={this.onPressMail}>
<Text>{i18n.t('aboutScreen.bugsMail')}</Text>
{i18n.t('aboutScreen.bugsMail')}
</Button>
<Button
icon="git"
mode="contained"
dark={true}
color={this.colors.primary}
style={{
marginTop: 20,
marginLeft: 'auto',
marginRight: 'auto',
}}
onPress={this.onPressGit}>
<Text>{i18n.t('aboutScreen.bugsGit')}</Text>
{i18n.t('aboutScreen.bugsGit')}
</Button>
</View>
);
@ -476,8 +466,6 @@ class AboutScreen extends React.Component<Props, State> {
<FlatList
style={{padding: 5}}
data={this.dataOrder}
extraData={this.state}
keyExtractor={(item) => item.id}
renderItem={this.getMainCard}
/>
</View>

View file

@ -32,8 +32,8 @@ const emailRegex = /^.+@.+\..+$/;
class LoginScreen extends React.Component<Props, State> {
state = {
email: '',
password: '',
email: 'vergnet@etud.insa-toulouse.fr',
password: '3D514ùdsqg',
isEmailValidated: false,
isPasswordValidated: false,
loading: false,

View file

@ -12,6 +12,8 @@ import PreviewEventDashboardItem from "../components/Home/PreviewEventDashboardI
import {stringToDate} from "../utils/Planning";
import {openBrowser} from "../utils/WebBrowser";
import ActionsDashBoardItem from "../components/Home/ActionsDashboardItem";
import HeaderButton from "../components/Custom/HeaderButton";
import ConnectionManager from "../managers/ConnectionManager";
// import DATA from "../dashboard_data.json";
@ -30,15 +32,10 @@ type Props = {
theme: Object,
}
type State = {
imageModalVisible: boolean,
imageList: Array<Object>,
}
/**
* Class defining the app's home screen
*/
class HomeScreen extends React.Component<Props, State> {
class HomeScreen extends React.Component<Props> {
onProxiwashClick: Function;
onTutorInsaClick: Function;
@ -49,10 +46,7 @@ class HomeScreen extends React.Component<Props, State> {
colors: Object;
state = {
imageModalVisible: false,
imageList: [],
};
isLoggedIn: boolean | null;
constructor(props) {
super(props);
@ -63,6 +57,8 @@ class HomeScreen extends React.Component<Props, State> {
this.getRenderItem = this.getRenderItem.bind(this);
this.createDataset = this.createDataset.bind(this);
this.colors = props.theme.colors;
this.isLoggedIn = null;
}
/**
@ -76,6 +72,35 @@ class HomeScreen extends React.Component<Props, State> {
return date.toLocaleString();
}
componentDidMount() {
this.props.navigation.addListener('focus', this.onScreenFocus);
}
onScreenFocus = () => {
if (this.isLoggedIn !== ConnectionManager.getInstance().isLoggedIn()) {
this.isLoggedIn = ConnectionManager.getInstance().isLoggedIn();
this.props.navigation.setOptions({
headerRight: this.getHeaderButton,
});
}
};
getHeaderButton = () => {
const screen = this.isLoggedIn
? "ProfileScreen"
: "LoginScreen";
const icon = this.isLoggedIn
? "account"
: "login";
const onPress = () => this.props.navigation.navigate(screen);
return <HeaderButton
icon={icon}
onPress={onPress}
color={this.isLoggedIn ? undefined : this.colors.primary}
/>;
};
onProxiwashClick() {
this.props.navigation.navigate('Proxiwash');
}
@ -92,16 +117,6 @@ class HomeScreen extends React.Component<Props, State> {
this.props.navigation.navigate('SelfMenuScreen');
}
/**
* Extract a key for the given item
*
* @param item The item to extract the key from
* @return {*} The extracted key
*/
getKeyExtractor(item: Object) {
return item !== undefined ? item.id : undefined;
}
/**
* Creates the dataset to be used in the FlatList
*
@ -120,15 +135,11 @@ class HomeScreen extends React.Component<Props, State> {
{
title: '',
data: dashboardData,
extraData: super.state,
keyExtractor: this.getKeyExtractor,
id: SECTIONS_ID[0]
},
{
title: i18n.t('homeScreen.newsFeed'),
data: newsData,
extraData: super.state,
keyExtractor: this.getKeyExtractor,
id: SECTIONS_ID[1]
}
];
@ -426,18 +437,6 @@ class HomeScreen extends React.Component<Props, State> {
openBrowser(link, this.colors.primary);
}
showImageModal(imageList) {
this.setState({
imageModalVisible: true,
imageList: imageList,
});
};
hideImageModal = () => {
this.setState({imageModalVisible: false});
};
/**
* Gets a render item for the given feed object
*
@ -472,16 +471,13 @@ class HomeScreen extends React.Component<Props, State> {
render() {
const nav = this.props.navigation;
return (
<View>
<WebSectionList
createDataset={this.createDataset}
navigation={nav}
autoRefreshTime={REFRESH_TIME}
refreshOnFocus={true}
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}/>
</View>
<WebSectionList
createDataset={this.createDataset}
navigation={nav}
autoRefreshTime={REFRESH_TIME}
refreshOnFocus={true}
fetchUrl={DATA_URL}
renderItem={this.getRenderItem}/>
);
}
}

View file

@ -1,11 +1,12 @@
// @flow
import * as React from 'react';
import {Image, Platform, ScrollView, View} from "react-native";
import {FlatList, Image, Platform, ScrollView, View} from "react-native";
import i18n from "i18n-js";
import CustomModal from "../../components/Custom/CustomModal";
import {Avatar, IconButton, List, RadioButton, Searchbar, Subheading, Text, Title, withTheme} from "react-native-paper";
import PureFlatList from "../../components/Lists/PureFlatList";
import {IconButton, RadioButton, Searchbar, Subheading, Text, Title, withTheme} from "react-native-paper";
import {stringMatchQuery} from "../../utils/Search";
import ProximoListItem from "../../components/Lists/ProximoListItem";
function sortPrice(a, b) {
return a.price - b.price;
@ -39,7 +40,7 @@ type Props = {
type State = {
currentSortMode: number,
modalCurrentDisplayItem: React.Node,
currentlyDisplayedData: Array<Object>,
currentSearchString: string,
};
/**
@ -48,30 +49,21 @@ type State = {
class ProximoListScreen extends React.Component<Props, State> {
modalRef: Object;
originalData: Array<Object>;
listData: Array<Object>;
shouldFocusSearchBar: boolean;
onSearchStringChange: Function;
onSortMenuPress: Function;
renderItem: Function;
onModalRef: Function;
colors: Object;
constructor(props) {
super(props);
this.originalData = this.props.route.params['data']['data'];
this.listData = this.props.route.params['data']['data'];
this.shouldFocusSearchBar = this.props.route.params['shouldFocusSearchBar'];
this.state = {
currentlyDisplayedData: this.originalData.sort(sortName),
currentSearchString: '',
currentSortMode: 3,
modalCurrentDisplayItem: null,
};
this.onSearchStringChange = this.onSearchStringChange.bind(this);
this.onSortMenuPress = this.onSortMenuPress.bind(this);
this.renderItem = this.renderItem.bind(this);
this.onModalRef = this.onModalRef.bind(this);
this.colors = props.theme.colors;
}
@ -80,11 +72,9 @@ class ProximoListScreen extends React.Component<Props, State> {
* Creates the header content
*/
componentDidMount() {
const button = this.getSortMenuButton.bind(this);
const title = this.getSearchBar.bind(this);
this.props.navigation.setOptions({
headerRight: button,
headerTitle: title,
headerRight: this.getSortMenuButton,
headerTitle: this.getSearchBar,
headerBackTitleVisible: false,
headerTitleContainerStyle: Platform.OS === 'ios' ?
{marginHorizontal: 0, width: '70%'} :
@ -97,21 +87,21 @@ class ProximoListScreen extends React.Component<Props, State> {
*
* @return {*}
*/
getSearchBar() {
getSearchBar = () => {
return (
<Searchbar
placeholder={i18n.t('proximoScreen.search')}
onChangeText={this.onSearchStringChange}
/>
);
}
};
/**
* Gets the sort menu header button
*
* @return {*}
*/
getSortMenuButton() {
getSortMenuButton = () => {
return (
<IconButton
icon="sort"
@ -120,20 +110,20 @@ class ProximoListScreen extends React.Component<Props, State> {
onPress={this.onSortMenuPress}
/>
);
}
};
/**
* Callback used when clicking on the sort menu button.
* It will open the modal to show a sort selection
*/
onSortMenuPress() {
onSortMenuPress = () => {
this.setState({
modalCurrentDisplayItem: this.getModalSortMenu()
});
if (this.modalRef) {
this.modalRef.open();
}
}
};
/**
* Sets the current sort mode.
@ -144,19 +134,18 @@ class ProximoListScreen extends React.Component<Props, State> {
this.setState({
currentSortMode: mode,
});
let data = this.state.currentlyDisplayedData;
switch (mode) {
case 1:
data.sort(sortPrice);
this.listData.sort(sortPrice);
break;
case 2:
data.sort(sortPriceReverse);
this.listData.sort(sortPriceReverse);
break;
case 3:
data.sort(sortName);
this.listData.sort(sortName);
break;
case 4:
data.sort(sortNameReverse);
this.listData.sort(sortNameReverse);
break;
}
if (this.modalRef && mode !== this.state.currentSortMode) {
@ -181,46 +170,14 @@ class ProximoListScreen extends React.Component<Props, State> {
return color;
}
/**
* Sanitizes the given string to improve search performance
*
* @param str The string to sanitize
* @return {string} The sanitized string
*/
sanitizeString(str: string): string {
return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}
/**
* Returns only articles whose name contains the given string.
* Case and accents insensitive.
*
* @param str The string used to filter article names
* @returns {[]}
*/
filterData(str: string) {
let filteredData = [];
const testStr = this.sanitizeString(str);
const articles = this.originalData;
for (const article of articles) {
const name = this.sanitizeString(article.name);
if (name.includes(testStr)) {
filteredData.push(article)
}
}
return filteredData;
}
/**
* Callback used when the search changes
*
* @param str The new search string
*/
onSearchStringChange(str: string) {
this.setState({
currentlyDisplayedData: this.filterData(str)
})
}
onSearchStringChange = (str: string) => {
this.setState({currentSearchString: str})
};
/**
* Gets the modal content depending on the given article
@ -333,23 +290,20 @@ class ProximoListScreen extends React.Component<Props, State> {
* @param item The article to render
* @return {*}
*/
renderItem({item}: Object) {
const onPress = this.onListItemPress.bind(this, item);
return (
<List.Item
title={item.name}
description={item.quantity + ' ' + i18n.t('proximoScreen.inStock')}
descriptionStyle={{color: this.getStockColor(parseInt(item.quantity))}}
onPress={onPress}
left={() => <Avatar.Image style={{backgroundColor: 'transparent'}} size={64}
source={{uri: item.image}}/>}
right={() =>
<Text style={{fontWeight: "bold"}}>
{item.price}
</Text>}
/>
);
}
renderItem = ({item}: Object) => {
if (stringMatchQuery(item.name, this.state.currentSearchString)) {
const onPress = this.onListItemPress.bind(this, item);
const color = this.getStockColor(parseInt(item.quantity));
return (
<ProximoListItem
item={item}
onPress={onPress}
color={color}
/>
);
} else
return null;
};
/**
* Extracts a key for the given article
@ -366,9 +320,9 @@ class ProximoListScreen extends React.Component<Props, State> {
*
* @param ref
*/
onModalRef(ref: Object) {
onModalRef = (ref: Object) => {
this.modalRef = ref;
}
};
render() {
return (
@ -378,11 +332,12 @@ class ProximoListScreen extends React.Component<Props, State> {
<CustomModal onRef={this.onModalRef}>
{this.state.modalCurrentDisplayItem}
</CustomModal>
<PureFlatList
data={this.state.currentlyDisplayedData}
{/*$FlowFixMe*/}
<FlatList
data={this.listData}
extraData={this.state.currentSearchString + this.state.currentSortMode}
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
updateData={this.state.currentSortMode}
/>
</View>
);

View file

@ -142,7 +142,6 @@ class ProximoMainScreen extends React.Component<Props, State> {
{
title: '',
data: this.generateData(fetchedData),
extraData: this.state,
keyExtractor: this.getKeyExtractor
}
];

View file

@ -272,14 +272,12 @@ class ProxiwashScreen extends React.Component<Props, State> {
title: i18n.t('proxiwashScreen.dryers'),
icon: 'tumble-dryer',
data: data.dryers === undefined ? [] : data.dryers,
extraData: this.state,
keyExtractor: this.getKeyExtractor
},
{
title: i18n.t('proxiwashScreen.washers'),
icon: 'washing-machine',
data: data.washers === undefined ? [] : data.washers,
extraData: this.state,
keyExtractor: this.getKeyExtractor
},
];

View file

@ -56,7 +56,6 @@ class SelfMenuScreen extends React.Component<Props> {
{
title: '',
data: [],
extraData: super.state,
keyExtractor: this.getKeyExtractor
}
];
@ -69,7 +68,6 @@ class SelfMenuScreen extends React.Component<Props> {
{
title: DateManager.getInstance().getTranslatedDate(fetchedData[i].date),
data: fetchedData[i].meal[0].foodcategory,
extraData: super.state,
keyExtractor: this.getKeyExtractor,
}
);

View file

@ -5,7 +5,7 @@ import ScoreManager from "./ScoreManager";
import type {coordinates} from './Shapes/BaseShape';
export type cell = {color: string, isEmpty: boolean};
export type cell = {color: string, isEmpty: boolean, key: string};
export type grid = Array<Array<cell>>;
/**
@ -50,6 +50,7 @@ export default class GridManager {
line.push({
color: this.#colors.tetrisBackground,
isEmpty: true,
key: col.toString(),
});
}
return line;

View file

@ -57,6 +57,7 @@ export default class Piece {
grid[coord[i].y][coord[i].x] = {
color: this.#colors.tetrisBackground,
isEmpty: true,
key: grid[coord[i].y][coord[i].x].key
};
}
}
@ -73,6 +74,7 @@ export default class Piece {
grid[coord[i].y][coord[i].x] = {
color: this.#currentShape.getColor(),
isEmpty: false,
key: grid[coord[i].y][coord[i].x].key
};
}
}

View file

@ -5,9 +5,7 @@ import {View} from 'react-native';
import {withTheme} from 'react-native-paper';
type Props = {
color: string,
isEmpty: boolean,
id: string,
item: Object
}
class Cell extends React.PureComponent<Props> {
@ -20,17 +18,19 @@ class Cell extends React.PureComponent<Props> {
}
render() {
const item = this.props.item;
return (
<View
style={{
flex: 1,
backgroundColor: this.props.isEmpty ? 'transparent' : this.props.color,
borderColor: this.props.isEmpty ? 'transparent' : this.colors.tetrisBorder,
backgroundColor: item.isEmpty ? 'transparent' : item.color,
borderColor: item.isEmpty ? 'transparent' : this.colors.tetrisBorder,
borderStyle: 'solid',
borderRadius: 2,
borderWidth: 1,
aspectRatio: 1,
}}
key={item.key}
/>
);
}

View file

@ -11,11 +11,11 @@ type Props = {
backgroundColor: string,
height: number,
width: number,
containerMaxHeight: number|string,
containerMaxWidth: number|string,
containerMaxHeight: number | string,
containerMaxWidth: number | string,
}
class Grid extends React.Component<Props>{
class Grid extends React.Component<Props> {
colors: Object;
@ -25,22 +25,24 @@ class Grid extends React.Component<Props>{
}
getRow(rowNumber: number) {
let cells = [];
for (let i = 0; i < this.props.width; i++) {
let cell = this.props.grid[rowNumber][i];
let key = rowNumber + ':' + i;
cells.push(<Cell color={cell.color} isEmpty={cell.isEmpty} id={key}/>);
}
return(
<View style={{
flexDirection: 'row',
backgroundColor: this.props.backgroundColor,
}}>
let cells = this.props.grid[rowNumber].map(this.getCellRender);
return (
<View
style={{
flexDirection: 'row',
backgroundColor: this.props.backgroundColor,
}}
key={rowNumber.toString()}
>
{cells}
</View>
);
}
getCellRender = (item: Object) => {
return <Cell item={item} key={item.key}/>;
};
getGrid() {
let rows = [];
for (let i = 0; i < this.props.height; i++) {
@ -55,7 +57,7 @@ class Grid extends React.Component<Props>{
flexDirection: 'column',
maxWidth: this.props.containerMaxWidth,
maxHeight: this.props.containerMaxHeight,
aspectRatio: this.props.width/this.props.height,
aspectRatio: this.props.width / this.props.height,
marginLeft: 'auto',
marginRight: 'auto',
}}>

View file

@ -22,19 +22,24 @@ class Preview extends React.PureComponent<Props> {
let grids = [];
for (let i = 0; i < this.props.next.length; i++) {
grids.push(
<Grid
width={this.props.next[i][0].length}
height={this.props.next[i].length}
grid={this.props.next[i]}
containerMaxHeight={50}
containerMaxWidth={50}
backgroundColor={'transparent'}
/>
this.getGridRender(this.props.next[i], i)
);
}
return grids;
}
getGridRender(item: Object, index: number) {
return <Grid
width={item[0].length}
height={item.length}
grid={item}
containerMaxHeight={50}
containerMaxWidth={50}
backgroundColor={'transparent'}
key={index.toString()}
/>;
};
render() {
if (this.props.next.length > 0) {
return (

View file

@ -80,6 +80,7 @@
"homeScreen": {
"listUpdated": "List updated!",
"listUpdateFail": "Error while updating list",
"servicesButton": "More services",
"newsFeed": "Campus News",
"dashboard": {
"seeMore": "Click to see more",

View file

@ -80,6 +80,7 @@
"homeScreen": {
"listUpdated": "List mise à jour!",
"listUpdateFail": "Erreur lors de la mise à jour de la liste",
"servicesButton": "Plus de services",
"newsFeed": "Nouvelles du campus",
"dashboard": {
"seeMore": "Cliquez pour plus d'infos",
@ -159,7 +160,7 @@
"loading": "Chargement...",
"description": "C'est le service de laverie proposé par promologis pour les résidences INSA (On t'en voudra pas si tu loges pas sur le campus et que tu fais ta machine ici). Le local situé au pied du R2 avec ses 3 sèche-linges et 9 machines est ouvert 7J/7 24h/24 ! Ici tu peux vérifier leur disponibilité ! Tu peux amener ta lessive, la prendre sur place ou encore mieux l'acheter au Proximo (moins chère qu'à la laverie directement). Tu peux payer par CB ou espèces.",
"informationTab": "Informations",
"paymentTab" : "Paiement",
"paymentTab": "Paiement",
"tariffs": "Tarifs",
"washersTariff": "3€ la machine + 0.80€ avec la lessive.",
"dryersTariff": "0.35€ pour 5min de sèche linge.",
@ -186,7 +187,6 @@
"error": "Il y a eu une erreur et il est impossible de récupérer les informations de cette machine. Veuillez nous excuser pour le gène occasionnée.",
"notificationErrorTitle": "Erreur",
"notificationErrorDescription": "Impossible de créer les notifications. Merci de vérifier que vous avez activé les notifications puis redémarrez l'appli."
},
"states": {
"finished": "TERMINÉ",